diff --git a/.editorconfig b/.editorconfig index dd065ed451c..5bc89604c76 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,6 +13,12 @@ trim_trailing_whitespace = true [*.{cs,cshtml,htm,html,md,py,sln,xml}] indent_size = 4 +############################### +# Copyright header # +############################### +[*.cs] +file_header_template = Copyright The OpenTelemetry Authors\nSPDX-License-Identifier: Apache-2.0 + ############################### # .NET Coding Conventions # ############################### @@ -34,7 +40,7 @@ csharp_indent_switch_labels = true csharp_indent_labels = flush_left # Modifier preferences -csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent # this. preferences @@ -106,6 +112,7 @@ csharp_style_prefer_index_operator = false:none csharp_style_prefer_range_operator = false:none csharp_style_pattern_local_over_anonymous_function = true:suggestion csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_namespace_declarations = file_scoped:warning # Space preferences csharp_space_after_cast = false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 39203dabfb6..0ff333446eb 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,7 +7,7 @@ Please provide a brief description of the changes here. ## Merge requirement checklist -* [ ] [CONTRIBUTING](https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/CONTRIBUTING.md) guidelines followed (nullable enabled, static analysis, etc.) +* [ ] [CONTRIBUTING](https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/CONTRIBUTING.md) guidelines followed (license requirements, nullable enabled, static analysis, etc.) * [ ] Unit tests added/updated * [ ] Appropriate `CHANGELOG.md` files updated for non-trivial changes * [ ] Changes in public API reviewed (if applicable) diff --git a/.github/codecov.yml b/.github/codecov.yml index 1abd5d69c37..b20bf1b2972 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -23,7 +23,32 @@ comment: require_changes: no ignore: - - "docs/**/*" - - "examples/**/*" - - "test/**/*" - "**.md" + - ".github" + - ".vscode" + - "build" + - "docs" + - "examples" + - "src/Shared" + - "test" + +flags: + unittests-Solution-Stable: + carryforward: true + paths: + - src + + unittests-Solution-Experimental: + carryforward: true + paths: + - src + + unittests-Instrumentation-Stable: + carryforward: true + paths: + - src + + unittests-Instrumentation-Experimental: + carryforward: true + paths: + - src diff --git a/.github/workflows/Component.BuildTest.yml b/.github/workflows/Component.BuildTest.yml new file mode 100644 index 00000000000..f97c8b94f9b --- /dev/null +++ b/.github/workflows/Component.BuildTest.yml @@ -0,0 +1,82 @@ +# Called by ci.yml to build & test project files +# See: https://docs.github.com/en/actions/using-workflows/reusing-workflows#creating-a-reusable-workflow +name: Build Component + +on: + workflow_call: + inputs: + project-name: + required: true + type: string + project-build-commands: + default: '' + required: false + type: string + code-cov-name: + required: true + type: string + code-cov-prefix: + default: 'unittests' + required: false + type: string + os-list: + default: '[ "windows-latest", "ubuntu-latest" ]' + required: false + type: string + tfm-list: + default: '[ "net462", "net6.0", "net7.0", "net8.0" ]' + required: false + type: string + +jobs: + build-test: + + strategy: + fail-fast: false # ensures the entire test matrix is run, even if one permutation fails + matrix: + os: ${{ fromJSON(inputs.os-list) }} + version: ${{ fromJSON(inputs.tfm-list) }} + exclude: + - os: ubuntu-latest + version: net462 + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + with: + # Note: By default GitHub only fetches 1 commit. MinVer needs to find + # the version tag which is typically NOT on the first commit so we + # retrieve them all. + fetch-depth: 0 + + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + + - name: dotnet restore ${{ inputs.project-name }} + run: dotnet restore ${{ inputs.project-name }} ${{ inputs.project-build-commands }} + + - name: dotnet build ${{ inputs.project-name }} + run: dotnet build ${{ inputs.project-name }} --configuration Release --no-restore ${{ inputs.project-build-commands }} + + - name: dotnet test ${{ inputs.project-name }} + run: dotnet test ${{ inputs.project-name }} --collect:"Code Coverage" --results-directory:TestResults --framework ${{ matrix.version }} --configuration Release --no-restore --no-build --logger:"console;verbosity=detailed" -- RunConfiguration.DisableAppDomain=true + + - name: Install coverage tool + run: dotnet tool install -g dotnet-coverage + + - name: Merging test results + run: dotnet-coverage merge -r -f cobertura -o ./TestResults/Cobertura.xml ./TestResults/*.coverage + + - name: Upload code coverage ${{ inputs.code-cov-prefix }}-${{ inputs.code-cov-name }} + uses: codecov/codecov-action@v4 + continue-on-error: true # Note: Don't fail for upload failures + env: + OS: ${{ matrix.os }} + TFM: ${{ matrix.version }} + token: ${{ secrets.CODECOV_TOKEN }} + with: + file: TestResults/Cobertura.xml + env_vars: OS,TFM + flags: ${{ inputs.code-cov-prefix }}-${{ inputs.code-cov-name }} + name: Code Coverage for ${{ inputs.code-cov-prefix }}-${{ inputs.code-cov-name }} on [${{ matrix.os }}.${{ matrix.version }}] + codecov_yml_path: .github/codecov.yml diff --git a/.github/workflows/apicompatibility.yml b/.github/workflows/apicompatibility.yml deleted file mode 100644 index ec6706fdfab..00000000000 --- a/.github/workflows/apicompatibility.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: API Compatibility - -on: - pull_request: - branches: [ 'main*' ] - paths-ignore: - - '**.md' - -jobs: - build-test: - runs-on: windows-latest - env: - CheckAPICompatibility: true - - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 # fetching all - - - name: Install dependencies - run: dotnet restore - - - name: Build - run: dotnet build --configuration Release --no-restore diff --git a/.github/workflows/ci-md.yml b/.github/workflows/ci-md.yml deleted file mode 100644 index 1dde2355a3e..00000000000 --- a/.github/workflows/ci-md.yml +++ /dev/null @@ -1,28 +0,0 @@ -# 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' as the non -md workflow. - -name: Build - -on: - pull_request: - branches: [ 'main*' ] - paths: - - '**.md' - -jobs: - build-test: - strategy: - matrix: - os: [ windows-latest, ubuntu-latest ] - version: [ net462, net6.0, net7.0 ] - exclude: - - os: ubuntu-latest - version: net462 - - runs-on: ubuntu-latest - steps: - - run: 'echo "No build required"' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 734959b70de..dc3494d93cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,37 +1,193 @@ name: Build on: + workflow_dispatch: + # The push trigger would run the CI workflow when any changes get merged to main branch. + # The build badge on the main README uses the CI results on the main branch to report the build status. push: branches: [ 'main*' ] - paths-ignore: - - '**.md' pull_request: branches: [ 'main*' ] - paths-ignore: - - '**.md' jobs: - build-test: + lint-misspell-sanitycheck: + uses: ./.github/workflows/sanitycheck.yml + + detect-changes: + runs-on: windows-latest + outputs: + changes: ${{ steps.changes.outputs.changes }} + steps: + - uses: actions/checkout@v4 + - uses: AurorNZ/paths-filter@v4 + id: changes + with: + filters: | + md: ['**.md'] + build: ['build/**', '.github/**/*.yml', '**/*.targets', '**/*.props'] + shared: ['src/Shared/**'] + code: ['**.cs', '**.csproj', '.editorconfig'] + packaged-code: ['src/**', '!**/*.md'] + api-code: ['*/OpenTelemetry.Api*/**', '!**/*.md'] + api-packages: ['src/OpenTelemetry.Api*/**', '!**/*.md'] + instrumentation: ['*/OpenTelemetry.Instrumentation*/**', 'test/TestApp.AspNetCore/**', '!**/*.md'] + instrumentation-packages: ['src/OpenTelemetry.Instrumentation*/**', '!**/*.md'] + sdk-code: ['src/OpenTelemetry/**', 'test/OpenTelemetry.Tests/**', '!**/*.md'] + sdk-package: ['src/OpenTelemetry/**', '!**/*.md'] + otlp: ['*/OpenTelemetry.Exporter.OpenTelemetryProtocol*/**', '!**/*.md'] + + lint-md: + needs: detect-changes + if: | + contains(needs.detect-changes.outputs.changes, 'md') + || contains(needs.detect-changes.outputs.changes, 'build') + uses: ./.github/workflows/markdownlint.yml + + lint-dotnet-format: + needs: detect-changes + if: | + contains(needs.detect-changes.outputs.changes, 'code') + || contains(needs.detect-changes.outputs.changes, 'build') + uses: ./.github/workflows/dotnet-format.yml + + build-test-solution-stable: + needs: detect-changes + if: | + contains(needs.detect-changes.outputs.changes, 'code') + || contains(needs.detect-changes.outputs.changes, 'build') + || contains(needs.detect-changes.outputs.changes, 'shared') + uses: ./.github/workflows/Component.BuildTest.yml + with: + project-name: 'OpenTelemetry.sln' + project-build-commands: '-p:ExposeExperimentalFeatures=false' + code-cov-name: 'Solution-Stable' + + build-test-solution-experimental: + needs: detect-changes + if: | + contains(needs.detect-changes.outputs.changes, 'code') + || contains(needs.detect-changes.outputs.changes, 'build') + || contains(needs.detect-changes.outputs.changes, 'shared') + uses: ./.github/workflows/Component.BuildTest.yml + with: + project-name: 'OpenTelemetry.sln' + project-build-commands: '-p:ExposeExperimentalFeatures=true' + code-cov-name: 'Solution-Experimental' + + # Build instrumentation libraries using stable packages released to NuGet + build-test-instrumentation-stable: + needs: detect-changes + if: | + contains(needs.detect-changes.outputs.changes, 'instrumentation-packages') + || contains(needs.detect-changes.outputs.changes, 'build') + || contains(needs.detect-changes.outputs.changes, 'shared') + uses: ./.github/workflows/Component.BuildTest.yml + with: + project-name: './build/InstrumentationLibraries.proj' + project-build-commands: '-p:RunningDotNetPack=true -p:ExposeExperimentalFeatures=false' + code-cov-name: 'Instrumentation-Stable' + + # Build instrumentation libraries using stable packages released to NuGet + build-test-instrumentation-experimental: + needs: detect-changes + if: | + contains(needs.detect-changes.outputs.changes, 'instrumentation-packages') + || contains(needs.detect-changes.outputs.changes, 'build') + || contains(needs.detect-changes.outputs.changes, 'shared') + uses: ./.github/workflows/Component.BuildTest.yml + with: + project-name: './build/InstrumentationLibraries.proj' + project-build-commands: '-p:RunningDotNetPack=true -p:ExposeExperimentalFeatures=true' + code-cov-name: 'Instrumentation-Experimental' + + otlp-integration-test: + needs: detect-changes + if: | + contains(needs.detect-changes.outputs.changes, 'api-packages') + || contains(needs.detect-changes.outputs.changes, 'sdk-package') + || contains(needs.detect-changes.outputs.changes, 'otlp') + || contains(needs.detect-changes.outputs.changes, 'build') + || contains(needs.detect-changes.outputs.changes, 'shared') + runs-on: ubuntu-latest strategy: - fail-fast: false # ensures the entire test matrix is run, even if one permutation fails + fail-fast: false matrix: - os: [ windows-latest, ubuntu-latest ] - version: [ net462, net6.0, net7.0 ] - exclude: - - os: ubuntu-latest - version: net462 + version: [ net6.0, net7.0, net8.0 ] + steps: + - uses: actions/checkout@v4 + - name: Run OTLP Exporter docker-compose + run: docker-compose --file=test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/docker-compose.yml --file=build/docker-compose.${{ matrix.version }}.yml --project-directory=. up --exit-code-from=tests --build - runs-on: ${{ matrix.os }} + w3c-trace-context-integration-test: + needs: detect-changes + if: | + contains(needs.detect-changes.outputs.changes, 'api-packages') + || contains(needs.detect-changes.outputs.changes, 'sdk-package') + || contains(needs.detect-changes.outputs.changes, 'instrumentation') + || contains(needs.detect-changes.outputs.changes, 'build') + || contains(needs.detect-changes.outputs.changes, 'shared') + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + version: [ net6.0, net7.0 ] steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 # fetching all + - uses: actions/checkout@v4 + - name: Run W3C Trace Context docker-compose + run: docker-compose --file=test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/docker-compose.yml --file=build/docker-compose.${{ matrix.version }}.yml --project-directory=. up --exit-code-from=tests --build - - name: Install dependencies - run: dotnet restore + validate-packages: + needs: detect-changes + if: | + contains(needs.detect-changes.outputs.changes, 'packaged-code') + || contains(needs.detect-changes.outputs.changes, 'build') + || contains(needs.detect-changes.outputs.changes, 'shared') + uses: ./.github/workflows/package-validation.yml - - name: Build - run: dotnet build --configuration Release --no-restore + generate-docs: + needs: detect-changes + if: | + contains(needs.detect-changes.outputs.changes, 'packaged-code') + || contains(needs.detect-changes.outputs.changes, 'md') + || contains(needs.detect-changes.outputs.changes, 'build') + || contains(needs.detect-changes.outputs.changes, 'shared') + uses: ./.github/workflows/docfx.yml - - name: Test ${{ matrix.version }} - run: dotnet test **/bin/**/${{ matrix.version }}/*.Tests.dll --logger:"console;verbosity=detailed" + verify-aot-compat: + needs: detect-changes + if: | + contains(needs.detect-changes.outputs.changes, 'packaged-code') + || contains(needs.detect-changes.outputs.changes, 'build') + || contains(needs.detect-changes.outputs.changes, 'shared') + uses: ./.github/workflows/verifyaotcompat.yml + + concurrency-tests: + needs: detect-changes + if: | + contains(needs.detect-changes.outputs.changes, 'api-code') + || contains(needs.detect-changes.outputs.changes, 'sdk-code') + || contains(needs.detect-changes.outputs.changes, 'build') + || contains(needs.detect-changes.outputs.changes, 'shared') + uses: ./.github/workflows/concurrency-tests.yml + + build-test: + needs: [ + lint-misspell-sanitycheck, + detect-changes, + lint-md, + lint-dotnet-format, + build-test-solution-stable, + build-test-solution-experimental, + build-test-instrumentation-stable, + build-test-instrumentation-experimental, + otlp-integration-test, + w3c-trace-context-integration-test, + validate-packages, + generate-docs, + verify-aot-compat, + concurrency-tests + ] + if: always() && !cancelled() && !contains(needs.*.result, 'failure') + runs-on: windows-latest + steps: + - run: echo 'build complete' diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml deleted file mode 100644 index 4a68cb93e41..00000000000 --- a/.github/workflows/code-coverage.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Code Coverage - -on: - push: - branches: [ 'main*' ] - paths-ignore: - - '**.md' - pull_request: - branches: [ 'main*' ] - paths-ignore: - - '**.md' - -jobs: - build-test-report: - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: false - matrix: - os: [windows-latest] - - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 # fetching all - - - name: Install dependencies - run: dotnet restore - - - name: dotnet build - run: dotnet build --configuration Release --no-restore - -# - name: dotnet test -# run: dotnet test --collect:"XPlat Code Coverage" --results-directory:"TestResults" --configuration Release --no-build -- RunConfiguration.DisableAppDomain=true - - - name: dotnet test - run: dotnet test --collect:"Code Coverage" --results-directory:"TestResults" --configuration Release --no-build -- RunConfiguration.DisableAppDomain=true - - - name: Process code coverage - run: .\build\process-codecoverage.ps1 - shell: powershell - - - name: Install report tool - run: dotnet tool install -g dotnet-reportgenerator-globaltool - - - name: Merging test results - run: reportgenerator -reports:TestResults/**/*.xml -targetdir:TestResults -reporttypes:Cobertura -assemblyFilters:"-microsoft.data.sqlclient*;-grpc.core*;-opentracing*" - - - uses: codecov/codecov-action@v3.1.4 - with: - file: TestResults/Cobertura.xml - env_vars: OS - name: Code Coverage for ${{ matrix.os }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 2b6d32c3ead..c091dc85184 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -8,6 +8,7 @@ name: "CodeQL" on: schedule: - cron: '0 0 * * *' # once in a day at 00:00 + workflow_dispatch: jobs: analyze: @@ -17,48 +18,29 @@ jobs: strategy: fail-fast: false matrix: - # Override automatic language detection by changing the below list - # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] language: ['csharp'] - # Learn more... - # https://docs.github.com/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection steps: - name: configure Pagefile - uses: al-cheb/configure-pagefile-action@v1.3 + uses: al-cheb/configure-pagefile-action@v1.4 with: minimum-size: 8GB maximum-size: 32GB disk-root: "D:" - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 + - name: Setup dotnet + uses: actions/setup-dotnet@v4 - # Command-line programs to run using the OS shell. - # https://git.io/JvXDl - - # If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release + - name: dotnet pack OpenTelemetry.proj + run: dotnet pack OpenTelemetry.proj --configuration Release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/concurrency-tests.yml b/.github/workflows/concurrency-tests.yml new file mode 100644 index 00000000000..43c438ef2d9 --- /dev/null +++ b/.github/workflows/concurrency-tests.yml @@ -0,0 +1,34 @@ +# Called by ci.yml to run coyote concurrency tests +# See: https://docs.github.com/en/actions/using-workflows/reusing-workflows#creating-a-reusable-workflow +name: Concurrency Tests + +on: + workflow_call: + +jobs: + run-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 + + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + + - 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@v4 + with: + name: ${{ matrix.os }}-${{ matrix.project }}-${{ matrix.version }}-coyoteoutput + path: '**/*_CoyoteOutput.*' diff --git a/.github/workflows/docfx.yml b/.github/workflows/docfx.yml index 53e13b5b4e8..34a94672acf 100644 --- a/.github/workflows/docfx.yml +++ b/.github/workflows/docfx.yml @@ -1,18 +1,17 @@ -name: docfx +# Called by ci.yml to run documentation build +# See: https://docs.github.com/en/actions/using-workflows/reusing-workflows#creating-a-reusable-workflow +name: Build docfx on: - push: - branches: [ 'main*' ] - pull_request: - branches: [ 'main*' ] + workflow_call: jobs: - build: + run-docfx-build: runs-on: windows-latest steps: - name: check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: install docfx run: choco install docfx -y diff --git a/.github/workflows/dotnet-format-md.yml b/.github/workflows/dotnet-format-md.yml deleted file mode 100644 index d35d16849bb..00000000000 --- a/.github/workflows/dotnet-format-md.yml +++ /dev/null @@ -1,22 +0,0 @@ -# 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: dotnet format - -on: - pull_request: - branches: [ 'main*' ] - paths-ignore: - - '**.cs' - - '.editorconfig' - -jobs: - check-format: - runs-on: ubuntu-latest - - steps: - - run: 'echo "No build required"' diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index 73c2cde0bdc..b6baf18aab0 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -1,27 +1,43 @@ -name: dotnet format +# Called by ci.yml to perform dotnet format linting +# See: https://docs.github.com/en/actions/using-workflows/reusing-workflows#creating-a-reusable-workflow +name: Lint - dotnet format on: - push: - branches: [ 'main*' ] - paths: - - '**.cs' - - '.editorconfig' - pull_request: - branches: [ 'main*' ] - paths: - - '**.cs' - - '.editorconfig' + workflow_call: jobs: - check-format: + run-dotnet-format-stable: runs-on: windows-latest steps: - name: check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - name: Install format tool - run: dotnet tool install -g dotnet-format + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + + - name: dotnet restore + run: dotnet restore + + - name: dotnet format + run: dotnet format OpenTelemetry.sln --no-restore --verify-no-changes + env: + ExposeExperimentalFeatures: false + + run-dotnet-format-experimental: + runs-on: windows-latest + + steps: + - name: check out code + uses: actions/checkout@v4 + + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + + - name: dotnet restore + run: dotnet restore - name: dotnet format - run: dotnet-format --folder --check + run: dotnet format OpenTelemetry.sln --no-restore --verify-no-changes + env: + ExposeExperimentalFeatures: true diff --git a/.github/workflows/integration-md.yml b/.github/workflows/integration-md.yml deleted file mode 100644 index 516bcbb7f53..00000000000 --- a/.github/workflows/integration-md.yml +++ /dev/null @@ -1,39 +0,0 @@ -# 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: Integration Tests - -on: - pull_request: - branches: [ 'main*' ] - paths: - - '**.md' - -jobs: - sql-test: - runs-on: ubuntu-latest - strategy: - matrix: - version: [net6.0,net7.0] - steps: - - run: 'echo "No build required"' - - w3c-trace-context-test: - runs-on: ubuntu-latest - strategy: - matrix: - version: [net6.0,net7.0] - steps: - - run: 'echo "No build required"' - - otlp-exporter-test: - runs-on: ubuntu-latest - strategy: - matrix: - version: [net6.0,net7.0] - steps: - - run: 'echo "No build required"' diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml deleted file mode 100644 index 3127c132789..00000000000 --- a/.github/workflows/integration.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Integration Tests - -on: - push: - branches: [ 'main*' ] - paths-ignore: - - '**.md' - pull_request: - branches: [ 'main*' ] - paths-ignore: - - '**.md' - -jobs: - w3c-trace-context-test: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - version: [net6.0,net7.0] - steps: - - uses: actions/checkout@v3 - - - name: Run W3C Trace Context docker-compose.integration - run: docker-compose --file=test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/docker-compose.yml --file=build/docker-compose.${{ matrix.version }}.yml --project-directory=. up --exit-code-from=tests --build - - otlp-exporter-test: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - version: [net6.0,net7.0] - steps: - - uses: actions/checkout@v3 - - - name: Run OTLP Exporter docker-compose.integration - run: docker-compose --file=test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/docker-compose.yml --file=build/docker-compose.${{ matrix.version }}.yml --project-directory=. up --exit-code-from=tests --build diff --git a/.github/workflows/markdownlint.yml b/.github/workflows/markdownlint.yml index 643c5f6f1d8..462832652d5 100644 --- a/.github/workflows/markdownlint.yml +++ b/.github/workflows/markdownlint.yml @@ -1,25 +1,21 @@ -name: markdownlint +# Called by ci.yml to perform markdown linting +# See: https://docs.github.com/en/actions/using-workflows/reusing-workflows#creating-a-reusable-workflow +name: Lint - Markdown on: - push: - branches: [ 'main*' ] - paths: - - '**.md' - pull_request: - branches: [ 'main*' ] - paths: - - '**.md' + workflow_call: jobs: - build: + run-markdownlint: runs-on: ubuntu-latest steps: - name: check out code - uses: actions/checkout@v3 - - - name: install markdownlint-cli - run: sudo npm install -g markdownlint-cli + uses: actions/checkout@v4 - name: run markdownlint - run: markdownlint . + uses: DavidAnson/markdownlint-cli2-action@v15.0.0 + with: + globs: | + **/*.md + !.github/**/*.md diff --git a/.github/workflows/package-validation.yml b/.github/workflows/package-validation.yml new file mode 100644 index 00000000000..5fc56716624 --- /dev/null +++ b/.github/workflows/package-validation.yml @@ -0,0 +1,41 @@ +# Called by ci.yml to perform package validation +# See: https://docs.github.com/en/actions/using-workflows/reusing-workflows#creating-a-reusable-workflow +name: Package Validation + +on: + workflow_call: + +jobs: + run-package-validation-stable: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + with: + # Note: By default GitHub only fetches 1 commit. MinVer needs to find + # the version tag which is typically NOT on the first commit so we + # retrieve them all. + fetch-depth: 0 + + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + + - name: Pack + run: dotnet pack OpenTelemetry.proj --configuration Release /p:EnablePackageValidation=true /p:ExposeExperimentalFeatures=false + + run-package-validation-experimental: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + with: + # Note: By default GitHub only fetches 1 commit. MinVer needs to find + # the version tag which is typically NOT on the first commit so we + # retrieve them all. + fetch-depth: 0 + + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + + - name: Pack + run: dotnet pack OpenTelemetry.proj --configuration Release /p:EnablePackageValidation=true /p:ExposeExperimentalFeatures=true diff --git a/.github/workflows/publish-packages-1.0.yml b/.github/workflows/publish-packages-1.0.yml index e479ec186b4..3fa6ad549a3 100644 --- a/.github/workflows/publish-packages-1.0.yml +++ b/.github/workflows/publish-packages-1.0.yml @@ -5,7 +5,7 @@ ################### IMPORTANT ################### ################################################# -name: Pack and publish to MyGet +name: Build, pack, and publish to MyGet on: release: @@ -14,26 +14,32 @@ on: - cron: '0 0 * * *' # once in a day at 00:00 jobs: - build-pack: + build-pack-publish: runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: - fetch-depth: 0 # fetching all + # Note: By default GitHub only fetches 1 commit. MinVer needs to find + # the version tag which is typically NOT on the first commit so we + # retrieve them all. + fetch-depth: 0 ref: ${{ github.ref || 'main' }} - - name: Install dependencies - run: dotnet restore + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + + - name: dotnet restore + run: dotnet restore OpenTelemetry.proj -p:RunningDotNetPack=true - name: dotnet build - run: dotnet build --configuration Release --no-restore -p:Deterministic=true -p:BuildNumber=${{ github.run_number }} + run: dotnet build OpenTelemetry.proj --configuration Release --no-restore -p:Deterministic=true -p:BuildNumber=${{ github.run_number }} -p:RunningDotNetPack=true - name: dotnet pack - run: dotnet pack OpenTelemetry.proj --configuration Release --no-build + run: dotnet pack OpenTelemetry.proj --configuration Release --no-restore --no-build - name: Publish Artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ github.ref_name }}-packages path: '**/bin/**/*.*nupkg' diff --git a/.github/workflows/sanitycheck.yml b/.github/workflows/sanitycheck.yml index d452206e4bf..1c01e5a8235 100644 --- a/.github/workflows/sanitycheck.yml +++ b/.github/workflows/sanitycheck.yml @@ -1,18 +1,17 @@ -name: sanitycheck +# Called by ci.yml to perform general linting +# See: https://docs.github.com/en/actions/using-workflows/reusing-workflows#creating-a-reusable-workflow +name: Lint - Spelling & Encoding on: - push: - branches: [ 'main*' ] - pull_request: - branches: [ 'main*' ] + workflow_call: jobs: - misspell: + run-misspell: runs-on: ubuntu-latest steps: - name: check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: install misspell run: | @@ -22,12 +21,12 @@ jobs: - name: run misspell run: ./bin/misspell -error . - encoding: + run-sanitycheck: runs-on: ubuntu-latest steps: - name: check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: detect non-ASCII encoding and trailing space run: python3 ./build/sanitycheck.py diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 95287991b18..f5657aa9f54 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -10,7 +10,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v8 + - uses: actions/stale@v9 with: stale-pr-message: 'This PR was marked stale due to lack of activity and will be closed in 7 days. Commenting or Pushing will instruct the bot to automatically remove the label. This bot runs once per day.' close-pr-message: 'Closed as inactive. Feel free to reopen if this PR is still being worked on.' diff --git a/.github/workflows/verifyaotcompat.yml b/.github/workflows/verifyaotcompat.yml new file mode 100644 index 00000000000..6a599bb5369 --- /dev/null +++ b/.github/workflows/verifyaotcompat.yml @@ -0,0 +1,27 @@ +# Called by ci.yml to perform AOT validation +# See: https://docs.github.com/en/actions/using-workflows/reusing-workflows#creating-a-reusable-workflow +name: Publish & Verify AOT Compatibility + +on: + workflow_call: + +jobs: + run-verify-aot-compat: + + strategy: + fail-fast: false # ensures the entire test matrix is run, even if one permutation fails + matrix: + os: [ ubuntu-latest ] + version: [ net8.0 ] + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + + - name: publish AOT testApp, assert static analysis warning count, and run the app + shell: pwsh + run: .\build\test-aot-compatibility.ps1 ${{ matrix.version }} + 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/CONTRIBUTING.md b/CONTRIBUTING.md index c3ed947ad40..7a9590b068a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,7 +44,7 @@ You can contribute to this project from a Windows, macOS or Linux machine. On all platforms, the minimum requirements are: * Git client and command line tools. -* .NET 7.0+ +* .NET 8.0 ### Linux or MacOS @@ -232,7 +232,7 @@ analysis](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/overview New projects MUST enable static analysis by specifying `latest-all` in the project file (`.csproj`). -> **Note** +> [!NOTE] > There are other project-level features enabled automatically via [Common.props](https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/build/Common.props) new projects must NOT manually override these settings. @@ -248,8 +248,34 @@ context in every project by updating code as it is worked on, this requirement is to make sure the surface area of code needing updates is shrinking and not expanding. -> **Note** +> [!NOTE] > The first time a project is updated to use nullable context in public APIs some housekeeping needs to be done in public API definitions (`.publicApi` folder). This can be done automatically via a code fix offered by the public API analyzer. + +## License requirements + +OpenTelemetry .NET is licensed under the [Apache License, Version +2.0](./LICENSE.TXT). + +### Copying files from other projects + +OpenTelemetry .NET uses some files from other projects, typically where a binary +distribution does not exist or would be inconvenient. + +The following rules must be followed for PRs that include files from another +project: + +* The license of the file is + [permissive](https://en.wikipedia.org/wiki/Permissive_free_software_licence). + +* The license of the file is left intact. + +* The contribution is correctly attributed in the [3rd party + notices](./THIRD-PARTY-NOTICES.TXT) file in the repository, as needed. + +See +[EnvironmentVariablesExtensions.cs](./src/Shared/EnvironmentVariables/EnvironmentVariablesExtensions.cs) +for an example of a file copied from another project and attributed in the [3rd +party notices](./THIRD-PARTY-NOTICES.TXT) file. diff --git a/Directory.Packages.props b/Directory.Packages.props index 626320dbe96..4c50a651adc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,29 +1,47 @@ true + 1.7.0 - + - + - - + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - + @@ -49,29 +64,51 @@ - - + + - + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LICENSE b/LICENSE.TXT similarity index 100% rename from LICENSE rename to LICENSE.TXT diff --git a/NuGet.config b/NuGet.config index c821ba89f4e..ca17f1b8088 100644 --- a/NuGet.config +++ b/NuGet.config @@ -1,12 +1,9 @@ - + - - + @@ -16,8 +13,8 @@ - - + + diff --git a/OpenTelemetry.proj b/OpenTelemetry.proj index 8ceedc47bf5..410e43df0a1 100644 --- a/OpenTelemetry.proj +++ b/OpenTelemetry.proj @@ -1,19 +1,32 @@ - + - - - + + + + + + - + + + + + + + + + diff --git a/OpenTelemetry.sln b/OpenTelemetry.sln index 7c75ed0c9b2..b3a52180514 100644 --- a/OpenTelemetry.sln +++ b/OpenTelemetry.sln @@ -11,16 +11,15 @@ 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 + LICENSE.TXT = LICENSE.TXT NuGet.config = NuGet.config OpenTelemetry.proj = OpenTelemetry.proj README.md = README.md + THIRD-PARTY-NOTICES.TXT = THIRD-PARTY-NOTICES.TXT VERSIONING.md = VERSIONING.md EndProjectSection EndProject @@ -30,19 +29,22 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{7CB2F02E build\Common.prod.props = build\Common.prod.props build\Common.props = build\Common.props build\debug.snk = build\debug.snk + Directory.Packages.props = Directory.Packages.props 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 build\OpenTelemetry.prod.loose.ruleset = build\OpenTelemetry.prod.loose.ruleset build\OpenTelemetry.prod.ruleset = build\OpenTelemetry.prod.ruleset build\OpenTelemetry.test.ruleset = build\OpenTelemetry.test.ruleset - build\PreBuild.ps1 = build\PreBuild.ps1 build\process-codecoverage.ps1 = build\process-codecoverage.ps1 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 @@ -58,10 +60,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testdata", "testdata", "{77 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{E359BB2B-9AEC-497D-B321-7DF2450C3B8E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Exporter.Jaeger", "src\OpenTelemetry.Exporter.Jaeger\OpenTelemetry.Exporter.Jaeger.csproj", "{8D47E3CF-9AE3-42FE-9084-FEB72D9AD769}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Exporter.Jaeger.Tests", "test\OpenTelemetry.Exporter.Jaeger.Tests\OpenTelemetry.Exporter.Jaeger.Tests.csproj", "{21E69213-72D5-453F-BD00-75EF36AC4965}" -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}" @@ -80,6 +78,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Exporter.Open EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{F1D0972B-38CF-49C2-9F4B-4C5DE02FB71D}" ProjectSection(SolutionItems) = preProject + .github\codecov.yml = .github\codecov.yml .github\CODEOWNERS = .github\CODEOWNERS .github\PULL_REQUEST_TEMPLATE.md = .github\PULL_REQUEST_TEMPLATE.md EndProjectSection @@ -93,20 +92,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ISSUE_TEMPLATE", "ISSUE_TEM EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{E69578EB-B456-4062-A645-877CD964528B}" ProjectSection(SolutionItems) = preProject - .github\workflows\apicompatibility.yml = .github\workflows\apicompatibility.yml - .github\workflows\ci-md.yml = .github\workflows\ci-md.yml .github\workflows\ci.yml = .github\workflows\ci.yml - .github\workflows\code-coverage.yml = .github\workflows\code-coverage.yml .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml + .github\workflows\Component.BuildTest.yml = .github\workflows\Component.BuildTest.yml + .github\workflows\concurrency-tests.yml = .github\workflows\concurrency-tests.yml .github\workflows\docfx.yml = .github\workflows\docfx.yml - .github\workflows\dotnet-format-md.yml = .github\workflows\dotnet-format-md.yml .github\workflows\dotnet-format.yml = .github\workflows\dotnet-format.yml - .github\workflows\integration-md.yml = .github\workflows\integration-md.yml - .github\workflows\integration.yml = .github\workflows\integration.yml .github\workflows\markdownlint.yml = .github\workflows\markdownlint.yml + .github\workflows\package-validation.yml = .github\workflows\package-validation.yml .github\workflows\publish-packages-1.0.yml = .github\workflows\publish-packages-1.0.yml .github\workflows\sanitycheck.yml = .github\workflows\sanitycheck.yml .github\workflows\stale.yml = .github\workflows\stale.yml + .github\workflows\verifyaotcompat.yml = .github\workflows\verifyaotcompat.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C1542297-8763-4DF4-957C-489ED771C21D}" @@ -119,6 +116,7 @@ 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}" @@ -144,6 +142,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}" @@ -155,7 +154,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "metrics", "metrics", "{3277 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "logs", "logs", "{3862190B-E2C5-418E-AFDC-DB281FB5C705}" ProjectSection(SolutionItems) = preProject - docs\logs\getting-started\README.md = docs\logs\getting-started\README.md + docs\logs\README.md = docs\logs\README.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MicroserviceExample", "MicroserviceExample", "{4D492D62-5150-45F9-817F-C99562E364E2}" @@ -174,6 +173,7 @@ 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 @@ -184,7 +184,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentati 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", "docs\logs\getting-started\getting-started.csproj", "{B3F03725-23A0-4582-9526-F6A7E38F35CC}" +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}" EndProject @@ -208,8 +208,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "customizing-the-sdk", "docs EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Tests.Stress", "test\OpenTelemetry.Tests.Stress\OpenTelemetry.Tests.Stress.csproj", "{2770158A-D220-414B-ABC6-179371323579}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "source-generation", "docs\logs\source-generation\source-generation.csproj", "{1F6CC903-04C9-4E7C-B388-C215C467BFB9}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "getting-started-prometheus-grafana", "docs\metrics\getting-started-prometheus-grafana\getting-started-prometheus-grafana.csproj", "{41B784AA-3301-4126-AF9F-1D59BD04B0BF}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.SemanticConventions", "src\OpenTelemetry.SemanticConventions\OpenTelemetry.SemanticConventions.csproj", "{D4519DF6-CC72-4AC4-A851-E21383098D11}" @@ -236,9 +234,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Tests.Stress. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestApp.AspNetCore", "test\TestApp.AspNetCore\TestApp.AspNetCore.csproj", "{5FDAF679-DE5A-4C73-A49B-8ABCF2399229}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "redaction", "docs\logs\redaction\redaction.csproj", "{A2DF46DE-50D7-4887-8C9D-4BD79CA19FAA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "complex-objects", "docs\logs\complex-objects\complex-objects.csproj", "{9AAB00EC-ED6F-4462-82DE-7D864A9DB6C5}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Exporter.OpenTelemetryProtocol.Grpc", "src\OpenTelemetry.Exporter.OpenTelemetryProtocol.Grpc\OpenTelemetry.Exporter.OpenTelemetryProtocol.Grpc.csproj", "{7263001A-49F8-4C3C-AAA8-998F12DAAF64}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "redaction", "docs\logs\redaction\redaction.csproj", "{A2DF46DE-50D7-4887-8C9D-4BD79CA19FAA}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Exporter.Console.Tests", "test\OpenTelemetry.Exporter.Console.Tests\OpenTelemetry.Exporter.Console.Tests.csproj", "{011E70E1-152A-47BB-AF83-12DD12B125ED}" EndProject @@ -256,28 +254,94 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "tail-based-sampling-example EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "stratified-sampling-example", "docs\trace\stratified-sampling-example\stratified-sampling-example.csproj", "{9C99621C-343E-479C-A943-332DB6129B71}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Api.Tests", "test\OpenTelemetry.Api.Tests\OpenTelemetry.Api.Tests.csproj", "{FD8433F4-EDCF-475C-9B4A-625D3DE11671}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.AotCompatibility.TestApp", "test\OpenTelemetry.AotCompatibility.TestApp\OpenTelemetry.AotCompatibility.TestApp.csproj", "{13A59BD9-9475-4991-B74D-7C20F1C63409}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.AotCompatibility.Tests", "test\OpenTelemetry.AotCompatibility.Tests\OpenTelemetry.AotCompatibility.Tests.csproj", "{D438EF9C-7959-47A0-B2A2-DEBFCDC2A8DC}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "links-sampler", "docs\trace\links-based-sampler\links-sampler.csproj", "{62AF4BD3-DCAE-4D44-AA5B-991C1071166B}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A49299FB-C5CD-4E0E-B7E1-B7867BBD67CC}" ProjectSection(SolutionItems) = preProject + src\Shared\ActivityHelperExtensions.cs = src\Shared\ActivityHelperExtensions.cs src\Shared\ActivityInstrumentationHelper.cs = src\Shared\ActivityInstrumentationHelper.cs + src\Shared\DiagnosticDefinitions.cs = src\Shared\DiagnosticDefinitions.cs + src\Shared\ExceptionExtensions.cs = src\Shared\ExceptionExtensions.cs + src\Shared\Guard.cs = src\Shared\Guard.cs + src\Shared\MathHelper.cs = src\Shared\MathHelper.cs + src\Shared\PeerServiceResolver.cs = src\Shared\PeerServiceResolver.cs + src\Shared\PeriodicExportingMetricReaderHelper.cs = src\Shared\PeriodicExportingMetricReaderHelper.cs + src\Shared\PooledList.cs = src\Shared\PooledList.cs + src\Shared\RequestMethodHelper.cs = src\Shared\RequestMethodHelper.cs + src\Shared\ResourceSemanticConventions.cs = src\Shared\ResourceSemanticConventions.cs + src\Shared\SemanticConventions.cs = src\Shared\SemanticConventions.cs + src\Shared\SpanAttributeConstants.cs = src\Shared\SpanAttributeConstants.cs + src\Shared\SpanHelper.cs = src\Shared\SpanHelper.cs + src\Shared\StatusHelper.cs = src\Shared\StatusHelper.cs + src\Shared\TagAndValueTransformer.cs = src\Shared\TagAndValueTransformer.cs + src\Shared\TagTransformer.cs = src\Shared\TagTransformer.cs + src\Shared\TagTransformerJsonHelper.cs = src\Shared\TagTransformerJsonHelper.cs EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DiagnosticSourceInstrumentation", "DiagnosticSourceInstrumentation", "{28F3EC79-660C-4659-8B73-F90DC1173316}" ProjectSection(SolutionItems) = preProject src\Shared\DiagnosticSourceInstrumentation\DiagnosticSourceListener.cs = src\Shared\DiagnosticSourceInstrumentation\DiagnosticSourceListener.cs src\Shared\DiagnosticSourceInstrumentation\DiagnosticSourceSubscriber.cs = src\Shared\DiagnosticSourceInstrumentation\DiagnosticSourceSubscriber.cs - src\Shared\DiagnosticSourceInstrumentation\InstrumentationEventSource.cs = src\Shared\DiagnosticSourceInstrumentation\InstrumentationEventSource.cs src\Shared\DiagnosticSourceInstrumentation\ListenerHandler.cs = src\Shared\DiagnosticSourceInstrumentation\ListenerHandler.cs src\Shared\DiagnosticSourceInstrumentation\PropertyFetcher.cs = src\Shared\DiagnosticSourceInstrumentation\PropertyFetcher.cs EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EnvironmentVariables", "EnvironmentVariables", "{6D4B4FB2-0A8A-4044-948B-C063FD340439}" + ProjectSection(SolutionItems) = preProject + src\Shared\EnvironmentVariables\EnvironmentVariablesConfigurationProvider.cs = src\Shared\EnvironmentVariables\EnvironmentVariablesConfigurationProvider.cs + src\Shared\EnvironmentVariables\EnvironmentVariablesConfigurationSource.cs = src\Shared\EnvironmentVariables\EnvironmentVariablesConfigurationSource.cs + src\Shared\EnvironmentVariables\EnvironmentVariablesExtensions.cs = src\Shared\EnvironmentVariables\EnvironmentVariablesExtensions.cs + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Options", "Options", "{494902DD-C63F-48E0-BED3-B58EFB4051C8}" + ProjectSection(SolutionItems) = preProject + src\Shared\Options\ConfigurationExtensions.cs = src\Shared\Options\ConfigurationExtensions.cs + src\Shared\Options\DelegatingOptionsFactory.cs = src\Shared\Options\DelegatingOptionsFactory.cs + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shims", "Shims", "{A0CB9A10-F22D-4E66-A449-74B3D0361A9C}" + ProjectSection(SolutionItems) = preProject + src\Shared\Shims\IsExternalInit.cs = src\Shared\Shims\IsExternalInit.cs + src\Shared\Shims\NullableAttributes.cs = src\Shared\Shims\NullableAttributes.cs + src\Shared\Shims\UnconditionalSuppressMessageAttribute.cs = src\Shared\Shims\UnconditionalSuppressMessageAttribute.cs + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Api.Tests", "test\OpenTelemetry.Api.Tests\OpenTelemetry.Api.Tests.csproj", "{777C04B8-1BD5-43D7-B3CD-D2189DFABCF3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Metrics", "Metrics", "{1C459B5B-C702-46FF-BF1A-EE795E420FFA}" + ProjectSection(SolutionItems) = preProject + src\Shared\Metrics\Base2ExponentialBucketHistogramHelper.cs = src\Shared\Metrics\Base2ExponentialBucketHistogramHelper.cs + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "getting-started-aspnetcore", "docs\logs\getting-started-aspnetcore\getting-started-aspnetcore.csproj", "{99B4D965-8782-4694-8DFA-B7A3630CEF60}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "links-creation", "docs\trace\links-creation-with-new-activities\links-creation.csproj", "{B4856711-6D4C-4246-A686-49458D4C1301}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "diagnostics", "diagnostics", "{52AF6D7D-9E66-4234-9A2C-5D16C6F22B40}" + ProjectSection(SolutionItems) = preProject + docs\diagnostics\README.md = docs\diagnostics\README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "experimental-apis", "experimental-apis", "{17A22B0E-6EC3-4A39-B955-0A486AD06699}" + ProjectSection(SolutionItems) = preProject + docs\diagnostics\experimental-apis\OTEL1000.md = docs\diagnostics\experimental-apis\OTEL1000.md + docs\diagnostics\experimental-apis\OTEL1001.md = docs\diagnostics\experimental-apis\OTEL1001.md + docs\diagnostics\experimental-apis\OTEL1002.md = docs\diagnostics\experimental-apis\OTEL1002.md + docs\diagnostics\experimental-apis\OTEL1003.md = docs\diagnostics\experimental-apis\OTEL1003.md + docs\diagnostics\experimental-apis\README.md = docs\diagnostics\experimental-apis\README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "resources", "resources", "{A115CE4C-71A8-4B95-96A5-C1DF46FD94C2}" + ProjectSection(SolutionItems) = preProject + docs\resources\README.md = docs\resources\README.md + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "extending-the-sdk", "docs\resources\extending-the-sdk\extending-the-sdk.csproj", "{7BE494FC-4B0D-4340-A62A-9C9F3E7389FE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dedicated-pipeline", "docs\logs\dedicated-pipeline\dedicated-pipeline.csproj", "{19545B37-8518-4BDD-AD49-00C031FB3C2A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -308,14 +372,6 @@ Global {2A47F6A8-63E5-4237-8046-94CAF321E797}.Debug|Any CPU.Build.0 = Debug|Any CPU {2A47F6A8-63E5-4237-8046-94CAF321E797}.Release|Any CPU.ActiveCfg = Release|Any CPU {2A47F6A8-63E5-4237-8046-94CAF321E797}.Release|Any CPU.Build.0 = Release|Any CPU - {8D47E3CF-9AE3-42FE-9084-FEB72D9AD769}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8D47E3CF-9AE3-42FE-9084-FEB72D9AD769}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8D47E3CF-9AE3-42FE-9084-FEB72D9AD769}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8D47E3CF-9AE3-42FE-9084-FEB72D9AD769}.Release|Any CPU.Build.0 = Release|Any CPU - {21E69213-72D5-453F-BD00-75EF36AC4965}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {21E69213-72D5-453F-BD00-75EF36AC4965}.Debug|Any CPU.Build.0 = Debug|Any CPU - {21E69213-72D5-453F-BD00-75EF36AC4965}.Release|Any CPU.ActiveCfg = Release|Any CPU - {21E69213-72D5-453F-BD00-75EF36AC4965}.Release|Any CPU.Build.0 = Release|Any CPU {AAC408FE-40EF-4479-97D9-697F2C1A0B28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AAC408FE-40EF-4479-97D9-697F2C1A0B28}.Debug|Any CPU.Build.0 = Debug|Any CPU {AAC408FE-40EF-4479-97D9-697F2C1A0B28}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -336,10 +392,6 @@ Global {A38AC295-2745-4B85-8B6B-DCA864CEDD5B}.Debug|Any CPU.Build.0 = Debug|Any CPU {A38AC295-2745-4B85-8B6B-DCA864CEDD5B}.Release|Any CPU.ActiveCfg = Release|Any CPU {A38AC295-2745-4B85-8B6B-DCA864CEDD5B}.Release|Any CPU.Build.0 = Release|Any CPU - {56A34828-621A-478B-A0B8-C065FE938383}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {56A34828-621A-478B-A0B8-C065FE938383}.Debug|Any CPU.Build.0 = Debug|Any CPU - {56A34828-621A-478B-A0B8-C065FE938383}.Release|Any CPU.ActiveCfg = Release|Any CPU - {56A34828-621A-478B-A0B8-C065FE938383}.Release|Any CPU.Build.0 = Release|Any CPU {1AFFF251-3B0C-47CA-BE94-937083732C0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1AFFF251-3B0C-47CA-BE94-937083732C0A}.Debug|Any CPU.Build.0 = Debug|Any CPU {1AFFF251-3B0C-47CA-BE94-937083732C0A}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -356,10 +408,6 @@ 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 - {98F9556B-116F-49B5-9211-BB1D418446FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {98F9556B-116F-49B5-9211-BB1D418446FF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {98F9556B-116F-49B5-9211-BB1D418446FF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {98F9556B-116F-49B5-9211-BB1D418446FF}.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 @@ -464,10 +512,6 @@ Global {2770158A-D220-414B-ABC6-179371323579}.Debug|Any CPU.Build.0 = Debug|Any CPU {2770158A-D220-414B-ABC6-179371323579}.Release|Any CPU.ActiveCfg = Release|Any CPU {2770158A-D220-414B-ABC6-179371323579}.Release|Any CPU.Build.0 = Release|Any CPU - {1F6CC903-04C9-4E7C-B388-C215C467BFB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1F6CC903-04C9-4E7C-B388-C215C467BFB9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1F6CC903-04C9-4E7C-B388-C215C467BFB9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1F6CC903-04C9-4E7C-B388-C215C467BFB9}.Release|Any CPU.Build.0 = Release|Any CPU {41B784AA-3301-4126-AF9F-1D59BD04B0BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {41B784AA-3301-4126-AF9F-1D59BD04B0BF}.Debug|Any CPU.Build.0 = Debug|Any CPU {41B784AA-3301-4126-AF9F-1D59BD04B0BF}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -520,14 +564,14 @@ Global {5FDAF679-DE5A-4C73-A49B-8ABCF2399229}.Debug|Any CPU.Build.0 = Debug|Any CPU {5FDAF679-DE5A-4C73-A49B-8ABCF2399229}.Release|Any CPU.ActiveCfg = Release|Any CPU {5FDAF679-DE5A-4C73-A49B-8ABCF2399229}.Release|Any CPU.Build.0 = Release|Any CPU + {9AAB00EC-ED6F-4462-82DE-7D864A9DB6C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9AAB00EC-ED6F-4462-82DE-7D864A9DB6C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9AAB00EC-ED6F-4462-82DE-7D864A9DB6C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9AAB00EC-ED6F-4462-82DE-7D864A9DB6C5}.Release|Any CPU.Build.0 = Release|Any CPU {A2DF46DE-50D7-4887-8C9D-4BD79CA19FAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A2DF46DE-50D7-4887-8C9D-4BD79CA19FAA}.Debug|Any CPU.Build.0 = Debug|Any CPU {A2DF46DE-50D7-4887-8C9D-4BD79CA19FAA}.Release|Any CPU.ActiveCfg = Release|Any CPU {A2DF46DE-50D7-4887-8C9D-4BD79CA19FAA}.Release|Any CPU.Build.0 = Release|Any CPU - {7263001A-49F8-4C3C-AAA8-998F12DAAF64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7263001A-49F8-4C3C-AAA8-998F12DAAF64}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7263001A-49F8-4C3C-AAA8-998F12DAAF64}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7263001A-49F8-4C3C-AAA8-998F12DAAF64}.Release|Any CPU.Build.0 = Release|Any CPU {011E70E1-152A-47BB-AF83-12DD12B125ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {011E70E1-152A-47BB-AF83-12DD12B125ED}.Debug|Any CPU.Build.0 = Debug|Any CPU {011E70E1-152A-47BB-AF83-12DD12B125ED}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -560,22 +604,34 @@ Global {9C99621C-343E-479C-A943-332DB6129B71}.Debug|Any CPU.Build.0 = Debug|Any CPU {9C99621C-343E-479C-A943-332DB6129B71}.Release|Any CPU.ActiveCfg = Release|Any CPU {9C99621C-343E-479C-A943-332DB6129B71}.Release|Any CPU.Build.0 = Release|Any CPU - {FD8433F4-EDCF-475C-9B4A-625D3DE11671}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FD8433F4-EDCF-475C-9B4A-625D3DE11671}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FD8433F4-EDCF-475C-9B4A-625D3DE11671}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FD8433F4-EDCF-475C-9B4A-625D3DE11671}.Release|Any CPU.Build.0 = Release|Any CPU {13A59BD9-9475-4991-B74D-7C20F1C63409}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {13A59BD9-9475-4991-B74D-7C20F1C63409}.Debug|Any CPU.Build.0 = Debug|Any CPU {13A59BD9-9475-4991-B74D-7C20F1C63409}.Release|Any CPU.ActiveCfg = Release|Any CPU {13A59BD9-9475-4991-B74D-7C20F1C63409}.Release|Any CPU.Build.0 = Release|Any CPU - {D438EF9C-7959-47A0-B2A2-DEBFCDC2A8DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D438EF9C-7959-47A0-B2A2-DEBFCDC2A8DC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D438EF9C-7959-47A0-B2A2-DEBFCDC2A8DC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D438EF9C-7959-47A0-B2A2-DEBFCDC2A8DC}.Release|Any CPU.Build.0 = Release|Any CPU {62AF4BD3-DCAE-4D44-AA5B-991C1071166B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {62AF4BD3-DCAE-4D44-AA5B-991C1071166B}.Debug|Any CPU.Build.0 = Debug|Any CPU {62AF4BD3-DCAE-4D44-AA5B-991C1071166B}.Release|Any CPU.ActiveCfg = Release|Any CPU {62AF4BD3-DCAE-4D44-AA5B-991C1071166B}.Release|Any CPU.Build.0 = Release|Any CPU + {777C04B8-1BD5-43D7-B3CD-D2189DFABCF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {777C04B8-1BD5-43D7-B3CD-D2189DFABCF3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {777C04B8-1BD5-43D7-B3CD-D2189DFABCF3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {777C04B8-1BD5-43D7-B3CD-D2189DFABCF3}.Release|Any CPU.Build.0 = Release|Any CPU + {99B4D965-8782-4694-8DFA-B7A3630CEF60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {99B4D965-8782-4694-8DFA-B7A3630CEF60}.Debug|Any CPU.Build.0 = Debug|Any CPU + {99B4D965-8782-4694-8DFA-B7A3630CEF60}.Release|Any CPU.ActiveCfg = Release|Any CPU + {99B4D965-8782-4694-8DFA-B7A3630CEF60}.Release|Any CPU.Build.0 = Release|Any CPU + {B4856711-6D4C-4246-A686-49458D4C1301}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B4856711-6D4C-4246-A686-49458D4C1301}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B4856711-6D4C-4246-A686-49458D4C1301}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B4856711-6D4C-4246-A686-49458D4C1301}.Release|Any CPU.Build.0 = Release|Any CPU + {7BE494FC-4B0D-4340-A62A-9C9F3E7389FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7BE494FC-4B0D-4340-A62A-9C9F3E7389FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7BE494FC-4B0D-4340-A62A-9C9F3E7389FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7BE494FC-4B0D-4340-A62A-9C9F3E7389FE}.Release|Any CPU.Build.0 = Release|Any CPU + {19545B37-8518-4BDD-AD49-00C031FB3C2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {19545B37-8518-4BDD-AD49-00C031FB3C2A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {19545B37-8518-4BDD-AD49-00C031FB3C2A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {19545B37-8518-4BDD-AD49-00C031FB3C2A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -607,11 +663,11 @@ Global {0C3E7D40-E0B3-4B77-8139-0E85C3600688} = {3277B1C0-BDFE-4460-9B0D-D9A661FB48DB} {1F9D7748-D099-4E25-97F5-9C969D6FF969} = {3277B1C0-BDFE-4460-9B0D-D9A661FB48DB} {81234AFA-B4E7-4D0D-AB97-FD559C78EDA2} = {3277B1C0-BDFE-4460-9B0D-D9A661FB48DB} - {1F6CC903-04C9-4E7C-B388-C215C467BFB9} = {3862190B-E2C5-418E-AFDC-DB281FB5C705} {41B784AA-3301-4126-AF9F-1D59BD04B0BF} = {3277B1C0-BDFE-4460-9B0D-D9A661FB48DB} {6C7A1595-36D6-4229-BBB5-5A6B5791791D} = {3862190B-E2C5-418E-AFDC-DB281FB5C705} {9A07D215-90AC-4BAF-BCDB-73D74FD3A5C5} = {3862190B-E2C5-418E-AFDC-DB281FB5C705} {5FDAF679-DE5A-4C73-A49B-8ABCF2399229} = {77C7929A-2EED-4AA6-8705-B5C443C8AA0F} + {9AAB00EC-ED6F-4462-82DE-7D864A9DB6C5} = {3862190B-E2C5-418E-AFDC-DB281FB5C705} {A2DF46DE-50D7-4887-8C9D-4BD79CA19FAA} = {3862190B-E2C5-418E-AFDC-DB281FB5C705} {DEDE8442-03CA-48CF-99B9-EA224D89D148} = {5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818} {EF4F6280-14D1-49D4-8095-1AC36E169AA8} = {5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818} @@ -620,6 +676,17 @@ Global {9C99621C-343E-479C-A943-332DB6129B71} = {5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818} {62AF4BD3-DCAE-4D44-AA5B-991C1071166B} = {5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818} {28F3EC79-660C-4659-8B73-F90DC1173316} = {A49299FB-C5CD-4E0E-B7E1-B7867BBD67CC} + {6D4B4FB2-0A8A-4044-948B-C063FD340439} = {A49299FB-C5CD-4E0E-B7E1-B7867BBD67CC} + {494902DD-C63F-48E0-BED3-B58EFB4051C8} = {A49299FB-C5CD-4E0E-B7E1-B7867BBD67CC} + {A0CB9A10-F22D-4E66-A449-74B3D0361A9C} = {A49299FB-C5CD-4E0E-B7E1-B7867BBD67CC} + {1C459B5B-C702-46FF-BF1A-EE795E420FFA} = {A49299FB-C5CD-4E0E-B7E1-B7867BBD67CC} + {99B4D965-8782-4694-8DFA-B7A3630CEF60} = {3862190B-E2C5-418E-AFDC-DB281FB5C705} + {B4856711-6D4C-4246-A686-49458D4C1301} = {5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818} + {52AF6D7D-9E66-4234-9A2C-5D16C6F22B40} = {7C87CAF9-79D7-4C26-9FFB-F3F1FB6911F1} + {17A22B0E-6EC3-4A39-B955-0A486AD06699} = {52AF6D7D-9E66-4234-9A2C-5D16C6F22B40} + {A115CE4C-71A8-4B95-96A5-C1DF46FD94C2} = {7C87CAF9-79D7-4C26-9FFB-F3F1FB6911F1} + {7BE494FC-4B0D-4340-A62A-9C9F3E7389FE} = {A115CE4C-71A8-4B95-96A5-C1DF46FD94C2} + {19545B37-8518-4BDD-AD49-00C031FB3C2A} = {3862190B-E2C5-418E-AFDC-DB281FB5C705} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {55639B5C-0770-4A22-AB56-859604650521} diff --git a/README.md b/README.md index 308ba716c9a..d4cf02381b1 100644 --- a/README.md +++ b/README.md @@ -19,16 +19,7 @@ files. ## Project Status -| Signal | Status | -| ------- | ---------- | -| Logs | Stable* | -| Metrics | Stable | -| Traces | Stable | - -*While the `OpenTelemetryLoggerProvider` (i.e integration with `ILogger`) is - stable, the [OTLP Exporter for - Logs](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src/OpenTelemetry.Exporter.OpenTelemetryProtocol#enable-log-exporter) - is still non-stable. +**Stable** across all 3 signals i.e. `Logs`, `Metrics`, and `Traces`. See [Spec Compliance Matrix](https://github.com/open-telemetry/opentelemetry-specification/blob/main/spec-compliance-matrix.md) @@ -39,10 +30,14 @@ repo. If you are new here, please read the getting started docs: -* [logs](./docs/logs/getting-started/README.md) -* metrics: [ASP.NET Core](./docs/metrics/getting-started-aspnetcore/README.md) | +* [Logs](./docs/logs/README.md): [ASP.NET + Core](./docs/logs/getting-started-aspnetcore/README.md) | + [Console](./docs/logs/getting-started-console/README.md) +* [Metrics](./docs/metrics/README.md): [ASP.NET + Core](./docs/metrics/getting-started-aspnetcore/README.md) | [Console](./docs/metrics/getting-started-console/README.md) -* traces: [ASP.NET Core](./docs/trace/getting-started-aspnetcore/README.md) | +* [Traces](./docs/trace/README.md): [ASP.NET + Core](./docs/trace/getting-started-aspnetcore/README.md) | [Console](./docs/trace/getting-started-console/README.md) This repository includes multiple installable components, available on @@ -60,16 +55,18 @@ Here are the [instrumentation libraries](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#instrumentation-library): * [ASP.NET Core](./src/OpenTelemetry.Instrumentation.AspNetCore/README.md) -* [Grpc.Net.Client](./src/OpenTelemetry.Instrumentation.GrpcNetClient/README.md) -* [HTTP clients](./src/OpenTelemetry.Instrumentation.Http/README.md) -* [SQL client](./src/OpenTelemetry.Instrumentation.SqlClient/README.md) +* gRPC client: + [Grpc.Net.Client](./src/OpenTelemetry.Instrumentation.GrpcNetClient/README.md) +* HTTP clients: [System.Net.Http.HttpClient and + System.Net.HttpWebRequest](./src/OpenTelemetry.Instrumentation.Http/README.md) +* SQL clients: [Microsoft.Data.SqlClient and + System.Data.SqlClient](./src/OpenTelemetry.Instrumentation.SqlClient/README.md) Here are the [exporter libraries](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#exporter-library): * [Console](./src/OpenTelemetry.Exporter.Console/README.md) * [In-memory](./src/OpenTelemetry.Exporter.InMemory/README.md) -* [Jaeger](./src/OpenTelemetry.Exporter.Jaeger/README.md) * [OTLP](./src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md) (OpenTelemetry Protocol) * [Prometheus AspNetCore](./src/OpenTelemetry.Exporter.Prometheus.AspNetCore/README.md) @@ -108,7 +105,7 @@ extension scenarios: See [CONTRIBUTING.md](CONTRIBUTING.md) -We meet weekly on Tuesdays, and the time of the meeting alternates between 11AM +We meet weekly on Tuesdays, and the time of the meeting alternates between 9AM PT and 4PM PT. The meeting is subject to change depending on contributors' availability. Check the [OpenTelemetry community calendar](https://calendar.google.com/calendar/embed?src=google.com_b79e3e90j7bbsa2n2p5an5lf60%40group.calendar.google.com) @@ -119,6 +116,11 @@ doc](https://docs.google.com/document/d/1yjjD6aBcLxlRazYrawukDgrhZMObwHARJbB9glW If you have trouble accessing the doc, please get in touch on [Slack](https://cloud-native.slack.com/archives/C01N3BC2W7Q). +The meeting is open for all to join. We invite everyone to join our meeting, +regardless of your experience level. Whether you're a seasoned OpenTelemetry +developer, just starting your journey, or simply curious about the work we do, +you're more than welcome to participate! + [Maintainers](https://github.com/open-telemetry/community/blob/main/community-membership.md#maintainer) ([@open-telemetry/dotnet-maintainers](https://github.com/orgs/open-telemetry/teams/dotnet-maintainers)): @@ -131,9 +133,13 @@ If you have trouble accessing the doc, please get in touch on * [Cijo Thomas](https://github.com/cijothomas), Microsoft * [Reiley Yang](https://github.com/reyang), Microsoft -* [Robert Pająk](https://github.com/pellared), Splunk * [Vishwesh Bankwar](https://github.com/vishweshbankwar), Microsoft +[Triagers](https://github.com/open-telemetry/community/blob/main/community-membership.md#triager) +([@open-telemetry/dotnet-triagers](https://github.com/orgs/open-telemetry/teams/dotnet-triagers)): + +* [Martin Thwaites](https://github.com/martinjt), Honeycomb + [Emeritus Maintainer/Approver/Triager](https://github.com/open-telemetry/community/blob/main/community-membership.md#emeritus-maintainerapprovertriager): @@ -142,6 +148,7 @@ Maintainer/Approver/Triager](https://github.com/open-telemetry/community/blob/ma * [Liudmila Molkova](https://github.com/lmolkova) * [Mike Goldsmith](https://github.com/MikeGoldsmith) * [Paulo Janotti](https://github.com/pjanotti) +* [Robert Pająk](https://github.com/pellared) * [Sergey Kanzhelev](https://github.com/SergeyKanzhelev) * [Victor Lu](https://github.com/victlu) @@ -151,20 +158,21 @@ Maintainer/Approver/Triager](https://github.com/open-telemetry/community/blob/ma ## Release Schedule -Only the [core components](./VERSIONING.md#core-components) of the repo have -released a stable version. Components which are marked -[pre-release](https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/VERSIONING.md#pre-releases), -are still work in progress and can undergo many breaking changes before stable -release. +See the [project +milestones](https://github.com/open-telemetry/opentelemetry-dotnet/milestones) +for details on upcoming releases. The dates and features described in issues and +milestones are estimates, and subject to change. See the [release notes](https://github.com/open-telemetry/opentelemetry-dotnet/releases) for existing releases. -See the [project -milestones](https://github.com/open-telemetry/opentelemetry-dotnet/milestones) -for details on upcoming releases. The dates and features described in issues and -milestones are estimates, and subject to change. +> [!CAUTION] +> Certain components, marked as +[pre-release](https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/VERSIONING.md#pre-releases), +are still work in progress and can undergo breaking changes before stable +release. Check the individual `README.md` file for each component to understand its +current state. Daily builds from this repo are published to MyGet, and can be installed from [this source](https://www.myget.org/F/opentelemetry/api/v3/index.json). diff --git a/THIRD-PARTY-NOTICES.TXT b/THIRD-PARTY-NOTICES.TXT new file mode 100644 index 00000000000..b65f5723308 --- /dev/null +++ b/THIRD-PARTY-NOTICES.TXT @@ -0,0 +1,31 @@ +OpenTelemetry .NET uses third-party libraries or other resources that may be +distributed under licenses different than the OpenTelemetry .NET software. + +The attached notices are provided for information only. + +License notice for .NET +------------------------------- + +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/VERSIONING.md b/VERSIONING.md index 507d877b460..cb70a80cd57 100644 --- a/VERSIONING.md +++ b/VERSIONING.md @@ -28,7 +28,7 @@ versions [1.1.0, 2.0.0). Core components refer to the set of components which are required as per the spec. This includes API, SDK, and exporters which are required by the -specification. These exporters are OTLP, Jaeger, Zipkin, Console and InMemory. +specification. These exporters are OTLP, Zipkin, Console and InMemory. The core components are always versioned and released together. For example, if Zipkin exporter has a bug fix and is released as 1.0.1, then all other core diff --git a/build/Common.nonprod.props b/build/Common.nonprod.props index cdcba2f64b6..7f617c45496 100644 --- a/build/Common.nonprod.props +++ b/build/Common.nonprod.props @@ -7,6 +7,10 @@ $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), 'OpenTelemetry.sln'))\build\OpenTelemetry.test.ruleset + + net8.0 + + true @@ -15,8 +19,6 @@ PreserveNewest - - @@ -28,4 +30,18 @@ + + + + + <_SkipTests>true + false + + + + diff --git a/build/Common.prod.props b/build/Common.prod.props index e6d3e2f9220..95c9f0c63ea 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -1,98 +1,98 @@ - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - All - - - - - true - - - - - - - - - - - - - - - - - <_ReferencePathDirectories Include="@(ReferencePath -> '%(RootDir)%(Directory)')" /> - - - @(_ReferencePathDirectories->Distinct()) - - - $(MSBuildThisFileDirectory)/OpenTelemetry.prod.ruleset - $(NoWarn),1573,1712 - $(Build_ArtifactStagingDirectory) - true - - $(RepoRoot)\build\GlobalAttrExclusions.txt + true + false - - - $(MinVerMajor).$(MinVerMinor).$(MinVerPatch).$(BuildNumber) - - - - + git https://github.com/open-telemetry/opentelemetry-dotnet - Observability;OpenTelemetry;Monitoring;Telemetry;Tracing + Observability;OpenTelemetry;Monitoring;Telemetry;Tracing;Metrics;Logging opentelemetry-icon-color.png https://opentelemetry.io - Apache-2.0 OpenTelemetry Authors + Copyright The OpenTelemetry Authors + $(Build_ArtifactStagingDirectory) + true + snupkg + Apache-2.0 true + $(RepoRoot)\LICENSE.TXT + $(RepoRoot)\THIRD-PARTY-NOTICES.TXT - - - - - + true true - true - snupkg + true - - + + + + - - true - - - - - - - + + + + + + + + $(MinVerMajor).$(MinVerMinor).$(MinVerPatch).$(BuildNumber) + true + false + + + + + + + + + + $(DefineConstants);EXPOSE_EXPERIMENTAL_FEATURES + + + + + + + + + + + + + + + + + + + + diff --git a/build/Common.props b/build/Common.props index c2538e31727..d54badad2c6 100644 --- a/build/Common.props +++ b/build/Common.props @@ -1,17 +1,45 @@ - 10.0 + latest true $([System.IO.Directory]::GetParent($(MSBuildThisFileDirectory)).Parent.FullName) $(MSBuildThisFileDirectory)debug.snk $(DefineConstants);SIGNED true + true enable enable + + $(NoWarn);OTEL1000;OTEL1001;OTEL1002;OTEL1003 + + + net462 + net481;net48;net472;net471;net47;net462 + + + net8.0;net6.0;netstandard2.0;$(NetFrameworkMinimumSupportedVersion) + net8.0;net6.0;netstandard2.1;netstandard2.0;$(NetFrameworkMinimumSupportedVersion) + net8.0;net7.0;net6.0;netstandard2.0 + net8.0;net6.0;netstandard2.1;netstandard2.0 + net8.0;net6.0 + + + net8.0;net7.0;net6.0 + net8.0 + net8.0;net7.0;net6.0 + + $(TargetFrameworksForDocs);$(NetFrameworkSupportedVersions) + + net8.0;net7.0;net6.0 + + $(TargetFrameworksForTests);$(NetFrameworkMinimumSupportedVersion) + + + full true @@ -19,14 +47,7 @@ true - - - - - 1.5.1 + true @@ -35,31 +56,24 @@ - - All - - - + - - - - - - - + + + + + + diff --git a/build/InstrumentationLibraries.proj b/build/InstrumentationLibraries.proj new file mode 100644 index 00000000000..4a2ee62aaa4 --- /dev/null +++ b/build/InstrumentationLibraries.proj @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/build/PreBuild.ps1 b/build/PreBuild.ps1 deleted file mode 100644 index 988e5b9bbc6..00000000000 --- a/build/PreBuild.ps1 +++ /dev/null @@ -1,46 +0,0 @@ -param( - [string]$package, - [string]$version, - [string]$workDir = ".\LastMajorVersionBinaries" -) - -if (-Not (Test-Path $workDir)) -{ - Write-Host "Working directory for compatibility check packages '$workDir' not found, creating..." - New-Item -Path $workDir -ItemType "directory" | Out-Null -} - -if (Test-Path -Path "$workDir\$package.$version.zip") -{ - Write-Host "Previous package $package@$version already downloaded for compatibility check" -} -else -{ - Write-Host "Retrieving package $package@$version for compatibility check" - try - { - $Response = Invoke-WebRequest -Uri https://www.nuget.org/api/v2/package/$package/$version -Outfile "$workDir\$package.$version.zip" - } - catch - { - $StatusCode = $_.Exception.Response.StatusCode.value__ - throw "Error downloading the package $package@$version. Status code of the received response: $StatusCode" - } -} - -if (Test-Path -Path "$workDir\$package\$version\lib") -{ - Write-Host "Previous package $package@$version already extracted to '$workDir\$package\$version\lib'" -} -else -{ - Write-Host "Extracting package $package@$version from '$workDir\$package.$version.zip' to '$workDir\$package\$version' for compatibility check" - try - { - Expand-Archive -LiteralPath "$workDir\$package.$version.zip" -DestinationPath "$workDir\$package\$version" -Force - } - catch - { - throw "Error extracting $package@$version.zip" - } -} diff --git a/build/RELEASING.md b/build/RELEASING.md index cbc5e825bbf..9d942f92dc7 100644 --- a/build/RELEASING.md +++ b/build/RELEASING.md @@ -84,7 +84,16 @@ Only for Maintainers. git push origin 1.0.0-rc9.7 ``` - If releasing both, push both tags above. + If releasing only a particular non-core component which has a dedicated + MinverTagPrefix such as AspNetCore instrumentation, only add and push the + tag with that particular prefix. For example: + + ```sh + git tag -a Instrumentation.AspNetCore-1.6.0 -m "1.6.0 of AspNetCore instrumentation library" + git push origin Instrumentation.AspNetCore-1.6.0 + ``` + + If releasing multiple kinds of components, push both tags for each of them. 7. Go to the [list of tags](https://github.com/open-telemetry/opentelemetry-dotnet/tags) @@ -128,4 +137,5 @@ Only for Maintainers. by sending a Pull Request. 17. If a new stable version of the core packages were released, update - `OTelPreviousStableVer` in Common.props to the just released stable version. + `OTelLatestStableVer` in Directory.Packages.props to the just released + stable version. diff --git a/build/docker-compose.net6.0.yml b/build/docker-compose.net6.0.yml index 0d592597539..099f1007277 100644 --- a/build/docker-compose.net6.0.yml +++ b/build/docker-compose.net6.0.yml @@ -5,5 +5,5 @@ services: build: args: PUBLISH_FRAMEWORK: net6.0 - TEST_SDK_VERSION: 6.0 - BUILD_SDK_VERSION: 7.0 + TEST_SDK_VERSION: "6.0" + BUILD_SDK_VERSION: "8.0" diff --git a/build/docker-compose.net7.0.yml b/build/docker-compose.net7.0.yml index d79fa366bb8..48a2589cda9 100644 --- a/build/docker-compose.net7.0.yml +++ b/build/docker-compose.net7.0.yml @@ -5,5 +5,5 @@ services: build: args: PUBLISH_FRAMEWORK: net7.0 - TEST_SDK_VERSION: 7.0 - BUILD_SDK_VERSION: 7.0 + TEST_SDK_VERSION: "7.0" + BUILD_SDK_VERSION: "8.0" diff --git a/build/docker-compose.net8.0.yml b/build/docker-compose.net8.0.yml new file mode 100644 index 00000000000..a5ac999e43e --- /dev/null +++ b/build/docker-compose.net8.0.yml @@ -0,0 +1,9 @@ +version: '3.7' + +services: + tests: + build: + args: + PUBLISH_FRAMEWORK: net8.0 + TEST_SDK_VERSION: "8.0" + BUILD_SDK_VERSION: "8.0" diff --git a/build/process-codecoverage.ps1 b/build/process-codecoverage.ps1 deleted file mode 100644 index 6d14a4e973a..00000000000 --- a/build/process-codecoverage.ps1 +++ /dev/null @@ -1,16 +0,0 @@ -$rootDirectory = Split-Path $PSScriptRoot -Parent -[xml]$commonProps = Get-Content -Path $rootDirectory\Directory.Packages.props - -$packages = $commonProps.Project.ItemGroup.PackageVersion -$microsoftCodeCoveragePkgVer = [string]($packages | Where-Object {$_.Include -eq "Microsoft.CodeCoverage"}).Version # This is collected in the format: "[17.4.1]" -$microsoftCodeCoveragePkgVer = $microsoftCodeCoveragePkgVer.Trim(); -$microsoftCodeCoveragePkgVer = $microsoftCodeCoveragePkgVer.SubString(1, $microsoftCodeCoveragePkgVer.Length - 2) # Removing square brackets - -$files = Get-ChildItem "TestResults" -Filter "*.coverage" -Recurse -Write-Host $env:USERPROFILE -foreach ($file in $files) -{ - $command = $env:USERPROFILE+ '\.nuget\packages\microsoft.codecoverage\' + $microsoftCodeCoveragePkgVer + '\build\netstandard2.0\CodeCoverage\CodeCoverage.exe analyze /output:' + $file.DirectoryName + '\' + $file.Name + '.xml '+ $file.FullName - Write-Host $command - Invoke-Expression $command -} diff --git a/build/stylecop.json b/build/stylecop.json index 064a1ef5a76..6d32e9d35e1 100644 --- a/build/stylecop.json +++ b/build/stylecop.json @@ -2,8 +2,8 @@ "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", "settings": { "documentationRules": { - "companyName": "OpenTelemetry Authors", - "copyrightText": "Copyright The OpenTelemetry Authors\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License." + "copyrightText": "Copyright The OpenTelemetry Authors\nSPDX-License-Identifier: Apache-2.0", + "xmlHeader": false }, "orderingRules": { "usingDirectivesPlacement": "outsideNamespace" diff --git a/build/test-aot-compatibility.ps1 b/build/test-aot-compatibility.ps1 new file mode 100644 index 00000000000..37483488f50 --- /dev/null +++ b/build/test-aot-compatibility.ps1 @@ -0,0 +1,41 @@ +param([string]$targetNetFramework) + +$rootDirectory = Split-Path $PSScriptRoot -Parent +$publishOutput = dotnet publish $rootDirectory/test/OpenTelemetry.AotCompatibility.TestApp/OpenTelemetry.AotCompatibility.TestApp.csproj -nodeReuse:false /p:UseSharedCompilation=false /p:ExposeExperimentalFeatures=true + +$actualWarningCount = 0 + +foreach ($line in $($publishOutput -split "`r`n")) +{ + if ($line -like "*analysis warning IL*") + { + Write-Host $line + + $actualWarningCount += 1 + } +} + +pushd $rootDirectory/test/OpenTelemetry.AotCompatibility.TestApp/bin/Release/$targetNetFramework/linux-x64 + +Write-Host "Executing test App..." +./OpenTelemetry.AotCompatibility.TestApp +Write-Host "Finished executing test App" + +if ($LastExitCode -ne 0) +{ + Write-Host "There was an error while executing AotCompatibility Test App. LastExitCode is:", $LastExitCode +} + +popd + +Write-Host "Actual warning count is:", $actualWarningCount +$expectedWarningCount = 0 + +$testPassed = 0 +if ($actualWarningCount -ne $expectedWarningCount) +{ + $testPassed = 1 + Write-Host "Actual warning count:", actualWarningCount, "is not as expected. Expected warning count is:", $expectedWarningCount +} + +Exit $testPassed 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/Directory.Build.props b/docs/Directory.Build.props index 080d8d81dea..942eea5974c 100644 --- a/docs/Directory.Build.props +++ b/docs/Directory.Build.props @@ -3,10 +3,6 @@ Exe - - - net7.0;net6.0 - - $(TargetFrameworks);net462;net47;net471;net472;net48 + $(TargetFrameworksForDocs) diff --git a/docs/Directory.Packages.props b/docs/Directory.Packages.props index 5c0fe4f3857..9dc0ffc0c6f 100644 --- a/docs/Directory.Packages.props +++ b/docs/Directory.Packages.props @@ -1,6 +1,3 @@ - - - diff --git a/docs/diagnostics/README.md b/docs/diagnostics/README.md new file mode 100644 index 00000000000..70758e8aa1f --- /dev/null +++ b/docs/diagnostics/README.md @@ -0,0 +1,24 @@ +# OpenTelemetry Diagnostics + +This document describes the diagnostic categories used in OpenTelemetry .NET +components. Diagnostics are used by the compiler to report information to users +about experimental and/or obsolete code being invoked or to suggest improvements +to specific code patterns identified through static analysis. + +## Experimental APIs + +Range: OTEL1000 - OTEL1999 + +Experimental APIs exposed in OpenTelemetry .NET pre-relase builds. APIs are +exposed experimentally when either the OpenTelemetry Specification has +explicitly marked some feature as +[experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/document-status.md) +or when the SIG members are still working through the design for a feature and +want to solicit feedback from the community. + +> [!NOTE] +> Experimental APIs are exposed as `public` in pre-release builds and `internal` +in stable builds. + +For defined diagnostics see: [OpenTelemetry .NET Experimental +APIs](./experimental-apis/README.md) diff --git a/docs/diagnostics/experimental-apis/OTEL1000.md b/docs/diagnostics/experimental-apis/OTEL1000.md new file mode 100644 index 00000000000..1ea4be023a3 --- /dev/null +++ b/docs/diagnostics/experimental-apis/OTEL1000.md @@ -0,0 +1,42 @@ +# OpenTelemetry .NET Diagnostic: OTEL1000 + +## Overview + +This is an Experimental API diagnostic covering the following APIs: + +* `LoggerProviderBuilder` +* `LoggerProvider` +* `IDeferredLoggerProviderBuilder` + +Experimental APIs may be changed or removed in the future. + +## Details + +The OpenTelemetry Specification defines a `LoggerProvider` as part of its +[API](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/bridge-api.md) +& +[SDK](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/sdk.md) +components. + +The SDK allows calling `Shutdown` and `ForceFlush` on the `LoggerProvider` and +also allows processors to be added dynamically to a pipeline after its creation. + +Today the OpenTelemetry .NET log pipeline is built on top of the +Microsoft.Extensions.Logging `ILogger` \ `ILoggerProvider` \ `ILoggerFactory` +APIs which do not expose such features. + +We also have an issue with the `ILoggingBuilder.AddOpenTelemetry` API in that it +interacts with the `OpenTelemetryLoggerOptions` class. Options classes are NOT +available until the `IServiceProvider` is available and services can no longer +be registered at that point. This prevents the current logging pipeline from +exposing the same dependency injection surface we have for traces and metrics. + +We are exposing these APIs to solve these issues and gather feedback about their +usefulness. + +## Logs Bridge API + +The OpenTelemetry Specification defines a Logs Bridge API which is rooted off of +the `LoggerProvider` (`GetLogger`) and exposes a `Logger` API to submit log +records. See [OTEL1001](./OTEL1001.md) for details about the Logs Bridge API +implementation status. diff --git a/docs/diagnostics/experimental-apis/OTEL1001.md b/docs/diagnostics/experimental-apis/OTEL1001.md new file mode 100644 index 00000000000..5386726e644 --- /dev/null +++ b/docs/diagnostics/experimental-apis/OTEL1001.md @@ -0,0 +1,35 @@ +# OpenTelemetry .NET Diagnostic: OTEL1001 + +## Overview + +This is an Experimental API diagnostic covering the following APIs: + +* `LoggerProvider.GetLogger` +* `Logger` +* `LogRecordAttributeList` +* `LogRecordData` +* `LogRecordSeverity` + +Experimental APIs may be changed or removed in the future. + +## Details + +The OpenTelemetry Specification defines a [Logs Bridge +API](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/bridge-api.md). + +The Logs Bridge API is used by library authors to build log appenders which +route messages from different log frameworks into OpenTelemetry. + +Today the OpenTelemetry .NET log pipeline is built on top of the +Microsoft.Extensions.Logging `ILogger` \ `ILoggerProvider` \ `ILoggerFactory` +APIs. + +We are exposing these APIs gather feedback about their usefulness. An +alternative approach may be taken which would be to append into `ILogger` +instead of OpenTelemetry directly. + +## LoggerProvider API + +The OpenTelemetry Specification defines a `LoggerProvider` API. See +[OTEL1000](./OTEL1000.md) for details about the `LoggerProvider` implementation +status. diff --git a/docs/diagnostics/experimental-apis/OTEL1002.md b/docs/diagnostics/experimental-apis/OTEL1002.md new file mode 100644 index 00000000000..8371f0d3bcd --- /dev/null +++ b/docs/diagnostics/experimental-apis/OTEL1002.md @@ -0,0 +1,30 @@ +# OpenTelemetry .NET Diagnostic: OTEL1002 + +## Overview + +This is an Experimental API diagnostic covering the following APIs: + +* `AlwaysOnExemplarFilter` +* `AlwaysOffExemplarFilter` +* `Exemplar` +* `ExemplarFilter` +* `MeterProviderBuilder.SetExemplarFilter` extension method +* `TraceBasedExemplarFilter` + +Experimental APIs may be changed or removed in the future. + +## Details + +The OpenTelemetry Specification defines an [Exemplar +API](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#exemplar) +in the Metrics SDK. + +From the specification: + +> Exemplars are example data points for aggregated data. They provide specific +> context to otherwise general aggregations. Exemplars allow correlation between +> aggregated metric data and the original API calls where measurements are +> recorded. + +We are exposing these APIs experimentally until the specification declares them +stable. diff --git a/docs/diagnostics/experimental-apis/OTEL1003.md b/docs/diagnostics/experimental-apis/OTEL1003.md new file mode 100644 index 00000000000..5f62f03575f --- /dev/null +++ b/docs/diagnostics/experimental-apis/OTEL1003.md @@ -0,0 +1,47 @@ +# OpenTelemetry .NET Diagnostic: OTEL1003 + +## Overview + +This is an Experimental API diagnostic covering the following API: + +* `MetricStreamConfiguration.CardinalityLimit.get` +* `MetricStreamConfiguration.CardinalityLimit.set` + +Experimental APIs may be changed or removed in the future. + +## Details + +From the specification: + +> The cardinality limit for an aggregation is defined in one of three ways: +> +> 1. A view with criteria matching the instrument an aggregation is created for +> has an `aggregation_cardinality_limit` value defined for the stream, that +> value SHOULD be used. +> 2. If there is no matching view, but the `MetricReader` defines a default +> cardinality limit value based on the instrument an aggregation is created +> for, that value SHOULD be used. +> 3. If none of the previous values are defined, the default value of 2000 +> SHOULD be used. + +We are exposing these APIs experimentally until the specification declares them +stable. + +### Setting cardinality limit for a specific Metric via the View API + +The OpenTelemetry Specification defines the [cardinality +limit](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#cardinality-limits) +of a metric can be set by the matching view. + +```csharp +using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddView( + instrumentName: "MyFruitCounter", + new MetricStreamConfiguration { CardinalityLimit = 10 }) + .Build(); +``` + +### Setting cardinality limit for a specific MetricReader + +[This is not currently supported by OpenTelemetry +.NET.](https://github.com/open-telemetry/opentelemetry-dotnet/issues/5331) diff --git a/docs/diagnostics/experimental-apis/README.md b/docs/diagnostics/experimental-apis/README.md new file mode 100644 index 00000000000..a5d527de3ba --- /dev/null +++ b/docs/diagnostics/experimental-apis/README.md @@ -0,0 +1,54 @@ +# OpenTelemetry .NET Experimental APIs + +This document describes experimental APIs exposed in OpenTelemetry .NET +pre-relase builds. APIs are exposed experimentally when either the OpenTelemetry +Specification has explicitly marked some feature as +[experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/document-status.md) +or when the SIG members are still working through the design for a feature and +want to solicit feedback from the community. + +> [!NOTE] +> Experimental APIs are exposed as `public` in pre-release builds and `internal` +in stable builds. + +## Active + +Experimental APIs available in the pre-release builds: + +### OTEL1000 + +Description: `LoggerProvider` and `LoggerProviderBuilder` + +Details: [OTEL1000](./OTEL1000.md) + +### OTEL1001 + +Description: Logs Bridge API + +Details: [OTEL1001](./OTEL1001.md) + +### OTEL1002 + +Description: Metrics Exemplar Support + +Details: [OTEL1002](./OTEL1002.md) + +### OTEL1003 + +Description: MetricStreamConfiguration CardinalityLimit Support + +Details: [OTEL1003](./OTEL1003.md) + +## Inactive + +Experimental APIs which have been released stable or removed: + + + +None diff --git a/docs/docfx.json b/docs/docfx.json index 5fc4dfb889f..6fdd09cd28d 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -29,7 +29,10 @@ { "files": [ ".editorconfig", - "**.cs" + "**.cs", + "Directory.Packages.props", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT" ] } ], diff --git a/docs/logs/README.md b/docs/logs/README.md new file mode 100644 index 00000000000..77a46c847e8 --- /dev/null +++ b/docs/logs/README.md @@ -0,0 +1,230 @@ +# OpenTelemetry .NET Logs + + +
+Table of Contents + +* [Best Practices](#best-practices) +* [Package Version](#package-version) +* [Logging API](#logging-api) + * [ILogger](#ilogger) + * [LoggerFactory](#loggerfactory) +* [Log Correlation](#log-correlation) +* [Log Enrichment](#log-enrichment) +* [Log Filtering](#log-filtering) +* [Log Redaction](#log-redaction) + +
+ + +## Best Practices + +The following tutorials have demonstrated the best practices for logging with +OpenTelemetry .NET: + +* [Getting Started - ASP.NET Core + Application](./getting-started-aspnetcore/README.md) +* [Getting Started - Console Application](./getting-started-console/README.md) +* [Logging with Complex Objects](./complex-objects/README.md) + +## Structured Logging + +:heavy_check_mark: You should use structured logging. + +* Structured logging is more efficient than unstructured logging. + * Filtering and redaction can happen on individual key-value pairs instead of + the entire log message. + * Storage and indexing are more efficient. +* Structured logging makes it easier to manage and consume logs. + +:stop_sign: You should avoid string interpolation. + +> [!WARNING] +> The following code has bad performance due to [string + interpolation](https://learn.microsoft.com/dotnet/csharp/tutorials/string-interpolation): + +```csharp +var food = "tomato"; +var price = 2.99; + +logger.LogInformation($"Hello from {food} {price}."); +``` + +Refer to the [logging performance +benchmark](../../test/Benchmarks/Logs/LogBenchmarks.cs) for more details. + +## Package Version + +:heavy_check_mark: You should always use the +[`ILogger`](https://docs.microsoft.com/dotnet/api/microsoft.extensions.logging.ilogger) +interface (including +[`ILogger`](https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.ilogger-1)) +from the latest stable version of +[Microsoft.Extensions.Logging](https://www.nuget.org/packages/Microsoft.Extensions.Logging/) +package, regardless of the .NET runtime version being used: + +* If you are using the latest stable version of [OpenTelemetry .NET + SDK](../../src/OpenTelemetry/README.md), you do not have to worry about the + version of `Microsoft.Extensions.Logging` package because it is already taken + care of for you via [package dependency](../../Directory.Packages.props). +* Starting from version `3.1.0`, the .NET runtime team is holding a high bar for + backward compatibility on `Microsoft.Extensions.Logging` even during major + version bumps, so compatibility is not a concern here. + +## Logging API + +### ILogger + +.NET supports high performance, structured logging via the +[`Microsoft.Extensions.Logging.ILogger`](https://docs.microsoft.com/dotnet/api/microsoft.extensions.logging.ilogger) +interface (including +[`ILogger`](https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.ilogger-1)) +to help monitor application behavior and diagnose issues. + +#### Get Logger + +In order to use the `ILogger` interface, you need to first get a logger. How to +get a logger depends on two things: + +* The type of application you are building. +* The place where you want to log. + +Here is the rule of thumb: + +* If you are building an application with [dependency injection + (DI)](https://learn.microsoft.com/dotnet/core/extensions/dependency-injection) + (e.g. [ASP.NET Core](https://learn.microsoft.com/aspnet/core) and [.NET + Worker](https://learn.microsoft.com/dotnet/core/extensions/workers)), in most + cases you should use the logger provided by DI, there are special cases when + you want log before DI logging pipeline is available or after DI logging + pipeline is disposed. Refer to the [.NET official + document](https://learn.microsoft.com/dotnet/core/extensions/logging#integration-with-hosts-and-dependency-injection) + and [Getting Started with OpenTelemetry .NET Logs in 5 Minutes - ASP.NET Core + Application](./getting-started-aspnetcore/README.md) tutorial to learn more. +* If you are building an application without DI, create a + [LoggerFactory](#loggerfactory) instance and configure OpenTelemetry to work + with it. Refer to the [Getting Started with OpenTelemetry .NET Logs in 5 + Minutes - Console Application](./getting-started-console/README.md) tutorial + to learn more. + +:heavy_check_mark: You should use dot-separated +[UpperCamelCase](https://en.wikipedia.org/wiki/Camel_case) as the log category +name, which makes it convenient to [filter logs](#log-filtering). A common +practice is to use fully qualified class name, and if further categorization is +desired, append a subcategory name. Refer to the [.NET official +document](https://learn.microsoft.com/dotnet/core/extensions/logging#log-category) +to learn more. + +```csharp +loggerFactory.CreateLogger(); // this is equivalent to CreateLogger("MyProduct.MyLibrary.MyClass") +loggerFactory.CreateLogger("MyProduct.MyLibrary.MyClass"); // use the fully qualified class name +loggerFactory.CreateLogger("MyProduct.MyLibrary.MyClass.DatabaseOperations"); // append a subcategory name +loggerFactory.CreateLogger("MyProduct.MyLibrary.MyClass.FileOperations"); // append another subcategory name +``` + +:stop_sign: You should avoid creating loggers too frequently. Although loggers +are not super expensive, they still come with CPU and memory cost, and are meant +to be reused throughout the application. Refer to the [logging performance +benchmark](../../test/Benchmarks/Logs/LogBenchmarks.cs) for more details. + +#### Use Logger + +:heavy_check_mark: You should use [compile-time logging source +generation](https://docs.microsoft.com/dotnet/core/extensions/logger-message-generator) +pattern to achieve the best performance. + +```csharp +var food = "tomato"; +var price = 2.99; + +logger.SayHello(food, price); + +internal static partial class LoggerExtensions +{ + [LoggerMessage(Level = LogLevel.Information, Message = "Hello from {food} {price}.")] + public static partial void SayHello(this ILogger logger, string food, double price); +} +``` + +> [!NOTE] +> There is no need to pass in an explicit + [EventId](https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.eventid) + while using + [LoggerMessageAttribute](https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.loggermessageattribute). + A durable `EventId` will be automatically assigned based on the hash of the + method name during code generation. + +:heavy_check_mark: You can use +[LogPropertiesAttribute](https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.logpropertiesattribute) +from +[Microsoft.Extensions.Telemetry.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.Telemetry.Abstractions/) +if you need to log complex objects. Check out the [Logging with Complex +Objects](./complex-objects/README.md) tutorial for more details. + +:stop_sign: You should avoid the extension methods from +[LoggerExtensions](https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.loggerextensions), +these methods are not optimized for performance. + +> [!WARNING] +> The following code has bad performance due to + [boxing](https://learn.microsoft.com/dotnet/csharp/programming-guide/types/boxing-and-unboxing): + +```csharp +var food = "tomato"; +var price = 2.99; + +logger.LogInformation("Hello from {food} {price}.", food, price); +``` + +Refer to the [logging performance +benchmark](../../test/Benchmarks/Logs/LogBenchmarks.cs) for more details. + +## LoggerFactory + +In many cases, you can use [ILogger](#ilogger) without having to interact with +[Microsoft.Extensions.Logging.LoggerFactory](https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.loggerfactory) +directly. This section is intended for users who need to create and manage +`LoggerFactory` explicitly. + +:stop_sign: You should avoid creating `LoggerFactory` instances too frequently, +`LoggerFactory` is fairly expensive and meant to be reused throughout the +application. For most applications, one `LoggerFactory` instance per process +would be sufficient. + +:heavy_check_mark: You should properly manage the lifecycle of +[LoggerFactory](https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.loggerfactory) +instances if they are created by you. + +* If you forget to dispose the `LoggerFactory` instance before the application + ends, logs might get dropped due to the lack of proper flush. +* If you dispose the `LoggerFactory` instance too early, any subsequent logging + API invocation associated with the logger factory could become no-op (i.e. no + logs will be emitted). + +## Log Correlation + +In OpenTelemetry, logs are automatically correlated to +[traces](../trace/README.md). Check the [Log +Correlation](./correlation/README.md) tutorial to learn more. + +## Log Enrichment + +TBD + +## Log Filtering + +The [Customizing OpenTelemetry .NET SDK for +Logs](./customizing-the-sdk/README.md#log-filtering) document has provided +instructions for basic filtering based on logger category name and severity +level. + +For more advanced filtering and sampling, the .NET team has a plan to cover it +in .NET 9 timeframe, please use this [runtime +issue](https://github.com/dotnet/runtime/issues/82465) to track the progress or +provide feedback and suggestions. + +## Log Redaction + +Logs might contain sensitive information such as passwords and credit card +numbers, proper redaction is required to prevent privacy and security incidents. +Check the [Log Redaction](./redaction/README.md) tutorial to learn more. diff --git a/docs/logs/complex-objects/FoodRecallNotice.cs b/docs/logs/complex-objects/FoodRecallNotice.cs new file mode 100644 index 00000000000..b04c01d7780 --- /dev/null +++ b/docs/logs/complex-objects/FoodRecallNotice.cs @@ -0,0 +1,15 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +public class FoodRecallNotice +{ + public string? BrandName { get; set; } + + public string? ProductDescription { get; set; } + + public string? ProductType { get; set; } + + public string? RecallReasonDescription { get; set; } + + public string? CompanyName { get; set; } +} diff --git a/docs/logs/complex-objects/Program.cs b/docs/logs/complex-objects/Program.cs new file mode 100644 index 00000000000..ca6fa891955 --- /dev/null +++ b/docs/logs/complex-objects/Program.cs @@ -0,0 +1,38 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.Logging; +using OpenTelemetry.Logs; + +var loggerFactory = LoggerFactory.Create(builder => +{ + builder.AddOpenTelemetry(logging => + { + logging.AddConsoleExporter(); + }); +}); + +var logger = loggerFactory.CreateLogger(); + +var foodRecallNotice = new FoodRecallNotice +{ + BrandName = "Contoso", + ProductDescription = "Salads", + ProductType = "Food & Beverages", + RecallReasonDescription = "due to a possible health risk from Listeria monocytogenes", + CompanyName = "Contoso Fresh Vegetables, Inc.", +}; + +logger.FoodRecallNotice(foodRecallNotice); + +// Dispose logger factory before the application ends. +// This will flush the remaining logs and shutdown the logging pipeline. +loggerFactory.Dispose(); + +internal static partial class LoggerExtensions +{ + [LoggerMessage(LogLevel.Critical)] + public static partial void FoodRecallNotice( + this ILogger logger, + [LogProperties(OmitReferenceName = true)] FoodRecallNotice foodRecallNotice); +} diff --git a/docs/logs/complex-objects/README.md b/docs/logs/complex-objects/README.md new file mode 100644 index 00000000000..7bcc666601a --- /dev/null +++ b/docs/logs/complex-objects/README.md @@ -0,0 +1,109 @@ +# Logging with Complex Objects + +In the [Getting Started with OpenTelemetry .NET Logs in 5 Minutes - Console +Application](../getting-started-console/README.md) tutorial, we've learned how +to log primitive data types. In this tutorial, we'll learn how to log complex +objects. + +Complex objects logging was introduced in .NET 8.0 via +[LogPropertiesAttribute](https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.logpropertiesattribute). +This attribute and the corresponding code generation logic are provided by an +extension package called +[Microsoft.Extensions.Telemetry.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.Telemetry.Abstractions/). + +> [!NOTE] +> Although `Microsoft.Extensions.Telemetry.Abstractions` was introduced in .NET +8.0, it supports previous versions of the target framework (e.g. .NET 6.0). +Refer to the [compatible target +frameworks](https://www.nuget.org/packages/Microsoft.Extensions.Telemetry.Abstractions/#supportedframeworks-body-tab) +for more information. + +First, complete the [getting started](../getting-started-console/README.md) +tutorial, then install the +[Microsoft.Extensions.Telemetry.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.Telemetry.Abstractions/) +package: + +```sh +dotnet add package Microsoft.Extensions.Telemetry.Abstractions +``` + +Define a new complex data type, as shown in [FoodRecallNotice.cs](./FoodRecallNotice.cs): + +```csharp +public class FoodRecallNotice +{ + public string? BrandName { get; set; } + + public string? ProductDescription { get; set; } + + public string? ProductType { get; set; } + + public string? RecallReasonDescription { get; set; } + + public string? CompanyName { get; set; } +} +``` + +Update the `Program.cs` file with the code from [Program.cs](./Program.cs). Note +that the following code is added which uses the `LogPropertiesAttribute` to log +the `FoodRecallNotice` object: + +```csharp +internal static partial class LoggerExtensions +{ + [LoggerMessage(LogLevel.Critical)] + public static partial void FoodRecallNotice( + this ILogger logger, + [LogProperties(OmitReferenceName = true)] FoodRecallNotice foodRecallNotice); +} +``` + +The following code is used to create a `FoodRecallNotice` object and log it: + +```csharp +var foodRecallNotice = new FoodRecallNotice +{ + BrandName = "Contoso", + ProductDescription = "Salads", + ProductType = "Food & Beverages", + RecallReasonDescription = "due to a possible health risk from Listeria monocytogenes", + CompanyName = "Contoso Fresh Vegetables, Inc.", +}; + +logger.FoodRecallNotice(foodRecallNotice); +``` + +Run the application again (using `dotnet run`) and you should see the log output +on the console. + +```text +LogRecord.Timestamp: 2024-01-12T19:01:16.0604084Z +LogRecord.CategoryName: Program +LogRecord.Severity: Fatal +LogRecord.SeverityText: Critical +LogRecord.FormattedMessage: +LogRecord.Body: +LogRecord.Attributes (Key:Value): + CompanyName: Contoso Fresh Vegetables, Inc. + RecallReasonDescription: due to a possible health risk from Listeria monocytogenes + ProductType: Food & Beverages + ProductDescription: Salads + BrandName: Contoso +LogRecord.EventId: 252550133 +LogRecord.EventName: FoodRecallNotice +``` + +> [!NOTE] +> In this tutorial we've used +[LogPropertiesAttribute.OmitReferenceName](https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.logpropertiesattribute.omitreferencename) +which changed the style of attribute names. There are more options available, +check out the [learn more](#learn-more) section for more information. + +## Learn more + +* [Microsoft.Extensions.Logging.LogPropertiesAttribute](https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.logpropertiesattribute) +* [Microsoft.Extensions.Telemetry.Abstractions](https://github.com/dotnet/extensions/blob/main/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/README.md) +* [Strong-type support feature + request](https://github.com/dotnet/runtime/issues/61947) +* [LogPropertiesAttribute design + proposal](https://github.com/dotnet/runtime/issues/81730) diff --git a/docs/logs/complex-objects/complex-objects.csproj b/docs/logs/complex-objects/complex-objects.csproj new file mode 100644 index 00000000000..cb2c93fa1ff --- /dev/null +++ b/docs/logs/complex-objects/complex-objects.csproj @@ -0,0 +1,9 @@ + + + true + + + + + + diff --git a/docs/logs/correlation/LoggerExtensions.cs b/docs/logs/correlation/LoggerExtensions.cs new file mode 100644 index 00000000000..facd6d895c8 --- /dev/null +++ b/docs/logs/correlation/LoggerExtensions.cs @@ -0,0 +1,10 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.Logging; + +internal static partial class LoggerExtensions +{ + [LoggerMessage(LogLevel.Information, "Food `{name}` price changed to `{price}`.")] + public static partial void FoodPriceChanged(this ILogger logger, string name, double price); +} diff --git a/docs/logs/correlation/Program.cs b/docs/logs/correlation/Program.cs index 722e66347e6..b1a3284fbcc 100644 --- a/docs/logs/correlation/Program.cs +++ b/docs/logs/correlation/Program.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using Microsoft.Extensions.Logging; @@ -20,40 +7,39 @@ using OpenTelemetry.Logs; using OpenTelemetry.Trace; -namespace Correlation; - public class Program { - private static readonly ActivitySource MyActivitySource = new( - "MyCompany.MyProduct.MyLibrary"); + private static readonly ActivitySource MyActivitySource = new("MyCompany.MyProduct.MyLibrary"); public static void Main() { - // Setup Logging - using var loggerFactory = LoggerFactory.Create(builder => + var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("MyCompany.MyProduct.MyLibrary") + .AddConsoleExporter() + .Build(); + + var loggerFactory = LoggerFactory.Create(builder => { - builder.AddOpenTelemetry(options => + builder.AddOpenTelemetry(logging => { - options.AddConsoleExporter(); + logging.AddConsoleExporter(); }); }); var logger = loggerFactory.CreateLogger(); - // Setup Traces - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("MyCompany.MyProduct.MyLibrary") - .AddConsoleExporter() - .Build(); - - // Emit activity using (var activity = MyActivitySource.StartActivity("SayHello")) { - activity?.SetTag("foo", 1); - - // emit logs within the context - // of activity - logger.LogInformation("Hello from {name} {price}.", "tomato", 2.99); + // Write a log within the context of an activity + logger.FoodPriceChanged("artichoke", 9.99); } + + // Dispose logger factory before the application ends. + // This will flush the remaining logs and shutdown the logging pipeline. + loggerFactory.Dispose(); + + // Dispose tracer provider before the application ends. + // This will flush the remaining spans and shutdown the tracing pipeline. + tracerProvider.Dispose(); } } diff --git a/docs/logs/correlation/README.md b/docs/logs/correlation/README.md index 89878735d06..309cfea64d0 100644 --- a/docs/logs/correlation/README.md +++ b/docs/logs/correlation/README.md @@ -1,6 +1,6 @@ -# Logs correlation +# Log Correlation -The getting started docs for [logs](../getting-started/README.md) and +The getting started docs for [logs](../getting-started-console/README.md) and [traces](../../trace/getting-started-console/README.md) showed how to emit logs and traces independently, and export them to console exporter. @@ -27,35 +27,38 @@ of an active `Activity`. Running the application will show the following output on the console: ```text -LogRecord.Timestamp: 2022-05-18T18:51:16.4348626Z -LogRecord.TraceId: d7aca5b2422ed8d15f56b6a93be4537d -LogRecord.SpanId: c90ac2ad41ab4d46 +LogRecord.Timestamp: 2024-01-26T17:55:39.2273475Z +LogRecord.TraceId: aed89c3b250fb9d8e16ccab1a4a9bbb5 +LogRecord.SpanId: bd44308753200c58 LogRecord.TraceFlags: Recorded -LogRecord.CategoryName: Correlation.Program -LogRecord.LogLevel: Information -LogRecord.State: Hello from tomato 2.99. +LogRecord.CategoryName: Program +LogRecord.Severity: Info +LogRecord.SeverityText: Information +LogRecord.Body: Food `{name}` price changed to `{price}`. +LogRecord.Attributes (Key:Value): + name: artichoke + price: 9.99 + OriginalFormat (a.k.a Body): Food `{name}` price changed to `{price}`. +LogRecord.EventId: 344095174 +LogRecord.EventName: FoodPriceChanged -Resource associated with LogRecord: -service.name: unknown_service:correlation +... -Activity.TraceId: d7aca5b2422ed8d15f56b6a93be4537d -Activity.SpanId: c90ac2ad41ab4d46 -Activity.TraceFlags: Recorded +Activity.TraceId: aed89c3b250fb9d8e16ccab1a4a9bbb5 +Activity.SpanId: bd44308753200c58 +Activity.TraceFlags: Recorded Activity.ActivitySourceName: MyCompany.MyProduct.MyLibrary -Activity.DisplayName: SayHello -Activity.Kind: Internal -Activity.StartTime: 2022-05-18T18:51:16.3427411Z -Activity.Duration: 00:00:00.2248932 -Activity.Tags: - foo: 1 -Resource associated with Activity: - service.name: unknown_service:correlation +Activity.DisplayName: SayHello +Activity.Kind: Internal +Activity.StartTime: 2024-01-26T17:55:39.2223849Z +Activity.Duration: 00:00:00.0361682 +... ``` As you can see, the `LogRecord` automatically had the `TraceId`, `SpanId` fields matching the ones from the `Activity`. In [the logs getting -started](../getting-started/README.md) doc, the logging was done outside of an -`Activity` context, hence these fields in `LogRecord` were not populated. +started](../getting-started-console/README.md) doc, the logging was done outside +of an `Activity` context, hence these fields in `LogRecord` were not populated. ## Learn more diff --git a/docs/logs/correlation/correlation.csproj b/docs/logs/correlation/correlation.csproj index 24f3305944a..19aa9791432 100644 --- a/docs/logs/correlation/correlation.csproj +++ b/docs/logs/correlation/correlation.csproj @@ -1,6 +1,5 @@ - diff --git a/docs/logs/customizing-the-sdk/Program.cs b/docs/logs/customizing-the-sdk/Program.cs index 549737954c9..1b22fdbfda9 100644 --- a/docs/logs/customizing-the-sdk/Program.cs +++ b/docs/logs/customizing-the-sdk/Program.cs @@ -1,54 +1,56 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.Logging; using OpenTelemetry.Logs; using OpenTelemetry.Resources; -namespace CustomizingTheSdk; - -public class Program +var loggerFactory = LoggerFactory.Create(builder => { - public static void Main() + builder.AddOpenTelemetry(logging => { - using var loggerFactory = LoggerFactory.Create(builder => - { - builder.AddOpenTelemetry(options => - { - options.IncludeScopes = true; - options.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService( - serviceName: "MyService", - serviceVersion: "1.0.0")); - options.AddConsoleExporter(); - }); - }); - - var logger = loggerFactory.CreateLogger(); - - logger.LogInformation("Hello from {name} {price}.", "tomato", 2.99); - logger.LogWarning("Hello from {name} {price}.", "tomato", 2.99); - logger.LogError("Hello from {name} {price}.", "tomato", 2.99); - - // log with scopes - using (logger.BeginScope(new List> - { - new KeyValuePair("store", "Seattle"), - })) - { - logger.LogInformation("Hello from {food} {price}.", "tomato", 2.99); - } - } + logging.IncludeScopes = true; + logging.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService( + serviceName: "MyService", + serviceVersion: "1.0.0")); + logging.AddConsoleExporter(); + }); +}); + +var logger = loggerFactory.CreateLogger(); + +logger.FoodPriceChanged("artichoke", 9.99); + +using (logger.BeginScope(new List> +{ + new KeyValuePair("store", "Seattle"), +})) +{ + logger.FoodPriceChanged("truffle", 999.99); +} + +logger.FoodRecallNotice( + brandName: "Contoso", + productDescription: "Salads", + productType: "Food & Beverages", + recallReasonDescription: "due to a possible health risk from Listeria monocytogenes", + companyName: "Contoso Fresh Vegetables, Inc."); + +// Dispose logger factory before the application ends. +// This will flush the remaining logs and shutdown the logging pipeline. +loggerFactory.Dispose(); + +internal static partial class LoggerExtensions +{ + [LoggerMessage(LogLevel.Information, "Food `{name}` price changed to `{price}`.")] + public static partial void FoodPriceChanged(this ILogger logger, string name, double price); + + [LoggerMessage(LogLevel.Critical, "A `{productType}` recall notice was published for `{brandName} {productDescription}` produced by `{companyName}` ({recallReasonDescription}).")] + public static partial void FoodRecallNotice( + this ILogger logger, + string brandName, + string productDescription, + string productType, + string recallReasonDescription, + string companyName); } diff --git a/docs/logs/customizing-the-sdk/README.md b/docs/logs/customizing-the-sdk/README.md index a642d0621e2..cd48fd608ab 100644 --- a/docs/logs/customizing-the-sdk/README.md +++ b/docs/logs/customizing-the-sdk/README.md @@ -36,16 +36,16 @@ TODO ### AddProcessor -[Processors](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/logging-library-sdk.md#logprocessor) +[Processors](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/sdk.md#logrecordprocessor) must be added using `OpenTelemetryLoggerOptions.AddProcessor()`. It is not supported to add Processors after building the `LoggerFactory`. ```csharp var loggerFactory = LoggerFactory.Create(builder => { - builder.AddOpenTelemetry(options => + builder.AddOpenTelemetry(logging => { - options.AddProcessor(...) + logging.AddProcessor(...); }); }); ``` @@ -56,9 +56,12 @@ 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 -SDK](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/resource/semantic_conventions#telemetry-sdk). +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 times, only the last is kept. It is not possible to change the resource builder @@ -69,9 +72,9 @@ The snippet below shows configuring a custom `ResourceBuilder` to the provider. ```csharp var loggerFactory = LoggerFactory.Create(builder => { - builder.AddOpenTelemetry(options => + builder.AddOpenTelemetry(logging => { - options.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService( + logging.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService( serviceName: "MyService", serviceVersion: "1.0.0")); }); @@ -104,8 +107,8 @@ and also defines "Warning" as the minimum `LogLevel` for a user defined category These rules as defined only apply to the `OpenTelemetryLoggerProvider`. ```csharp -ILoggingBuilder.AddFilter("*", LogLevel.Error); -ILoggingBuilder.AddFilter("category name", LogLevel.Warning); +builder.AddFilter("*", LogLevel.Error); +builder.AddFilter("MyProduct.MyLibrary.MyClass", LogLevel.Warning); ``` ## Learn more diff --git a/docs/logs/customizing-the-sdk/customizing-the-sdk.csproj b/docs/logs/customizing-the-sdk/customizing-the-sdk.csproj index 24f3305944a..19aa9791432 100644 --- a/docs/logs/customizing-the-sdk/customizing-the-sdk.csproj +++ b/docs/logs/customizing-the-sdk/customizing-the-sdk.csproj @@ -1,6 +1,5 @@ - diff --git a/docs/logs/dedicated-pipeline/DedicatedLogging/DedicatedLoggingServiceCollectionExtensions.cs b/docs/logs/dedicated-pipeline/DedicatedLogging/DedicatedLoggingServiceCollectionExtensions.cs new file mode 100644 index 00000000000..538f23b5cb4 --- /dev/null +++ b/docs/logs/dedicated-pipeline/DedicatedLogging/DedicatedLoggingServiceCollectionExtensions.cs @@ -0,0 +1,73 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.DependencyInjection.Extensions; +using OpenTelemetry.Logs; + +namespace DedicatedLogging; + +public static class DedicatedLoggingServiceCollectionExtensions +{ + public static IServiceCollection AddDedicatedLogging( + this IServiceCollection services, + IConfiguration configuration, + Action configureOpenTelemetry) + { + ArgumentNullException.ThrowIfNull(configureOpenTelemetry); + + services.TryAddSingleton(sp => + { + var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddConfiguration(configuration); + + builder.AddOpenTelemetry(configureOpenTelemetry); + }); + + return new DedicatedLoggerFactory(loggerFactory); + }); + + services.TryAdd(ServiceDescriptor.Singleton(typeof(IDedicatedLogger<>), typeof(DedicatedLogger<>))); + + return services; + } + + private sealed class DedicatedLogger : IDedicatedLogger + { + private readonly ILogger innerLogger; + + public DedicatedLogger(DedicatedLoggerFactory loggerFactory) + { + this.innerLogger = loggerFactory.CreateLogger(typeof(T).FullName!); + } + + public IDisposable? BeginScope(TState state) + where TState : notnull + => this.innerLogger.BeginScope(state); + + public bool IsEnabled(LogLevel logLevel) + => this.innerLogger.IsEnabled(logLevel); + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + => this.innerLogger.Log(logLevel, eventId, state, exception, formatter); + } + + private sealed class DedicatedLoggerFactory : ILoggerFactory + { + private readonly ILoggerFactory innerLoggerFactory; + + public DedicatedLoggerFactory(ILoggerFactory loggerFactory) + { + this.innerLoggerFactory = loggerFactory; + } + + public void AddProvider(ILoggerProvider provider) + => this.innerLoggerFactory.AddProvider(provider); + + public ILogger CreateLogger(string categoryName) + => this.innerLoggerFactory.CreateLogger(categoryName); + + public void Dispose() + => this.innerLoggerFactory.Dispose(); + } +} diff --git a/docs/logs/dedicated-pipeline/DedicatedLogging/IDedicatedLogger.cs b/docs/logs/dedicated-pipeline/DedicatedLogging/IDedicatedLogger.cs new file mode 100644 index 00000000000..ba09f27e271 --- /dev/null +++ b/docs/logs/dedicated-pipeline/DedicatedLogging/IDedicatedLogger.cs @@ -0,0 +1,12 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace DedicatedLogging; + +public interface IDedicatedLogger : ILogger +{ +} + +public interface IDedicatedLogger : IDedicatedLogger +{ +} diff --git a/docs/logs/dedicated-pipeline/Program.cs b/docs/logs/dedicated-pipeline/Program.cs new file mode 100644 index 00000000000..ad671445cde --- /dev/null +++ b/docs/logs/dedicated-pipeline/Program.cs @@ -0,0 +1,47 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using DedicatedLogging; +using OpenTelemetry.Logs; + +var builder = WebApplication.CreateBuilder(args); + +builder.Logging.ClearProviders(); + +builder.Logging.AddOpenTelemetry(options => +{ + // Set up primary pipeline for common app logs + options.AddConsoleExporter(); +}); + +builder.Services.AddDedicatedLogging( + builder.Configuration.GetSection("DedicatedLogging"), // Bind configuration for dedicated logging pipeline + options => + { + // Set up secondary pipeline for dedicated logs + options.AddConsoleExporter(); + }); + +var app = builder.Build(); + +app.MapGet("/", (HttpContext context, ILogger logger, IDedicatedLogger dedicatedLogger) => +{ + // Standard log written + logger.FoodPriceChanged("artichoke", 9.99); + + // Dedicated log written + dedicatedLogger.RequestInitiated(context.Connection.RemoteIpAddress?.ToString() ?? "unknown"); + + return "Hello from OpenTelemetry Logs!"; +}); + +app.Run(); + +internal static partial class LoggerExtensions +{ + [LoggerMessage(LogLevel.Information, "Food `{name}` price changed to `{price}`.")] + public static partial void FoodPriceChanged(this ILogger logger, string name, double price); + + [LoggerMessage(LogLevel.Information, "Request initiated from `{ipAddress}`.")] + public static partial void RequestInitiated(this IDedicatedLogger logger, string ipAddress); +} diff --git a/docs/logs/dedicated-pipeline/README.md b/docs/logs/dedicated-pipeline/README.md new file mode 100644 index 00000000000..9e620e193ad --- /dev/null +++ b/docs/logs/dedicated-pipeline/README.md @@ -0,0 +1,4 @@ +# Dedicated pipeline + +This example shows how to create a dedicated logging pipeline for specific logs +which will be sent to a different location than normal application logs. diff --git a/docs/logs/dedicated-pipeline/appsettings.Development.json b/docs/logs/dedicated-pipeline/appsettings.Development.json new file mode 100644 index 00000000000..770d3e93146 --- /dev/null +++ b/docs/logs/dedicated-pipeline/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/docs/logs/dedicated-pipeline/appsettings.json b/docs/logs/dedicated-pipeline/appsettings.json new file mode 100644 index 00000000000..b16f05cea55 --- /dev/null +++ b/docs/logs/dedicated-pipeline/appsettings.json @@ -0,0 +1,14 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "DedicatedLogging": { + "LogLevel": { + "Default": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/docs/logs/dedicated-pipeline/dedicated-pipeline.csproj b/docs/logs/dedicated-pipeline/dedicated-pipeline.csproj new file mode 100644 index 00000000000..cebc8460c42 --- /dev/null +++ b/docs/logs/dedicated-pipeline/dedicated-pipeline.csproj @@ -0,0 +1,6 @@ + + + + + + diff --git a/docs/logs/extending-the-sdk/LoggerExtensions.cs b/docs/logs/extending-the-sdk/LoggerExtensions.cs index 5b9f87eb3a9..7476d833334 100644 --- a/docs/logs/extending-the-sdk/LoggerExtensions.cs +++ b/docs/logs/extending-the-sdk/LoggerExtensions.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry; using OpenTelemetry.Logs; diff --git a/docs/logs/extending-the-sdk/MyExporter.cs b/docs/logs/extending-the-sdk/MyExporter.cs index d2552616e62..6093f57394b 100644 --- a/docs/logs/extending-the-sdk/MyExporter.cs +++ b/docs/logs/extending-the-sdk/MyExporter.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Text; using OpenTelemetry; diff --git a/docs/logs/extending-the-sdk/MyProcessor.cs b/docs/logs/extending-the-sdk/MyProcessor.cs index 4c0c54ec173..5fcd34e1d8d 100644 --- a/docs/logs/extending-the-sdk/MyProcessor.cs +++ b/docs/logs/extending-the-sdk/MyProcessor.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry; using OpenTelemetry.Logs; diff --git a/docs/logs/extending-the-sdk/Program.cs b/docs/logs/extending-the-sdk/Program.cs index 5fea2ce69d4..cada6ec1f5d 100644 --- a/docs/logs/extending-the-sdk/Program.cs +++ b/docs/logs/extending-the-sdk/Program.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.Logging; using OpenTelemetry; diff --git a/docs/logs/extending-the-sdk/README.md b/docs/logs/extending-the-sdk/README.md index d18b88624f0..55ea13dfaf4 100644 --- a/docs/logs/extending-the-sdk/README.md +++ b/docs/logs/extending-the-sdk/README.md @@ -3,6 +3,7 @@ * [Building your own exporter](#exporter) * [Building your own processor](#processor) * [Building your own sampler](#sampler) +* [Building your own resource detector](../../resources/README.md#resource-detector) * [References](#references) ## Exporter diff --git a/docs/logs/extending-the-sdk/extending-the-sdk.csproj b/docs/logs/extending-the-sdk/extending-the-sdk.csproj index 92d3323112a..4d96c349671 100644 --- a/docs/logs/extending-the-sdk/extending-the-sdk.csproj +++ b/docs/logs/extending-the-sdk/extending-the-sdk.csproj @@ -1,6 +1,5 @@  - diff --git a/docs/logs/getting-started-aspnetcore/Program.cs b/docs/logs/getting-started-aspnetcore/Program.cs new file mode 100644 index 00000000000..179d9d2237a --- /dev/null +++ b/docs/logs/getting-started-aspnetcore/Program.cs @@ -0,0 +1,49 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Logs; +using OpenTelemetry.Resources; + +var builder = WebApplication.CreateBuilder(args); + +// Remove default providers and add OpenTelemetry logging provider. +// For instructional purposes only, disable the default .NET console logging provider to +// use the verbose OpenTelemetry console exporter instead. For most development +// and production scenarios the default console provider works well and there is no need to +// clear these providers. +builder.Logging.ClearProviders(); + +builder.Logging.AddOpenTelemetry(logging => +{ + var resourceBuilder = ResourceBuilder + .CreateDefault() + .AddService(builder.Environment.ApplicationName); + + logging.SetResourceBuilder(resourceBuilder) + + // ConsoleExporter is used for demo purpose only. + // In production environment, ConsoleExporter should be replaced with other exporters (e.g. OTLP Exporter). + .AddConsoleExporter(); +}); + +var app = builder.Build(); + +app.MapGet("/", (ILogger logger) => +{ + logger.FoodPriceChanged("artichoke", 9.99); + + return "Hello from OpenTelemetry Logs!"; +}); + +app.Logger.StartingApp(); + +app.Run(); + +internal static partial class LoggerExtensions +{ + [LoggerMessage(LogLevel.Information, "Starting the app...")] + public static partial void StartingApp(this ILogger logger); + + [LoggerMessage(LogLevel.Information, "Food `{name}` price changed to `{price}`.")] + public static partial void FoodPriceChanged(this ILogger logger, string name, double price); +} diff --git a/docs/logs/getting-started-aspnetcore/README.md b/docs/logs/getting-started-aspnetcore/README.md new file mode 100644 index 00000000000..6c6bbe26dc2 --- /dev/null +++ b/docs/logs/getting-started-aspnetcore/README.md @@ -0,0 +1,142 @@ +# Getting Started with OpenTelemetry .NET Logs in 5 Minutes - ASP.NET Core Application + +First, download and install the [.NET +SDK](https://dotnet.microsoft.com/download) on your computer. + +Create a new web application: + +```sh +dotnet new web -o aspnetcoreapp +cd aspnetcoreapp +``` + +Install the +[OpenTelemetry.Exporter.Console](../../../src/OpenTelemetry.Exporter.Console/README.md) +and +[OpenTelemetry.Extensions.Hosting](../../../src/OpenTelemetry.Extensions.Hosting/README.md) +packages: + +```sh +dotnet add package OpenTelemetry.Exporter.Console +dotnet add package OpenTelemetry.Extensions.Hosting +``` + +Update the `Program.cs` file with the code from [Program.cs](./Program.cs). + +Run the application (using `dotnet run`) and then browse to the URL shown in the +console for your application (e.g. `http://localhost:5000`). You should see the +logs output from the console: + +```text +LogRecord.Timestamp: 2023-09-06T22:59:17.9787564Z +LogRecord.CategoryName: getting-started-aspnetcore +LogRecord.Severity: Info +LogRecord.SeverityText: Information +LogRecord.Body: Starting the app... +LogRecord.Attributes (Key:Value): + OriginalFormat (a.k.a Body): Starting the app... +LogRecord.EventId: 225744744 +LogRecord.EventName: StartingApp + +... + +LogRecord.Timestamp: 2023-09-06T22:59:18.0644378Z +LogRecord.CategoryName: Microsoft.Hosting.Lifetime +LogRecord.Severity: Info +LogRecord.SeverityText: Information +LogRecord.Body: Now listening on: {address} +LogRecord.Attributes (Key:Value): + address: http://localhost:5000 + OriginalFormat (a.k.a Body): Now listening on: {address} +LogRecord.EventId: 14 +LogRecord.EventName: ListeningOnAddress + +... + +LogRecord.Timestamp: 2023-09-06T23:00:46.1639248Z +LogRecord.TraceId: 3507087d60ae4b1d2f10e68f4e40784a +LogRecord.SpanId: c51be9f19c598b69 +LogRecord.TraceFlags: None +LogRecord.CategoryName: Program +LogRecord.Severity: Info +LogRecord.SeverityText: Information +LogRecord.Body: Food `{name}` price changed to `{price}`. +LogRecord.Attributes (Key:Value): + name: artichoke + price: 9.99 + OriginalFormat (a.k.a Body): Food `{name}` price changed to `{price}`. +LogRecord.EventId: 344095174 +LogRecord.EventName: FoodPriceChanged + +... +``` + +Congratulations! You are now collecting logs using OpenTelemetry. + +What does the above program do? + +The program has cleared the default [logging +providers](https://learn.microsoft.com/dotnet/core/extensions/logging-providers) +then added OpenTelemetry as a logging provider to the ASP.NET Core logging +pipeline. OpenTelemetry SDK is then configured with a +[ConsoleExporter](../../../src/OpenTelemetry.Exporter.Console/README.md) to +export the logs to the console for demonstration purpose (note: ConsoleExporter +is not intended for production usage, other exporters such as [OTLP +Exporter](../../../src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md) +should be used instead). From the console output we can see logs from both our +logger and the ASP.NET Core framework loggers, as indicated by the +`LogRecord.CategoryName`. + +The example has demonstrated the best practice from ASP.NET Core by injecting +generic `ILogger`: + +```csharp +app.MapGet("/", (ILogger logger) => +{ + logger.FoodPriceChanged("artichoke", 9.99); + + return "Hello from OpenTelemetry Logs!"; +}); +``` + +Following the .NET logging best practice, [compile-time logging source +generation](https://docs.microsoft.com/dotnet/core/extensions/logger-message-generator) +has been used across the example, which delivers high performance, structured +logging, and type-checked parameters: + +```csharp +internal static partial class LoggerExtensions +{ + [LoggerMessage(LogLevel.Information, "Starting the app...")] + public static partial void StartingApp(this ILogger logger); + + [LoggerMessage(LogLevel.Information, "Food `{name}` price changed to `{price}`.")] + public static partial void FoodPriceChanged(this ILogger logger, string name, double price); +} +``` + +For logs that occur between `builder.Build()` and `app.Run()` when injecting a +generic `ILogger` is not an option, `app.Logger` is used instead: + +```csharp +app.Logger.StartingApp(); +``` + +> [!NOTE] +> There are cases where logging is needed before the [dependency injection +(DI)](https://learn.microsoft.com/dotnet/core/extensions/dependency-injection) +logging pipeline is available (e.g. before `builder.Build()`) or after the DI +logging pipeline is disposed (e.g. after `app.Run()`). The common practice is to +use a separate logging pipeline by creating a `LoggerFactory` instance. +> +> Refer to the [Getting Started with OpenTelemetry .NET Logs in 5 Minutes - +Console Application](../getting-started-console/README.md) tutorial to learn +more about how to create a `LoggerFactory` instance and configure OpenTelemetry +to work with it. + +## Learn more + +* [Logging in C# and .NET](https://learn.microsoft.com/dotnet/core/extensions/logging) +* [Logging with Complex Objects](../complex-objects/README.md) +* [Customizing the OpenTelemetry .NET SDK](../customizing-the-sdk/README.md) +* [Extending the OpenTelemetry .NET SDK](../extending-the-sdk/README.md) diff --git a/docs/logs/getting-started-aspnetcore/appsettings.Development.json b/docs/logs/getting-started-aspnetcore/appsettings.Development.json new file mode 100644 index 00000000000..770d3e93146 --- /dev/null +++ b/docs/logs/getting-started-aspnetcore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/docs/logs/getting-started-aspnetcore/appsettings.json b/docs/logs/getting-started-aspnetcore/appsettings.json new file mode 100644 index 00000000000..10f68b8c8b4 --- /dev/null +++ b/docs/logs/getting-started-aspnetcore/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/docs/logs/getting-started-aspnetcore/getting-started-aspnetcore.csproj b/docs/logs/getting-started-aspnetcore/getting-started-aspnetcore.csproj new file mode 100644 index 00000000000..cebc8460c42 --- /dev/null +++ b/docs/logs/getting-started-aspnetcore/getting-started-aspnetcore.csproj @@ -0,0 +1,6 @@ + + + + + + diff --git a/docs/logs/getting-started-console/Program.cs b/docs/logs/getting-started-console/Program.cs new file mode 100644 index 00000000000..b907d169d20 --- /dev/null +++ b/docs/logs/getting-started-console/Program.cs @@ -0,0 +1,43 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.Logging; +using OpenTelemetry.Logs; + +var loggerFactory = LoggerFactory.Create(builder => +{ + builder.AddOpenTelemetry(logging => + { + logging.AddConsoleExporter(); + }); +}); + +var logger = loggerFactory.CreateLogger(); + +logger.FoodPriceChanged("artichoke", 9.99); + +logger.FoodRecallNotice( + brandName: "Contoso", + productDescription: "Salads", + productType: "Food & Beverages", + recallReasonDescription: "due to a possible health risk from Listeria monocytogenes", + companyName: "Contoso Fresh Vegetables, Inc."); + +// Dispose logger factory before the application ends. +// This will flush the remaining logs and shutdown the logging pipeline. +loggerFactory.Dispose(); + +internal static partial class LoggerExtensions +{ + [LoggerMessage(LogLevel.Information, "Food `{name}` price changed to `{price}`.")] + public static partial void FoodPriceChanged(this ILogger logger, string name, double price); + + [LoggerMessage(LogLevel.Critical, "A `{productType}` recall notice was published for `{brandName} {productDescription}` produced by `{companyName}` ({recallReasonDescription}).")] + public static partial void FoodRecallNotice( + this ILogger logger, + string brandName, + string productDescription, + string productType, + string recallReasonDescription, + string companyName); +} diff --git a/docs/logs/getting-started-console/README.md b/docs/logs/getting-started-console/README.md new file mode 100644 index 00000000000..aa251d09951 --- /dev/null +++ b/docs/logs/getting-started-console/README.md @@ -0,0 +1,119 @@ +# Getting Started with OpenTelemetry .NET Logs in 5 Minutes - Console Application + +First, download and install the [.NET +SDK](https://dotnet.microsoft.com/download) on your computer. + +Create a new console application and run it: + +```sh +dotnet new console --output getting-started +cd getting-started +dotnet run +``` + +You should see the following output: + +```text +Hello World! +``` + +Install the +[OpenTelemetry.Exporter.Console](../../../src/OpenTelemetry.Exporter.Console/README.md) +package: + +```sh +dotnet add package OpenTelemetry.Exporter.Console +``` + +Update the `Program.cs` file with the code from [Program.cs](./Program.cs). + +Run the application again (using `dotnet run`) and you should see the log output +on the console. + +```text +LogRecord.Timestamp: 2023-09-15T06:07:03.5502083Z +LogRecord.CategoryName: Program +LogRecord.Severity: Info +LogRecord.SeverityText: Information +LogRecord.Body: Food `{name}` price changed to `{price}`. +LogRecord.Attributes (Key:Value): + name: artichoke + price: 9.99 + OriginalFormat (a.k.a Body): Food `{name}` price changed to `{price}`. +LogRecord.EventId: 344095174 +LogRecord.EventName: FoodPriceChanged + +... + +LogRecord.Timestamp: 2023-09-15T06:07:03.5683511Z +LogRecord.CategoryName: Program +LogRecord.Severity: Fatal +LogRecord.SeverityText: Critical +LogRecord.Body: A `{productType}` recall notice was published for `{brandName} {productDescription}` produced by `{companyName}` ({recallReasonDescription}). +LogRecord.Attributes (Key:Value): + brandName: Contoso + productDescription: Salads + productType: Food & Beverages + recallReasonDescription: due to a possible health risk from Listeria monocytogenes + companyName: Contoso Fresh Vegetables, Inc. + OriginalFormat (a.k.a Body): A `{productType}` recall notice was published for `{brandName} {productDescription}` produced by `{companyName}` ({recallReasonDescription}). +LogRecord.EventId: 1338249384 +LogRecord.EventName: FoodRecallNotice + +... +``` + +Congratulations! You are now collecting logs using OpenTelemetry. + +What does the above program do? + +The program has created a logging pipeline by instantiating a +[`LoggerFactory`](https://docs.microsoft.com/dotnet/api/microsoft.extensions.logging.iloggerfactory) +instance, with OpenTelemetry added as a [logging +provider](https://docs.microsoft.com/dotnet/core/extensions/logging-providers). +OpenTelemetry SDK is then configured with a +[ConsoleExporter](../../../src/OpenTelemetry.Exporter.Console/README.md) to +export the logs to the console for demonstration purpose (note: ConsoleExporter +is not intended for production usage, other exporters such as [OTLP +Exporter](../../../src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md) +should be used instead). + +The `LoggerFactory` instance is used to create an +[`ILogger`](https://docs.microsoft.com/dotnet/api/microsoft.extensions.logging.ilogger) +instance, which is used to do the actual logging. + +Following the .NET logging best practice, [compile-time logging source +generation](https://docs.microsoft.com/dotnet/core/extensions/logger-message-generator) +has been used across the example, which delivers high performance, structured +logging, and type-checked parameters: + +```csharp +internal static partial class LoggerExtensions +{ + [LoggerMessage(LogLevel.Information, "Food `{name}` price changed to `{price}`.")] + public static partial void FoodPriceChanged(this ILogger logger, string name, double price); + + ... +} +``` + +> [!NOTE] +> For applications which use `ILogger` with [dependency injection +(DI)](https://learn.microsoft.com/dotnet/core/extensions/dependency-injection) +(e.g. [ASP.NET Core](https://learn.microsoft.com/aspnet/core) and [.NET +Worker](https://learn.microsoft.com/dotnet/core/extensions/workers)), the common +practice is to add OpenTelemetry as a [logging +provider](https://docs.microsoft.com/dotnet/core/extensions/logging-providers) +to the DI logging pipeline, rather than set up a completely new logging pipeline +by creating a new `LoggerFactory` instance. +> +> Refer to the [Getting Started with OpenTelemetry .NET Logs in 5 Minutes - +ASP.NET Core Application](../getting-started-aspnetcore/README.md) tutorial to +learn more. + +## Learn more + +* [Logging in C# and .NET](https://learn.microsoft.com/dotnet/core/extensions/logging) +* [Logging with Complex Objects](../complex-objects/README.md) +* [Customizing the OpenTelemetry .NET SDK](../customizing-the-sdk/README.md) +* [Extending the OpenTelemetry .NET SDK](../extending-the-sdk/README.md) diff --git a/docs/logs/getting-started/getting-started.csproj b/docs/logs/getting-started-console/getting-started-console.csproj similarity index 75% rename from docs/logs/getting-started/getting-started.csproj rename to docs/logs/getting-started-console/getting-started-console.csproj index 6e8adc423c3..f68cdb2a7fe 100644 --- a/docs/logs/getting-started/getting-started.csproj +++ b/docs/logs/getting-started-console/getting-started-console.csproj @@ -1,6 +1,5 @@  - diff --git a/docs/logs/getting-started/Program.cs b/docs/logs/getting-started/Program.cs deleted file mode 100644 index de8291df904..00000000000 --- a/docs/logs/getting-started/Program.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 Microsoft.Extensions.Logging; -using OpenTelemetry.Logs; - -namespace GettingStarted; - -public class Program -{ - public static void Main() - { - using var loggerFactory = LoggerFactory.Create(builder => - { - builder.AddOpenTelemetry(options => - { - options.AddConsoleExporter(); - }); - }); - - var logger = loggerFactory.CreateLogger(); - - logger.LogInformation(eventId: 123, "Hello from {name} {price}.", "tomato", 2.99); - - if (logger.IsEnabled(LogLevel.Debug)) - { - // If logger.IsEnabled returned false, the code doesn't have to spend time evaluating the arguments. - // This can be especially helpful if the arguments are expensive to calculate. - logger.LogDebug(eventId: 501, "System.Environment.Version: {version}.", System.Environment.Version); - } - } -} diff --git a/docs/logs/getting-started/README.md b/docs/logs/getting-started/README.md deleted file mode 100644 index f37377bb7e8..00000000000 --- a/docs/logs/getting-started/README.md +++ /dev/null @@ -1,81 +0,0 @@ -# Getting Started with OpenTelemetry .NET Logs in 5 Minutes - -First, download and install the [.NET -SDK](https://dotnet.microsoft.com/download) on your computer. - -Create a new console application and run it: - -```sh -dotnet new console --output getting-started -cd getting-started -dotnet run -``` - -You should see the following output: - -```text -Hello World! -``` - -Install the latest `Microsoft.Extensions.Logging` package: - - ```sh - dotnet add package Microsoft.Extensions.Logging - ``` - -Install the -[OpenTelemetry.Exporter.Console](../../../src/OpenTelemetry.Exporter.Console/README.md) -package: - -```sh -dotnet add package OpenTelemetry.Exporter.Console -``` - -Update the `Program.cs` file with the code from [Program.cs](./Program.cs): - -Run the application again (using `dotnet run`) and you should see the log output -on the console. - -```text -LogRecord.Timestamp: 2023-01-21T00:33:08.1467491Z -LogRecord.CategoryName: GettingStarted.Program -LogRecord.LogLevel: Information -LogRecord.State (Key:Value): - name: tomato - price: 2.99 - OriginalFormat (a.k.a Body): Hello from {name} {price}. -LogRecord.EventId: 123 -``` - -Congratulations! You are now collecting logs using OpenTelemetry. - -What does the above program do? - -The program creates a -[`LoggerFactory`](https://docs.microsoft.com/dotnet/api/microsoft.extensions.logging.iloggerfactory) -with OpenTelemetry added as a -[LoggerProvider](https://docs.microsoft.com/dotnet/core/extensions/logging-providers). -This `LoggerFactory` is used to create an -[`ILogger`](https://docs.microsoft.com/dotnet/api/microsoft.extensions.logging.ilogger) -instance, which is then used to do the logging. The log is sent to the -`OpenTelemetryLoggerProvider`, which is configured to export logs to -`ConsoleExporter`. `ConsoleExporter` simply displays it on the console. - -> **Note** -> Certain types of applications (e.g. [ASP.NET -Core](https://learn.microsoft.com/aspnet/core) and [.NET -Worker](https://learn.microsoft.com/dotnet/core/extensions/workers)) have an -`ILogger` based logging pipeline set up by default. In such apps, enabling -OpenTelemetry should be done by adding OpenTelemetry as a provider to the -*existing* logging pipeline, and users should not create a new `LoggerFactory` -(which sets up a totally new logging pipeline). Also, obtaining `ILogger` -instance could be done differently as well. See [Example ASP.NET Core -application](../../../examples/AspNetCore/Program.cs) for an example which shows -how to add OpenTelemetry to the logging pipeline already setup by the -application. - -## Learn more - -* [Compile-time logging source generation](../source-generation/README.md) -* [Customizing the OpenTelemetry .NET SDK](../customizing-the-sdk/README.md) -* [Extending the OpenTelemetry .NET SDK](../extending-the-sdk/README.md) diff --git a/docs/logs/redaction/MyClassWithRedactionEnumerator.cs b/docs/logs/redaction/MyClassWithRedactionEnumerator.cs deleted file mode 100644 index e8030a01581..00000000000 --- a/docs/logs/redaction/MyClassWithRedactionEnumerator.cs +++ /dev/null @@ -1,60 +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.Collections; - -namespace Redaction -{ - internal class MyClassWithRedactionEnumerator : IReadOnlyList> - { - private readonly IReadOnlyList> state; - - public MyClassWithRedactionEnumerator(IReadOnlyList> state) - { - this.state = state; - } - - public int Count => this.state.Count; - - public KeyValuePair this[int index] - { - get - { - var item = this.state[index]; - var entryVal = item.Value; - if (entryVal != null && entryVal.ToString() != null && entryVal.ToString().Contains("")) - { - return new KeyValuePair(item.Key, "newRedactedValueHere"); - } - - return item; - } - } - - public IEnumerator> GetEnumerator() - { - for (var i = 0; i < this.Count; i++) - { - yield return this[i]; - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return this.GetEnumerator(); - } - } -} diff --git a/docs/logs/redaction/MyRedactionProcessor.cs b/docs/logs/redaction/MyRedactionProcessor.cs index 14dc33033e8..7959faf3d60 100644 --- a/docs/logs/redaction/MyRedactionProcessor.cs +++ b/docs/logs/redaction/MyRedactionProcessor.cs @@ -1,25 +1,11 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +using System.Collections; using OpenTelemetry; using OpenTelemetry.Logs; -namespace Redaction; - -internal class MyRedactionProcessor : BaseProcessor +internal sealed class MyRedactionProcessor : BaseProcessor { public override void OnEnd(LogRecord logRecord) { @@ -28,4 +14,44 @@ public override void OnEnd(LogRecord logRecord) logRecord.Attributes = new MyClassWithRedactionEnumerator(logRecord.Attributes); } } + + internal sealed class MyClassWithRedactionEnumerator : IReadOnlyList> + { + private readonly IReadOnlyList> state; + + public MyClassWithRedactionEnumerator(IReadOnlyList> state) + { + this.state = state; + } + + public int Count => this.state.Count; + + public KeyValuePair this[int index] + { + get + { + var item = this.state[index]; + var entryVal = item.Value?.ToString(); + if (entryVal != null && entryVal.Contains("")) + { + return new KeyValuePair(item.Key, "newRedactedValueHere"); + } + + return item; + } + } + + public IEnumerator> GetEnumerator() + { + for (var i = 0; i < this.Count; i++) + { + yield return this[i]; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + } } diff --git a/docs/logs/redaction/Program.cs b/docs/logs/redaction/Program.cs index f381856234e..fa33d884581 100644 --- a/docs/logs/redaction/Program.cs +++ b/docs/logs/redaction/Program.cs @@ -1,38 +1,29 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.Logging; using OpenTelemetry.Logs; -namespace Redaction; - -public class Program +var loggerFactory = LoggerFactory.Create(builder => { - public static void Main() + builder.AddOpenTelemetry(logging => { - using var loggerFactory = LoggerFactory.Create(builder => - builder.AddOpenTelemetry(options => - { - options.AddProcessor(new MyRedactionProcessor()); - options.AddConsoleExporter(); - })); + logging.AddProcessor(new MyRedactionProcessor()); + logging.AddConsoleExporter(); + }); +}); + +var logger = loggerFactory.CreateLogger(); - var logger = loggerFactory.CreateLogger(); +// Message will be redacted by MyRedactionProcessor +logger.FoodPriceChanged("", 9.99); - // message will be redacted by MyRedactionProcessor - logger.LogInformation("OpenTelemetry {sensitiveString}.", ""); - } +// Dispose logger factory before the application ends. +// This will flush the remaining logs and shutdown the logging pipeline. +loggerFactory.Dispose(); + +internal static partial class LoggerExtensions +{ + [LoggerMessage(LogLevel.Information, "Food `{name}` price changed to `{price}`.")] + public static partial void FoodPriceChanged(this ILogger logger, string name, double price); } diff --git a/docs/logs/redaction/redaction.csproj b/docs/logs/redaction/redaction.csproj index db220b16f4c..2dc5d8deb63 100644 --- a/docs/logs/redaction/redaction.csproj +++ b/docs/logs/redaction/redaction.csproj @@ -4,7 +4,6 @@ disable - diff --git a/docs/logs/source-generation/FoodSupplyLogs.cs b/docs/logs/source-generation/FoodSupplyLogs.cs deleted file mode 100644 index c7c54610bfb..00000000000 --- a/docs/logs/source-generation/FoodSupplyLogs.cs +++ /dev/null @@ -1,40 +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 Microsoft.Extensions.Logging; - -namespace SourceGeneration; - -public static partial class FoodSupplyLogs -{ - [LoggerMessage( - EventId = 1, - Level = LogLevel.Information, - Message = "Food `{name}` price changed to `{price}`.")] - public static partial void FoodPriceChanged(this ILogger logger, string name, double price); - - [LoggerMessage( - EventId = 2, - Message = "A `{productType}` recall notice was published for `{brandName} {productDescription}` produced by `{companyName}` ({recallReasonDescription}).")] - public static partial void FoodRecallNotice( - this ILogger logger, - LogLevel logLevel, - string brandName, - string productDescription, - string productType, - string recallReasonDescription, - string companyName); -} diff --git a/docs/logs/source-generation/Program.cs b/docs/logs/source-generation/Program.cs deleted file mode 100644 index 42c0e41ec3a..00000000000 --- a/docs/logs/source-generation/Program.cs +++ /dev/null @@ -1,49 +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 Microsoft.Extensions.Logging; -using OpenTelemetry.Logs; - -namespace SourceGeneration; - -public class Program -{ - public static void Main() - { - using var loggerFactory = LoggerFactory.Create(builder => - { - builder.AddOpenTelemetry(options => - { - options.IncludeScopes = true; - options.ParseStateValues = true; - options.IncludeFormattedMessage = true; - options.AddConsoleExporter(); - }); - }); - - var logger = loggerFactory.CreateLogger(); - - logger.FoodPriceChanged("artichoke", 9.99); - - logger.FoodRecallNotice( - logLevel: LogLevel.Critical, - brandName: "Contoso", - productDescription: "Salads", - productType: "Food & Beverages", - recallReasonDescription: "due to a possible health risk from Listeria monocytogenes", - companyName: "Contoso Fresh Vegetables, Inc."); - } -} diff --git a/docs/logs/source-generation/README.md b/docs/logs/source-generation/README.md deleted file mode 100644 index c24f9d45e37..00000000000 --- a/docs/logs/source-generation/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Compile-time logging source generation - -.NET 6 has introduced a more usable and performant logging solution with -[`LoggerMessageAttribute`](https://docs.microsoft.com/dotnet/api/microsoft.extensions.logging.loggermessageattribute). - -## References - -* [Compile-time logging source generation](https://docs.microsoft.com/dotnet/core/extensions/logger-message-generator) diff --git a/docs/metrics/README.md b/docs/metrics/README.md index 390604e64a0..23df6fbf172 100644 --- a/docs/metrics/README.md +++ b/docs/metrics/README.md @@ -1,93 +1,499 @@ # OpenTelemetry .NET Metrics + +
+Table of Contents + +* [Best Practices](#best-practices) +* [Package Version](#package-version) +* [Metrics API](#metrics-api) + * [Meter](#meter) + * [Instruments](#instruments) +* [MeterProvider Management](#meterprovider-management) +* [Memory Management](#memory-management) + * [Pre-Aggregation](#pre-aggregation) + * [Cardinality Limits](#cardinality-limits) + * [Memory Preallocation](#memory-preallocation) +* [Metrics Correlation](#metrics-correlation) +* [Metrics Enrichment](#metrics-enrichment) + +
+ + ## Best Practices -- Instruments SHOULD only be created once and reused throughout the application - lifetime. This - [example](../../docs/metrics/getting-started-console/Program.cs) shows how an - instrument is created a `static` field and then used in the application. You - could also look at this ASP.NET Core - [example](../../examples/AspNetCore/Program.cs) which shows a more Dependency - Injection friendly way of doing this by extracting the `Meter` and an - instrument into a dedicated class called - [Instrumentation](../../examples/AspNetCore/Instrumentation.cs) which is then - added as a `Singleton` service. - -- When emitting metrics with tags, DO NOT change the order in which you provide - tags. Changing the order of tag keys would increase the time taken by the SDK - to record the measurement. +The following tutorials have demonstrated the best practices for using metrics +with OpenTelemetry .NET: + +* [Getting Started - ASP.NET Core + Application](./getting-started-aspnetcore/README.md) +* [Getting Started - Console Application](./getting-started-console/README.md) + +## Package Version + +:heavy_check_mark: You should always use the +[System.Diagnostics.Metrics](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics) +APIs from the latest stable version of +[System.Diagnostics.DiagnosticSource](https://www.nuget.org/packages/System.Diagnostics.DiagnosticSource/) +package, regardless of the .NET runtime version being used: + +* If you are using the latest stable version of [OpenTelemetry .NET + SDK](../../src/OpenTelemetry/README.md), you do not have to worry about the + version of `System.Diagnostics.DiagnosticSource` package because it is already + taken care of for you via [package + dependency](../../Directory.Packages.props). +* The .NET runtime team is holding a high bar for backward compatibility on + `System.Diagnostics.DiagnosticSource` even during major version bumps, so + compatibility is not a concern here. + +## Metrics API + +### Meter + +:stop_sign: You should avoid creating +[`System.Diagnostics.Metrics.Meter`](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.meter) +too frequently. `Meter` is fairly expensive and meant to be reused throughout +the application. For most applications, it can be modeled as static readonly +field (e.g. [Program.cs](./getting-started-console/Program.cs)) or singleton via +dependency injection (e.g. +[Instrumentation.cs](../../examples/AspNetCore/Instrumentation.cs)). + +:heavy_check_mark: You should use dot-separated +[UpperCamelCase](https://en.wikipedia.org/wiki/Camel_case) as the +[`Meter.Name`](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.meter.name). +In many cases, using the fully qualified class name might be a good option. ```csharp -// If you emit the tag keys in this order: name -> color -> taste, stick to this order of tag keys for subsequent measurements. -MyFruitCounter.Add(5, new("name", "apple"), new("color", "red"), new("taste", "sweet")); -... -... -... -// Same measurement with the order of tags changed: color -> name -> taste. This order of tags is different from the one that was first encountered by the SDK. -MyFruitCounter.Add(7, new("color", "red"), new("name", "apple"), new("taste", "sweet")); // <--- DON'T DO THIS +static readonly Meter MyMeter = new("MyCompany.MyProduct.MyLibrary", "1.0"); ``` -- When emitting metrics with more than three tags, use `TagList` for better - performance. Using - [`TagList`](https://learn.microsoft.com/dotnet/api/system.diagnostics.taglist?view=net-7.0#remarks) - avoids allocating any memory for up to eight tags, thereby, reducing the - pressure on GC to free up memory. +### Instruments + +:heavy_check_mark: You should understand and pick the right instrument type. + + > [!NOTE] + > .NET runtime has provided several instrument types based on the [OpenTelemetry + Specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument). + Picking the right instrument type for your use case is crucial to ensure the + correct semantics and performance. Check the [Instrument + Selection](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/supplementary-guidelines.md#instrument-selection) + section from the supplementary guidelines for more information. + + | OpenTelemetry Specification | .NET Instrument Type | + | --------------------------- | -------------------- | + | [Asynchronous Counter](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#asynchronous-counter) | [`ObservableCounter`](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.observablecounter-1) | + | [Asynchronous Gauge](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#asynchronous-gauge) | [`ObservableGauge`](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.observablegauge-1) | + | [Asynchronous UpDownCounter](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#asynchronous-updowncounter) | [`ObservableUpDownCounter`](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.observableupdowncounter-1) | + | [Counter](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#counter) | [`Counter`](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.counter-1) | + | [Gauge](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#gauge) (experimental) | N/A | + | [Histogram](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#histogram) | [`Histogram`](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.histogram-1) | + | [UpDownCounter](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#updowncounter) | [`UpDownCounter`](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.updowncounter-1) | + +:stop_sign: You should avoid creating instruments (e.g. `Counter`) too +frequently. Instruments are fairly expensive and meant to be reused throughout +the application. For most applications, instruments can be modeled as static +readonly fields (e.g. [Program.cs](./getting-started-console/Program.cs)) or +singleton via dependency injection (e.g. +[Instrumentation.cs](../../examples/AspNetCore/Instrumentation.cs)). + +:stop_sign: You should avoid invalid instrument names. + +> [!NOTE] +> OpenTelemetry will not collect metrics from instruments that are using invalid + names. Refer to the [OpenTelemetry + Specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument-name-syntax) + for the valid syntax. + +:stop_sign: You should avoid changing the order of tags while reporting +measurements. + +> [!WARNING] +> The last line of code has bad performance since the tags are not following + the same order: ```csharp -var tags = new TagList -{ - { "DimName1", "DimValue1" }, - { "DimName2", "DimValue2" }, - { "DimName3", "DimValue3" }, - { "DimName4", "DimValue4" }, -}; +counter.Add(2, new("name", "apple"), new("color", "red")); +counter.Add(3, new("name", "lime"), new("color", "green")); +counter.Add(5, new("name", "lemon"), new("color", "yellow")); +counter.Add(8, new("color", "yellow"), new("name", "lemon")); // bad perf +``` + +:heavy_check_mark: You should use TagList properly to achieve the best +performance. -// Uses a TagList as there are more than three tags -counter.Add(100, tags); // <--- DO THIS +There are two different ways of passing tags to an instrument API: +* Pass the tags directly to the instrument API: -// Avoid the below mentioned approaches when there are more than three tags -var tag1 = new KeyValuePair("DimName1", "DimValue1"); -var tag2 = new KeyValuePair("DimName2", "DimValue2"); -var tag3 = new KeyValuePair("DimName3", "DimValue3"); -var tag4 = new KeyValuePair("DimName4", "DimValue4"); + ```csharp + counter.Add(100, ("Key1", "Value1"), ("Key2", "Value2")); + ``` -counter.Add(100, tag1, tag2, tag3, tag4); // <--- DON'T DO THIS +* Use + [`TagList`](https://learn.microsoft.com/dotnet/api/system.diagnostics.taglist): -var readOnlySpanOfTags = new KeyValuePair[4] { tag1, tag2, tag3, tag4}; -counter.Add(100, readOnlySpanOfTags); // <--- DON'T DO THIS + ```csharp + var tags = new TagList + { + { "DimName1", "DimValue1" }, + { "DimName2", "DimValue2" }, + { "DimName3", "DimValue3" }, + { "DimName4", "DimValue4" }, + }; + + counter.Add(100, tags); + ``` + +Here is the rule of thumb: + +* When reporting measurements with 3 tags or less, pass the tags directly to the + instrument API. +* When reporting measurements with 4 to 8 tags (inclusive), use + [`TagList`](https://learn.microsoft.com/dotnet/api/system.diagnostics.taglist?#remarks) + to avoid heap allocation if avoiding GC pressure is a primary performance + goal. For high performance code which consider reducing CPU utilization more + important (e.g. to reduce latency, to save battery, etc.) than optimizing + memory allocations, use profiler and stress test to determine which approach + is better. + Here are some [metrics benchmark + results](../../test/Benchmarks/Metrics/MetricsBenchmarks.cs) for reference. +* When reporting measurements with more than 8 tags, the two approaches share + very similar CPU performance and heap allocation. `TagList` is recommended due + to its better readability and maintainability. + +> [!NOTE] +> When reporting measurements with more than 8 tags, the API allocates memory on + the hot code path. You SHOULD try to keep the number of tags less than or + equal to 8. If you are exceeding this, check if you can model some of the tags + as Resource, as [shown here](#metrics-enrichment). + +## MeterProvider Management + +:stop_sign: You should avoid creating `MeterProvider` instances too frequently, +`MeterProvider` is fairly expensive and meant to be reused throughout the +application. For most applications, one `MeterProvider` instance per process +would be sufficient. + +```mermaid +graph LR + +subgraph Meter A + InstrumentX +end + +subgraph Meter B + InstrumentY + InstrumentZ +end + +subgraph Meter Provider 2 + MetricReader2 + MetricExporter2 + MetricReader3 + MetricExporter3 +end + +subgraph Meter Provider 1 + MetricReader1 + MetricExporter1 +end + +InstrumentX --> | Measurements | MetricReader1 +InstrumentY --> | Measurements | MetricReader1 --> MetricExporter1 +InstrumentZ --> | Measurements | MetricReader2 --> MetricExporter2 +InstrumentZ --> | Measurements | MetricReader3 --> MetricExporter3 ``` -- When emitting metrics with more than eight tags, the SDK allocates memory on - the hot-path. You SHOULD try to keep the number of tags less than or equal to - eight. Check if you can extract any shared tags such as `MachineName`, - `Environment` etc. into `Resource` attributes. Refer to this - [doc](../../docs/metrics/customizing-the-sdk/README.md#resource) for more - information. +:heavy_check_mark: You should properly manage the lifecycle of `MeterProvider` +instances if they are created by you. + +Here is the rule of thumb when managing the lifecycle of `MeterProvider`: + +* If you are building an application with [dependency injection + (DI)](https://learn.microsoft.com/dotnet/core/extensions/dependency-injection) + (e.g. [ASP.NET Core](https://learn.microsoft.com/aspnet/core) and [.NET + Worker](https://learn.microsoft.com/dotnet/core/extensions/workers)), in most + cases you should create the `MeterProvider` instance and let DI manage its + lifecycle. Refer to the [Getting Started with OpenTelemetry .NET Metrics in 5 + Minutes - ASP.NET Core Application](./getting-started-aspnetcore/README.md) + tutorial to learn more. +* If you are building an application without DI, create a `MeterProvider` + instance and manage the lifecycle explicitly. Refer to the [Getting Started + with OpenTelemetry .NET Metrics in 5 Minutes - Console + Application](./getting-started-console/README.md) tutorial to learn more. +* If you forget to dispose the `MeterProvider` instance before the application + ends, metrics might get dropped due to the lack of proper flush. +* If you dispose the `MeterProvider` instance too early, any subsequent + measurements will not be collected. + +## Memory Management + +In OpenTelemetry, +[measurements](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#measurement) +are reported via the metrics API. The SDK +[aggregates](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#aggregation) +metrics using certain algorithms and memory management strategies to achieve +good performance and efficiency. Here are the rules which OpenTelemetry .NET +follows while implementing the metrics aggregation logic: + +1. [**Pre-Aggregation**](#pre-aggregation): aggregation occurs within the SDK. +2. [**Cardinality Limits**](#cardinality-limits): the aggregation logic respects + [cardinality + limits](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#cardinality-limits), + so the SDK does not use indefinite amount of memory when there is cardinality + explosion. +3. [**Memory Preallocation**](#memory-preallocation): the memory used by + aggregation logic is allocated during the SDK initialization, so the SDK does + not have to allocate memory on-the-fly. This is to avoid garbage collection + being triggered on the hot code path. + +### Example + +Let us take the following example: + +* During the time range (T0, T1]: + * value = 1, name = `apple`, color = `red` + * value = 2, name = `lemon`, color = `yellow` +* During the time range (T1, T2]: + * no fruit has been received +* During the time range (T2, T3]: + * value = 5, name = `apple`, color = `red` + * value = 2, name = `apple`, color = `green` + * value = 4, name = `lemon`, color = `yellow` + * value = 2, name = `lemon`, color = `yellow` + * value = 1, name = `lemon`, color = `yellow` + * value = 3, name = `lemon`, color = `yellow` + +If we aggregate and export the metrics using [Cumulative Aggregation +Temporality](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#temporality): + +* (T0, T1] + * attributes: {name = `apple`, color = `red`}, count: `1` + * attributes: {verb = `lemon`, color = `yellow`}, count: `2` +* (T0, T2] + * attributes: {name = `apple`, color = `red`}, count: `1` + * attributes: {verb = `lemon`, color = `yellow`}, count: `2` +* (T0, T3] + * attributes: {name = `apple`, color = `red`}, count: `6` + * attributes: {name = `apple`, color = `green`}, count: `2` + * attributes: {verb = `lemon`, color = `yellow`}, count: `12` + +If we aggregate and export the metrics using [Delta Aggregation +Temporality](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#temporality): + +* (T0, T1] + * attributes: {name = `apple`, color = `red`}, count: `1` + * attributes: {verb = `lemon`, color = `yellow`}, count: `2` +* (T1, T2] + * nothing since we do not have any measurement received +* (T2, T3] + * attributes: {name = `apple`, color = `red`}, count: `5` + * attributes: {name = `apple`, color = `green`}, count: `2` + * attributes: {verb = `lemon`, color = `yellow`}, count: `10` + +### Pre-Aggregation + +Taking the [fruit example](#example), there are 6 measurements reported during +`(T2, T3]`. Instead of exporting every individual measurement event, the SDK +aggregates them and only exports the summarized results. This approach, as +illustrated in the following diagram, is called pre-aggregation: + +```mermaid +graph LR + +subgraph SDK + Instrument --> | Measurements | Pre-Aggregation[Pre-Aggregation] +end + +subgraph Collector + Aggregation +end + +Pre-Aggregation --> | Metrics | Aggregation +``` + +Pre-aggregation brings several benefits: + +1. Although the amount of calculation remains the same, the amount of data + transmitted can be significantly reduced using pre-aggregation, thus + improving the overall efficiency. +2. Pre-aggregation makes it possible to apply [cardinality + limits](#cardinality-limits) during SDK initialization, combined with [memory + preallocation](#memory-preallocation), they make the metrics data collection + behavior more predictable (e.g. a server under denial-of-service attack would + still produce a constant volume of metrics data, rather than flooding the + observability system with large volume of measurement events). + +There are cases where users might want to export raw measurement events instead +of using pre-aggregation, as illustrated in the following diagram. OpenTelemetry +does not support this scenario at the moment, if you are interested, please join +the discussion by replying to this [feature +ask](https://github.com/open-telemetry/opentelemetry-specification/issues/617). + +```mermaid +graph LR + +subgraph SDK + Instrument +end + +subgraph Collector + Aggregation +end + +Instrument --> | Measurements | Aggregation +``` + +### Cardinality Limits + +The number of unique combinations of attributes is called cardinality. Taking +the [fruit example](#example), if we know that we can only have apple/lemon as +the name, red/yellow/green as the color, then we can say the cardinality is 6. +No matter how many apples and lemons we have, we can always use the following +table to summarize the total number of fruits based on the name and color. + +| Name | Color | Count | +| ----- | ------ | ----- | +| apple | red | 6 | +| apple | yellow | 0 | +| apple | green | 2 | +| lemon | red | 0 | +| lemon | yellow | 12 | +| lemon | green | 0 | + +In other words, we know how much storage and network are needed to collect and +transmit these metrics, regardless of the traffic pattern. + +In real world applications, the cardinality can be extremely high. Imagine if we +have a long running service and we collect metrics with 7 attributes and each +attribute can have 30 different values. We might eventually end up having to +remember the complete set of all 21,870,000,000 combinations! This cardinality +explosion is a well-known challenge in the metrics space. For example, it can +cause surprisingly high costs in the observability system, or even be leveraged +by hackers to launch a denial-of-service attack. + +[Cardinality +limit](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#cardinality-limits) +is a throttling mechanism which allows the metrics collection system to have a +predictable and reliable behavior when excessive cardinality happens, whether it +was due to a malicious attack or developer making mistakes while writing code. + +OpenTelemetry has a default cardinality limit of `2000` per metric. This limit +can be configured at the individual metric level using the [View +API](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view) +and the `MetricStreamConfiguration.CardinalityLimit` setting. Refer to this +[doc](../../docs/metrics/customizing-the-sdk/README.md#changing-the-cardinality-limit-for-a-metric) +for more information. + +Given a metric, once the cardinality limit is reached, any new measurement which +cannot be independently aggregated because of the limit will be dropped or +aggregated using the [overflow +attribute](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#overflow-attribute) +(if enabled). When NOT using the overflow attribute feature a warning is written +to the [self-diagnostic log](../../src/OpenTelemetry/README.md#self-diagnostics) +the first time an overflow is detected for a given metric. + +> [!NOTE] +> Overflow attribute was introduced in OpenTelemetry .NET + [1.6.0-rc.1](../../src/OpenTelemetry/CHANGELOG.md#160-rc1). It is currently an + experimental feature which can be turned on by setting the environment + variable `OTEL_DOTNET_EXPERIMENTAL_METRICS_EMIT_OVERFLOW_ATTRIBUTE=true`. Once + the [OpenTelemetry + Specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#overflow-attribute) + become stable, this feature will be turned on by default. + +When [Delta Aggregation +Temporality](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#temporality) +is used, it is possible to choose a smaller cardinality limit by allowing the +SDK to reclaim unused metric points. + +> [!NOTE] +> Reclaim unused metric points feature was introduced in OpenTelemetry .NET + [1.7.0-alpha.1](../../src/OpenTelemetry/CHANGELOG.md#170-alpha1). It is + currently an experimental feature which can be turned on by setting the + environment variable + `OTEL_DOTNET_EXPERIMENTAL_METRICS_RECLAIM_UNUSED_METRIC_POINTS=true`. Once the + [OpenTelemetry + Specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#overflow-attribute) + become stable, this feature will be turned on by default. + +### Memory Preallocation + +OpenTelemetry .NET SDK aims to avoid memory allocation on the hot code path. +When this is combined with [proper use of Metrics API](#metrics-api), heap +allocation can be avoided on the hot code path. Refer to the [metrics benchmark +results](../../test/Benchmarks/Metrics/MetricsBenchmarks.cs) to learn more. + +:heavy_check_mark: You should measure memory allocation on hot code path, and +ideally avoid any heap allocation while using the metrics API and SDK, +especially when you use metrics to measure the performance of your application +(for example, you do not want to spend 2 seconds doing [garbage +collection](https://learn.microsoft.com/dotnet/standard/garbage-collection/) +while measuring an operation which normally takes 10 milliseconds). + +## Metrics Correlation + +In OpenTelemetry, metrics can be correlated to [traces](../trace/README.md) via +[exemplars](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#exemplar). +Check the [Exemplars](./exemplars/README.md) tutorial to learn more. + +## Metrics Enrichment + +When metrics are being collected, they normally get stored in a [time series +database](https://en.wikipedia.org/wiki/Time_series_database). From storage and +consumption perspective, metrics can be multi-dimensional. Taking the [fruit +example](#example), there are two dimensions - "name" and "color". For basic +scenarios, all the dimensions can be reported during the [Metrics +API](#metrics-api) invocation, however, for less trivial scenarios, the +dimensions can come from different sources: + +* [Measurements](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#measurement) + reported via the [Metrics API](#metrics-api). +* Additional tags provided at instrument creation time. For example, the + [`Meter.CreateCounter(name, unit, description, + tags)`](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.meter.createcounter) + overload. +* Additional tags provided at meter creation time. For example, the + [`Meter(name, version, tags, + scope)`](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.meter.-ctor) + overload. +* [Resources](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md) + configured at the `MeterProvider` level. Refer to this + [doc](./customizing-the-sdk/README.md#resource) for details and examples. +* Additional attributes provided by the exporter or collector. For example, + [jobs and instances](https://prometheus.io/docs/concepts/jobs_instances/) in + Prometheus. + +> [!NOTE] +> Instrument level tags support is not yet implemented in OpenTelemetry .NET + since the [OpenTelemetry + Specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument) + does not support it. + +Here is the rule of thumb when modeling the dimensions: + +* If the dimension is static throughout the process lifetime (e.g. the name of + the machine, data center): + * If the dimension applies to all metrics, model it as Resource, or even + better, let the collector add these dimensions if feasible (e.g. a collector + running in the same data center should know the name of the data center, + rather than relying on / trusting each service instance to report the data + center name). + * If the dimension applies to a subset of metrics (e.g. the version of a + client library), model it as meter level tags. +* If the dimension value is dynamic, report it via the [Metrics + API](#metrics-api). + +> [!NOTE] +> There were discussions around adding a new concept called + `MeasurementProcessor`, which allows dimensions to be added to / removed from + measurements dynamically. This idea did not get traction due to the complexity + and performance implications, refer to this [pull + request](https://github.com/open-telemetry/opentelemetry-specification/pull/1938) + for more context. ## Common issues that lead to missing metrics -- The `Meter` used to create the instruments is not added to the +* The `Meter` used to create the instruments is not added to the `MeterProvider`. Use `AddMeter` method to enable the processing for the required metrics. -- Instrument name is invalid. When naming instruments, ensure that the name you - choose meets the criteria defined in the - [spec](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument-name-syntax). - A few notable characters that are not allowed in the instrument name: `/` - (forward slash), `\` (backward slash), any space character in the name. -- MetricPoint limit is reached. By default, the SDK limits the number of maximum - MetricPoints (unique combination of keys and values for a given Metric stream) - to `2000`. This limit can be configured using - `SetMaxMetricPointsPerMetricStream` method. Refer to this - [doc](../../docs/metrics/customizing-the-sdk/README.md#changing-maximum-metricpoints-per-metricstream) - for more information. The SDK would not process any newer unique key-value - combination that it encounters, once this limit is reached. -- MeterProvider is disposed. You need to ensure that the `MeterProvider` - instance is kept active for metrics to be collected. In a typical application, - a single MeterProvider is built at application startup, and is disposed of at - application shutdown. For an ASP.NET Core application, use `AddOpenTelemetry` - and `WithMetrics` methods from the `OpenTelemetry.Extensions.Hosting` package - to correctly setup `MeterProvider`. Here's a [sample ASP.NET Core - app](../../examples/AspNetCore/Program.cs) for reference. For simpler - applications such as Console apps, refer to this - [example](../../docs/metrics/getting-started-console/Program.cs). diff --git a/docs/metrics/customizing-the-sdk/Program.cs b/docs/metrics/customizing-the-sdk/Program.cs index c8b08c3cc5b..48c9a975311 100644 --- a/docs/metrics/customizing-the-sdk/Program.cs +++ b/docs/metrics/customizing-the-sdk/Program.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Metrics; using OpenTelemetry; @@ -29,7 +16,12 @@ public class Program public static void Main() { using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureResource(res => res.AddService("example-service")) + .ConfigureResource(resource => resource.AddAttributes(new List> + { + new KeyValuePair("static-attribute1", "v1"), + new KeyValuePair("static-attribute2", "v2"), + })) + .ConfigureResource(resource => resource.AddService("MyServiceName")) .AddMeter(Meter1.Name) .AddMeter(Meter2.Name) diff --git a/docs/metrics/customizing-the-sdk/README.md b/docs/metrics/customizing-the-sdk/README.md index aaa7acb909c..4cb57f11693 100644 --- a/docs/metrics/customizing-the-sdk/README.md +++ b/docs/metrics/customizing-the-sdk/README.md @@ -102,7 +102,7 @@ using var meterProvider = Sdk.CreateMeterProviderBuilder() See [Program.cs](./Program.cs) for complete example. -> **Note** +> [!NOTE] > A common mistake while configuring `MeterProvider` is forgetting to add the required `Meter`s to the provider. It is recommended to leverage the wildcard subscription model where it makes sense. For example, if your @@ -198,6 +198,7 @@ with the metric are of interest to you. MyFruitCounter.Add(1, new("name", "apple"), new("color", "red")); MyFruitCounter.Add(2, new("name", "lemon"), new("color", "yellow")); MyFruitCounter.Add(2, new("name", "apple"), new("color", "green")); + // Because "color" is dropped the resulting metric values are - name:apple LongSum Value:3 and name:lemon LongSum Value:2 ... // If you provide an empty `string` array as `TagKeys` to the `MetricStreamConfiguration` @@ -214,6 +215,7 @@ with the metric are of interest to you. MyFruitCounter.Add(1, new("name", "apple"), new("color", "red")); MyFruitCounter.Add(2, new("name", "lemon"), new("color", "yellow")); MyFruitCounter.Add(2, new("name", "apple"), new("color", "green")); + // Because both "name" and "color" are dropped the resulting metric value is - LongSum Value:5 ... ``` @@ -321,7 +323,7 @@ within the maximum number of buckets defined by `MaxSize`. The default }) ``` -> **Note** +> [!NOTE] > The SDK currently does not support any changes to `Aggregation` type by using Views. @@ -365,88 +367,28 @@ MyFruitCounter.Add(1, new("name", "apple"), new("color", "red")); AnotherFruitCounter.Add(1, new("name", "apple"), new("color", "red")); ``` -### Changing maximum MetricPoints per MetricStream - -A Metric stream can contain as many Metric points as the number of unique -combination of keys and values. To protect the SDK from unbounded memory usage, -SDK limits the maximum number of metric points per metric stream, to a default -of 2000. Once the limit is hit, any new key/value combination for that metric is -ignored. The SDK chooses the key/value combinations in the order in which they -are emitted. `SetMaxMetricPointsPerMetricStream` can be used to override the -default. - -> **Note** -> One `MetricPoint` is reserved for every `MetricStream` for the -special case where there is no key/value pair associated with the metric. The -maximum number of `MetricPoint`s has to accommodate for this special case. - -Consider the below example. Here we set the maximum number of `MetricPoint`s -allowed to be `3`. This means that for every `MetricStream`, the SDK will export -measurements for up to `3` distinct key/value combinations of the metric. There -are two instruments published here: `MyFruitCounter` and `AnotherFruitCounter`. -There are two total `MetricStream`s created one for each of these instruments. -SDK will limit the maximum number of distinct key/value combinations for each of -these `MetricStream`s to `3`. +### Changing the cardinality limit for a Metric -```csharp -using System.Collections.Generic; -using System.Diagnostics.Metrics; -using OpenTelemetry; -using OpenTelemetry.Metrics; +To set the [cardinality limit](../README.md#cardinality-limits) for an +individual metric, use `MetricStreamConfiguration.CardinalityLimit` setting on +the View API: -Counter MyFruitCounter = MyMeter.CreateCounter("MyFruitCounter"); -Counter AnotherFruitCounter = MyMeter.CreateCounter("AnotherFruitCounter"); +> [!NOTE] +> `MetricStreamConfiguration.CardinalityLimit` is an experimental API only + available in pre-release builds. For details see: + [OTEL1003](../../diagnostics/experimental-apis/OTEL1003.md). -using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter("*") +```csharp +var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter("MyCompany.MyProduct.MyLibrary") + // Set a custom CardinalityLimit (10) for "MyFruitCounter" + .AddView( + instrumentName: "MyFruitCounter", + new MetricStreamConfiguration { CardinalityLimit = 10 }) .AddConsoleExporter() - .SetMaxMetricPointsPerMetricStream(3) // The default value is 2000 .Build(); - -// There are four distinct key/value combinations emitted for `MyFruitCounter`: -// 1. No key/value pair -// 2. (name:apple, color:red) -// 3. (name:lemon, color:yellow) -// 4. (name:apple, color:green) - -// Since the maximum number of `MetricPoint`s allowed is `3`, the SDK will only export measurements for the following three combinations: -// 1. No key/value pair -// 2. (name:apple, color:red) -// 3. (name:lemon, color:yellow) - -MyFruitCounter.Add(1); // Exported (No key/value pair) -MyFruitCounter.Add(1, new("name", "apple"), new("color", "red")); // Exported -MyFruitCounter.Add(2, new("name", "lemon"), new("color", "yellow")); // Exported -MyFruitCounter.Add(1, new("name", "lemon"), new("color", "yellow")); // Exported -MyFruitCounter.Add(2, new("name", "apple"), new("color", "green")); // Not exported -MyFruitCounter.Add(5, new("name", "apple"), new("color", "red")); // Exported -MyFruitCounter.Add(4, new("name", "lemon"), new("color", "yellow")); // Exported - -// There are four distinct key/value combinations emitted for `AnotherFruitCounter`: -// 1. (name:kiwi) -// 2. (name:banana, color:yellow) -// 3. (name:mango, color:yellow) -// 4. (name:banana, color:green) - -// Since the maximum number of `MetricPoint`s allowed is `3`, the SDK will only export measurements for the following three combinations: -// 1. No key/value pair (This is a special case. The SDK reserves a `MetricPoint` for it even if it's not explicitly emitted.) -// 2. (name:kiwi) -// 3. (name:banana, color:yellow) - -AnotherFruitCounter.Add(4, new KeyValuePair("name", "kiwi")); // Exported -AnotherFruitCounter.Add(1, new("name", "banana"), new("color", "yellow")); // Exported -AnotherFruitCounter.Add(2, new("name", "mango"), new("color", "yellow")); // Not exported -AnotherFruitCounter.Add(1, new("name", "mango"), new("color", "yellow")); // Not exported -AnotherFruitCounter.Add(2, new("name", "banana"), new("color", "green")); // Not exported -AnotherFruitCounter.Add(5, new("name", "banana"), new("color", "yellow")); // Exported -AnotherFruitCounter.Add(4, new("name", "mango"), new("color", "yellow")); // Not exported ``` -> **Note** -> The above limit is *per* metric stream, and applies to all the metric -streams. There is no ability to apply different limits for each instrument at -this moment. - ### Exemplars Exemplars are example data points for aggregated data. They provide access to @@ -470,26 +412,23 @@ exemplars. #### ExemplarFilter -`ExemplarFilter` determines which measurements are eligible to become an -Exemplar. i.e. `ExemplarFilter` determines which measurements are offered to -`ExemplarReservoir`, which makes the final decision about whether the offered -measurement gets stored as an exemplar. They can be used to control the noise -and overhead associated with Exemplar collection. +`ExemplarFilter` determines which measurements are offered to the configured +`ExemplarReservoir`, which makes the final decision about whether or not the +offered measurement gets recorded as an `Exemplar`. Generally `ExemplarFilter` +is a mechanism to control the overhead associated with `Exemplar` offering. -OpenTelemetry SDK comes with the following Filters: +OpenTelemetry SDK comes with the following `ExemplarFilters` (defined on +`ExemplarFilterType`): -* `AlwaysOnExemplarFilter` - makes all measurements eligible for being an Exemplar. -* `AlwaysOffExemplarFilter` - makes no measurements eligible for being an - Exemplar. Using this is as good as turning off Exemplar feature, and is the current +* `AlwaysOff`: Makes no measurements eligible for becoming an `Exemplar`. Using + this is as good as turning off the `Exemplar` feature and is the current default. -* `TraceBasedExemplarFilter` - makes those measurements eligible for being an -Exemplar, which are recorded in the context of a sampled parent `Activity` -(span). - -`SetExemplarFilter` method on `MeterProviderBuilder` can be used to set the -desired `ExemplarFilter`. +* `AlwaysOn`: Makes all measurements eligible for becoming an `Exemplar`. +* `TraceBased`: Makes those measurements eligible for becoming an `Exemplar` + which are recorded in the context of a sampled `Activity` (span). -The snippet below shows how to set `ExemplarFilter`. +The `SetExemplarFilter` extension method on `MeterProviderBuilder` can be used +to set the desired `ExemplarFilterType` and enable `Exemplar` collection: ```csharp using OpenTelemetry; @@ -497,31 +436,14 @@ using OpenTelemetry.Metrics; using var meterProvider = Sdk.CreateMeterProviderBuilder() // rest of config not shown - .SetExemplarFilter(new TraceBasedExemplarFilter()) + .SetExemplarFilter(ExemplarFilterType.TraceBased) .Build(); ``` -> **Note** -> As of today, there is no separate toggle for enable/disable Exemplar feature. -Exemplars can be disabled by setting filter as `AlwaysOffExemplarFilter`, which -is also the default (i.e Exemplar feature is disabled by default). Users can -enable the feature by setting filter to anything other than -`AlwaysOffExemplarFilter`. For example: `.SetExemplarFilter(new TraceBasedExemplarFilter())`. - -If the built-in `ExemplarFilter`s are not meeting the needs, one may author -custom `ExemplarFilter` as shown -[here](../extending-the-sdk/README.md#exemplarfilter). A custom filter, which -eliminates all un-interesting measurements from becoming Exemplar is a -recommended way to control performance overhead associated with collecting -Exemplars. See -[benchmark](../../../test/Benchmarks/Metrics/ExemplarBenchmarks.cs) to see how -much impact can `ExemplarFilter` have on performance. - #### ExemplarReservoir -`ExemplarReservoir` receives the measurements sampled in by the `ExemplarFilter` -and is responsible for storing Exemplars. `ExemplarReservoir` ultimately decides -which measurements get stored as exemplars. The following are the default +`ExemplarReservoir` receives the measurements sampled by the `ExemplarFilter` +and is responsible for recording `Exemplar`s. The following are the default reservoirs: * `AlignedHistogramBucketExemplarReservoir` is the default reservoir used for @@ -529,15 +451,15 @@ Histograms with buckets, and it stores at most one exemplar per histogram bucket. The exemplar stored is the last measurement recorded - i.e. any new measurement overwrites the previous one in that bucket. -`SimpleExemplarReservoir` is the default reservoir used for all metrics except -Histograms with buckets. It has a fixed reservoir pool, and implements the -equivalent of [naive +* `SimpleFixedSizeExemplarReservoir` is the default reservoir used for all +metrics except Histograms with buckets. It has a fixed reservoir pool, and +implements the equivalent of [naive reservoir](https://en.wikipedia.org/wiki/Reservoir_sampling). The reservoir pool -size (currently defaulting to 10) determines the maximum number of exemplars +size (currently defaulting to 1) determines the maximum number of exemplars stored. -> **Note** -> Currently there is no ability to change or configure Reservoir. +> [!NOTE] +> Currently there is no ability to change or configure `ExemplarReservoir`. ### Instrumentation @@ -570,17 +492,32 @@ Refer to the individual exporter docs to learn how to use them: [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](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/README.md#semantic-attributes-with-sdk-provided-default-value) +[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/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/README.md#service). -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`. -There can only be a single `Resource` associated with a -provider. It is not possible to change the resource builder *after* the provider -is built, by calling the `Build()` method on the `MeterProviderBuilder`. +[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 +configure the resource on the provider. `ConfigureResource` accepts an `Action` +to configure the `ResourceBuilder`. Multiple calls to `ConfigureResource` can be +made. When the provider is built, it builds the final `Resource` combining all +the `ConfigureResource` calls. There can only be a single `Resource` associated +with a provider. It is not possible to change the resource builder *after* the +provider is built, by calling the `Build()` method on the +`MeterProviderBuilder`. + `ResourceBuilder` offers various methods to construct resource comprising of -multiple attributes from various sources. +attributes from various sources. For example, `AddService()` adds +[Service](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md#service) +resource. `AddAttributes` can be used to add any additional attributes to the +`Resource`. It also allows adding `ResourceDetector`s. + +It is recommended to model attributes that are static throughout the lifetime of +the process as Resources, instead of adding them as attributes(tags) on each +measurement. + +Follow [this](../../resources/README.md#resource-detector) document +to learn about writing custom resource detectors. The snippet below shows configuring the `Resource` associated with the provider. @@ -590,7 +527,12 @@ using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureResource(r => r.AddService("MyServiceName")) + .ConfigureResource(r => r.AddAttributes(new List> + { + new KeyValuePair("static-attribute1", "v1"), + new KeyValuePair("static-attribute2", "v2"), + })) + .ConfigureResource(resourceBuilder => resourceBuilder.AddService("service-name")) .Build(); ``` diff --git a/docs/metrics/exemplars/docker-compose.yaml b/docs/metrics/exemplars/docker-compose.yaml index 87cd7a6c6d6..c8cc94fa4b1 100644 --- a/docs/metrics/exemplars/docker-compose.yaml +++ b/docs/metrics/exemplars/docker-compose.yaml @@ -48,4 +48,3 @@ services: - GF_FEATURE_TOGGLES_ENABLE=traceqlEditor ports: - "3000:3000" - diff --git a/docs/metrics/extending-the-sdk/MyExporter.cs b/docs/metrics/extending-the-sdk/MyExporter.cs index 36cb6cc4663..228d3e0c342 100644 --- a/docs/metrics/extending-the-sdk/MyExporter.cs +++ b/docs/metrics/extending-the-sdk/MyExporter.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Text; using OpenTelemetry; diff --git a/docs/metrics/extending-the-sdk/MyExporterExtensions.cs b/docs/metrics/extending-the-sdk/MyExporterExtensions.cs index bc1ea34eae0..bb5cb6deff6 100644 --- a/docs/metrics/extending-the-sdk/MyExporterExtensions.cs +++ b/docs/metrics/extending-the-sdk/MyExporterExtensions.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Metrics; diff --git a/docs/metrics/extending-the-sdk/Program.cs b/docs/metrics/extending-the-sdk/Program.cs index 469a9aff087..a2952062261 100644 --- a/docs/metrics/extending-the-sdk/Program.cs +++ b/docs/metrics/extending-the-sdk/Program.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Diagnostics.Metrics; diff --git a/docs/metrics/extending-the-sdk/README.md b/docs/metrics/extending-the-sdk/README.md index 4c409961d86..c7293ac418a 100644 --- a/docs/metrics/extending-the-sdk/README.md +++ b/docs/metrics/extending-the-sdk/README.md @@ -4,6 +4,7 @@ * [Building your own reader](#reader) * [Building your own exemplar filter](#exemplarfilter) * [Building your own exemplar reservoir](#exemplarreservoir) +* [Building your own resource detector](../../resources/README.md#resource-detector) * [References](#references) ## Exporter @@ -73,44 +74,7 @@ Not supported. ## ExemplarFilter -OpenTelemetry .NET SDK has provided the following built-in `ExemplarFilter`s: - -* [AlwaysOnExemplarFilter](../../../src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs) -* [AlwaysOffExemplarFilter](../../../src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs) -* [TraceBasedExemplarFilter](../../../src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs) - -Custom exemplar filters can be implemented to achieve filtering based on other criterion: - -* `ExemplarFilter` should derive from `OpenTelemetry.ExemplarFilter` (which - belongs to the [OpenTelemetry](../../../src/OpenTelemetry/README.md) package) - and implement the `ShouldSample` method. - -One example is a filter, which filters all measurements of value lower -than given threshold is given below. Such a filter prevents any measurements -below the given threshold from ever becoming a `Exemplar`. Such filters could -also incorporate the `TraceBasedExemplarFilter` condition as well, as storing -exemplars for non-sampled traces may be undesired. - -```csharp -public sealed class HighValueFilter : ExemplarFilter -{ - private readonly double maxValue; - - public HighValueFilter(double maxValue) - { - this.maxValue = maxValue; - } - public override bool ShouldSample(long value, ReadOnlySpan> tags) - { - return Activity.Current?.Recorded && value > this.maxValue; - } - - public override bool ShouldSample(double value, ReadOnlySpan> tags) - { - return Activity.Current?.Recorded && value > this.maxValue; - } -} -``` +Not supported. ## ExemplarReservoir diff --git a/docs/metrics/getting-started-aspnetcore/Program.cs b/docs/metrics/getting-started-aspnetcore/Program.cs index 835703f0c81..3c38b73c16a 100644 --- a/docs/metrics/getting-started-aspnetcore/Program.cs +++ b/docs/metrics/getting-started-aspnetcore/Program.cs @@ -1,20 +1,6 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -using System.Diagnostics; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; diff --git a/docs/metrics/getting-started-aspnetcore/README.md b/docs/metrics/getting-started-aspnetcore/README.md index 5b76615e4ff..7f202931b90 100644 --- a/docs/metrics/getting-started-aspnetcore/README.md +++ b/docs/metrics/getting-started-aspnetcore/README.md @@ -20,15 +20,9 @@ packages: ```sh dotnet add package OpenTelemetry.Exporter.Console dotnet add package OpenTelemetry.Extensions.Hosting -dotnet add package OpenTelemetry.Instrumentation.AspNetCore --prerelease +dotnet add package OpenTelemetry.Instrumentation.AspNetCore ``` -> **Note** This quickstart guide uses prerelease packages. For a quickstart -> which only relies on stable packages see: [Getting Started - Console -> Application](../getting-started-console/README.md). For more information about -> when instrumentation will be marked as stable see: [Instrumentation-1.0.0 -> milestone](https://github.com/open-telemetry/opentelemetry-dotnet/milestone/23). - Update the `Program.cs` file with the code from [Program.cs](./Program.cs). Run the application again (using `dotnet run`) and then browse to the url shown @@ -84,7 +78,7 @@ appBuilder.Services.AddOpenTelemetry() ); ``` -> **Note** +> [!NOTE] > The `AddOpenTelemetry` extension is part of the [OpenTelemetry.Extensions.Hosting](../../../src/OpenTelemetry.Extensions.Hosting/README.md) package. diff --git a/docs/metrics/getting-started-aspnetcore/getting-started-aspnetcore.csproj b/docs/metrics/getting-started-aspnetcore/getting-started-aspnetcore.csproj index 1956f427004..375079fc34c 100644 --- a/docs/metrics/getting-started-aspnetcore/getting-started-aspnetcore.csproj +++ b/docs/metrics/getting-started-aspnetcore/getting-started-aspnetcore.csproj @@ -1,15 +1,7 @@ - - - net6.0;net7.0 - enable - enable - - - diff --git a/docs/metrics/getting-started-console/Program.cs b/docs/metrics/getting-started-console/Program.cs index 4911d3ce73c..89425c6d41c 100644 --- a/docs/metrics/getting-started-console/Program.cs +++ b/docs/metrics/getting-started-console/Program.cs @@ -1,25 +1,10 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Metrics; using OpenTelemetry; using OpenTelemetry.Metrics; -namespace GettingStarted; - public class Program { private static readonly Meter MyMeter = new("MyCompany.MyProduct.MyLibrary", "1.0"); @@ -27,16 +12,23 @@ public class Program public static void Main() { - using var meterProvider = Sdk.CreateMeterProviderBuilder() + var meterProvider = Sdk.CreateMeterProviderBuilder() .AddMeter("MyCompany.MyProduct.MyLibrary") .AddConsoleExporter() .Build(); + // In this example, we have low cardinality which is below the 2000 + // default limit. If you have high cardinality, you need to set the + // cardinality limit properly. MyFruitCounter.Add(1, new("name", "apple"), new("color", "red")); MyFruitCounter.Add(2, new("name", "lemon"), new("color", "yellow")); MyFruitCounter.Add(1, new("name", "lemon"), new("color", "yellow")); MyFruitCounter.Add(2, new("name", "apple"), new("color", "green")); MyFruitCounter.Add(5, new("name", "apple"), new("color", "red")); MyFruitCounter.Add(4, new("name", "lemon"), new("color", "yellow")); + + // Dispose meter provider before the application ends. + // This will flush the remaining metrics and shutdown the metrics pipeline. + meterProvider.Dispose(); } } diff --git a/docs/metrics/getting-started-console/README.md b/docs/metrics/getting-started-console/README.md index c73961de86f..3f30652b313 100644 --- a/docs/metrics/getting-started-console/README.md +++ b/docs/metrics/getting-started-console/README.md @@ -69,19 +69,33 @@ MyFruitCounter.Add(2, new("name", "lemon"), new("color", "yellow")); MyFruitCounter.Add(1, new("name", "lemon"), new("color", "yellow")); ``` -An OpenTelemetry -[MeterProvider](#meterprovider) -is configured to subscribe to instruments from the Meter -`MyCompany.MyProduct.MyLibrary`, and aggregate the measurements in-memory. The +An OpenTelemetry [MeterProvider](#meterprovider) is configured to subscribe to +an instrument named "MyFruitCounter" from the Meter +`MyCompany.MyProduct.MyLibrary`, and aggregate the measurements in-memory with a +default [cardinality limit](../README.md#cardinality-limits) of `2000`. The pre-aggregated metrics are exported to a `ConsoleExporter`. ```csharp -using var meterProvider = Sdk.CreateMeterProviderBuilder() +var meterProvider = Sdk.CreateMeterProviderBuilder() .AddMeter("MyCompany.MyProduct.MyLibrary") .AddConsoleExporter() .Build(); ``` +> [!NOTE] +> If you need to collect metrics with cardinality higher than the default limit + `2000`, please follow the [cardinality + limits](../README.md#cardinality-limits) guidance. Here is a quick example of + how to change the cardinality limit to `10` for this particular metric: + + ```csharp + var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter("MyCompany.MyProduct.MyLibrary") + .AddView(instrumentName: "MyFruitCounter", new MetricStreamConfiguration { CardinalityLimit = 10 }) + .AddConsoleExporter() + .Build(); + ``` + ```mermaid graph LR diff --git a/docs/metrics/getting-started-prometheus-grafana/Program.cs b/docs/metrics/getting-started-prometheus-grafana/Program.cs index 1920698f308..0fd2437eefd 100644 --- a/docs/metrics/getting-started-prometheus-grafana/Program.cs +++ b/docs/metrics/getting-started-prometheus-grafana/Program.cs @@ -1,25 +1,11 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Metrics; using OpenTelemetry; +using OpenTelemetry.Exporter; using OpenTelemetry.Metrics; -namespace GettingStartedPrometheusGrafana; - public class Program { private static readonly Meter MyMeter = new("MyCompany.MyProduct.MyLibrary", "1.0"); @@ -27,21 +13,32 @@ public class Program public static void Main() { - using var meterProvider = Sdk.CreateMeterProviderBuilder() + var meterProvider = Sdk.CreateMeterProviderBuilder() .AddMeter("MyCompany.MyProduct.MyLibrary") - .AddPrometheusHttpListener() + .AddOtlpExporter((exporterOptions, metricReaderOptions) => + { + exporterOptions.Endpoint = new Uri("http://localhost:9090/api/v1/otlp/v1/metrics"); + exporterOptions.Protocol = OtlpExportProtocol.HttpProtobuf; + metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 1000; + }) .Build(); Console.WriteLine("Press any key to exit"); + while (!Console.KeyAvailable) { - Thread.Sleep(1000); MyFruitCounter.Add(1, new("name", "apple"), new("color", "red")); MyFruitCounter.Add(2, new("name", "lemon"), new("color", "yellow")); MyFruitCounter.Add(1, new("name", "lemon"), new("color", "yellow")); MyFruitCounter.Add(2, new("name", "apple"), new("color", "green")); MyFruitCounter.Add(5, new("name", "apple"), new("color", "red")); MyFruitCounter.Add(4, new("name", "lemon"), new("color", "yellow")); + + Thread.Sleep(300); } + + // Dispose meter provider before the application ends. + // This will flush the remaining metrics and shutdown the metrics pipeline. + meterProvider.Dispose(); } } diff --git a/docs/metrics/getting-started-prometheus-grafana/README.md b/docs/metrics/getting-started-prometheus-grafana/README.md index 69e87c9f793..7f39b121a72 100644 --- a/docs/metrics/getting-started-prometheus-grafana/README.md +++ b/docs/metrics/getting-started-prometheus-grafana/README.md @@ -1,10 +1,8 @@ # Getting Started with Prometheus and Grafana - [Export metrics from the application](#export-metrics-from-the-application) - - [Check results in the browser](#check-results-in-the-browser) - [Collect metrics using Prometheus](#collect-metrics-using-prometheus) - - [Configuration](#configuration) - - [Start Prometheus](#start-prometheus) + - [Install and run Prometheus](#install-and-run-prometheus) - [View results in Prometheus](#view-results-in-prometheus) - [Explore metrics using Grafana](#explore-metrics-using-grafana) - [Learn more](#learn-more) @@ -18,43 +16,25 @@ this document. Create a new console application and run it: ```sh -dotnet new console --output getting-started-prometheus -cd getting-started-prometheus +dotnet new console --output getting-started-prometheus-grafana +cd getting-started-prometheus-grafana dotnet run ``` -Add a reference to [Prometheus -Exporter Http Listener](../../../src/OpenTelemetry.Exporter.Prometheus.HttpListener/README.md): +Add reference to [OTLP +Exporter](../../../src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md): ```sh -dotnet add package OpenTelemetry.Exporter.Prometheus.HttpListener --prerelease +dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol ``` -Now, we are going to make some small tweaks to the example in the -getting-started metrics `Program.cs` to make the metrics available via -OpenTelemetry Prometheus Exporter. +Now copy the code from [Program.cs](./Program.cs) and run the application again. -First, copy and paste everything from getting-started metrics -[example](../getting-started-console/Program.cs) to the Program.cs file of the -new console application (getting-started-prometheus) we've created. - -And replace the below line: - -```csharp -.AddConsoleExporter() -``` - -with - -```csharp -.AddPrometheusHttpListener() -``` - -`PrometheusHttpListener` is a wrapper that contains `PrometheusExporter`. With -`AddPrometheusHttpListener()`, OpenTelemetry `PrometheusExporter` will export -data via the endpoint defined by -[PrometheusHttpListenerOptions.UriPrefixes](../../../src/OpenTelemetry.Exporter.Prometheus.HttpListener/README.md#uriprefixes), -which is `http://localhost:9464/` by default. +When we ran the application, the OTLP Exporter was attempting to export the +metrics to `http://localhost:9090/api/v1/otlp/v1/metrics`. Since Prometheus +server was not running, the metrics received by `OtlpExporter` were simply +dropped on the floor. In the next step, we are going to learn about how to use +Prometheus to collect and visualize the metrics. ```mermaid graph LR @@ -62,7 +42,7 @@ graph LR subgraph SDK MeterProvider MetricReader[BaseExportingMetricReader] - PrometheusHttpListener["PrometheusHttpListener
(http://localhost:9464/)"] + OtlpExporter end subgraph API @@ -71,7 +51,7 @@ end Instrument --> | Measurements | MeterProvider -MeterProvider --> | Metrics | MetricReader --> | Pull | PrometheusHttpListener +MeterProvider --> | Metrics | MetricReader --> | Push | OtlpExporter --> | HTTP/protobuf | PrometheusServer[Prometheus server] ``` Also, for our learning purpose, use a while-loop to keep increasing the counter @@ -79,86 +59,48 @@ value until any key is pressed. ```csharp Console.WriteLine("Press any key to exit"); + while (!Console.KeyAvailable) { - Thread.Sleep(1000); MyFruitCounter.Add(1, new("name", "apple"), new("color", "red")); MyFruitCounter.Add(2, new("name", "lemon"), new("color", "yellow")); - MyFruitCounter.Add(1, new("name", "lemon"), new("color", "yellow")); - ... - ... ... + + Thread.Sleep(300); } ``` -After the above modifications, now our `Program.cs` should look like [this](./Program.cs). - -### Check results in the browser - -Start the application and keep it running. Now we should be able to see the -metrics at [http://localhost:9464/metrics](http://localhost:9464/metrics) from a -web browser: - -![Browser UI](https://user-images.githubusercontent.com/17327289/151633547-736c6d91-62d2-4e66-a53f-2e16c44bfabc.png) - -Now, we understand how we can configure `PrometheusHttpListener` to export metrics. -Next, we are going to learn about how to use Prometheus to collect the metrics. - ## Collect metrics using Prometheus +### Install and run Prometheus + Follow the [first steps](https://prometheus.io/docs/introduction/first_steps/) to download the [latest release](https://prometheus.io/download/) of Prometheus. -### Configuration - After finished downloading, extract it to a local location that's easy to -access. We will find the default Prometheus configuration YAML file in the -folder, named `prometheus.yml`. - -Let's create a new file in the same location as where `prometheus.yml` locates, -and named the new file as `otel.yml` for this exercise. Then, copy and paste the -entire content below into the `otel.yml` file we have created just now. - -```yaml -global: - scrape_interval: 10s - evaluation_interval: 10s -scrape_configs: - - job_name: "otel" - static_configs: - - targets: ["localhost:9464"] -``` +access. Run the `prometheus(.exe)` server executable with feature flag +[otlp-receiver](https://prometheus.io/docs/prometheus/latest/feature_flags/#otlp-receiver) +enabled: -### Start Prometheus - -Follow the instructions from -[starting-prometheus](https://prometheus.io/docs/introduction/first_steps/#starting-prometheus) -to start the Prometheus server and verify it has been started successfully. - -Please note that we will need pass in `otel.yml` file as the argument: - -```console -./prometheus --config.file=otel.yml +```sh +./prometheus --enable-feature=otlp-write-receiver ``` ### View results in Prometheus To use the graphical interface for viewing our metrics with Prometheus, navigate to [http://localhost:9090/graph](http://localhost:9090/graph), and type -`MyFruitCounter` in the expression bar of the UI; finally, click the execute -button. +`MyFruitCounter_total` in the expression bar of the UI; finally, click the +execute button. We should be able to see the following chart from the browser: ![Prometheus UI](https://user-images.githubusercontent.com/17327289/151636225-6e4ce4c7-09f3-4996-8ca5-d404a88d9195.png) -From the legend, we can see that the `instance` name and the `job` name are the -values we have set in `otel.yml`. - Congratulations! Now we know how to configure Prometheus server and deploy OpenTelemetry -`PrometheusHttpListener` to export our metrics. Next, we are going to explore a tool +`OtlpExporter` to export our metrics. Next, we are going to explore a tool called Grafana, which has powerful visualizations for the metrics. ## Explore metrics using Grafana @@ -191,28 +133,15 @@ Feel free to find some handy PromQL [here](https://promlabs.com/promql-cheat-sheet/). In the below example, the query targets to find out what is the per-second rate -of increase of myFruitCounter over the past 5 minutes: +of increase of `MyFruitCounter_total` over the past 5 minutes: ![Grafana UI](https://user-images.githubusercontent.com/17327289/151636769-138ecb4f-b44f-477b-88eb-247fc4340252.png) -```mermaid -graph TD - -subgraph Prometheus - PrometheusScraper - PrometheusDatabase -end - -PrometheusHttpListener["PrometheusHttpListener
(listening at #quot;http://localhost:9464/#quot;)"] -->|HTTP GET| PrometheusScraper{{"Prometheus scraper
(polling #quot;http://localhost:9464/metrics#quot; every 10 seconds)"}} -PrometheusScraper --> PrometheusDatabase[("Prometheus TSDB (time series database)")] -PrometheusDatabase -->|http://localhost:9090/graph| PrometheusUI["Browser
(Prometheus Dashboard)"] -PrometheusDatabase -->|http://localhost:9090/api/| Grafana[Grafana Server] -Grafana -->|http://localhost:3000/dashboard| GrafanaUI["Browser
(Grafana Dashboard)"] -``` - ## Learn more - [What is Prometheus?](https://prometheus.io/docs/introduction/overview/) +- [Prometheus now supports OpenTelemetry + Metrics](https://horovits.medium.com/prometheus-now-supports-opentelemetry-metrics-83f85878e46a) - [Grafana support for Prometheus](https://prometheus.io/docs/visualization/grafana/#creating-a-prometheus-graph) diff --git a/docs/metrics/getting-started-prometheus-grafana/getting-started-prometheus-grafana.csproj b/docs/metrics/getting-started-prometheus-grafana/getting-started-prometheus-grafana.csproj index 8d59ff99ce3..cce12eec60d 100644 --- a/docs/metrics/getting-started-prometheus-grafana/getting-started-prometheus-grafana.csproj +++ b/docs/metrics/getting-started-prometheus-grafana/getting-started-prometheus-grafana.csproj @@ -1,5 +1,5 @@ - + diff --git a/docs/metrics/learning-more-instruments/Program.cs b/docs/metrics/learning-more-instruments/Program.cs index 40b36c11500..c887e281461 100644 --- a/docs/metrics/learning-more-instruments/Program.cs +++ b/docs/metrics/learning-more-instruments/Program.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Diagnostics.Metrics; diff --git a/docs/resources/README.md b/docs/resources/README.md new file mode 100644 index 00000000000..059acbc063d --- /dev/null +++ b/docs/resources/README.md @@ -0,0 +1,109 @@ +# Resources + +## Resource Detector + +OpenTelemetry .NET SDK provides a resource detector for detecting resource +information from the `OTEL_RESOURCE_ATTRIBUTES` and `OTEL_SERVICE_NAME` +environment variables. + +Custom resource detectors can be implemented: + +* ResourceDetectors should inherit from + `OpenTelemetry.Resources.IResourceDetector`, (which belongs to the + [OpenTelemetry](../../src/OpenTelemetry/README.md) package), and implement + the `Detect` method. + +A demo `ResourceDetector` is shown [here](./extending-the-sdk/MyResourceDetector.cs): + +```csharp +using OpenTelemetry.Resources; + +internal class MyResourceDetector : IResourceDetector +{ + public Resource Detect() + { + var attributes = new List> + { + new KeyValuePair("key", "val"), + }; + + return new Resource(attributes); + } +} +``` + +There are two different ways to add the custom `ResourceDetector` to the +OTEL signals, via the `Sdk.Create` approach: + +```csharp +using System.Diagnostics; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +namespace ExtendingTheSdk; + +public class Program +{ + private static readonly ActivitySource DemoSource = new("OTel.Demo"); + private static readonly Meter MeterDemoSource = new("OTel.Demo"); + + public static void Main() + { + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("OTel.Demo") + .SetResourceBuilder(ResourceBuilder.CreateEmpty().AddDetector( + new MyResourceDetector())) + .Build(); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .SetResourceBuilder(ResourceBuilder.CreateEmpty().AddDetector( + new MyResourceDetector())) + .Build(); + + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddOpenTelemetry(options => + { + options.SetResourceBuilder(ResourceBuilder + .CreateDefault().AddDetector( + new MyResourceDetector())); + }); + }); + + using (var foo = DemoSource.StartActivity("Foo")) + { + using (var bar = DemoSource.StartActivity("Bar")) + { + using (var baz = DemoSource.StartActivity("Baz")) + { + } + } + } + + var counter = MeterDemoSource.CreateCounter("counter"); + for (var i = 0; i < 20000; i++) + counter.Add(1, new("tag1", "value1"), new("tag2", "value2")); + + var logger = loggerFactory.CreateLogger("OTel.Demo"); + logger + .LogInformation("Hello from {name} {price}.", "tomato", 2.99); + } +} +``` + +or via `OpenTelemetry.Extensions.Hosting` method: + +```csharp + services.AddSingleton(); + + services.AddOpenTelemetry() + .ConfigureResource(builder => builder + .AddDetector(sp => + sp.GetRequiredService())) + .WithTracing(builder => builder.AddConsoleExporter()) + .WithMetrics(builder => builder.AddConsoleExporter()); +``` diff --git a/docs/resources/extending-the-sdk/MyResourceDetector.cs b/docs/resources/extending-the-sdk/MyResourceDetector.cs new file mode 100644 index 00000000000..df40d9b3302 --- /dev/null +++ b/docs/resources/extending-the-sdk/MyResourceDetector.cs @@ -0,0 +1,17 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Resources; + +internal class MyResourceDetector : IResourceDetector +{ + public Resource Detect() + { + var attributes = new List> + { + new KeyValuePair("key", "val"), + }; + + return new Resource(attributes); + } +} diff --git a/docs/resources/extending-the-sdk/Program.cs b/docs/resources/extending-the-sdk/Program.cs new file mode 100644 index 00000000000..849bdcc182b --- /dev/null +++ b/docs/resources/extending-the-sdk/Program.cs @@ -0,0 +1,61 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +namespace ExtendingTheSdk; + +public class Program +{ + private static readonly ActivitySource DemoSource = new("OTel.Demo"); + private static readonly Meter MeterDemoSource = new("OTel.Demo"); + + public static void Main() + { + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("OTel.Demo") + .SetResourceBuilder(ResourceBuilder.CreateEmpty().AddDetector( + new MyResourceDetector())) + .Build(); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .SetResourceBuilder(ResourceBuilder.CreateEmpty().AddDetector( + new MyResourceDetector())) + .Build(); + + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddOpenTelemetry(options => + { + options.SetResourceBuilder(ResourceBuilder.CreateDefault().AddDetector( + new MyResourceDetector())); + }); + }); + + using (var foo = DemoSource.StartActivity("Foo")) + { + using (var bar = DemoSource.StartActivity("Bar")) + { + using (var baz = DemoSource.StartActivity("Baz")) + { + } + } + } + + var counter = MeterDemoSource.CreateCounter("counter"); + for (var i = 0; i < 20000; i++) + { + counter.Add(1, new KeyValuePair("tag1", "value1"), new KeyValuePair("tag2", "value2")); + } + + var logger = loggerFactory.CreateLogger("OTel.Demo"); + logger + .LogInformation("Hello from {Name} {Price}", "tomato", 2.99); + } +} diff --git a/docs/resources/extending-the-sdk/extending-the-sdk.csproj b/docs/resources/extending-the-sdk/extending-the-sdk.csproj new file mode 100644 index 00000000000..85aab7a7ed3 --- /dev/null +++ b/docs/resources/extending-the-sdk/extending-the-sdk.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/trace/README.md b/docs/trace/README.md new file mode 100644 index 00000000000..c472800b3ba --- /dev/null +++ b/docs/trace/README.md @@ -0,0 +1,172 @@ +# OpenTelemetry .NET Traces + + +
+Table of Contents + +* [Best Practices](#best-practices) +* [Package Version](#package-version) +* [Tracing API](#tracing-api) + * [ActivitySource](#activitysource) + * [Activity](#activity) +* [TracerProvider Management](#tracerprovider-management) +* [Correlation](#correlation) + +
+ + +## Best Practices + +The following tutorials have demonstrated the best practices for using traces +with OpenTelemetry .NET: + +* [Getting Started - ASP.NET Core + Application](./getting-started-aspnetcore/README.md) +* [Getting Started - Console Application](./getting-started-console/README.md) + +## Package Version + +:heavy_check_mark: You should always use the +[System.Diagnostics.Activity](https://learn.microsoft.com/dotnet/api/system.diagnostics.activity) +APIs from the latest stable version of +[System.Diagnostics.DiagnosticSource](https://www.nuget.org/packages/System.Diagnostics.DiagnosticSource/) +package, regardless of the .NET runtime version being used: + +* If you are using the latest stable version of [OpenTelemetry .NET + SDK](../../src/OpenTelemetry/README.md), you do not have to worry about the + version of `System.Diagnostics.DiagnosticSource` package because it is already + taken care of for you via [package + dependency](../../Directory.Packages.props). +* The .NET runtime team is holding a high bar for backward compatibility on + `System.Diagnostics.DiagnosticSource` even during major version bumps, so + compatibility is not a concern here. + +## Tracing API + +### ActivitySource + +:stop_sign: You should avoid creating +[`System.Diagnostics.ActivitySource`](https://learn.microsoft.com/dotnet/api/system.diagnostics.activitysource) +too frequently. `ActivitySource` is fairly expensive and meant to be reused +throughout the application. For most applications, it can be modeled as static +readonly field (e.g. [Program.cs](./getting-started-console/Program.cs)) or +singleton via dependency injection (e.g. +[Instrumentation.cs](../../examples/AspNetCore/Instrumentation.cs)). + +:heavy_check_mark: You should use dot-separated +[UpperCamelCase](https://en.wikipedia.org/wiki/Camel_case) as the +[`ActivitySource.Name`](https://learn.microsoft.com/dotnet/api/system.diagnostics.activitysource.name). +In many cases, using the fully qualified class name might be a good option. + +```csharp +static readonly ActivitySource MyActivitySource = new("MyCompany.MyProduct.MyLibrary"); +``` + +### Activity + +:heavy_check_mark: You should check +[`Activity.IsAllDataRequested`](https://learn.microsoft.com/dotnet/api/system.diagnostics.activity.isalldatarequested) +before [setting +Tags](https://learn.microsoft.com/dotnet/api/system.diagnostics.activity.settag) +for better performance. + +```csharp +using (var activity = MyActivitySource.StartActivity("SayHello")) +{ + if (activity != null && activity.IsAllDataRequested == true) + { + activity.SetTag("http.url", "http://www.mywebsite.com"); + } +} +``` + +:heavy_check_mark: You should use +[Activity.SetTag](https://learn.microsoft.com/dotnet/api/system.diagnostics.activity.settag) +to [set +attributes](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#set-attributes). + +:heavy_check_mark: You should finish/stop the activity properly. This can be +done implicitly via a `using` statement, which is recommended. You can also +explicitly call +[Activity.Dispose](https://learn.microsoft.com/dotnet/api/system.diagnostics.activity.dispose) +or +[Activity.Stop](https://learn.microsoft.com/dotnet/api/system.diagnostics.activity.stop). + +> [!NOTE] +> Activities which are not yet finished/stopped will not be exported. + +## TracerProvider Management + +:stop_sign: You should avoid creating `TracerProvider` instances too frequently, +`TracerProvider` is fairly expensive and meant to be reused throughout the +application. For most applications, one `TracerProvider` instance per process +would be sufficient. + +:heavy_check_mark: You should properly manage the lifecycle of `TracerProvider` +instances if they are created by you. + +Here is the rule of thumb when managing the lifecycle of `TracerProvider`: + +* If you are building an application with [dependency injection + (DI)](https://learn.microsoft.com/dotnet/core/extensions/dependency-injection) + (e.g. [ASP.NET Core](https://learn.microsoft.com/aspnet/core) and [.NET + Worker](https://learn.microsoft.com/dotnet/core/extensions/workers)), in most + cases you should create the `TracerProvider` instance and let DI manage its + lifecycle. Refer to the [Getting Started with OpenTelemetry .NET Traces in 5 + Minutes - ASP.NET Core Application](./getting-started-aspnetcore/README.md) + tutorial to learn more. +* If you are building an application without DI, create a `TracerProvider` + instance and manage the lifecycle explicitly. Refer to the [Getting Started + with OpenTelemetry .NET Traces in 5 Minutes - Console + Application](./getting-started-console/README.md) tutorial to learn more. +* If you forget to dispose the `TracerProvider` instance before the application + ends, activities might get dropped due to the lack of proper flush. +* If you dispose the `TracerProvider` instance too early, any subsequent + activities will not be collected. + +## Correlation + +In OpenTelemetry, traces are automatically [correlated to +logs](../logs/README.md#log-correlation) and can be [correlated to +metrics](../metrics/README.md#metrics-correlation) via +[exemplars](../metrics/exemplars/README.md). + +### Manually creating Activities + +As shown in the [getting started](getting-started-console/README.md) guide, it +is very easy to manually create `Activity`. Due to this, it can be tempting to +create too many activities (eg: for each method call). In addition to being +expensive, excessive activities can also make trace visualization harder. +Instead of manually creating `Activity`, check if you can leverage +instrumentation libraries, such as [ASP.NET +Core](../../src/OpenTelemetry.Instrumentation.AspNetCore/README.md), +[HttpClient](../../src/OpenTelemetry.Instrumentation.Http/README.md) which will +not only create and populate `Activity` with tags(attributes), but also take +care of propagating/restoring the context across process boundaries. If the +`Activity` produced by the instrumentation library is missing some information +you need, it is generally recommended to enrich the existing Activity with that +information, as opposed to creating a new one. + +### Modelling static tags as Resource + +Tags such as `MachineName`, `Environment` etc. which are static throughout the +process lifetime should be modelled as `Resource`, instead of adding them to +each `Activity`. Refer to this [doc](./customizing-the-sdk/README.md#resource) +for details and examples. + +## Common issues that lead to missing traces + +* The `ActivitySource` used to create the `Activity` is not added to the + `TracerProvider`. Use `AddSource` method to enable the activity from a given + `ActivitySource`. +* `TracerProvider` is disposed too early. You need to ensure that the + `TracerProvider` instance is kept active for traces to be collected. In a + typical application, a single TracerProvider is built at application startup, + and is disposed of at application shutdown. For an ASP.NET Core application, + use `AddOpenTelemetry` and `WithTraces` methods from the + `OpenTelemetry.Extensions.Hosting` package to correctly setup + `TracerProvider`. Here is a [sample ASP.NET Core + app](../../examples/AspNetCore/Program.cs) for reference. For simpler + applications such as Console apps, refer to this + [example](../../docs/trace/getting-started-console/Program.cs). +* TODO: Sampling diff --git a/docs/trace/customizing-the-sdk/Program.cs b/docs/trace/customizing-the-sdk/Program.cs index 20f0f9df73e..fead3aa0be2 100644 --- a/docs/trace/customizing-the-sdk/Program.cs +++ b/docs/trace/customizing-the-sdk/Program.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry; @@ -46,8 +33,12 @@ public static void Main() // The following adds subscription to activities from all Activity Sources // whose name starts with "AbcCompany.XyzProduct.". .AddSource("AbcCompany.XyzProduct.*") - .ConfigureResource(resourceBuilder => resourceBuilder.AddTelemetrySdk()) - .ConfigureResource(r => r.AddService("MyServiceName")) + .ConfigureResource(resource => resource.AddAttributes(new List> + { + new KeyValuePair("static-attribute1", "v1"), + new KeyValuePair("static-attribute2", "v2"), + })) + .ConfigureResource(resource => resource.AddService("MyServiceName")) .AddConsoleExporter() .Build(); diff --git a/docs/trace/customizing-the-sdk/README.md b/docs/trace/customizing-the-sdk/README.md index e440446448c..4d4a0c772e4 100644 --- a/docs/trace/customizing-the-sdk/README.md +++ b/docs/trace/customizing-the-sdk/README.md @@ -37,7 +37,7 @@ appBuilder.Services.AddOpenTelemetry() .WithTracing(builder => builder.AddConsoleExporter()); ``` -> **Note** +> [!NOTE] > The [AddOpenTelemetry](../../../src/OpenTelemetry.Extensions.Hosting/README.md#extension-method-reference) extension automatically starts and stops the `TracerProvider` with the host. @@ -51,7 +51,7 @@ required. Call `Sdk.CreateTracerProviderBuilder()` to obtain a builder and then call `Build()` once configuration is done to retrieve the `TracerProvider` instance. -> **Note** +> [!NOTE] > Once built changes to `TracerProvider` configuration are not allowed, with the exception of adding more processors. @@ -128,7 +128,7 @@ var tracerProvider = Sdk.CreateTracerProviderBuilder() See [Program.cs](./Program.cs) for complete example. -> **Note** +> [!NOTE] > A common mistake while configuring `TracerProvider` is forgetting to add all `ActivitySources` to the provider. It is recommended to leverage the wild card subscription model where it makes sense. For example, if your @@ -173,7 +173,7 @@ processor classes `SimpleExportProcessor` & `BatchExportProcessor` are provided to support invoking exporters through the processor pipeline and implement the standard behaviors prescribed by the OpenTelemetry specification. -> **Note** +> [!NOTE] > The SDK only ever invokes processors and has no direct knowledge of any registered exporters. @@ -196,13 +196,13 @@ var tracerProvider = Sdk.CreateTracerProviderBuilder() tracerProvider.AddProcessor(new MyProcessor3()); ``` -> **Note** +> [!NOTE] > The order of processor registration is important. Each processor added is invoked in order by the SDK. For example if a simple exporting processor is added before an enrichment processor the exported data will not contain anything added by the enrichment because it happens after the export. -> **Note** +> [!NOTE] > A `TracerProvider` assumes ownership of **all** processors added to it. This means that the provider will call the `Shutdown` method on all registered processors when it is shutting down and call the `Dispose` method on @@ -238,13 +238,13 @@ For exporting purposes, the SDK provides the following built-in processors: : This is an exporting processor which passes telemetry to the configured exporter immediately without any batching. -> **Note** +> [!NOTE] > A special processor [CompositeProcessor<T>](../../../src/OpenTelemetry/CompositeProcessor.cs) is used by the SDK to chain multiple processors together and may be used as needed by users to define sub-pipelines. -> **Note** +> [!NOTE] > The processors shipped from this SDK are generic implementations and support tracing and logging by implementing `Activity` and `LogRecord` respectively. @@ -268,15 +268,15 @@ var tracerProvider = Sdk.CreateTracerProviderBuilder() It is also common for exporters to provide their own extensions to simplify registration. The snippet below shows how to add the -[JaegerExporter](../../../src/OpenTelemetry.Exporter.Jaeger/README.md) to the -provider before it is built. +[OtlpExporter](../../../src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md) +to the provider before it is built. ```csharp using OpenTelemetry; using OpenTelemetry.Trace; var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddJaegerExporter() + .AddOtlpExporter() .Build(); ``` @@ -288,9 +288,11 @@ writing custom exporters. [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](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/README.md#semantic-attributes-with-sdk-provided-default-value) -resource is used to indicate the -[Service](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/README.md#service). +[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 `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 @@ -301,14 +303,16 @@ provider is built, by calling the `Build()` method on the `TracerProviderBuilder`. `ResourceBuilder` offers various methods to construct resource comprising of -multiple attributes from various sources. Examples include `AddTelemetrySdk()` -which adds [Telemetry -Sdk](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/README.md#telemetry-sdk) -resource, and `AddService()` which adds -[Service](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/README.md#service) -resource. It also allows adding `ResourceDetector`s. - -Follow [this](../extending-the-sdk/README.md#resource-detector) document +attributes from various sources. For example, `AddService()` adds +[Service](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md#service) +resource. `AddAttributes` can be used to add any additional attribute to the +`Resource`. It also allows adding `ResourceDetector`s. + +It is recommended to model attributes that are static throughout the lifetime of +the process as Resources, instead of adding them as attributes(tags) on each +`Activity`. + +Follow [this](../../resources/README.md#resource-detector) document to learn about writing custom resource detectors. The snippet below shows configuring the `Resource` associated with the provider. @@ -319,7 +323,11 @@ using OpenTelemetry.Resources; using OpenTelemetry.Trace; var tracerProvider = Sdk.CreateTracerProviderBuilder() - .ConfigureResource(resourceBuilder => resourceBuilder.AddTelemetrySdk()) + .ConfigureResource(r => r.AddAttributes(new List> + { + new KeyValuePair("static-attribute1", "v1"), + new KeyValuePair("static-attribute2", "v2"), + })) .ConfigureResource(resourceBuilder => resourceBuilder.AddService("service-name")) .Build(); ``` @@ -374,7 +382,7 @@ Sdk.SetDefaultTextMapPropagator(new MyCustomPropagator()); ## Dependency injection support -> **Note** +> [!NOTE] > This information applies to the OpenTelemetry SDK version 1.4.0 and newer only. @@ -419,14 +427,14 @@ When using the `Sdk.CreateTracerProviderBuilder` method the `TracerProvider` owns its own `IServiceCollection`. It will only be able to see services registered into that collection. -> **Note** +> [!NOTE] > It is important to correctly manage the lifecycle of the `TracerProvider`. See [Building a TracerProvider](#building-a-tracerprovider) for details. #### Using the OpenTelemetry.Extensions.Hosting package -> **Note** +> [!NOTE] > If you are authoring an [ASP.NET Core application](https://learn.microsoft.com/aspnet/core/fundamentals/host/web-host) or using the [.NET Generic @@ -455,7 +463,7 @@ which is used to automatically start the `TracerProvider` when the host starts and the host will automatically shutdown and dispose the `TracerProvider` when it is shutdown. -> **Note** +> [!NOTE] > Multiple calls to `WithTracing` will configure the same `TracerProvider`. Only a single `TraceProvider` may exist in an `IServiceCollection` \ `IServiceProvider`. @@ -480,7 +488,7 @@ it is shutdown. * `ConfigureServices`: Registers a callback function for configuring the `IServiceCollection` used by the `TracerProviderBuilder`. - > **Note** + > [!NOTE] > `ConfigureServices` may only be called before the `IServiceProvider` has been created after which point services can no longer be added. @@ -491,7 +499,7 @@ it is shutdown. implementationFactory)`: Adds a sampler into the `TracerProvider` using a factory function to create the sampler instance. -> **Note** +> [!NOTE] > The factory functions accepting `IServiceProvider` may always be used regardless of how the SDK is initialized. When using an external service collection (ex: `appBuilder.Services.AddOpenTelemetry()`), as is common in @@ -502,7 +510,7 @@ build an `IServiceProvider` from it to make available to extensions. ## Configuration files and environment variables -> **Note** +> [!NOTE] > This information applies to the OpenTelemetry SDK version 1.4.0 and newer only. @@ -558,7 +566,7 @@ var provider = Sdk.CreateTracerProviderBuilder() The [OpenTelemetry Specification](https://github.com/open-telemetry/opentelemetry-specification) defines [specific environment -variables](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md) +variables](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md) which may be used to configure SDK implementations. The OpenTelemetry .NET SDK will look for the environment variables defined in @@ -567,7 +575,7 @@ variables users may also manage these settings via the command-line, configuration files, or any other source registered with the .NET configuration engine. This provides greater flexibility than what the specification defines. -> **Note** +> [!NOTE] > Not all of the environment variables defined in the specification are supported. Consult the individual project README files for details on specific environment variable support. @@ -602,7 +610,7 @@ environment variables. dotnet run --OTEL_SERVICE_NAME "MyService" ``` -> **Note** +> [!NOTE] > The [.NET Configuration](https://learn.microsoft.com/dotnet/core/extensions/configuration) pattern is hierarchical meaning the order of registered configuration sources @@ -620,7 +628,7 @@ components. Options classes can always be configured through code but users typically want to control key settings through configuration. -The following example shows how to configure `JaegerExporterOptions` by binding +The following example shows how to configure `OtlpExporterOptions` by binding to an `IConfiguration` section. Json config file (usually appsettings.json): @@ -628,13 +636,8 @@ Json config file (usually appsettings.json): ```json { "OpenTelemetry": { - "Jaeger": { - "Protocol": "UdpCompactThrift" - "AgentHost": "localhost", - "AgentPort": 6831, - "BatchExportProcessorOptions": { - "ScheduledDelayMilliseconds": 5000 - } + "Otlp": { + "Endpoint": "http://localhost:4317" } } } @@ -645,11 +648,11 @@ Code: ```csharp var appBuilder = WebApplication.CreateBuilder(args); -appBuilder.Services.Configure( - appBuilder.Configuration.GetSection("OpenTelemetry:Jaeger")); +appBuilder.Services.Configure( + appBuilder.Configuration.GetSection("OpenTelemetry:Otlp")); appBuilder.Services.AddOpenTelemetry() - .WithTracing(builder => builder.AddJaegerExporter()); + .WithTracing(builder => builder.AddOtlpExporter()); ``` The OpenTelemetry .NET SDK supports running multiple `TracerProvider`s inside @@ -659,7 +662,7 @@ users to target configuration at specific components a "name" parameter is typically supported on configuration extensions to control the options instance used for the component being registered. -The below example shows how to configure two `JaegerExporter` instances inside a +The below example shows how to configure two `OtlpExporter` instances inside a single `TracerProvider` sending to different ports. Json config file (usually appsettings.json): @@ -667,12 +670,12 @@ Json config file (usually appsettings.json): ```json { "OpenTelemetry": { - "JaegerPrimary": { - "AgentPort": 1818 + "OtlpPrimary": { + "Endpoint": "http://localhost:4317" + }, + "OtlpSecondary": { + "Endpoint": "http://localhost:4327" }, - "JaegerSecondary": { - "AgentPort": 8818 - } } } ``` @@ -682,16 +685,16 @@ Code: ```csharp var appBuilder = WebApplication.CreateBuilder(args); -appBuilder.Services.Configure( - "JaegerPrimary", - appBuilder.Configuration.GetSection("OpenTelemetry:JaegerPrimary")); +appBuilder.Services.Configure( + "OtlpPrimary", + appBuilder.Configuration.GetSection("OpenTelemetry:OtlpPrimary")); -appBuilder.Services.Configure( - "JaegerSecondary", - appBuilder.Configuration.GetSection("OpenTelemetry:JaegerSecondary")); +appBuilder.Services.Configure( + "OtlpSecondary", + appBuilder.Configuration.GetSection("OpenTelemetry:OtlpSecondary")); appBuilder.Services.AddOpenTelemetry() .WithTracing(builder => builder - .AddJaegerExporter(name: "JaegerPrimary", configure: null) - .AddJaegerExporter(name: "JaegerSecondary", configure: null)); + .AddOtlpExporter(name: "OtlpPrimary", configure: null) + .AddOtlpExporter(name: "OtlpSecondary", configure: null)); ``` diff --git a/docs/trace/extending-the-sdk/MyEnrichingProcessor.cs b/docs/trace/extending-the-sdk/MyEnrichingProcessor.cs index 4565852c1b1..a6107330485 100644 --- a/docs/trace/extending-the-sdk/MyEnrichingProcessor.cs +++ b/docs/trace/extending-the-sdk/MyEnrichingProcessor.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry; diff --git a/docs/trace/extending-the-sdk/MyExporter.cs b/docs/trace/extending-the-sdk/MyExporter.cs index 3bdefc0d339..d37b9daac46 100644 --- a/docs/trace/extending-the-sdk/MyExporter.cs +++ b/docs/trace/extending-the-sdk/MyExporter.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Text; diff --git a/docs/trace/extending-the-sdk/MyExporterExtensions.cs b/docs/trace/extending-the-sdk/MyExporterExtensions.cs index d4d9c8072e2..b317591fc5f 100644 --- a/docs/trace/extending-the-sdk/MyExporterExtensions.cs +++ b/docs/trace/extending-the-sdk/MyExporterExtensions.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry; using OpenTelemetry.Trace; diff --git a/docs/trace/extending-the-sdk/MyFilteringProcessor.cs b/docs/trace/extending-the-sdk/MyFilteringProcessor.cs index 37571b3fd7a..04fdb77ad59 100644 --- a/docs/trace/extending-the-sdk/MyFilteringProcessor.cs +++ b/docs/trace/extending-the-sdk/MyFilteringProcessor.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry; diff --git a/docs/trace/extending-the-sdk/MyProcessor.cs b/docs/trace/extending-the-sdk/MyProcessor.cs index 8b42d1f4016..4171821d49a 100644 --- a/docs/trace/extending-the-sdk/MyProcessor.cs +++ b/docs/trace/extending-the-sdk/MyProcessor.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry; diff --git a/docs/trace/extending-the-sdk/MyResourceDetector.cs b/docs/trace/extending-the-sdk/MyResourceDetector.cs index 1d7d4ae7b9c..df40d9b3302 100644 --- a/docs/trace/extending-the-sdk/MyResourceDetector.cs +++ b/docs/trace/extending-the-sdk/MyResourceDetector.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Resources; diff --git a/docs/trace/extending-the-sdk/MySampler.cs b/docs/trace/extending-the-sdk/MySampler.cs index a39f0e664b3..bbb4ae04793 100644 --- a/docs/trace/extending-the-sdk/MySampler.cs +++ b/docs/trace/extending-the-sdk/MySampler.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Trace; diff --git a/docs/trace/extending-the-sdk/Program.cs b/docs/trace/extending-the-sdk/Program.cs index 382a5be6c85..ce001aee3e3 100644 --- a/docs/trace/extending-the-sdk/Program.cs +++ b/docs/trace/extending-the-sdk/Program.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry; diff --git a/docs/trace/extending-the-sdk/README.md b/docs/trace/extending-the-sdk/README.md index 7e8600eeb7f..3a7aa623da1 100644 --- a/docs/trace/extending-the-sdk/README.md +++ b/docs/trace/extending-the-sdk/README.md @@ -6,7 +6,7 @@ Quick links: * [Building your own instrumentation library](#instrumentation-library) * [Building your own processor](#processor) * [Building your own sampler](#sampler) -* [Building your own resource detector](#resource-detector) +* [Building your own resource detector](../../resources/README.md#resource-detector) * [Registration extension method guidance for library authors](#registration-extension-method-guidance-for-library-authors) * [References](#references) @@ -16,7 +16,6 @@ OpenTelemetry .NET SDK has provided the following built-in trace exporters: * [Console](../../../src/OpenTelemetry.Exporter.Console/README.md) * [InMemory](../../../src/OpenTelemetry.Exporter.InMemory/README.md) -* [Jaeger](../../../src/OpenTelemetry.Exporter.Jaeger/README.md) * [OpenTelemetryProtocol](../../../src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md) * [Zipkin](../../../src/OpenTelemetry.Exporter.Zipkin/README.md) @@ -124,7 +123,7 @@ guidelines. This section describes the steps required to write a custom instrumentation library. -> **Note** +> [!NOTE] > If you are writing a new library or modifying an existing library the recommendation is to use the [ActivitySource API/OpenTelemetry API](../../../src/OpenTelemetry.Api/README.md#introduction-to-opentelemetry-net-tracing-api) @@ -181,35 +180,36 @@ Writing an instrumentation library typically involves 3 steps. * If the instrumentation library requires state management tied to that of `TracerProvider` then it should: - * Implement `IDisposable`. + * Implement `IDisposable`. - * Provide an extension method which calls `AddSource` (to enable its - `ActivitySource`) and `AddInstrumentation` (to enable state management) - on the `TracerProviderBuilder` being configured. + * Provide an extension method which calls `AddSource` (to enable its + `ActivitySource`) and `AddInstrumentation` (to enable state management) + on the `TracerProviderBuilder` being configured. - An example instrumentation using this approach is [SqlClient - instrumentation](../../../src/OpenTelemetry.Instrumentation.SqlClient/TracerProviderBuilderExtensions.cs). + An example instrumentation using this approach is [SqlClient + instrumentation](../../../src/OpenTelemetry.Instrumentation.SqlClient/TracerProviderBuilderExtensions.cs). - **CAUTION**: The instrumentation libraries requiring state management are - usually hard to auto-instrument. Therefore, they take the risk of not - being supported by [OpenTelemetry .NET Automatic - Instrumentation](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation). + > [!WARNING] + > The instrumentation libraries requiring state management are + usually hard to auto-instrument. Therefore, they take the risk of not + being supported by [OpenTelemetry .NET Automatic + Instrumentation](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation). * If the instrumentation library does not require any state management, then providing an extension method is optional. - * If an extension is provided it should call `AddSource` on the - `TracerProviderBuilder` being configured to enable its - `ActivitySource`. + * If an extension is provided it should call `AddSource` on the + `TracerProviderBuilder` being configured to enable its + `ActivitySource`. - * If an extension is not provided, then the name of the `ActivitySource` - used by the instrumented library must be documented so that end users - can enable it by calling `AddSource` on the `TracerProviderBuilder` - being configured. + * If an extension is not provided, then the name of the `ActivitySource` + used by the instrumented library must be documented so that end users + can enable it by calling `AddSource` on the `TracerProviderBuilder` + being configured. - > **Note** - > Changing the name of the source should be considered a - breaking change. + > [!NOTE] + > Changing the name of the source should be considered a + breaking change. ### Special case : Instrumentation for libraries producing legacy Activity @@ -341,24 +341,9 @@ class MySampler : Sampler A demo sampler is shown [here](./MySampler.cs). -## Resource Detector - -OpenTelemetry .NET SDK provides a resource detector for detecting resource -information from the `OTEL_RESOURCE_ATTRIBUTES` and `OTEL_SERVICE_NAME` -environment variables. - -Custom resource detectors can be implemented: - -* ResourceDetectors should inherit from - `OpenTelemetry.Resources.IResourceDetector`, (which belongs to the - [OpenTelemetry](../../../src/OpenTelemetry/README.md) package), and implement - the `Detect` method. - -A demo ResourceDetector is shown [here](./MyResourceDetector.cs). - ## Registration extension method guidance for library authors -> **Note** +> [!NOTE] > This information applies to the OpenTelemetry SDK version 1.4.0 and newer only. @@ -367,7 +352,7 @@ register custom OpenTelemetry components into their `TracerProvider`s. These extension methods can target either the `TracerProviderBuilder` or the `IServiceCollection` classes. Both of these patterns are described below. -> **Note** +> [!NOTE] > Libraries providing SDK plugins such as exporters, resource detectors, and/or samplers should take a dependency on the [OpenTelemetry SDK package](https://www.nuget.org/packages/opentelemetry). Library authors @@ -403,7 +388,7 @@ When providing registration extensions: from starting. The OpenTelemetry SDK is allowed to crash if it cannot be started. It **MUST NOT** crash once running. -> **Note** +> [!NOTE] > The SDK implementation of `TracerProviderBuilder` ensures that the [.NET Configuration](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration) @@ -632,7 +617,7 @@ single `AddMyLibrary` extension to configure the library itself and optionally turn on OpenTelemetry integration for multiple signals (tracing & metrics in this case). -> **Note** +> [!NOTE] > `ConfigureOpenTelemetryTracerProvider` and `ConfigureOpenTelemetryMeterProvider` do not automatically start OpenTelemetry. The host is responsible for either calling `AddOpenTelemetry` in the diff --git a/docs/trace/getting-started-aspnetcore/Program.cs b/docs/trace/getting-started-aspnetcore/Program.cs index 178057b991a..5c2affb0c43 100644 --- a/docs/trace/getting-started-aspnetcore/Program.cs +++ b/docs/trace/getting-started-aspnetcore/Program.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Resources; diff --git a/docs/trace/getting-started-aspnetcore/README.md b/docs/trace/getting-started-aspnetcore/README.md index 5c9ea7ded58..982c6ff688a 100644 --- a/docs/trace/getting-started-aspnetcore/README.md +++ b/docs/trace/getting-started-aspnetcore/README.md @@ -20,15 +20,9 @@ packages: ```sh dotnet add package OpenTelemetry.Exporter.Console dotnet add package OpenTelemetry.Extensions.Hosting -dotnet add package OpenTelemetry.Instrumentation.AspNetCore --prerelease +dotnet add package OpenTelemetry.Instrumentation.AspNetCore ``` -> **Note** This quickstart guide uses prerelease packages. For a quickstart -> which only relies on stable packages see: [Getting Started - Console -> Application](../getting-started-console/README.md). For more information about -> when instrumentation will be marked as stable see: [Instrumentation-1.0.0 -> milestone](https://github.com/open-telemetry/opentelemetry-dotnet/milestone/23). - Update the `Program.cs` file with the code from [Program.cs](./Program.cs). Run the application again (using `dotnet run`) and then browse to the url shown @@ -84,7 +78,7 @@ builder.Services.AddOpenTelemetry() .AddConsoleExporter()); ``` -> **Note** +> [!NOTE] > The `AddOpenTelemetry` extension is part of the [OpenTelemetry.Extensions.Hosting](../../../src/OpenTelemetry.Extensions.Hosting/README.md) package. diff --git a/docs/trace/getting-started-aspnetcore/getting-started-aspnetcore.csproj b/docs/trace/getting-started-aspnetcore/getting-started-aspnetcore.csproj index 1956f427004..375079fc34c 100644 --- a/docs/trace/getting-started-aspnetcore/getting-started-aspnetcore.csproj +++ b/docs/trace/getting-started-aspnetcore/getting-started-aspnetcore.csproj @@ -1,15 +1,7 @@ - - - net6.0;net7.0 - enable - enable - - - diff --git a/docs/trace/getting-started-console/Program.cs b/docs/trace/getting-started-console/Program.cs index e7e061e185d..4acaf7b8a80 100644 --- a/docs/trace/getting-started-console/Program.cs +++ b/docs/trace/getting-started-console/Program.cs @@ -1,33 +1,17 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry; using OpenTelemetry.Trace; -namespace GettingStarted; - public class Program { - private static readonly ActivitySource MyActivitySource = new( - "MyCompany.MyProduct.MyLibrary"); + private static readonly ActivitySource MyActivitySource = new("MyCompany.MyProduct.MyLibrary"); public static void Main() { - using var tracerProvider = Sdk.CreateTracerProviderBuilder() + var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("MyCompany.MyProduct.MyLibrary") .AddConsoleExporter() .Build(); @@ -39,5 +23,9 @@ public static void Main() activity?.SetTag("baz", new int[] { 1, 2, 3 }); activity?.SetStatus(ActivityStatusCode.Ok); } + + // Dispose tracer provider before the application ends. + // This will flush the remaining spans and shutdown the tracing pipeline. + tracerProvider.Dispose(); } } diff --git a/docs/trace/getting-started-console/README.md b/docs/trace/getting-started-console/README.md index 818f18ce1c7..6fbfeb3737b 100644 --- a/docs/trace/getting-started-console/README.md +++ b/docs/trace/getting-started-console/README.md @@ -85,7 +85,7 @@ is configured to subscribe to the activities from the source `ConsoleExporter` simply displays it on the console. ```csharp -using var tracerProvider = Sdk.CreateTracerProviderBuilder() +var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("MyCompany.MyProduct.MyLibrary") .AddConsoleExporter() .Build(); diff --git a/docs/trace/getting-started-jaeger/Program.cs b/docs/trace/getting-started-jaeger/Program.cs index b57660f7470..d4603a71bd0 100644 --- a/docs/trace/getting-started-jaeger/Program.cs +++ b/docs/trace/getting-started-jaeger/Program.cs @@ -1,25 +1,11 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -using System; using System.Diagnostics; +#if NETFRAMEWORK using System.Net.Http; -using System.Threading.Tasks; +#endif using OpenTelemetry; -using OpenTelemetry.Exporter; using OpenTelemetry.Resources; using OpenTelemetry.Trace; diff --git a/docs/trace/getting-started-jaeger/README.md b/docs/trace/getting-started-jaeger/README.md index bc0359142bf..07d529e738d 100644 --- a/docs/trace/getting-started-jaeger/README.md +++ b/docs/trace/getting-started-jaeger/README.md @@ -74,7 +74,7 @@ using var tracerProvider = Sdk.CreateTracerProviderBuilder() .Build(); ``` -When we run the application, the `ConsoleExporter` was printing the traces on +When we ran the application, the `ConsoleExporter` was printing the traces on console, and the `OtlpExporter` was attempting to send the traces to Jaeger Agent via the default endpoint `http://localhost:4317`. @@ -142,7 +142,7 @@ Jaeger -->|http://localhost:16686/| JaegerUI["Browser
(Jaeger UI)"] ## Final cleanup -In the end, remove the Console Exporter so we only have Jaeger Exporter in the +In the end, remove the Console Exporter so we only have OTLP Exporter in the final application: ```csharp diff --git a/docs/trace/getting-started-jaeger/getting-started-jaeger.csproj b/docs/trace/getting-started-jaeger/getting-started-jaeger.csproj index 543e6766445..19bcfb68247 100644 --- a/docs/trace/getting-started-jaeger/getting-started-jaeger.csproj +++ b/docs/trace/getting-started-jaeger/getting-started-jaeger.csproj @@ -4,7 +4,8 @@ + - + diff --git a/docs/trace/links-based-sampler/LinksAndParentBasedSampler.cs b/docs/trace/links-based-sampler/LinksAndParentBasedSampler.cs index 2ea3ec78239..bca076a2734 100644 --- a/docs/trace/links-based-sampler/LinksAndParentBasedSampler.cs +++ b/docs/trace/links-based-sampler/LinksAndParentBasedSampler.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Trace; diff --git a/docs/trace/links-based-sampler/LinksBasedSampler.cs b/docs/trace/links-based-sampler/LinksBasedSampler.cs index 7d6342c756a..b9306fab232 100644 --- a/docs/trace/links-based-sampler/LinksBasedSampler.cs +++ b/docs/trace/links-based-sampler/LinksBasedSampler.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Trace; diff --git a/docs/trace/links-based-sampler/Program.cs b/docs/trace/links-based-sampler/Program.cs index 85ae36acac2..7ef89aa5fdb 100644 --- a/docs/trace/links-based-sampler/Program.cs +++ b/docs/trace/links-based-sampler/Program.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry; diff --git a/docs/trace/links-creation-with-new-activities/Program.cs b/docs/trace/links-creation-with-new-activities/Program.cs new file mode 100644 index 00000000000..c03af7bcc82 --- /dev/null +++ b/docs/trace/links-creation-with-new-activities/Program.cs @@ -0,0 +1,86 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using OpenTelemetry; +using OpenTelemetry.Trace; + +namespace LinksCreationWithNewRootActivitiesDemo; + +internal class Program +{ + private static readonly ActivitySource MyActivitySource = new("LinksCreationWithNewRootActivities"); + + public static async Task Main(string[] args) + { + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("LinksCreationWithNewRootActivities") + .AddConsoleExporter() + .Build(); + + using (var activity = MyActivitySource.StartActivity("OrchestratingActivity")) + { + activity?.SetTag("foo", 1); + await DoFanoutAsync(); + + using (var nestedActivity = MyActivitySource.StartActivity("WrapUp")) + { + nestedActivity?.SetTag("foo", 1); + } + } + } + + public static async Task DoFanoutAsync() + { + var previous = Activity.Current; + const int NumConcurrentOperations = 10; + + var activityContext = Activity.Current!.Context; + var links = new List + { + new ActivityLink(activityContext), + }; + + var tasks = new List(); + + // Fanning out to N concurrent operations. + // We create a new root activity for each operation and + // link it to an outer activity that happens to be the current + // activity. + for (int i = 0; i < NumConcurrentOperations; i++) + { + int operationIndex = i; + + var task = Task.Run(() => + { + // Reference: https://opentelemetry.io/docs/instrumentation/net/manual/#creating-new-root-activities + // Since we want to create a new root activity for each of the fanned out operations, + // this step helps us "de-parent" it from the current activity. + // Note: At least as of Oct 2023, this is the only mechanism to create a new root + // activity in the presence of an existing activity. This might change in the future + // if/when issue https://github.com/open-telemetry/opentelemetry-dotnet/issues/984 + // is addressed. + Activity.Current = null; + + // Reference: https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src/OpenTelemetry.Api#activity-creation-options + // Reference: https://opentelemetry.io/docs/instrumentation/net/manual/#adding-links + // We create a new root activity for each of the fanned out operations and link it to the outer activity. + using var newRootActivityForFannedOutOperation = MyActivitySource.StartActivity( + ActivityKind.Internal, // Set this to the appropriate ActivityKind depending on your scenario + name: $"FannedOutActivity {operationIndex + 1}", + links: links); + + // DO THE FANOUT WORK HERE... + }); + + tasks.Add(task); + } + + // Wait for all tasks to complete + await Task.WhenAll(tasks); + + // Reset to the previous activity now that we are done with the fanout + // This will ensure that the rest of the code executes in the context of the original activity. + Activity.Current = previous; + } +} diff --git a/docs/trace/links-creation-with-new-activities/README.md b/docs/trace/links-creation-with-new-activities/README.md new file mode 100644 index 00000000000..39552a7cbb6 --- /dev/null +++ b/docs/trace/links-creation-with-new-activities/README.md @@ -0,0 +1,158 @@ +# Creating new root activities that link to an existing activity: A Sample + +This sample shows how to create new root activities that +[link](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#links-between-spans) +to an existing activity. This can be useful in a fan-out or batched operation +situation when you want to create a new trace with a new root activity +BEFORE invoking each of the fanned out operations, and at the same time +you want each of these new traces to be linked to the original activity. + +To give an example, let's say that: + +- Service A receives a request for a customer operation that impacts 1000s of +resources. The term "resource" here means an entity that is managed by this +service and should not be confused with the term "resource" in OpenTelemetry. +- Service A orchestrates this overall operation by fanning out multiple +calls to Service B, with one call for EACH of the impacted resources. +- Let's say the number of spans generated for a single resource operation +is in the order of several thousands of spans. + +In the above example, if you used the same trace for the entire flow, then +you would end up with a huge trace with more than million spans. This will +make visualizing and understanding the trace difficult. + +Further, it may make it difficult to do programmatic analytics at the +*individual* resource operation level (for each of the 1000s of resource +operations) as there would be no single trace that corresponds to each +of the individual resource operations. + +Instead, by creating a new trace with a new root activity before the fanout +call, you get a separate trace for each of the resource operations. In +addition, by using the "span links" functionality in OpenTelemetry, we link +each of these new root activities to the original activity. + +This enables more granular visualization and analytics. + +## How does this example work? + +To be able to create new root activities, we first set the Activity.Current +to null so that we can "de-parent" the new activity from the current activity. + +For each of the fanned out operations, this creates a new root activity. As +part of this activity creation, it links it to the previously current activity. + +Finally, we reset Activity.Current to the previous activity now after we are +done with the fanout. This will ensure that the rest of the code executes +in the context of the original activity. + +## When should you consider such an option? What are the tradeoffs? + +This is a good option to consider for operations that involve batched or +fanout operations if using the same trace causes it to become huge. +Using this approach, you can create a new trace for each of the fanned out +operations and link them to the original activity. + +A tradeoff is that now we will have multiple traces instead of a single trace. +However, many Observability tools have the ability to visualize linked traces +together, and hence it is not necessarily a concern from that perspective. +However, this model has the potential to add some complexity to any +programmatic analysis since now it has to understand the concept of linked +traces. + +## References + +- [Links between spans](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#links-between-spans) +- [Creating new root activities](https://opentelemetry.io/docs/instrumentation/net/manual/#creating-new-root-activities) +- [Activity Creation Options](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src/OpenTelemetry.Api#activity-creation-options) +- [A sample where links are used in a fan-in scenario](https://github.com/PacktPublishing/Modern-Distributed-Tracing-in-.NET/tree/main/chapter6/links) + +## Sample Output + +You should see output such as the below when you run this example. You can see +that EACH of the "fanned out activities" have: + +- a new trace ID +- an activity link to the original activity + +```text +Activity.TraceId: 5ce4d8ad4926ecdd0084681f46fa38d9 +Activity.SpanId: 8f9e9441f0789f6e +Activity.TraceFlags: Recorded +Activity.ActivitySourceName: LinksCreationWithNewRootActivities +Activity.DisplayName: FannedOutActivity 1 +Activity.Kind: Internal +Activity.StartTime: 2023-10-17T01:24:40.4957326Z +Activity.Duration: 00:00:00.0008656 +Activity.Links: + 2890476acefb53b93af64a0d91939051 16b83c1517629363 +Resource associated with Activity: + telemetry.sdk.name: opentelemetry + telemetry.sdk.language: dotnet + telemetry.sdk.version: 0.0.0-alpha.0.2600 + service.name: unknown_service:links-creation + +Activity.TraceId: 16a8ad23d14a085f2a1f260a4b474d05 +Activity.SpanId: 0c3e835cfd60c604 +Activity.TraceFlags: Recorded +Activity.ActivitySourceName: LinksCreationWithNewRootActivities +Activity.DisplayName: FannedOutActivity 2 +Activity.Kind: Internal +Activity.StartTime: 2023-10-17T01:24:40.5908290Z +Activity.Duration: 00:00:00.0009197 +Activity.Links: + 2890476acefb53b93af64a0d91939051 16b83c1517629363 +Resource associated with Activity: + telemetry.sdk.name: opentelemetry + telemetry.sdk.language: dotnet + telemetry.sdk.version: 0.0.0-alpha.0.2600 + service.name: unknown_service:links-creation + +Activity.TraceId: 46f0b5b68173b4acf4f50e1f5cdb3e55 +Activity.SpanId: 42e7f4439fc2b416 +Activity.TraceFlags: Recorded +Activity.ActivitySourceName: LinksCreationWithNewRootActivities +Activity.DisplayName: FannedOutActivity 3 +Activity.Kind: Internal +Activity.StartTime: 2023-10-17T01:24:40.5930378Z +Activity.Duration: 00:00:00.0008622 +Activity.Links: + 2890476acefb53b93af64a0d91939051 16b83c1517629363 +Resource associated with Activity: + telemetry.sdk.name: opentelemetry + telemetry.sdk.language: dotnet + telemetry.sdk.version: 0.0.0-alpha.0.2600 + service.name: unknown_service:links-creation + +Activity.TraceId: 2890476acefb53b93af64a0d91939051 +Activity.SpanId: 6878c2a84d4d4996 +Activity.TraceFlags: Recorded +Activity.ParentSpanId: 16b83c1517629363 +Activity.ActivitySourceName: LinksCreationWithNewRootActivities +Activity.DisplayName: WrapUp +Activity.Kind: Internal +Activity.StartTime: 2023-10-17T01:24:40.5950683Z +Activity.Duration: 00:00:00.0008843 +Activity.Tags: + foo: 1 +Resource associated with Activity: + telemetry.sdk.name: opentelemetry + telemetry.sdk.language: dotnet + telemetry.sdk.version: 0.0.0-alpha.0.2600 + service.name: unknown_service:links-creation + +Activity.TraceId: 2890476acefb53b93af64a0d91939051 +Activity.SpanId: 16b83c1517629363 +Activity.TraceFlags: Recorded +Activity.ActivitySourceName: LinksCreationWithNewRootActivities +Activity.DisplayName: OrchestratingActivity +Activity.Kind: Internal +Activity.StartTime: 2023-10-17T01:24:40.4937024Z +Activity.Duration: 00:00:00.1043390 +Activity.Tags: + foo: 1 +Resource associated with Activity: + telemetry.sdk.name: opentelemetry + telemetry.sdk.language: dotnet + telemetry.sdk.version: 0.0.0-alpha.0.2600 + service.name: unknown_service:links-creation +``` diff --git a/docs/logs/source-generation/source-generation.csproj b/docs/trace/links-creation-with-new-activities/links-creation.csproj similarity index 61% rename from docs/logs/source-generation/source-generation.csproj rename to docs/trace/links-creation-with-new-activities/links-creation.csproj index 6e8adc423c3..19aa9791432 100644 --- a/docs/logs/source-generation/source-generation.csproj +++ b/docs/trace/links-creation-with-new-activities/links-creation.csproj @@ -1,6 +1,5 @@ - + - diff --git a/docs/trace/reporting-exceptions/Program.cs b/docs/trace/reporting-exceptions/Program.cs index 4ffd8a4cfd9..62f2a11914e 100644 --- a/docs/trace/reporting-exceptions/Program.cs +++ b/docs/trace/reporting-exceptions/Program.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry; diff --git a/docs/trace/reporting-exceptions/README.md b/docs/trace/reporting-exceptions/README.md index 60ad6573e10..72e70683412 100644 --- a/docs/trace/reporting-exceptions/README.md +++ b/docs/trace/reporting-exceptions/README.md @@ -95,7 +95,7 @@ using (var activity = MyActivitySource.StartActivity("Foo")) } catch (SomeException ex) { - activity?.SetStatus(ActivityStatusCode.Error, ex.message); + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); } } ``` @@ -107,7 +107,7 @@ leveraging Activity status. Neither of the approach actually records the Exception itself to do more richer debugging. `Activity.RecordException()` allows the exception to be stored in the Activity as ActivityEvent as per [OpenTelemetry -convention](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/exceptions.md), +convention](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/exceptions/exceptions-spans.md), as shown below: ```csharp @@ -119,7 +119,7 @@ using (var activity = MyActivitySource.StartActivity("Foo")) } catch (SomeException ex) { - activity?.SetStatus(ActivityStatusCode.Error, ex.message); + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); activity?.RecordException(ex); } } @@ -142,7 +142,7 @@ It might be useful to automatically capture the unhandled exceptions, travel through the unfinished activities and export them for troubleshooting. Here goes one possible way of doing this: -> **Warning** +> [!CAUTION] > Use `AppDomain.UnhandledException` with caution. A throw in the handler puts the process into an unrecoverable state. diff --git a/docs/trace/stratified-sampling-example/Program.cs b/docs/trace/stratified-sampling-example/Program.cs index e85179126e7..49d8e1b7e7b 100644 --- a/docs/trace/stratified-sampling-example/Program.cs +++ b/docs/trace/stratified-sampling-example/Program.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry; diff --git a/docs/trace/stratified-sampling-example/StratifiedSampler.cs b/docs/trace/stratified-sampling-example/StratifiedSampler.cs index 16ca4b437ce..cb01a23dee0 100644 --- a/docs/trace/stratified-sampling-example/StratifiedSampler.cs +++ b/docs/trace/stratified-sampling-example/StratifiedSampler.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Trace; diff --git a/docs/trace/stratified-sampling-example/stratified-sampling-example.csproj b/docs/trace/stratified-sampling-example/stratified-sampling-example.csproj index e089b70bbcf..19aa9791432 100644 --- a/docs/trace/stratified-sampling-example/stratified-sampling-example.csproj +++ b/docs/trace/stratified-sampling-example/stratified-sampling-example.csproj @@ -1,14 +1,5 @@ - - - Exe - enable - enable - - - - diff --git a/docs/trace/tail-based-sampling-span-level/ParentBasedElseAlwaysRecordSampler.cs b/docs/trace/tail-based-sampling-span-level/ParentBasedElseAlwaysRecordSampler.cs index ae15a40d370..bce691b0c16 100644 --- a/docs/trace/tail-based-sampling-span-level/ParentBasedElseAlwaysRecordSampler.cs +++ b/docs/trace/tail-based-sampling-span-level/ParentBasedElseAlwaysRecordSampler.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Trace; diff --git a/docs/trace/tail-based-sampling-span-level/Program.cs b/docs/trace/tail-based-sampling-span-level/Program.cs index 3dfc4cf4f08..de4f8ea9f17 100644 --- a/docs/trace/tail-based-sampling-span-level/Program.cs +++ b/docs/trace/tail-based-sampling-span-level/Program.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry; diff --git a/docs/trace/tail-based-sampling-span-level/TailSamplingProcessor.cs b/docs/trace/tail-based-sampling-span-level/TailSamplingProcessor.cs index 19352cfa898..5aa22092e75 100644 --- a/docs/trace/tail-based-sampling-span-level/TailSamplingProcessor.cs +++ b/docs/trace/tail-based-sampling-span-level/TailSamplingProcessor.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry; diff --git a/examples/AspNetCore/Controllers/WeatherForecastController.cs b/examples/AspNetCore/Controllers/WeatherForecastController.cs index fab6acf5a73..3d09fe9ed61 100644 --- a/examples/AspNetCore/Controllers/WeatherForecastController.cs +++ b/examples/AspNetCore/Controllers/WeatherForecastController.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 namespace Examples.AspNetCore.Controllers; @@ -62,7 +49,7 @@ public IEnumerable Get() // that calculating the forecast is an expensive operation and therefore // something to be distinguished from the overall request. // Note: Tags can be added to the current activity without the need for - // a manual activity using Acitivty.Current?.SetTag() + // a manual activity using Activity.Current?.SetTag() using var activity = this.activitySource.StartActivity("calculate forecast"); var rng = new Random(); diff --git a/examples/AspNetCore/Examples.AspNetCore.csproj b/examples/AspNetCore/Examples.AspNetCore.csproj index fe6b61f8962..d07169358a7 100644 --- a/examples/AspNetCore/Examples.AspNetCore.csproj +++ b/examples/AspNetCore/Examples.AspNetCore.csproj @@ -1,7 +1,7 @@ - net6.0 + $(DefaultTargetFrameworkForExampleApps) @@ -15,7 +15,6 @@ - diff --git a/examples/AspNetCore/Instrumentation.cs b/examples/AspNetCore/Instrumentation.cs index e4583887e07..4b0ede1157f 100644 --- a/examples/AspNetCore/Instrumentation.cs +++ b/examples/AspNetCore/Instrumentation.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 namespace Examples.AspNetCore; @@ -35,7 +22,7 @@ public Instrumentation() string? version = typeof(Instrumentation).Assembly.GetName().Version?.ToString(); this.ActivitySource = new ActivitySource(ActivitySourceName, version); this.meter = new Meter(MeterName, version); - this.FreezingDaysCounter = this.meter.CreateCounter("weather.days.freezing", "The number of days where the temperature is below freezing"); + this.FreezingDaysCounter = this.meter.CreateCounter("weather.days.freezing", description: "The number of days where the temperature is below freezing"); } public ActivitySource ActivitySource { get; } diff --git a/examples/AspNetCore/Models/WeatherForecast.cs b/examples/AspNetCore/Models/WeatherForecast.cs index d05d2d047ca..f86f30db78a 100644 --- a/examples/AspNetCore/Models/WeatherForecast.cs +++ b/examples/AspNetCore/Models/WeatherForecast.cs @@ -1,29 +1,15 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace Examples.AspNetCore +namespace Examples.AspNetCore; + +public class WeatherForecast { - public class WeatherForecast - { - public DateTime Date { get; set; } + public DateTime Date { get; set; } - public int TemperatureC { get; set; } + public int TemperatureC { get; set; } - public int TemperatureF => 32 + (int)(this.TemperatureC / 0.5556); + public int TemperatureF => 32 + (int)(this.TemperatureC / 0.5556); - public string? Summary { get; set; } - } + public string? Summary { get; set; } } diff --git a/examples/AspNetCore/Program.cs b/examples/AspNetCore/Program.cs index bc4e0602cda..18279940976 100644 --- a/examples/AspNetCore/Program.cs +++ b/examples/AspNetCore/Program.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Metrics; using Examples.AspNetCore; @@ -25,21 +12,21 @@ var appBuilder = WebApplication.CreateBuilder(args); -// Note: Switch between Zipkin/Jaeger/OTLP/Console by setting UseTracingExporter in appsettings.json. -var tracingExporter = appBuilder.Configuration.GetValue("UseTracingExporter").ToLowerInvariant(); +// Note: Switch between Zipkin/OTLP/Console by setting UseTracingExporter in appsettings.json. +var tracingExporter = appBuilder.Configuration.GetValue("UseTracingExporter", defaultValue: "console")!.ToLowerInvariant(); // Note: Switch between Prometheus/OTLP/Console by setting UseMetricsExporter in appsettings.json. -var metricsExporter = appBuilder.Configuration.GetValue("UseMetricsExporter").ToLowerInvariant(); +var metricsExporter = appBuilder.Configuration.GetValue("UseMetricsExporter", defaultValue: "console")!.ToLowerInvariant(); // Note: Switch between Console/OTLP by setting UseLogExporter in appsettings.json. -var logExporter = appBuilder.Configuration.GetValue("UseLogExporter").ToLowerInvariant(); +var logExporter = appBuilder.Configuration.GetValue("UseLogExporter", defaultValue: "console")!.ToLowerInvariant(); // Note: Switch between Explicit/Exponential by setting HistogramAggregation in appsettings.json -var histogramAggregation = appBuilder.Configuration.GetValue("HistogramAggregation").ToLowerInvariant(); +var histogramAggregation = appBuilder.Configuration.GetValue("HistogramAggregation", defaultValue: "explicit")!.ToLowerInvariant(); // Build a resource configuration action to set service information. Action configureResource = r => r.AddService( - serviceName: appBuilder.Configuration.GetValue("ServiceName"), + serviceName: appBuilder.Configuration.GetValue("ServiceName", defaultValue: "otel-test")!, serviceVersion: typeof(Program).Assembly.GetName().Version?.ToString() ?? "unknown", serviceInstanceId: Environment.MachineName); @@ -63,23 +50,10 @@ .AddAspNetCoreInstrumentation(); // Use IConfiguration binding for AspNetCore instrumentation options. - appBuilder.Services.Configure(appBuilder.Configuration.GetSection("AspNetCoreInstrumentation")); + appBuilder.Services.Configure(appBuilder.Configuration.GetSection("AspNetCoreInstrumentation")); switch (tracingExporter) { - case "jaeger": - builder.AddJaegerExporter(); - - builder.ConfigureServices(services => - { - // Use IConfiguration binding for Jaeger exporter options. - services.Configure(appBuilder.Configuration.GetSection("Jaeger")); - - // Customize the HttpClient that will be used when JaegerExporter is configured for HTTP transport. - services.AddHttpClient("JaegerExporter", configureClient: (client) => client.DefaultRequestHeaders.Add("X-MyCustomHeader", "value")); - }); - break; - case "zipkin": builder.AddZipkinExporter(); @@ -94,7 +68,7 @@ builder.AddOtlpExporter(otlpOptions => { // Use IConfiguration directly for Otlp exporter endpoint option. - otlpOptions.Endpoint = new Uri(appBuilder.Configuration.GetValue("Otlp:Endpoint")); + otlpOptions.Endpoint = new Uri(appBuilder.Configuration.GetValue("Otlp:Endpoint", defaultValue: "http://localhost:4317")!); }); break; @@ -110,7 +84,9 @@ // Ensure the MeterProvider subscribes to any custom Meters. builder .AddMeter(Instrumentation.MeterName) - .SetExemplarFilter(new TraceBasedExemplarFilter()) +#if EXPOSE_EXPERIMENTAL_FEATURES + .SetExemplarFilter(ExemplarFilterType.TraceBased) +#endif .AddRuntimeInstrumentation() .AddHttpClientInstrumentation() .AddAspNetCoreInstrumentation(); @@ -140,7 +116,7 @@ builder.AddOtlpExporter(otlpOptions => { // Use IConfiguration directly for Otlp exporter endpoint option. - otlpOptions.Endpoint = new Uri(appBuilder.Configuration.GetValue("Otlp:Endpoint")); + otlpOptions.Endpoint = new Uri(appBuilder.Configuration.GetValue("Otlp:Endpoint", defaultValue: "http://localhost:4317")!); }); break; default: @@ -167,7 +143,7 @@ options.AddOtlpExporter(otlpOptions => { // Use IConfiguration directly for Otlp exporter endpoint option. - otlpOptions.Endpoint = new Uri(appBuilder.Configuration.GetValue("Otlp:Endpoint")); + otlpOptions.Endpoint = new Uri(appBuilder.Configuration.GetValue("Otlp:Endpoint", defaultValue: "http://localhost:4317")!); }); break; default: diff --git a/examples/AspNetCore/README.md b/examples/AspNetCore/README.md index c437a99b8bf..001d3cdea08 100644 --- a/examples/AspNetCore/README.md +++ b/examples/AspNetCore/README.md @@ -1,6 +1,6 @@ -# OpenTelemetry ASP.Net Core 6 Web API Example +# OpenTelemetry ASP.NET Core 7 Web API Example -This example uses the new WebApplication host that ships with .Net 6 +This example uses the new WebApplication host that ships with .NET 7 and shows how to setup 1. OpenTelemetry logging diff --git a/examples/AspNetCore/appsettings.json b/examples/AspNetCore/appsettings.json index 1f2d3368dc9..a6c4d31a2ac 100644 --- a/examples/AspNetCore/appsettings.json +++ b/examples/AspNetCore/appsettings.json @@ -15,12 +15,6 @@ "UseMetricsExporter": "console", "UseLogExporter": "console", "HistogramAggregation": "explicit", - "Jaeger": { - "AgentHost": "localhost", - "AgentPort": 6831, - "Endpoint": "http://localhost:14268", - "Protocol": "UdpCompactThrift" - }, "Zipkin": { "Endpoint": "http://localhost:9411/api/v2/spans" }, diff --git a/examples/Console/Examples.Console.csproj b/examples/Console/Examples.Console.csproj index ee9d4d0c6ce..af250fb26f1 100644 --- a/examples/Console/Examples.Console.csproj +++ b/examples/Console/Examples.Console.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + $(DefaultTargetFrameworkForExampleApps) $(NoWarn),CS0618 @@ -17,12 +17,11 @@ - - all + runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -32,7 +31,6 @@ - diff --git a/examples/Console/InstrumentationWithActivitySource.cs b/examples/Console/InstrumentationWithActivitySource.cs index 439c5fb3efe..240f58289d8 100644 --- a/examples/Console/InstrumentationWithActivitySource.cs +++ b/examples/Console/InstrumentationWithActivitySource.cs @@ -1,176 +1,162 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Globalization; using System.Net; using System.Text; -namespace Examples.Console +namespace Examples.Console; + +internal class InstrumentationWithActivitySource : IDisposable { - internal class InstrumentationWithActivitySource : IDisposable + private const string RequestPath = "/api/request"; + private readonly SampleServer server = new(); + private readonly SampleClient client = new(); + + public void Start(ushort port = 19999) { - private const string RequestPath = "/api/request"; - private readonly SampleServer server = new(); - private readonly SampleClient client = new(); + var url = $"http://localhost:{port.ToString(CultureInfo.InvariantCulture)}{RequestPath}/"; + this.server.Start(url); + this.client.Start(url); + } - public void Start(ushort port = 19999) - { - var url = $"http://localhost:{port.ToString(CultureInfo.InvariantCulture)}{RequestPath}/"; - this.server.Start(url); - this.client.Start(url); - } + public void Dispose() + { + this.client.Dispose(); + this.server.Dispose(); + } - public void Dispose() - { - this.client.Dispose(); - this.server.Dispose(); - } + private class SampleServer : IDisposable + { + private readonly HttpListener listener = new(); - private class SampleServer : IDisposable + public void Start(string url) { - private readonly HttpListener listener = new(); + this.listener.Prefixes.Add(url); + this.listener.Start(); - public void Start(string url) + Task.Run(() => { - this.listener.Prefixes.Add(url); - this.listener.Start(); + using var source = new ActivitySource("Samples.SampleServer"); - Task.Run(() => + while (this.listener.IsListening) { - using var source = new ActivitySource("Samples.SampleServer"); - - while (this.listener.IsListening) + try { - try - { - var context = this.listener.GetContext(); - - using var activity = source.StartActivity( - $"{context.Request.HttpMethod}:{context.Request.Url.AbsolutePath}", - ActivityKind.Server); + var context = this.listener.GetContext(); - var headerKeys = context.Request.Headers.AllKeys; - foreach (var headerKey in headerKeys) - { - string headerValue = context.Request.Headers[headerKey]; - activity?.SetTag($"http.header.{headerKey}", headerValue); - } - - string requestContent; - using (var childSpan = source.StartActivity("ReadStream", ActivityKind.Consumer)) - using (var reader = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding)) - { - requestContent = reader.ReadToEnd(); - childSpan.AddEvent(new ActivityEvent("StreamReader.ReadToEnd")); - } - - activity?.SetTag("request.content", requestContent); - activity?.SetTag("request.length", requestContent.Length.ToString()); + using var activity = source.StartActivity( + $"{context.Request.HttpMethod}:{context.Request.Url.AbsolutePath}", + ActivityKind.Server); - var echo = Encoding.UTF8.GetBytes("echo: " + requestContent); - context.Response.ContentEncoding = Encoding.UTF8; - context.Response.ContentLength64 = echo.Length; - context.Response.OutputStream.Write(echo, 0, echo.Length); - context.Response.Close(); + var headerKeys = context.Request.Headers.AllKeys; + foreach (var headerKey in headerKeys) + { + string headerValue = context.Request.Headers[headerKey]; + activity?.SetTag($"http.header.{headerKey}", headerValue); } - catch (Exception) + + string requestContent; + using (var childSpan = source.StartActivity("ReadStream", ActivityKind.Consumer)) + using (var reader = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding)) { - // expected when closing the listener. + requestContent = reader.ReadToEnd(); + childSpan.AddEvent(new ActivityEvent("StreamReader.ReadToEnd")); } + + activity?.SetTag("request.content", requestContent); + activity?.SetTag("request.length", requestContent.Length.ToString()); + + var echo = Encoding.UTF8.GetBytes("echo: " + requestContent); + context.Response.ContentEncoding = Encoding.UTF8; + context.Response.ContentLength64 = echo.Length; + context.Response.OutputStream.Write(echo, 0, echo.Length); + context.Response.Close(); } - }); - } + catch (Exception) + { + // expected when closing the listener. + } + } + }); + } - public void Dispose() - { - ((IDisposable)this.listener).Dispose(); - } + public void Dispose() + { + ((IDisposable)this.listener).Dispose(); } + } + + private class SampleClient : IDisposable + { + private CancellationTokenSource cts; + private Task requestTask; - private class SampleClient : IDisposable + public void Start(string url) { - private CancellationTokenSource cts; - private Task requestTask; + this.cts = new CancellationTokenSource(); + var cancellationToken = this.cts.Token; - public void Start(string url) - { - this.cts = new CancellationTokenSource(); - var cancellationToken = this.cts.Token; + this.requestTask = Task.Run( + async () => + { + using var source = new ActivitySource("Samples.SampleClient"); + using var client = new HttpClient(); - this.requestTask = Task.Run( - async () => + var count = 1; + while (!cancellationToken.IsCancellationRequested) { - using var source = new ActivitySource("Samples.SampleClient"); - using var client = new HttpClient(); + var content = new StringContent($"client message: {DateTime.Now}", Encoding.UTF8); - var count = 1; - while (!cancellationToken.IsCancellationRequested) + using (var activity = source.StartActivity("POST:" + RequestPath, ActivityKind.Client)) { - var content = new StringContent($"client message: {DateTime.Now}", Encoding.UTF8); - - using (var activity = source.StartActivity("POST:" + RequestPath, ActivityKind.Client)) - { - count++; + count++; - activity?.AddEvent(new ActivityEvent("PostAsync:Started")); - using var response = await client.PostAsync(url, content, cancellationToken).ConfigureAwait(false); - activity?.AddEvent(new ActivityEvent("PostAsync:Ended")); + activity?.AddEvent(new ActivityEvent("PostAsync:Started")); + using var response = await client.PostAsync(url, content, cancellationToken).ConfigureAwait(false); + activity?.AddEvent(new ActivityEvent("PostAsync:Ended")); - activity?.SetTag("http.status_code", (int)response.StatusCode); + activity?.SetTag("http.status_code", (int)response.StatusCode); - var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - activity?.SetTag("response.content", responseContent); - activity?.SetTag("response.length", responseContent.Length.ToString(CultureInfo.InvariantCulture)); + var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + activity?.SetTag("response.content", responseContent); + activity?.SetTag("response.length", responseContent.Length.ToString(CultureInfo.InvariantCulture)); - foreach (var header in response.Headers) + foreach (var header in response.Headers) + { + if (header.Value is IEnumerable enumerable) { - if (header.Value is IEnumerable enumerable) - { - activity?.SetTag($"http.header.{header.Key}", string.Join(",", enumerable)); - } - else - { - activity?.SetTag($"http.header.{header.Key}", header.Value.ToString()); - } + activity?.SetTag($"http.header.{header.Key}", string.Join(",", enumerable)); + } + else + { + activity?.SetTag($"http.header.{header.Key}", header.Value.ToString()); } } + } - try - { - await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); - } - catch (TaskCanceledException) - { - return; - } + try + { + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); } - }, - cancellationToken); - } + catch (TaskCanceledException) + { + return; + } + } + }, + cancellationToken); + } - public void Dispose() + public void Dispose() + { + if (this.cts != null) { - if (this.cts != null) - { - this.cts.Cancel(); - this.requestTask.Wait(); - this.requestTask.Dispose(); - this.cts.Dispose(); - } + this.cts.Cancel(); + this.requestTask.Wait(); + this.requestTask.Dispose(); + this.cts.Dispose(); } } } diff --git a/examples/Console/Program.cs b/examples/Console/Program.cs index db0fb90389f..2b93979e071 100644 --- a/examples/Console/Program.cs +++ b/examples/Console/Program.cs @@ -1,175 +1,154 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using CommandLine; -namespace Examples.Console +namespace Examples.Console; + +/// +/// Main samples entry point. +/// +public class Program { /// - /// Main samples entry point. + /// Main method - invoke this using command line. + /// For example: + /// + /// dotnet run --project Examples.Console.csproj -- console + /// dotnet run --project Examples.Console.csproj -- inmemory + /// dotnet run --project Examples.Console.csproj -- zipkin -u http://localhost:9411/api/v2/spans + /// dotnet run --project Examples.Console.csproj -- prometheus -p 9464 + /// dotnet run --project Examples.Console.csproj -- otlp -e "http://localhost:4317" -p "grpc" + /// dotnet run --project Examples.Console.csproj -- metrics --help + /// + /// To see all available examples in the project run: + /// + /// dotnet run --project Examples.Console.csproj -- --help + /// + /// The above must be run from the project root folder + /// (eg: C:\repos\opentelemetry-dotnet\examples\Console\). /// - public class Program + /// Arguments from command line. + public static void Main(string[] args) { - /// - /// Main method - invoke this using command line. - /// For example: - /// - /// dotnet run --project Examples.Console.csproj -- console - /// dotnet run --project Examples.Console.csproj -- inmemory - /// dotnet run --project Examples.Console.csproj -- zipkin -u http://localhost:9411/api/v2/spans - /// dotnet run --project Examples.Console.csproj -- jaeger -h localhost -p 6831 - /// dotnet run --project Examples.Console.csproj -- prometheus -p 9464 - /// dotnet run --project Examples.Console.csproj -- otlp -e "http://localhost:4317" -p "grpc" - /// dotnet run --project Examples.Console.csproj -- metrics --help - /// - /// To see all available examples in the project run: - /// - /// dotnet run --project Examples.Console.csproj -- --help - /// - /// The above must be run from the project root folder - /// (eg: C:\repos\opentelemetry-dotnet\examples\Console\). - /// - /// Arguments from command line. - public static void Main(string[] args) - { - Parser.Default.ParseArguments(args) - .MapResult( - (JaegerOptions options) => TestJaegerExporter.Run(options.Host, options.Port), - (ZipkinOptions options) => TestZipkinExporter.Run(options.Uri), - (PrometheusOptions options) => TestPrometheusExporter.Run(options.Port), - (MetricsOptions options) => TestMetrics.Run(options), - (LogsOptions options) => TestLogs.Run(options), - (GrpcNetClientOptions options) => TestGrpcNetClient.Run(), - (HttpClientOptions options) => TestHttpClient.Run(), - (ConsoleOptions options) => TestConsoleExporter.Run(options), - (OpenTelemetryShimOptions options) => TestOTelShimWithConsoleExporter.Run(options), - (OpenTracingShimOptions options) => TestOpenTracingShim.Run(options), - (OtlpOptions options) => TestOtlpExporter.Run(options.Endpoint, options.Protocol), - (InMemoryOptions options) => TestInMemoryExporter.Run(options), - errs => 1); - } + Parser.Default.ParseArguments(args) + .MapResult( + (ZipkinOptions options) => TestZipkinExporter.Run(options.Uri), + (PrometheusOptions options) => TestPrometheusExporter.Run(options.Port), + (MetricsOptions options) => TestMetrics.Run(options), + (LogsOptions options) => TestLogs.Run(options), + (GrpcNetClientOptions options) => TestGrpcNetClient.Run(), + (HttpClientOptions options) => TestHttpClient.Run(), + (ConsoleOptions options) => TestConsoleExporter.Run(options), + (OpenTelemetryShimOptions options) => TestOTelShimWithConsoleExporter.Run(options), + (OpenTracingShimOptions options) => TestOpenTracingShim.Run(options), + (OtlpOptions options) => TestOtlpExporter.Run(options.Endpoint, options.Protocol), + (InMemoryOptions options) => TestInMemoryExporter.Run(options), + errs => 1); } +} #pragma warning disable SA1402 // File may only contain a single type - [Verb("jaeger", HelpText = "Specify the options required to test Jaeger exporter")] - internal class JaegerOptions - { - [Option('h', "host", HelpText = "Host of the Jaeger Agent", Default = "localhost")] - public string Host { get; set; } +[Verb("zipkin", HelpText = "Specify the options required to test Zipkin exporter")] +internal class ZipkinOptions +{ + [Option('u', "uri", HelpText = "Please specify the uri of Zipkin backend", Required = true)] + public string Uri { get; set; } +} - [Option('p', "port", HelpText = "Port of the Jaeger Agent", Default = 6831)] - public int Port { get; set; } - } +[Verb("prometheus", HelpText = "Specify the options required to test Prometheus")] +internal class PrometheusOptions +{ + [Option('p', "port", Default = 9464, HelpText = "The port to expose metrics. The endpoint will be http://localhost:port/metrics/ (this is the port from which your Prometheus server scraps metrics from.)", Required = false)] + public int Port { get; set; } +} - [Verb("zipkin", HelpText = "Specify the options required to test Zipkin exporter")] - internal class ZipkinOptions - { - [Option('u', "uri", HelpText = "Please specify the uri of Zipkin backend", Required = true)] - public string Uri { get; set; } - } +[Verb("metrics", HelpText = "Specify the options required to test Metrics")] +internal class MetricsOptions +{ + [Option('d', "IsDelta", HelpText = "Export Delta metrics", Required = false, Default = false)] + public bool IsDelta { get; set; } - [Verb("prometheus", HelpText = "Specify the options required to test Prometheus")] - internal class PrometheusOptions - { - [Option('p', "port", Default = 9464, HelpText = "The port to expose metrics. The endpoint will be http://localhost:port/metrics/ (this is the port from which your Prometheus server scraps metrics from.)", Required = false)] - public int Port { get; set; } - } + [Option('g', "Gauge", HelpText = "Include Observable Gauge.", Required = false, Default = false)] + public bool? FlagGauge { get; set; } - [Verb("metrics", HelpText = "Specify the options required to test Metrics")] - internal class MetricsOptions - { - [Option('d', "IsDelta", HelpText = "Export Delta metrics", Required = false, Default = false)] - public bool IsDelta { get; set; } + [Option('c', "Counter", HelpText = "Include Counter.", Required = false, Default = true)] + public bool? FlagCounter { get; set; } - [Option('g', "Gauge", HelpText = "Include Observable Gauge.", Required = false, Default = false)] - public bool? FlagGauge { get; set; } + [Option('h', "Histogram", HelpText = "Include Histogram.", Required = false, Default = false)] + public bool? FlagHistogram { get; set; } - [Option('c', "Counter", HelpText = "Include Counter.", Required = false, Default = true)] - public bool? FlagCounter { get; set; } + [Option("defaultCollection", Default = 1000, HelpText = "Default collection period in milliseconds.", Required = false)] + public int DefaultCollectionPeriodMilliseconds { get; set; } - [Option('h', "Histogram", HelpText = "Include Histogram.", Required = false, Default = false)] - public bool? FlagHistogram { get; set; } + [Option("useExporter", Default = "console", HelpText = "Options include otlp or console.", Required = false)] + public string UseExporter { get; set; } - [Option("defaultCollection", Default = 1000, HelpText = "Default collection period in milliseconds.", Required = false)] - public int DefaultCollectionPeriodMilliseconds { get; set; } + [Option('e', "endpoint", HelpText = "Target to which the exporter is going to send metrics (default value depends on protocol).", Default = null)] + public string Endpoint { get; set; } - [Option("useExporter", Default = "console", HelpText = "Options include otlp or console.", Required = false)] - public string UseExporter { get; set; } + [Option('p', "useGrpc", HelpText = "Use gRPC or HTTP when using the OTLP exporter", Required = false, Default = true)] + public bool UseGrpc { get; set; } +} - [Option('e', "endpoint", HelpText = "Target to which the exporter is going to send metrics (default value depends on protocol).", Default = null)] - public string Endpoint { get; set; } +[Verb("grpc", HelpText = "Specify the options required to test Grpc.Net.Client")] +internal class GrpcNetClientOptions +{ +} - [Option('p', "useGrpc", HelpText = "Use gRPC or HTTP when using the OTLP exporter", Required = false, Default = true)] - public bool UseGrpc { get; set; } - } +[Verb("httpclient", HelpText = "Specify the options required to test HttpClient")] +internal class HttpClientOptions +{ +} - [Verb("grpc", HelpText = "Specify the options required to test Grpc.Net.Client")] - internal class GrpcNetClientOptions - { - } +[Verb("console", HelpText = "Specify the options required to test console exporter")] +internal class ConsoleOptions +{ +} - [Verb("httpclient", HelpText = "Specify the options required to test HttpClient")] - internal class HttpClientOptions - { - } +[Verb("otelshim", HelpText = "Specify the options required to test OpenTelemetry Shim with console exporter")] +internal class OpenTelemetryShimOptions +{ +} - [Verb("console", HelpText = "Specify the options required to test console exporter")] - internal class ConsoleOptions - { - } +[Verb("opentracing", HelpText = "Specify the options required to test OpenTracing Shim with console exporter")] +internal class OpenTracingShimOptions +{ +} - [Verb("otelshim", HelpText = "Specify the options required to test OpenTelemetry Shim with console exporter")] - internal class OpenTelemetryShimOptions - { - } +[Verb("otlp", HelpText = "Specify the options required to test OpenTelemetry Protocol (OTLP)")] +internal class OtlpOptions +{ + [Option('e', "endpoint", HelpText = "Target to which the exporter is going to send traces (default value depends on protocol).", Default = null)] + public string Endpoint { get; set; } - [Verb("opentracing", HelpText = "Specify the options required to test OpenTracing Shim with console exporter")] - internal class OpenTracingShimOptions - { - } + [Option('p', "protocol", HelpText = "Transport protocol used by exporter. Supported values: grpc and http/protobuf.", Default = "grpc")] + public string Protocol { get; set; } +} - [Verb("otlp", HelpText = "Specify the options required to test OpenTelemetry Protocol (OTLP)")] - internal class OtlpOptions - { - [Option('e', "endpoint", HelpText = "Target to which the exporter is going to send traces (default value depends on protocol).", Default = null)] - public string Endpoint { get; set; } +[Verb("logs", HelpText = "Specify the options required to test Logs")] +internal class LogsOptions +{ + [Option("useExporter", Default = "otlp", HelpText = "Options include otlp or console.", Required = false)] + public string UseExporter { get; set; } - [Option('p', "protocol", HelpText = "Transport protocol used by exporter. Supported values: grpc and http/protobuf.", Default = "grpc")] - public string Protocol { get; set; } - } + [Option('e', "endpoint", HelpText = "Target to which the OTLP exporter is going to send logs (default value depends on protocol).", Default = null)] + public string Endpoint { get; set; } - [Verb("logs", HelpText = "Specify the options required to test Logs")] - internal class LogsOptions - { - [Option("useExporter", Default = "otlp", HelpText = "Options include otlp or console.", Required = false)] - public string UseExporter { get; set; } + [Option('p', "protocol", HelpText = "Transport protocol used by OTLP exporter. Supported values: grpc and http/protobuf. Only applicable if Exporter is OTLP", Default = "grpc")] + public string Protocol { get; set; } - [Option('e', "endpoint", HelpText = "Target to which the OTLP exporter is going to send logs (default value depends on protocol).", Default = null)] - public string Endpoint { get; set; } + [Option("processorType", Default = "batch", HelpText = "export processor type. Supported values: simple and batch", Required = false)] + public string ProcessorType { get; set; } - [Option('p', "protocol", HelpText = "Transport protocol used by OTLP exporter. Supported values: grpc and http/protobuf. Only applicable if Exporter is OTLP", Default = "grpc")] - public string Protocol { get; set; } - } + [Option("scheduledDelay", Default = 5000, HelpText = "The delay interval in milliseconds between two consecutive exports.", Required = false)] + public int ScheduledDelayInMilliseconds { get; set; } +} - [Verb("inmemory", HelpText = "Specify the options required to test InMemory Exporter")] - internal class InMemoryOptions - { - } +[Verb("inmemory", HelpText = "Specify the options required to test InMemory Exporter")] +internal class InMemoryOptions +{ +} #pragma warning restore SA1402 // File may only contain a single type - -} diff --git a/examples/Console/TestConsoleExporter.cs b/examples/Console/TestConsoleExporter.cs index 031e6fc4e09..cf3f803c4fd 100644 --- a/examples/Console/TestConsoleExporter.cs +++ b/examples/Console/TestConsoleExporter.cs @@ -1,82 +1,68 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry; using OpenTelemetry.Resources; using OpenTelemetry.Trace; -namespace Examples.Console +namespace Examples.Console; + +internal class TestConsoleExporter { - internal class TestConsoleExporter + // To run this example, run the following command from + // the reporoot\examples\Console\. + // (eg: C:\repos\opentelemetry-dotnet\examples\Console\) + // + // dotnet run console + internal static object Run(ConsoleOptions options) { - // To run this example, run the following command from - // the reporoot\examples\Console\. - // (eg: C:\repos\opentelemetry-dotnet\examples\Console\) - // - // dotnet run console - internal static object Run(ConsoleOptions options) - { - return RunWithActivitySource(); - } - - private static object RunWithActivitySource() - { - // Enable OpenTelemetry for the sources "Samples.SampleServer" and "Samples.SampleClient" - // and use Console exporter. - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("Samples.SampleClient", "Samples.SampleServer") - .ConfigureResource(res => res.AddService("console-test")) - .AddProcessor(new MyProcessor()) // This must be added before ConsoleExporter - .AddConsoleExporter() - .Build(); + return RunWithActivitySource(); + } - // The above line is required only in applications - // which decide to use OpenTelemetry. - using (var sample = new InstrumentationWithActivitySource()) - { - sample.Start(); + private static object RunWithActivitySource() + { + // Enable OpenTelemetry for the sources "Samples.SampleServer" and "Samples.SampleClient" + // and use Console exporter. + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("Samples.SampleClient", "Samples.SampleServer") + .ConfigureResource(res => res.AddService("console-test")) + .AddProcessor(new MyProcessor()) // This must be added before ConsoleExporter + .AddConsoleExporter() + .Build(); - System.Console.WriteLine("Traces are being created and exported " + - "to Console in the background. " + - "Press ENTER to stop."); - System.Console.ReadLine(); - } + // The above line is required only in applications + // which decide to use OpenTelemetry. + using (var sample = new InstrumentationWithActivitySource()) + { + sample.Start(); - return null; + System.Console.WriteLine("Traces are being created and exported " + + "to Console in the background. " + + "Press ENTER to stop."); + System.Console.ReadLine(); } - /// - /// An example of custom processor which - /// can be used to add more tags to an activity. - /// - internal class MyProcessor : BaseProcessor + return null; + } + + /// + /// An example of custom processor which + /// can be used to add more tags to an activity. + /// + internal class MyProcessor : BaseProcessor + { + public override void OnStart(Activity activity) { - public override void OnStart(Activity activity) + if (activity.IsAllDataRequested) { - if (activity.IsAllDataRequested) + if (activity.Kind == ActivityKind.Server) + { + activity.SetTag("customServerTag", "Custom Tag Value for server"); + } + else if (activity.Kind == ActivityKind.Client) { - if (activity.Kind == ActivityKind.Server) - { - activity.SetTag("customServerTag", "Custom Tag Value for server"); - } - else if (activity.Kind == ActivityKind.Client) - { - activity.SetTag("customClientTag", "Custom Tag Value for Client"); - } + activity.SetTag("customClientTag", "Custom Tag Value for Client"); } } } diff --git a/examples/Console/TestGrpcNetClient.cs b/examples/Console/TestGrpcNetClient.cs index 3d6c6e3e802..d179d93913e 100644 --- a/examples/Console/TestGrpcNetClient.cs +++ b/examples/Console/TestGrpcNetClient.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using Examples.GrpcService; @@ -21,54 +8,53 @@ using OpenTelemetry; using OpenTelemetry.Trace; -namespace Examples.Console +namespace Examples.Console; + +internal class TestGrpcNetClient { - internal class TestGrpcNetClient + internal static object Run() { - internal static object Run() + // Prerequisite for running this example. + // In a separate console window, start the example + // ASP.NET Core gRPC service by running the following command + // from the reporoot\examples\GrpcService\. + // (eg: C:\repos\opentelemetry-dotnet\examples\GrpcService\) + // + // dotnet run + + // To run this example, run the following command from + // the reporoot\examples\Console\. + // (eg: C:\repos\opentelemetry-dotnet\examples\Console\) + // + // dotnet run grpc + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddGrpcClientInstrumentation() + .AddSource("grpc-net-client-test") + .AddConsoleExporter() + .Build(); + + using var source = new ActivitySource("grpc-net-client-test"); + using (var parent = source.StartActivity("Main", ActivityKind.Server)) { - // Prerequisite for running this example. - // In a separate console window, start the example - // ASP.NET Core gRPC service by running the following command - // from the reporoot\examples\GrpcService\. - // (eg: C:\repos\opentelemetry-dotnet\examples\GrpcService\) - // - // dotnet run - - // To run this example, run the following command from - // the reporoot\examples\Console\. - // (eg: C:\repos\opentelemetry-dotnet\examples\Console\) - // - // dotnet run grpc + using var channel = GrpcChannel.ForAddress("https://localhost:44335"); + var client = new Greeter.GreeterClient(channel); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddGrpcClientInstrumentation() - .AddSource("grpc-net-client-test") - .AddConsoleExporter() - .Build(); - - using var source = new ActivitySource("grpc-net-client-test"); - using (var parent = source.StartActivity("Main", ActivityKind.Server)) + try { - using var channel = GrpcChannel.ForAddress("https://localhost:44335"); - var client = new Greeter.GreeterClient(channel); - - try - { - var reply = client.SayHelloAsync(new HelloRequest { Name = "GreeterClient" }).GetAwaiter().GetResult(); - System.Console.WriteLine($"Message received: {reply.Message}"); - } - catch (RpcException) - { - System.Console.Error.WriteLine($"To run this Grpc.Net.Client example, first start the Examples.GrpcService project."); - throw; - } + var reply = client.SayHelloAsync(new HelloRequest { Name = "GreeterClient" }).GetAwaiter().GetResult(); + System.Console.WriteLine($"Message received: {reply.Message}"); } + catch (RpcException) + { + System.Console.Error.WriteLine($"To run this Grpc.Net.Client example, first start the Examples.GrpcService project."); + throw; + } + } - System.Console.WriteLine("Press Enter key to exit."); - System.Console.ReadLine(); + System.Console.WriteLine("Press Enter key to exit."); + System.Console.ReadLine(); - return null; - } + return null; } } diff --git a/examples/Console/TestHttpClient.cs b/examples/Console/TestHttpClient.cs index 239f603c1aa..27305a50523 100644 --- a/examples/Console/TestHttpClient.cs +++ b/examples/Console/TestHttpClient.cs @@ -1,54 +1,41 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + using System.Diagnostics; using OpenTelemetry; using OpenTelemetry.Resources; using OpenTelemetry.Trace; -namespace Examples.Console +namespace Examples.Console; + +internal class TestHttpClient { - internal class TestHttpClient + // To run this example, run the following command from + // the reporoot\examples\Console\. + // (eg: C:\repos\opentelemetry-dotnet\examples\Console\) + // + // dotnet run httpclient + internal static object Run() { - // To run this example, run the following command from - // the reporoot\examples\Console\. - // (eg: C:\repos\opentelemetry-dotnet\examples\Console\) - // - // dotnet run httpclient - internal static object Run() - { - System.Console.WriteLine("Hello World!"); + System.Console.WriteLine("Hello World!"); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation() - .ConfigureResource(r => r.AddService("http-service-example")) - .AddSource("http-client-test") - .AddConsoleExporter() - .Build(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddHttpClientInstrumentation() + .ConfigureResource(r => r.AddService("http-service-example")) + .AddSource("http-client-test") + .AddConsoleExporter() + .Build(); - using var source = new ActivitySource("http-client-test"); - using (var parent = source.StartActivity("incoming request", ActivityKind.Server)) - { - using var client = new HttpClient(); - client.GetStringAsync("http://bing.com").GetAwaiter().GetResult(); - } + using var source = new ActivitySource("http-client-test"); + using (var parent = source.StartActivity("incoming request", ActivityKind.Server)) + { + using var client = new HttpClient(); + client.GetStringAsync("http://bing.com").GetAwaiter().GetResult(); + } - System.Console.WriteLine("Press Enter key to exit."); - System.Console.ReadLine(); + System.Console.WriteLine("Press Enter key to exit."); + System.Console.ReadLine(); - return null; - } + return null; } } diff --git a/examples/Console/TestInMemoryExporter.cs b/examples/Console/TestInMemoryExporter.cs index acb4ecfa169..53d3dd7f49e 100644 --- a/examples/Console/TestInMemoryExporter.cs +++ b/examples/Console/TestInMemoryExporter.cs @@ -1,68 +1,54 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry; using OpenTelemetry.Resources; using OpenTelemetry.Trace; -namespace Examples.Console +namespace Examples.Console; + +internal class TestInMemoryExporter { - internal class TestInMemoryExporter + // To run this example, run the following command from + // the reporoot\examples\Console\. + // (eg: C:\repos\opentelemetry-dotnet\examples\Console\) + // + // dotnet run inmemory + internal static object Run(InMemoryOptions options) { - // To run this example, run the following command from - // the reporoot\examples\Console\. - // (eg: C:\repos\opentelemetry-dotnet\examples\Console\) - // - // dotnet run inmemory - internal static object Run(InMemoryOptions options) - { - // List that will be populated with the traces by InMemoryExporter - var exportedItems = new List(); - - RunWithActivitySource(exportedItems); + // List that will be populated with the traces by InMemoryExporter + var exportedItems = new List(); - // List exportedItems is populated with the Activity objects logged by TracerProvider - foreach (var activity in exportedItems) - { - System.Console.WriteLine($"ActivitySource: {activity.Source.Name} logged the activity {activity.DisplayName}"); - } - - return null; - } + RunWithActivitySource(exportedItems); - private static void RunWithActivitySource(ICollection exportedItems) + // List exportedItems is populated with the Activity objects logged by TracerProvider + foreach (var activity in exportedItems) { - // Enable OpenTelemetry for the sources "Samples.SampleServer" and "Samples.SampleClient" - // and use InMemory exporter. - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("Samples.SampleClient", "Samples.SampleServer") - .ConfigureResource(r => r.AddService("inmemory-test")) - .AddInMemoryExporter(exportedItems) - .Build(); + System.Console.WriteLine($"ActivitySource: {activity.Source.Name} logged the activity {activity.DisplayName}"); + } - // The above line is required only in applications - // which decide to use OpenTelemetry. - using var sample = new InstrumentationWithActivitySource(); - sample.Start(); + return null; + } - System.Console.WriteLine("Traces are being created and exported " + - "to the collection passed in the background. " + - "Press ENTER to stop."); - System.Console.ReadLine(); - } + private static void RunWithActivitySource(ICollection exportedItems) + { + // Enable OpenTelemetry for the sources "Samples.SampleServer" and "Samples.SampleClient" + // and use InMemory exporter. + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("Samples.SampleClient", "Samples.SampleServer") + .ConfigureResource(r => r.AddService("inmemory-test")) + .AddInMemoryExporter(exportedItems) + .Build(); + + // The above line is required only in applications + // which decide to use OpenTelemetry. + using var sample = new InstrumentationWithActivitySource(); + sample.Start(); + + System.Console.WriteLine("Traces are being created and exported " + + "to the collection passed in the background. " + + "Press ENTER to stop."); + System.Console.ReadLine(); } } diff --git a/examples/Console/TestJaegerExporter.cs b/examples/Console/TestJaegerExporter.cs deleted file mode 100644 index 9f2adac33d8..00000000000 --- a/examples/Console/TestJaegerExporter.cs +++ /dev/null @@ -1,100 +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 OpenTelemetry; -using OpenTelemetry.Resources; -using OpenTelemetry.Trace; - -namespace Examples.Console -{ - internal class TestJaegerExporter - { - internal static object Run(string host, int port) - { - // Prerequisite for running this example. - // Setup Jaegar inside local docker using following command (Source: https://www.jaegertracing.io/docs/1.21/getting-started/#all-in-one): - /* - $ docker run -d --name jaeger \ - -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \ - -p 5775:5775/udp \ - -p 6831:6831/udp \ - -p 6832:6832/udp \ - -p 5778:5778 \ - -p 16686:16686 \ - -p 14268:14268 \ - -p 14250:14250 \ - -p 9411:9411 \ - jaegertracing/all-in-one:1.21 - */ - - // To run this example, run the following command from - // the reporoot\examples\Console\. - // (eg: C:\repos\opentelemetry-dotnet\examples\Console\) - // - // dotnet run --project Examples.Console.csproj -- jaeger -h localhost -p 6831 - // For non-Windows (e.g., MacOS) - // dotnet run jaeger -- -h localhost -p 6831 - // Navigate to http://localhost:16686 to access the Jaeger UI. - return RunWithActivity(host, port); - } - - internal static object RunWithActivity(string host, int port) - { - // Enable OpenTelemetry for the sources "Samples.SampleServer" and "Samples.SampleClient" - // and use the Jaeger exporter. - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .ConfigureResource(r => r.AddService("jaeger-test")) - .AddSource("Samples.SampleClient", "Samples.SampleServer") - .AddJaegerExporter(o => - { - o.AgentHost = host; - o.AgentPort = port; - - // Examples for the rest of the options, defaults unless otherwise specified - // Omitting Process Tags example as Resource API is recommended for additional tags - o.MaxPayloadSizeInBytes = 4096; - - // Using Batch Exporter (which is default) - // The other option is ExportProcessorType.Simple - o.ExportProcessorType = ExportProcessorType.Batch; - o.BatchExportProcessorOptions = new BatchExportProcessorOptions() - { - MaxQueueSize = 2048, - ScheduledDelayMilliseconds = 5000, - ExporterTimeoutMilliseconds = 30000, - MaxExportBatchSize = 512, - }; - }) - .Build(); - - // The above lines are required only in Applications - // which decide to use OpenTelemetry. - - using (var sample = new InstrumentationWithActivitySource()) - { - sample.Start(); - - System.Console.WriteLine("Traces are being created and exported " + - "to Jaeger in the background. Use Jaeger to view them. " + - "Press ENTER to stop."); - System.Console.ReadLine(); - } - - return null; - } - } -} diff --git a/examples/Console/TestLogs.cs b/examples/Console/TestLogs.cs index 98d60ab21e8..13a7b7f60e7 100644 --- a/examples/Console/TestLogs.cs +++ b/examples/Console/TestLogs.cs @@ -1,102 +1,113 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.Logging; +using OpenTelemetry; using OpenTelemetry.Logs; -namespace Examples.Console +namespace Examples.Console; + +internal class TestLogs { - internal class TestLogs + internal static object Run(LogsOptions options) { - internal static object Run(LogsOptions options) + using var loggerFactory = LoggerFactory.Create(builder => { - using var loggerFactory = LoggerFactory.Create(builder => + builder.AddOpenTelemetry((opt) => { - builder.AddOpenTelemetry((opt) => + opt.IncludeFormattedMessage = true; + opt.IncludeScopes = true; + if (options.UseExporter.Equals("otlp", StringComparison.OrdinalIgnoreCase)) { - opt.IncludeFormattedMessage = true; - opt.IncludeScopes = true; - if (options.UseExporter.Equals("otlp", StringComparison.OrdinalIgnoreCase)) - { - /* - * Prerequisite to run this example: - * Set up an OpenTelemetry Collector to run on local docker. - * - * Open a terminal window at the examples/Console/ directory and - * launch the OpenTelemetry Collector with an OTLP receiver, by running: - * - * - On Unix based systems use: - * docker run --rm -it -p 4317:4317 -p 4318:4318 -v $(pwd):/cfg otel/opentelemetry-collector:0.48.0 --config=/cfg/otlp-collector-example/config.yaml - * - * - On Windows use: - * docker run --rm -it -p 4317:4317 -p 4318:4318 -v "%cd%":/cfg otel/opentelemetry-collector:0.48.0 --config=/cfg/otlp-collector-example/config.yaml - * - * Open another terminal window at the examples/Console/ directory and - * launch the OTLP example by running: - * - * dotnet run logs --useExporter otlp -e http://localhost:4317 - * - * The OpenTelemetry Collector will output all received logs to the stdout of its terminal. - * - */ + /* + * Prerequisite to run this example: + * Set up an OpenTelemetry Collector to run on local docker. + * + * Open a terminal window at the examples/Console/ directory and + * launch the OpenTelemetry Collector with an OTLP receiver, by running: + * + * - On Unix based systems use: + * docker run --rm -it -p 4317:4317 -p 4318:4318 -v $(pwd):/cfg otel/opentelemetry-collector:0.48.0 --config=/cfg/otlp-collector-example/config.yaml + * + * - On Windows use: + * docker run --rm -it -p 4317:4317 -p 4318:4318 -v "%cd%":/cfg otel/opentelemetry-collector:0.48.0 --config=/cfg/otlp-collector-example/config.yaml + * + * Open another terminal window at the examples/Console/ directory and + * launch the OTLP example by running: + * + * dotnet run logs --useExporter otlp -e http://localhost:4317 + * + * The OpenTelemetry Collector will output all received logs to the stdout of its terminal. + * + */ - // Adding the OtlpExporter creates a GrpcChannel. - // This switch must be set before creating a GrpcChannel when calling an insecure gRPC service. - // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + // Adding the OtlpExporter creates a GrpcChannel. + // This switch must be set before creating a GrpcChannel when calling an insecure gRPC service. + // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - var protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc; + var protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc; + + if (options.Protocol.Trim().ToLower().Equals("grpc")) + { + protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc; + } + else if (options.Protocol.Trim().ToLower().Equals("http/protobuf")) + { + protocol = OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf; + } + else + { + System.Console.WriteLine($"Export protocol {options.Protocol} is not supported. Default protocol 'grpc' will be used."); + } - if (options.Protocol.Trim().ToLower().Equals("grpc")) + var processorType = ExportProcessorType.Batch; + if (options.ProcessorType.Trim().ToLower().Equals("batch")) + { + processorType = ExportProcessorType.Batch; + } + else if (options.ProcessorType.Trim().ToLower().Equals("simple")) + { + processorType = ExportProcessorType.Simple; + } + else + { + System.Console.WriteLine($"Export processor type {options.ProcessorType} is not supported. Default processor type 'batch' will be used."); + } + + opt.AddOtlpExporter((exporterOptions, processorOptions) => + { + exporterOptions.Protocol = protocol; + if (!string.IsNullOrWhiteSpace(options.Endpoint)) { - protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc; + exporterOptions.Endpoint = new Uri(options.Endpoint); } - else if (options.Protocol.Trim().ToLower().Equals("http/protobuf")) + + if (processorType == ExportProcessorType.Simple) { - protocol = OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf; + processorOptions.ExportProcessorType = ExportProcessorType.Simple; } else { - System.Console.WriteLine($"Export protocol {options.Protocol} is not supported. Default protocol 'grpc' will be used."); + processorOptions.ExportProcessorType = ExportProcessorType.Batch; + processorOptions.BatchExportProcessorOptions = new BatchExportLogRecordProcessorOptions() { ScheduledDelayMilliseconds = options.ScheduledDelayInMilliseconds }; } - - opt.AddOtlpExporter(otlpOptions => - { - otlpOptions.Protocol = protocol; - if (!string.IsNullOrWhiteSpace(options.Endpoint)) - { - otlpOptions.Endpoint = new Uri(options.Endpoint); - } - }); - } - else - { - opt.AddConsoleExporter(); - } - }); + }); + } + else + { + opt.AddConsoleExporter(); + } }); + }); - var logger = loggerFactory.CreateLogger(); - using (logger.BeginScope("{city}", "Seattle")) - using (logger.BeginScope("{storeType}", "Physical")) - { - logger.LogInformation("Hello from {name} {price}.", "tomato", 2.99); - } - - return null; + var logger = loggerFactory.CreateLogger(); + using (logger.BeginScope("{city}", "Seattle")) + using (logger.BeginScope("{storeType}", "Physical")) + { + logger.LogInformation("Hello from {name} {price}.", "tomato", 2.99); } + + return null; } } diff --git a/examples/Console/TestMetrics.cs b/examples/Console/TestMetrics.cs index da7e720f775..ba943e91775 100644 --- a/examples/Console/TestMetrics.cs +++ b/examples/Console/TestMetrics.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Diagnostics.Metrics; @@ -21,142 +8,148 @@ using OpenTelemetry.Metrics; using OpenTelemetry.Resources; -namespace Examples.Console +namespace Examples.Console; + +internal class TestMetrics { - internal class TestMetrics + internal static object Run(MetricsOptions options) { - internal static object Run(MetricsOptions options) + var meterVersion = "1.0"; + var meterTags = new List> { - using var meter = new Meter("TestMeter"); + new( + "MeterTagKey", + "MeterTagValue"), + }; + using var meter = new Meter("TestMeter", meterVersion, meterTags); - var providerBuilder = Sdk.CreateMeterProviderBuilder() - .ConfigureResource(r => r.AddService("myservice")) - .AddMeter(meter.Name); // All instruments from this meter are enabled. + var providerBuilder = Sdk.CreateMeterProviderBuilder() + .ConfigureResource(r => r.AddService("myservice")) + .AddMeter(meter.Name); // All instruments from this meter are enabled. - if (options.UseExporter.Equals("otlp", StringComparison.OrdinalIgnoreCase)) - { - /* - * Prerequisite to run this example: - * Set up an OpenTelemetry Collector to run on local docker. - * - * Open a terminal window at the examples/Console/ directory and - * launch the OpenTelemetry Collector with an OTLP receiver, by running: - * - * - On Unix based systems use: - * docker run --rm -it -p 4317:4317 -v $(pwd):/cfg otel/opentelemetry-collector:0.33.0 --config=/cfg/otlp-collector-example/config.yaml - * - * - On Windows use: - * docker run --rm -it -p 4317:4317 -v "%cd%":/cfg otel/opentelemetry-collector:0.33.0 --config=/cfg/otlp-collector-example/config.yaml - * - * Open another terminal window at the examples/Console/ directory and - * launch the OTLP example by running: - * - * dotnet run metrics --useExporter otlp -e http://localhost:4317 - * - * The OpenTelemetry Collector will output all received metrics to the stdout of its terminal. - * - */ - - providerBuilder - .AddOtlpExporter((exporterOptions, metricReaderOptions) => - { - exporterOptions.Protocol = options.UseGrpc ? OtlpExportProtocol.Grpc : OtlpExportProtocol.HttpProtobuf; - - if (!string.IsNullOrWhiteSpace(options.Endpoint)) - { - exporterOptions.Endpoint = new Uri(options.Endpoint); - } - - metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = options.DefaultCollectionPeriodMilliseconds; - metricReaderOptions.TemporalityPreference = options.IsDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }); - } - else - { - providerBuilder - .AddConsoleExporter((exporterOptions, metricReaderOptions) => + if (options.UseExporter.Equals("otlp", StringComparison.OrdinalIgnoreCase)) + { + /* + * Prerequisite to run this example: + * Set up an OpenTelemetry Collector to run on local docker. + * + * Open a terminal window at the examples/Console/ directory and + * launch the OpenTelemetry Collector with an OTLP receiver, by running: + * + * - On Unix based systems use: + * docker run --rm -it -p 4317:4317 -v $(pwd):/cfg otel/opentelemetry-collector:0.33.0 --config=/cfg/otlp-collector-example/config.yaml + * + * - On Windows use: + * docker run --rm -it -p 4317:4317 -v "%cd%":/cfg otel/opentelemetry-collector:0.33.0 --config=/cfg/otlp-collector-example/config.yaml + * + * Open another terminal window at the examples/Console/ directory and + * launch the OTLP example by running: + * + * dotnet run metrics --useExporter otlp -e http://localhost:4317 + * + * The OpenTelemetry Collector will output all received metrics to the stdout of its terminal. + * + */ + + providerBuilder + .AddOtlpExporter((exporterOptions, metricReaderOptions) => + { + exporterOptions.Protocol = options.UseGrpc ? OtlpExportProtocol.Grpc : OtlpExportProtocol.HttpProtobuf; + + if (!string.IsNullOrWhiteSpace(options.Endpoint)) { - exporterOptions.Targets = ConsoleExporterOutputTargets.Console; + exporterOptions.Endpoint = new Uri(options.Endpoint); + } - metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = options.DefaultCollectionPeriodMilliseconds; - metricReaderOptions.TemporalityPreference = options.IsDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }); - } + metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = options.DefaultCollectionPeriodMilliseconds; + metricReaderOptions.TemporalityPreference = options.IsDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; + }); + } + else + { + providerBuilder + .AddConsoleExporter((exporterOptions, metricReaderOptions) => + { + exporterOptions.Targets = ConsoleExporterOutputTargets.Console; - using var provider = providerBuilder.Build(); + metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = options.DefaultCollectionPeriodMilliseconds; + metricReaderOptions.TemporalityPreference = options.IsDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; + }); + } - Counter counter = null; - if (options.FlagCounter ?? true) - { - counter = meter.CreateCounter("counter", "things", "A count of things"); - } + using var provider = providerBuilder.Build(); - Histogram histogram = null; - if (options.FlagHistogram ?? false) - { - histogram = meter.CreateHistogram("histogram"); - } + Counter counter = null; + if (options.FlagCounter ?? true) + { + counter = meter.CreateCounter("counter", "things", "A count of things"); + } - if (options.FlagGauge ?? false) - { - var observableCounter = meter.CreateObservableGauge("gauge", () => - { - return new List>() - { - new Measurement( - (int)Process.GetCurrentProcess().PrivateMemorySize64, - new KeyValuePair("tag1", "value1")), - }; - }); - } + Histogram histogram = null; + if (options.FlagHistogram ?? false) + { + histogram = meter.CreateHistogram("histogram"); + } - System.Console.WriteLine("Press any key to exit."); - while (!System.Console.KeyAvailable) + if (options.FlagGauge ?? false) + { + var observableCounter = meter.CreateObservableGauge("gauge", () => { - histogram?.Record(10); + return new List>() + { + new Measurement( + (int)Process.GetCurrentProcess().PrivateMemorySize64, + new KeyValuePair("tag1", "value1")), + }; + }); + } - histogram?.Record( - 100, - new KeyValuePair("tag1", "value1")); + System.Console.WriteLine("Press any key to exit."); + while (!System.Console.KeyAvailable) + { + histogram?.Record(10); - histogram?.Record( - 200, - new KeyValuePair("tag1", "value2"), - new KeyValuePair("tag2", "value2")); + histogram?.Record( + 100, + new KeyValuePair("tag1", "value1")); - histogram?.Record( - 100, - new KeyValuePair("tag1", "value1")); + histogram?.Record( + 200, + new KeyValuePair("tag1", "value2"), + new KeyValuePair("tag2", "value2")); - histogram?.Record( - 200, - new KeyValuePair("tag2", "value2"), - new KeyValuePair("tag1", "value2")); + histogram?.Record( + 100, + new KeyValuePair("tag1", "value1")); - counter?.Add(10); + histogram?.Record( + 200, + new KeyValuePair("tag2", "value2"), + new KeyValuePair("tag1", "value2")); - counter?.Add( - 100, - new KeyValuePair("tag1", "value1")); + counter?.Add(10); - counter?.Add( - 200, - new KeyValuePair("tag1", "value2"), - new KeyValuePair("tag2", "value2")); + counter?.Add( + 100, + new KeyValuePair("tag1", "value1")); - counter?.Add( - 100, - new KeyValuePair("tag1", "value1")); + counter?.Add( + 200, + new KeyValuePair("tag1", "value2"), + new KeyValuePair("tag2", "value2")); - counter?.Add( - 200, - new KeyValuePair("tag2", "value2"), - new KeyValuePair("tag1", "value2")); + counter?.Add( + 100, + new KeyValuePair("tag1", "value1")); - Task.Delay(500).Wait(); - } + counter?.Add( + 200, + new KeyValuePair("tag2", "value2"), + new KeyValuePair("tag1", "value2")); - return null; + Task.Delay(500).Wait(); } + + return null; } } diff --git a/examples/Console/TestOTelShimWithConsoleExporter.cs b/examples/Console/TestOTelShimWithConsoleExporter.cs index 31bb3c85f6b..6557357a963 100644 --- a/examples/Console/TestOTelShimWithConsoleExporter.cs +++ b/examples/Console/TestOTelShimWithConsoleExporter.cs @@ -1,59 +1,45 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry; using OpenTelemetry.Resources; using OpenTelemetry.Trace; -namespace Examples.Console +namespace Examples.Console; + +internal class TestOTelShimWithConsoleExporter { - internal class TestOTelShimWithConsoleExporter + internal static object Run(OpenTelemetryShimOptions options) { - internal static object Run(OpenTelemetryShimOptions options) + // Enable OpenTelemetry for the source "MyCompany.MyProduct.MyWebServer" + // and use a single pipeline with a custom MyProcessor, and Console exporter. + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("MyCompany.MyProduct.MyWebServer") + .ConfigureResource(r => r.AddService("MyServiceName")) + .AddConsoleExporter() + .Build(); + + // The above line is required only in applications + // which decide to use OpenTelemetry. + + var tracer = TracerProvider.Default.GetTracer("MyCompany.MyProduct.MyWebServer"); + using (var parentSpan = tracer.StartActiveSpan("parent span")) { - // Enable OpenTelemetry for the source "MyCompany.MyProduct.MyWebServer" - // and use a single pipeline with a custom MyProcessor, and Console exporter. - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("MyCompany.MyProduct.MyWebServer") - .ConfigureResource(r => r.AddService("MyServiceName")) - .AddConsoleExporter() - .Build(); - - // The above line is required only in applications - // which decide to use OpenTelemetry. - - var tracer = TracerProvider.Default.GetTracer("MyCompany.MyProduct.MyWebServer"); - using (var parentSpan = tracer.StartActiveSpan("parent span")) - { - parentSpan.SetAttribute("mystring", "value"); - parentSpan.SetAttribute("myint", 100); - parentSpan.SetAttribute("mydouble", 101.089); - parentSpan.SetAttribute("mybool", true); - parentSpan.UpdateName("parent span new name"); - - var childSpan = tracer.StartSpan("child span"); - childSpan.AddEvent("sample event").SetAttribute("ch", "value").SetAttribute("more", "attributes"); - childSpan.SetStatus(Status.Ok); - childSpan.End(); - } - - System.Console.WriteLine("Press Enter key to exit."); - System.Console.ReadLine(); - - return null; + parentSpan.SetAttribute("mystring", "value"); + parentSpan.SetAttribute("myint", 100); + parentSpan.SetAttribute("mydouble", 101.089); + parentSpan.SetAttribute("mybool", true); + parentSpan.UpdateName("parent span new name"); + + var childSpan = tracer.StartSpan("child span"); + childSpan.AddEvent("sample event").SetAttribute("ch", "value").SetAttribute("more", "attributes"); + childSpan.SetStatus(Status.Ok); + childSpan.End(); } + + System.Console.WriteLine("Press Enter key to exit."); + System.Console.ReadLine(); + + return null; } } diff --git a/examples/Console/TestOpenTracingShim.cs b/examples/Console/TestOpenTracingShim.cs index 7361c3aa4d5..0de2419b6ef 100644 --- a/examples/Console/TestOpenTracingShim.cs +++ b/examples/Console/TestOpenTracingShim.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry; using OpenTelemetry.Context.Propagation; @@ -21,46 +8,51 @@ using OpenTelemetry.Trace; using OpenTracing; -namespace Examples.Console +namespace Examples.Console; + +internal class TestOpenTracingShim { - internal class TestOpenTracingShim + internal static object Run(OpenTracingShimOptions options) { - internal static object Run(OpenTracingShimOptions options) + // Enable OpenTelemetry for the source "opentracing-shim" + // and use Console exporter. + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("opentracing-shim") + .ConfigureResource(r => r.AddService("MyServiceName")) + .AddConsoleExporter() + .Build(); + + // Instantiate the OpenTracing shim. The underlying OpenTelemetry tracer will create + // spans using the "opentracing-shim" source. + var openTracingTracerShim = new TracerShim( + TracerProvider.Default, + Propagators.DefaultTextMapPropagator); + + // The OpenTracing Tracer shim instance must be registered prior to any calls + // to GlobalTracer.Instance, otherwise GlobalTracer.Instance will register a NoopTracer + // preventing sampling of any OpenTracing spans. + OpenTracing.Util.GlobalTracer.Register(openTracingTracerShim); + + // The code ahead could just use the OpenTracing Tracer shim instance directly. + // However, an instrumentation using OpenTracing API will use the GlobalTracer.Instance + // to create spans. + var openTracingTracer = OpenTracing.Util.GlobalTracer.Instance; + + // The code below is meant to resemble application code that has been instrumented + // with the OpenTracing API. + using (IScope parentScope = openTracingTracer.BuildSpan("Parent").StartActive(finishSpanOnDispose: true)) { - // Enable OpenTelemetry for the source "MyCompany.MyProduct.MyWebServer" - // and use Console exporter. - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("MyCompany.MyProduct.MyWebServer") - .ConfigureResource(r => r.AddService("MyServiceName")) - .AddConsoleExporter() - .Build(); - - // Instantiate the OpenTracing shim. The underlying OpenTelemetry tracer will create - // spans using the "MyCompany.MyProduct.MyWebServer" source. - var tracer = new TracerShim( - TracerProvider.Default.GetTracer("MyCompany.MyProduct.MyWebServer"), - Propagators.DefaultTextMapPropagator); - - // Not necessary for this example, though it is best practice per - // the OpenTracing project to register a GlobalTracer. - OpenTracing.Util.GlobalTracer.Register(tracer); + parentScope.Span.SetTag("my", "value"); + parentScope.Span.SetOperationName("parent span new name"); - // The code below is meant to resemble application code that has been instrumented - // with the OpenTracing API. - using (IScope parentScope = tracer.BuildSpan("Parent").StartActive(finishSpanOnDispose: true)) - { - parentScope.Span.SetTag("my", "value"); - parentScope.Span.SetOperationName("parent span new name"); - - // The child scope will automatically use parentScope as its parent. - using IScope childScope = tracer.BuildSpan("Child").StartActive(finishSpanOnDispose: true); - childScope.Span.SetTag("Child Tag", "Child Tag Value").SetTag("ch", "value").SetTag("more", "attributes"); - } + // The child scope will automatically use parentScope as its parent. + using IScope childScope = openTracingTracer.BuildSpan("Child").StartActive(finishSpanOnDispose: true); + childScope.Span.SetTag("Child Tag", "Child Tag Value").SetTag("ch", "value").SetTag("more", "attributes"); + } - System.Console.WriteLine("Press Enter key to exit."); - System.Console.ReadLine(); + System.Console.WriteLine("Press Enter key to exit."); + System.Console.ReadLine(); - return null; - } + return null; } } diff --git a/examples/Console/TestOtlpExporter.cs b/examples/Console/TestOtlpExporter.cs index eee1789991d..94d749eafef 100644 --- a/examples/Console/TestOtlpExporter.cs +++ b/examples/Console/TestOtlpExporter.cs @@ -1,106 +1,92 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry; using OpenTelemetry.Exporter; using OpenTelemetry.Resources; using OpenTelemetry.Trace; -namespace Examples.Console +namespace Examples.Console; + +internal static class TestOtlpExporter { - internal static class TestOtlpExporter + internal static object Run(string endpoint, string protocol) { - internal static object Run(string endpoint, string protocol) - { - /* - * Prerequisite to run this example: - * Set up an OpenTelemetry Collector to run on local docker. - * - * Open a terminal window at the examples/Console/ directory and - * launch the OpenTelemetry Collector with an OTLP receiver, by running: - * - * - On Unix based systems use: - * docker run --rm -it -p 4317:4317 -p 4318:4318 -v $(pwd):/cfg otel/opentelemetry-collector:0.48.0 --config=/cfg/otlp-collector-example/config.yaml - * - * - On Windows use: - * docker run --rm -it -p 4317:4317 -p 4318:4318 -v "%cd%":/cfg otel/opentelemetry-collector:0.48.0 --config=/cfg/otlp-collector-example/config.yaml - * - * Open another terminal window at the examples/Console/ directory and - * launch the OTLP example by running: - * - * dotnet run otlp - * - * The OpenTelemetry Collector will output all received spans to the stdout of its terminal until - * it is stopped via CTRL+C. - * - * For more information about the OpenTelemetry Collector go to https://github.com/open-telemetry/opentelemetry-collector - * - */ - return RunWithActivitySource(endpoint, protocol); - } + /* + * Prerequisite to run this example: + * Set up an OpenTelemetry Collector to run on local docker. + * + * Open a terminal window at the examples/Console/ directory and + * launch the OpenTelemetry Collector with an OTLP receiver, by running: + * + * - On Unix based systems use: + * docker run --rm -it -p 4317:4317 -p 4318:4318 -v $(pwd):/cfg otel/opentelemetry-collector:latest --config=/cfg/otlp-collector-example/config.yaml + * + * - On Windows use: + * docker run --rm -it -p 4317:4317 -p 4318:4318 -v "%cd%":/cfg otel/opentelemetry-collector:latest --config=/cfg/otlp-collector-example/config.yaml + * + * Open another terminal window at the examples/Console/ directory and + * launch the OTLP example by running: + * + * dotnet run otlp + * + * The OpenTelemetry Collector will output all received spans to the stdout of its terminal until + * it is stopped via CTRL+C. + * + * For more information about the OpenTelemetry Collector go to https://github.com/open-telemetry/opentelemetry-collector + * + */ + return RunWithActivitySource(endpoint, protocol); + } - private static object RunWithActivitySource(string endpoint, string protocol) + private static object RunWithActivitySource(string endpoint, string protocol) + { + var otlpExportProtocol = ToOtlpExportProtocol(protocol); + if (!otlpExportProtocol.HasValue) { - var otlpExportProtocol = ToOtlpExportProtocol(protocol); - if (!otlpExportProtocol.HasValue) - { - System.Console.WriteLine($"Export protocol {protocol} is not supported. Default protocol 'grpc' will be used."); - otlpExportProtocol = OtlpExportProtocol.Grpc; - } + System.Console.WriteLine($"Export protocol {protocol} is not supported. Default protocol 'grpc' will be used."); + otlpExportProtocol = OtlpExportProtocol.Grpc; + } - // Enable OpenTelemetry for the sources "Samples.SampleServer" and "Samples.SampleClient" - // and use OTLP exporter. - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("Samples.SampleClient", "Samples.SampleServer") - .ConfigureResource(r => r.AddService("otlp-test")) - .AddOtlpExporter(opt => + // Enable OpenTelemetry for the sources "Samples.SampleServer" and "Samples.SampleClient" + // and use OTLP exporter. + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("Samples.SampleClient", "Samples.SampleServer") + .ConfigureResource(r => r.AddService("otlp-test")) + .AddOtlpExporter(opt => + { + // If endpoint was not specified, the proper one will be selected according to the protocol. + if (!string.IsNullOrEmpty(endpoint)) { - // If endpoint was not specified, the proper one will be selected according to the protocol. - if (!string.IsNullOrEmpty(endpoint)) - { - opt.Endpoint = new Uri(endpoint); - } - - opt.Protocol = otlpExportProtocol.Value; + opt.Endpoint = new Uri(endpoint); + } - System.Console.WriteLine($"OTLP Exporter is using {opt.Protocol} protocol and endpoint {opt.Endpoint}"); - }) - .Build(); + opt.Protocol = otlpExportProtocol.Value; - // The above line is required only in Applications - // which decide to use OpenTelemetry. - using (var sample = new InstrumentationWithActivitySource()) - { - sample.Start(); + System.Console.WriteLine($"OTLP Exporter is using {opt.Protocol} protocol and endpoint {opt.Endpoint}"); + }) + .Build(); - System.Console.WriteLine("Traces are being created and exported " + - "to the OpenTelemetry Collector in the background. " + - "Press ENTER to stop."); - System.Console.ReadLine(); - } + // The above line is required only in Applications + // which decide to use OpenTelemetry. + using (var sample = new InstrumentationWithActivitySource()) + { + sample.Start(); - return null; + System.Console.WriteLine("Traces are being created and exported " + + "to the OpenTelemetry Collector in the background. " + + "Press ENTER to stop."); + System.Console.ReadLine(); } - private static OtlpExportProtocol? ToOtlpExportProtocol(string protocol) => - protocol.Trim().ToLower() switch - { - "grpc" => OtlpExportProtocol.Grpc, - "http/protobuf" => OtlpExportProtocol.HttpProtobuf, - _ => null, - }; + return null; } + + private static OtlpExportProtocol? ToOtlpExportProtocol(string protocol) => + protocol.Trim().ToLower() switch + { + "grpc" => OtlpExportProtocol.Grpc, + "http/protobuf" => OtlpExportProtocol.HttpProtobuf, + _ => null, + }; } diff --git a/examples/Console/TestPrometheusExporter.cs b/examples/Console/TestPrometheusExporter.cs index 170b2c241b5..57f38c466df 100644 --- a/examples/Console/TestPrometheusExporter.cs +++ b/examples/Console/TestPrometheusExporter.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Diagnostics.Metrics; diff --git a/examples/Console/TestZipkinExporter.cs b/examples/Console/TestZipkinExporter.cs index 94d3a4495ad..271826a7c2b 100644 --- a/examples/Console/TestZipkinExporter.cs +++ b/examples/Console/TestZipkinExporter.cs @@ -1,61 +1,47 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry; using OpenTelemetry.Resources; using OpenTelemetry.Trace; -namespace Examples.Console +namespace Examples.Console; + +internal class TestZipkinExporter { - internal class TestZipkinExporter + internal static object Run(string zipkinUri) { - internal static object Run(string zipkinUri) + // Prerequisite for running this example. + // Setup zipkin inside local docker using following command: + // docker run -d -p 9411:9411 openzipkin/zipkin + + // To run this example, run the following command from + // the reporoot\examples\Console\. + // (eg: C:\repos\opentelemetry-dotnet\examples\Console\) + // + // dotnet run zipkin -u http://localhost:9411/api/v2/spans + + // Enable OpenTelemetry for the sources "Samples.SampleServer" and "Samples.SampleClient" + // and use the Zipkin exporter. + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("Samples.SampleClient", "Samples.SampleServer") + .ConfigureResource(r => r.AddService("zipkin-test")) + .AddZipkinExporter(o => + { + o.Endpoint = new Uri(zipkinUri); + }) + .Build(); + + using (var sample = new InstrumentationWithActivitySource()) { - // Prerequisite for running this example. - // Setup zipkin inside local docker using following command: - // docker run -d -p 9411:9411 openzipkin/zipkin - - // To run this example, run the following command from - // the reporoot\examples\Console\. - // (eg: C:\repos\opentelemetry-dotnet\examples\Console\) - // - // dotnet run zipkin -u http://localhost:9411/api/v2/spans - - // Enable OpenTelemetry for the sources "Samples.SampleServer" and "Samples.SampleClient" - // and use the Zipkin exporter. - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("Samples.SampleClient", "Samples.SampleServer") - .ConfigureResource(r => r.AddService("zipkin-test")) - .AddZipkinExporter(o => - { - o.Endpoint = new Uri(zipkinUri); - }) - .Build(); - - using (var sample = new InstrumentationWithActivitySource()) - { - sample.Start(); - - System.Console.WriteLine("Traces are being created and exported " + - "to Zipkin in the background. Use Zipkin to view them. " + - "Press ENTER to stop."); - System.Console.ReadLine(); - } - - return null; + sample.Start(); + + System.Console.WriteLine("Traces are being created and exported " + + "to Zipkin in the background. Use Zipkin to view them. " + + "Press ENTER to stop."); + System.Console.ReadLine(); } + + return null; } } diff --git a/examples/Console/otlp-collector-example/config.yaml b/examples/Console/otlp-collector-example/config.yaml index b24851ab92d..932f24a193d 100644 --- a/examples/Console/otlp-collector-example/config.yaml +++ b/examples/Console/otlp-collector-example/config.yaml @@ -12,7 +12,7 @@ receivers: exporters: logging: - loglevel: debug + verbosity: detailed service: pipelines: diff --git a/examples/Directory.Packages.props b/examples/Directory.Packages.props index 9a73f74f978..902efc8cc04 100644 --- a/examples/Directory.Packages.props +++ b/examples/Directory.Packages.props @@ -1,8 +1,6 @@ - - diff --git a/examples/GrpcService/Examples.GrpcService.csproj b/examples/GrpcService/Examples.GrpcService.csproj index 4d7aed6d3b2..cd934426fb7 100644 --- a/examples/GrpcService/Examples.GrpcService.csproj +++ b/examples/GrpcService/Examples.GrpcService.csproj @@ -1,7 +1,7 @@ - net6.0 + $(DefaultTargetFrameworkForExampleApps) @@ -14,7 +14,7 @@ - + diff --git a/examples/GrpcService/Program.cs b/examples/GrpcService/Program.cs index f70331e24e4..5ed8ceb4c34 100644 --- a/examples/GrpcService/Program.cs +++ b/examples/GrpcService/Program.cs @@ -1,36 +1,22 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace Examples.GrpcService +namespace Examples.GrpcService; + +public class Program { - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - // Additional configuration is required to successfully run gRPC on macOS. - // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - webBuilder.UseUrls("https://localhost:44335"); - }); + CreateHostBuilder(args).Build().Run(); } + + // Additional configuration is required to successfully run gRPC on macOS. + // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + webBuilder.UseUrls("https://localhost:44335"); + }); } diff --git a/examples/GrpcService/Services/GreeterService.cs b/examples/GrpcService/Services/GreeterService.cs index 038b7b38a0f..011439970fb 100644 --- a/examples/GrpcService/Services/GreeterService.cs +++ b/examples/GrpcService/Services/GreeterService.cs @@ -1,38 +1,24 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Grpc.Core; -namespace Examples.GrpcService +namespace Examples.GrpcService; + +public class GreeterService : Greeter.GreeterBase { - public class GreeterService : Greeter.GreeterBase - { - private readonly ILogger logger; + private readonly ILogger logger; - public GreeterService(ILogger logger) - { - this.logger = logger; - } + public GreeterService(ILogger logger) + { + this.logger = logger; + } - public override Task SayHello(HelloRequest request, ServerCallContext context) + public override Task SayHello(HelloRequest request, ServerCallContext context) + { + return Task.FromResult(new HelloReply { - return Task.FromResult(new HelloReply - { - Message = "Hello " + request.Name, - }); - } + Message = "Hello " + request.Name, + }); } } diff --git a/examples/GrpcService/Startup.cs b/examples/GrpcService/Startup.cs index 0b2e09566f1..d23af73550f 100644 --- a/examples/GrpcService/Startup.cs +++ b/examples/GrpcService/Startup.cs @@ -1,86 +1,71 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Resources; using OpenTelemetry.Trace; -namespace Examples.GrpcService +namespace Examples.GrpcService; + +public class Startup { - public class Startup + public Startup(IConfiguration configuration) { - public Startup(IConfiguration configuration) - { - this.Configuration = configuration; - } + this.Configuration = configuration; + } - public IConfiguration Configuration { get; } + public IConfiguration Configuration { get; } - public void ConfigureServices(IServiceCollection services) - { - services.AddGrpc(); + public void ConfigureServices(IServiceCollection services) + { + services.AddGrpc(); + + services.AddOpenTelemetry() + .WithTracing(builder => + { + builder + .ConfigureResource(r => r.AddService(this.Configuration.GetValue("ServiceName", defaultValue: "otel-test")!)) + .AddAspNetCoreInstrumentation(); - services.AddOpenTelemetry() - .WithTracing(builder => + // Switch between Otlp/Zipkin/Console by setting UseExporter in appsettings.json. + var exporter = this.Configuration.GetValue("UseExporter", defaultValue: "console")!.ToLowerInvariant(); + switch (exporter) { - builder - .ConfigureResource(r => r.AddService(this.Configuration.GetValue("ServiceName"))) - .AddAspNetCoreInstrumentation(); + case "otlp": + builder.AddOtlpExporter(otlpOptions => + { + otlpOptions.Endpoint = new Uri(this.Configuration.GetValue("Otlp:Endpoint", defaultValue: "http://localhost:4317")!); + }); + break; + case "zipkin": + builder.AddZipkinExporter(zipkinOptions => + { + zipkinOptions.Endpoint = new Uri(this.Configuration.GetValue("Zipkin:Endpoint", defaultValue: "http://localhost:9411/api/v2/spans")!); + }); + break; + default: + builder.AddConsoleExporter(); + break; + } + }); + } - // Switch between Jaeger/Zipkin/Console by setting UseExporter in appsettings.json. - var exporter = this.Configuration.GetValue("UseExporter").ToLowerInvariant(); - switch (exporter) - { - case "jaeger": - builder.AddJaegerExporter(jaegerOptions => - { - jaegerOptions.AgentHost = this.Configuration.GetValue("Jaeger:Host"); - jaegerOptions.AgentPort = this.Configuration.GetValue("Jaeger:Port"); - }); - break; - case "zipkin": - builder.AddZipkinExporter(zipkinOptions => - { - zipkinOptions.Endpoint = new Uri(this.Configuration.GetValue("Zipkin:Endpoint")); - }); - break; - default: - builder.AddConsoleExporter(); - break; - } - }); + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); } - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } + app.UseRouting(); - app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapGrpcService(); - app.UseEndpoints(endpoints => + endpoints.MapGet("/", async context => { - endpoints.MapGrpcService(); - - endpoints.MapGet("/", async context => - { - await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909").ConfigureAwait(false); - }); + await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909").ConfigureAwait(false); }); - } + }); } } diff --git a/examples/GrpcService/appsettings.json b/examples/GrpcService/appsettings.json index 675b9d1c8e6..fe0cf59446c 100644 --- a/examples/GrpcService/appsettings.json +++ b/examples/GrpcService/appsettings.json @@ -14,9 +14,8 @@ }, "ServiceName": "otel-test", "UseExporter": "console", - "Jaeger": { - "Host": "localhost", - "Port": 6831 + "Otlp": { + "Endpoint": "http://localhost:4317" }, "Zipkin": { "Endpoint": "http://localhost:9411/api/v2/spans" diff --git a/examples/MicroserviceExample/Utils/Messaging/MessageReceiver.cs b/examples/MicroserviceExample/Utils/Messaging/MessageReceiver.cs index 1d84b1b08a7..09de208df21 100644 --- a/examples/MicroserviceExample/Utils/Messaging/MessageReceiver.cs +++ b/examples/MicroserviceExample/Utils/Messaging/MessageReceiver.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Text; @@ -22,82 +9,81 @@ using RabbitMQ.Client; using RabbitMQ.Client.Events; -namespace Utils.Messaging +namespace Utils.Messaging; + +public class MessageReceiver : IDisposable { - public class MessageReceiver : IDisposable - { - private static readonly ActivitySource ActivitySource = new(nameof(MessageReceiver)); - private static readonly TextMapPropagator Propagator = Propagators.DefaultTextMapPropagator; + private static readonly ActivitySource ActivitySource = new(nameof(MessageReceiver)); + private static readonly TextMapPropagator Propagator = Propagators.DefaultTextMapPropagator; - private readonly ILogger logger; - private readonly IConnection connection; - private readonly IModel channel; + private readonly ILogger logger; + private readonly IConnection connection; + private readonly IModel channel; - public MessageReceiver(ILogger logger) - { - this.logger = logger; - this.connection = RabbitMqHelper.CreateConnection(); - this.channel = RabbitMqHelper.CreateModelAndDeclareTestQueue(this.connection); - } + public MessageReceiver(ILogger logger) + { + this.logger = logger; + this.connection = RabbitMqHelper.CreateConnection(); + this.channel = RabbitMqHelper.CreateModelAndDeclareTestQueue(this.connection); + } - public void Dispose() - { - this.channel.Dispose(); - this.connection.Dispose(); - } + public void Dispose() + { + this.channel.Dispose(); + this.connection.Dispose(); + } - public void StartConsumer() - { - RabbitMqHelper.StartConsumer(this.channel, this.ReceiveMessage); - } + public void StartConsumer() + { + RabbitMqHelper.StartConsumer(this.channel, this.ReceiveMessage); + } - public void ReceiveMessage(BasicDeliverEventArgs ea) - { - // Extract the PropagationContext of the upstream parent from the message headers. - var parentContext = Propagator.Extract(default, ea.BasicProperties, this.ExtractTraceContextFromBasicProperties); - Baggage.Current = parentContext.Baggage; + public void ReceiveMessage(BasicDeliverEventArgs ea) + { + // Extract the PropagationContext of the upstream parent from the message headers. + var parentContext = Propagator.Extract(default, ea.BasicProperties, this.ExtractTraceContextFromBasicProperties); + Baggage.Current = parentContext.Baggage; - // Start an activity with a name following the semantic convention of the OpenTelemetry messaging specification. - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/messaging.md#span-name - var activityName = $"{ea.RoutingKey} receive"; + // Start an activity with a name following the semantic convention of the OpenTelemetry messaging specification. + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/messaging/messaging-spans.md#span-name + var activityName = $"{ea.RoutingKey} receive"; - using var activity = ActivitySource.StartActivity(activityName, ActivityKind.Consumer, parentContext.ActivityContext); - try - { - var message = Encoding.UTF8.GetString(ea.Body.Span.ToArray()); + using var activity = ActivitySource.StartActivity(activityName, ActivityKind.Consumer, parentContext.ActivityContext); + try + { + var message = Encoding.UTF8.GetString(ea.Body.Span.ToArray()); - this.logger.LogInformation($"Message received: [{message}]"); + this.logger.LogInformation($"Message received: [{message}]"); - activity?.SetTag("message", message); + activity?.SetTag("message", message); - // The OpenTelemetry messaging specification defines a number of attributes. These attributes are added here. - RabbitMqHelper.AddMessagingTags(activity); + // The OpenTelemetry messaging specification defines a number of attributes. These attributes are added here. + RabbitMqHelper.AddMessagingTags(activity); - // Simulate some work - Thread.Sleep(1000); - } - catch (Exception ex) - { - this.logger.LogError(ex, "Message processing failed."); - } + // Simulate some work + Thread.Sleep(1000); + } + catch (Exception ex) + { + this.logger.LogError(ex, "Message processing failed."); } + } - private IEnumerable ExtractTraceContextFromBasicProperties(IBasicProperties props, string key) + private IEnumerable ExtractTraceContextFromBasicProperties(IBasicProperties props, string key) + { + try { - try - { - if (props.Headers.TryGetValue(key, out var value)) - { - var bytes = value as byte[]; - return new[] { Encoding.UTF8.GetString(bytes) }; - } - } - catch (Exception ex) + if (props.Headers.TryGetValue(key, out var value)) { - this.logger.LogError(ex, "Failed to extract trace context."); + var bytes = value as byte[]; + return new[] { Encoding.UTF8.GetString(bytes) }; } - - return Enumerable.Empty(); } + catch (Exception ex) + { + this.logger.LogError(ex, "Failed to extract trace context."); + } + + return Enumerable.Empty(); } } diff --git a/examples/MicroserviceExample/Utils/Messaging/MessageSender.cs b/examples/MicroserviceExample/Utils/Messaging/MessageSender.cs index 96ee49017fb..969cf279cb4 100644 --- a/examples/MicroserviceExample/Utils/Messaging/MessageSender.cs +++ b/examples/MicroserviceExample/Utils/Messaging/MessageSender.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Text; @@ -21,95 +8,94 @@ using OpenTelemetry.Context.Propagation; using RabbitMQ.Client; -namespace Utils.Messaging +namespace Utils.Messaging; + +public class MessageSender : IDisposable { - public class MessageSender : IDisposable + private static readonly ActivitySource ActivitySource = new(nameof(MessageSender)); + private static readonly TextMapPropagator Propagator = Propagators.DefaultTextMapPropagator; + + private readonly ILogger logger; + private readonly IConnection connection; + private readonly IModel channel; + + public MessageSender(ILogger logger) { - private static readonly ActivitySource ActivitySource = new(nameof(MessageSender)); - private static readonly TextMapPropagator Propagator = Propagators.DefaultTextMapPropagator; + this.logger = logger; + this.connection = RabbitMqHelper.CreateConnection(); + this.channel = RabbitMqHelper.CreateModelAndDeclareTestQueue(this.connection); + } - private readonly ILogger logger; - private readonly IConnection connection; - private readonly IModel channel; + public void Dispose() + { + this.channel.Dispose(); + this.connection.Dispose(); + } - public MessageSender(ILogger logger) + public string SendMessage() + { + try { - this.logger = logger; - this.connection = RabbitMqHelper.CreateConnection(); - this.channel = RabbitMqHelper.CreateModelAndDeclareTestQueue(this.connection); - } + // Start an activity with a name following the semantic convention of the OpenTelemetry messaging specification. + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/messaging/messaging-spans.md#span-name + var activityName = $"{RabbitMqHelper.TestQueueName} send"; - public void Dispose() - { - this.channel.Dispose(); - this.connection.Dispose(); - } + using var activity = ActivitySource.StartActivity(activityName, ActivityKind.Producer); + var props = this.channel.CreateBasicProperties(); - public string SendMessage() - { - try + // Depending on Sampling (and whether a listener is registered or not), the + // activity above may not be created. + // If it is created, then propagate its context. + // If it is not created, the propagate the Current context, + // if any. + ActivityContext contextToInject = default; + if (activity != null) { - // Start an activity with a name following the semantic convention of the OpenTelemetry messaging specification. - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/messaging.md#span-name - var activityName = $"{RabbitMqHelper.TestQueueName} send"; - - using var activity = ActivitySource.StartActivity(activityName, ActivityKind.Producer); - var props = this.channel.CreateBasicProperties(); - - // Depending on Sampling (and whether a listener is registered or not), the - // activity above may not be created. - // If it is created, then propagate its context. - // If it is not created, the propagate the Current context, - // if any. - ActivityContext contextToInject = default; - if (activity != null) - { - contextToInject = activity.Context; - } - else if (Activity.Current != null) - { - contextToInject = Activity.Current.Context; - } - - // Inject the ActivityContext into the message headers to propagate trace context to the receiving service. - Propagator.Inject(new PropagationContext(contextToInject, Baggage.Current), props, this.InjectTraceContextIntoBasicProperties); - - // The OpenTelemetry messaging specification defines a number of attributes. These attributes are added here. - RabbitMqHelper.AddMessagingTags(activity); - var body = $"Published message: DateTime.Now = {DateTime.Now}."; - - this.channel.BasicPublish( - exchange: RabbitMqHelper.DefaultExchangeName, - routingKey: RabbitMqHelper.TestQueueName, - basicProperties: props, - body: Encoding.UTF8.GetBytes(body)); - - this.logger.LogInformation($"Message sent: [{body}]"); - - return body; + contextToInject = activity.Context; } - catch (Exception ex) + else if (Activity.Current != null) { - this.logger.LogError(ex, "Message publishing failed."); - throw; + contextToInject = Activity.Current.Context; } - } - private void InjectTraceContextIntoBasicProperties(IBasicProperties props, string key, string value) + // Inject the ActivityContext into the message headers to propagate trace context to the receiving service. + Propagator.Inject(new PropagationContext(contextToInject, Baggage.Current), props, this.InjectTraceContextIntoBasicProperties); + + // The OpenTelemetry messaging specification defines a number of attributes. These attributes are added here. + RabbitMqHelper.AddMessagingTags(activity); + var body = $"Published message: DateTime.Now = {DateTime.Now}."; + + this.channel.BasicPublish( + exchange: RabbitMqHelper.DefaultExchangeName, + routingKey: RabbitMqHelper.TestQueueName, + basicProperties: props, + body: Encoding.UTF8.GetBytes(body)); + + this.logger.LogInformation($"Message sent: [{body}]"); + + return body; + } + catch (Exception ex) { - try - { - if (props.Headers == null) - { - props.Headers = new Dictionary(); - } + this.logger.LogError(ex, "Message publishing failed."); + throw; + } + } - props.Headers[key] = value; - } - catch (Exception ex) + private void InjectTraceContextIntoBasicProperties(IBasicProperties props, string key, string value) + { + try + { + if (props.Headers == null) { - this.logger.LogError(ex, "Failed to inject trace context."); + props.Headers = new Dictionary(); } + + props.Headers[key] = value; + } + catch (Exception ex) + { + this.logger.LogError(ex, "Failed to inject trace context."); } } } diff --git a/examples/MicroserviceExample/Utils/Messaging/RabbitMqHelper.cs b/examples/MicroserviceExample/Utils/Messaging/RabbitMqHelper.cs index 69f19cf5eae..476bc26a853 100644 --- a/examples/MicroserviceExample/Utils/Messaging/RabbitMqHelper.cs +++ b/examples/MicroserviceExample/Utils/Messaging/RabbitMqHelper.cs @@ -1,82 +1,68 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using RabbitMQ.Client; using RabbitMQ.Client.Events; -namespace Utils.Messaging +namespace Utils.Messaging; + +public static class RabbitMqHelper { - public static class RabbitMqHelper - { - public const string DefaultExchangeName = ""; - public const string TestQueueName = "TestQueue"; + public const string DefaultExchangeName = ""; + public const string TestQueueName = "TestQueue"; - private static readonly ConnectionFactory ConnectionFactory; + private static readonly ConnectionFactory ConnectionFactory; - static RabbitMqHelper() + static RabbitMqHelper() + { + ConnectionFactory = new ConnectionFactory() { - ConnectionFactory = new ConnectionFactory() - { - HostName = Environment.GetEnvironmentVariable("RABBITMQ_HOSTNAME") ?? "localhost", - UserName = Environment.GetEnvironmentVariable("RABBITMQ_DEFAULT_USER") ?? "guest", - Password = Environment.GetEnvironmentVariable("RABBITMQ_DEFAULT_PASS") ?? "guest", - Port = 5672, - RequestedConnectionTimeout = TimeSpan.FromMilliseconds(3000), - }; - } + HostName = Environment.GetEnvironmentVariable("RABBITMQ_HOSTNAME") ?? "localhost", + UserName = Environment.GetEnvironmentVariable("RABBITMQ_DEFAULT_USER") ?? "guest", + Password = Environment.GetEnvironmentVariable("RABBITMQ_DEFAULT_PASS") ?? "guest", + Port = 5672, + RequestedConnectionTimeout = TimeSpan.FromMilliseconds(3000), + }; + } - public static IConnection CreateConnection() - { - return ConnectionFactory.CreateConnection(); - } + public static IConnection CreateConnection() + { + return ConnectionFactory.CreateConnection(); + } - public static IModel CreateModelAndDeclareTestQueue(IConnection connection) - { - var channel = connection.CreateModel(); + public static IModel CreateModelAndDeclareTestQueue(IConnection connection) + { + var channel = connection.CreateModel(); - channel.QueueDeclare( - queue: TestQueueName, - durable: false, - exclusive: false, - autoDelete: false, - arguments: null); + channel.QueueDeclare( + queue: TestQueueName, + durable: false, + exclusive: false, + autoDelete: false, + arguments: null); - return channel; - } + return channel; + } - public static void StartConsumer(IModel channel, Action processMessage) - { - var consumer = new EventingBasicConsumer(channel); + public static void StartConsumer(IModel channel, Action processMessage) + { + var consumer = new EventingBasicConsumer(channel); - consumer.Received += (bc, ea) => processMessage(ea); + consumer.Received += (bc, ea) => processMessage(ea); - channel.BasicConsume(queue: TestQueueName, autoAck: true, consumer: consumer); - } + channel.BasicConsume(queue: TestQueueName, autoAck: true, consumer: consumer); + } - public static void AddMessagingTags(Activity activity) - { - // These tags are added demonstrating the semantic conventions of the OpenTelemetry messaging specification - // See: - // * https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/messaging.md#messaging-attributes - // * https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/messaging.md#rabbitmq - activity?.SetTag("messaging.system", "rabbitmq"); - activity?.SetTag("messaging.destination_kind", "queue"); - activity?.SetTag("messaging.destination", DefaultExchangeName); - activity?.SetTag("messaging.rabbitmq.routing_key", TestQueueName); - } + public static void AddMessagingTags(Activity activity) + { + // These tags are added demonstrating the semantic conventions of the OpenTelemetry messaging specification + // See: + // * https://github.com/open-telemetry/semantic-conventions/blob/main/docs/messaging/messaging-spans.md#messaging-attributes + // * https://github.com/open-telemetry/semantic-conventions/blob/main/docs/messaging/rabbitmq.md + activity?.SetTag("messaging.system", "rabbitmq"); + activity?.SetTag("messaging.destination_kind", "queue"); + activity?.SetTag("messaging.destination", DefaultExchangeName); + activity?.SetTag("messaging.rabbitmq.routing_key", TestQueueName); } } diff --git a/examples/MicroserviceExample/WebApi/Controllers/SendMessageController.cs b/examples/MicroserviceExample/WebApi/Controllers/SendMessageController.cs index 304824d48b2..6a8a5f9564a 100644 --- a/examples/MicroserviceExample/WebApi/Controllers/SendMessageController.cs +++ b/examples/MicroserviceExample/WebApi/Controllers/SendMessageController.cs @@ -1,41 +1,27 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.AspNetCore.Mvc; using Utils.Messaging; -namespace WebApi.Controllers +namespace WebApi.Controllers; + +[ApiController] +[Route("[controller]")] +public class SendMessageController : ControllerBase { - [ApiController] - [Route("[controller]")] - public class SendMessageController : ControllerBase - { - private readonly ILogger logger; - private readonly MessageSender messageSender; + private readonly ILogger logger; + private readonly MessageSender messageSender; - public SendMessageController(ILogger logger, MessageSender messageSender) - { - this.logger = logger; - this.messageSender = messageSender; - } + public SendMessageController(ILogger logger, MessageSender messageSender) + { + this.logger = logger; + this.messageSender = messageSender; + } - [HttpGet] - public string Get() - { - return this.messageSender.SendMessage(); - } + [HttpGet] + public string Get() + { + return this.messageSender.SendMessage(); } } 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/WebApi/Program.cs b/examples/MicroserviceExample/WebApi/Program.cs index d13e10217ce..9ed5a0b9c01 100644 --- a/examples/MicroserviceExample/WebApi/Program.cs +++ b/examples/MicroserviceExample/WebApi/Program.cs @@ -1,33 +1,19 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace WebApi +namespace WebApi; + +public class Program { - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseUrls("http://*:5000").UseStartup(); - }); + CreateHostBuilder(args).Build().Run(); } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseUrls("http://*:5000").UseStartup(); + }); } diff --git a/examples/MicroserviceExample/WebApi/Startup.cs b/examples/MicroserviceExample/WebApi/Startup.cs index b6218891da9..b77ea597c73 100644 --- a/examples/MicroserviceExample/WebApi/Startup.cs +++ b/examples/MicroserviceExample/WebApi/Startup.cs @@ -1,63 +1,49 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Trace; using Utils.Messaging; -namespace WebApi +namespace WebApi; + +public class Startup { - public class Startup + public Startup(IConfiguration configuration) { - public Startup(IConfiguration configuration) - { - this.Configuration = configuration; - } + this.Configuration = configuration; + } - public IConfiguration Configuration { get; } + public IConfiguration Configuration { get; } - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers(); - - services.AddSingleton(); - - services.AddOpenTelemetry() - .WithTracing(builder => builder - .AddAspNetCoreInstrumentation() - .AddSource(nameof(MessageSender)) - .AddZipkinExporter(b => - { - var zipkinHostName = Environment.GetEnvironmentVariable("ZIPKIN_HOSTNAME") ?? "localhost"; - b.Endpoint = new Uri($"http://{zipkinHostName}:9411/api/v2/spans"); - })); - } + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + + services.AddSingleton(); + + services.AddOpenTelemetry() + .WithTracing(builder => builder + .AddAspNetCoreInstrumentation() + .AddSource(nameof(MessageSender)) + .AddZipkinExporter(b => + { + var zipkinHostName = Environment.GetEnvironmentVariable("ZIPKIN_HOSTNAME") ?? "localhost"; + b.Endpoint = new Uri($"http://{zipkinHostName}:9411/api/v2/spans"); + })); + } - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } + app.UseDeveloperExceptionPage(); + } - app.UseRouting(); + app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); } } diff --git a/examples/MicroserviceExample/WebApi/WebApi.csproj b/examples/MicroserviceExample/WebApi/WebApi.csproj index 3697a904239..57bf07e763f 100644 --- a/examples/MicroserviceExample/WebApi/WebApi.csproj +++ b/examples/MicroserviceExample/WebApi/WebApi.csproj @@ -1,6 +1,6 @@ - net7.0 + $(DefaultTargetFrameworkForExampleApps) 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/examples/MicroserviceExample/WorkerService/Program.cs b/examples/MicroserviceExample/WorkerService/Program.cs index c92602eac7d..24e03322c12 100644 --- a/examples/MicroserviceExample/WorkerService/Program.cs +++ b/examples/MicroserviceExample/WorkerService/Program.cs @@ -1,47 +1,33 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Trace; using Utils.Messaging; -namespace WorkerService +namespace WorkerService; + +public class Program { - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } + CreateHostBuilder(args).Build().Run(); + } - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureServices((hostContext, services) => - { - services.AddHostedService(); + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + services.AddHostedService(); - services.AddSingleton(); + services.AddSingleton(); - services.AddOpenTelemetry() - .WithTracing(builder => builder - .AddSource(nameof(MessageReceiver)) - .AddZipkinExporter(b => - { - var zipkinHostName = Environment.GetEnvironmentVariable("ZIPKIN_HOSTNAME") ?? "localhost"; - b.Endpoint = new Uri($"http://{zipkinHostName}:9411/api/v2/spans"); - })); - }); - } + services.AddOpenTelemetry() + .WithTracing(builder => builder + .AddSource(nameof(MessageReceiver)) + .AddZipkinExporter(b => + { + var zipkinHostName = Environment.GetEnvironmentVariable("ZIPKIN_HOSTNAME") ?? "localhost"; + b.Endpoint = new Uri($"http://{zipkinHostName}:9411/api/v2/spans"); + })); + }); } diff --git a/examples/MicroserviceExample/WorkerService/Worker.cs b/examples/MicroserviceExample/WorkerService/Worker.cs index 73fb96cf243..9b3fa484d19 100644 --- a/examples/MicroserviceExample/WorkerService/Worker.cs +++ b/examples/MicroserviceExample/WorkerService/Worker.cs @@ -1,49 +1,35 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Utils.Messaging; -namespace WorkerService +namespace WorkerService; + +public partial class Worker : BackgroundService { - public partial class Worker : BackgroundService - { - private readonly MessageReceiver messageReceiver; + private readonly MessageReceiver messageReceiver; - public Worker(MessageReceiver messageReceiver) - { - this.messageReceiver = messageReceiver; - } + public Worker(MessageReceiver messageReceiver) + { + this.messageReceiver = messageReceiver; + } - public override Task StartAsync(CancellationToken cancellationToken) - { - return base.StartAsync(cancellationToken); - } + public override Task StartAsync(CancellationToken cancellationToken) + { + return base.StartAsync(cancellationToken); + } - public override async Task StopAsync(CancellationToken cancellationToken) - { - await base.StopAsync(cancellationToken).ConfigureAwait(false); - } + public override async Task StopAsync(CancellationToken cancellationToken) + { + await base.StopAsync(cancellationToken).ConfigureAwait(false); + } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - stoppingToken.ThrowIfCancellationRequested(); + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + stoppingToken.ThrowIfCancellationRequested(); - this.messageReceiver.StartConsumer(); + this.messageReceiver.StartConsumer(); - await Task.CompletedTask.ConfigureAwait(false); - } + await Task.CompletedTask.ConfigureAwait(false); } } diff --git a/examples/MicroserviceExample/WorkerService/WorkerService.csproj b/examples/MicroserviceExample/WorkerService/WorkerService.csproj index 78f61661324..b9b1a680772 100644 --- a/examples/MicroserviceExample/WorkerService/WorkerService.csproj +++ b/examples/MicroserviceExample/WorkerService/WorkerService.csproj @@ -1,6 +1,6 @@ - net7.0 + $(DefaultTargetFrameworkForExampleApps) diff --git a/global.json b/global.json index 0f417661e6d..0aca8b12938 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "7.0.101" + "version": "8.0.100" } } diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index e3463cbd45f..f7426c0fe37 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -1,14 +1,11 @@ - - - all - runtime; build; native; contentfiles; analyzers - - + + $(OTelLatestStableVer) $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) + @@ -21,4 +18,16 @@ --> + + + + + + diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Api.ProviderBuilderExtensions/.publicApi/Experimental/PublicAPI.Unshipped.txt similarity index 98% rename from src/OpenTelemetry.Api.ProviderBuilderExtensions/.publicApi/net462/PublicAPI.Unshipped.txt rename to src/OpenTelemetry.Api.ProviderBuilderExtensions/.publicApi/Experimental/PublicAPI.Unshipped.txt index cfecde35d5f..a0f2e472dd6 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/.publicApi/net462/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/.publicApi/Experimental/PublicAPI.Unshipped.txt @@ -6,4 +6,4 @@ static OpenTelemetry.Logs.OpenTelemetryDependencyInjectionLoggerProviderBuilderE static OpenTelemetry.Logs.OpenTelemetryDependencyInjectionLoggerProviderBuilderExtensions.AddInstrumentation(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, T! instrumentation) -> OpenTelemetry.Logs.LoggerProviderBuilder! static OpenTelemetry.Logs.OpenTelemetryDependencyInjectionLoggerProviderBuilderExtensions.ConfigureServices(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, System.Action! configure) -> OpenTelemetry.Logs.LoggerProviderBuilder! static OpenTelemetry.Logs.OpenTelemetryDependencyInjectionLoggingServiceCollectionExtensions.ConfigureOpenTelemetryLoggerProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static OpenTelemetry.Logs.OpenTelemetryDependencyInjectionLoggingServiceCollectionExtensions.ConfigureOpenTelemetryLoggerProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! \ No newline at end of file +static OpenTelemetry.Logs.OpenTelemetryDependencyInjectionLoggingServiceCollectionExtensions.ConfigureOpenTelemetryLoggerProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/.publicApi/net462/PublicAPI.Shipped.txt b/src/OpenTelemetry.Api.ProviderBuilderExtensions/.publicApi/Stable/PublicAPI.Shipped.txt similarity index 100% rename from src/OpenTelemetry.Api.ProviderBuilderExtensions/.publicApi/net462/PublicAPI.Shipped.txt rename to src/OpenTelemetry.Api.ProviderBuilderExtensions/.publicApi/Stable/PublicAPI.Shipped.txt diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/.publicApi/Stable/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Api.ProviderBuilderExtensions/.publicApi/Stable/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..34e3029ac53 --- /dev/null +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/.publicApi/Stable/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +OpenTelemetry.IOpenTelemetryBuilder +OpenTelemetry.IOpenTelemetryBuilder.Services.get -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/.publicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Api.ProviderBuilderExtensions/.publicApi/netstandard2.0/PublicAPI.Shipped.txt deleted file mode 100644 index a647699c919..00000000000 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/.publicApi/netstandard2.0/PublicAPI.Shipped.txt +++ /dev/null @@ -1,19 +0,0 @@ -#nullable enable -OpenTelemetry.Metrics.OpenTelemetryDependencyInjectionMeterProviderBuilderExtensions -OpenTelemetry.Metrics.OpenTelemetryDependencyInjectionMetricsServiceCollectionExtensions -OpenTelemetry.Trace.OpenTelemetryDependencyInjectionTracerProviderBuilderExtensions -OpenTelemetry.Trace.OpenTelemetryDependencyInjectionTracingServiceCollectionExtensions -static OpenTelemetry.Metrics.OpenTelemetryDependencyInjectionMeterProviderBuilderExtensions.AddInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.OpenTelemetryDependencyInjectionMeterProviderBuilderExtensions.AddInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, System.Func! instrumentationFactory) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.OpenTelemetryDependencyInjectionMeterProviderBuilderExtensions.AddInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, System.Func! instrumentationFactory) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.OpenTelemetryDependencyInjectionMeterProviderBuilderExtensions.AddInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, T! instrumentation) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.OpenTelemetryDependencyInjectionMeterProviderBuilderExtensions.ConfigureServices(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, System.Action! configure) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.OpenTelemetryDependencyInjectionMetricsServiceCollectionExtensions.ConfigureOpenTelemetryMeterProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static OpenTelemetry.Metrics.OpenTelemetryDependencyInjectionMetricsServiceCollectionExtensions.ConfigureOpenTelemetryMeterProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static OpenTelemetry.Trace.OpenTelemetryDependencyInjectionTracerProviderBuilderExtensions.AddInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.OpenTelemetryDependencyInjectionTracerProviderBuilderExtensions.AddInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, System.Func! instrumentationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.OpenTelemetryDependencyInjectionTracerProviderBuilderExtensions.AddInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, System.Func! instrumentationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.OpenTelemetryDependencyInjectionTracerProviderBuilderExtensions.AddInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, T! instrumentation) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.OpenTelemetryDependencyInjectionTracerProviderBuilderExtensions.ConfigureServices(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, System.Action! configure) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.OpenTelemetryDependencyInjectionTracingServiceCollectionExtensions.ConfigureOpenTelemetryTracerProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static OpenTelemetry.Trace.OpenTelemetryDependencyInjectionTracingServiceCollectionExtensions.ConfigureOpenTelemetryTracerProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Api.ProviderBuilderExtensions/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt deleted file mode 100644 index cfecde35d5f..00000000000 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,9 +0,0 @@ -OpenTelemetry.Logs.OpenTelemetryDependencyInjectionLoggerProviderBuilderExtensions -OpenTelemetry.Logs.OpenTelemetryDependencyInjectionLoggingServiceCollectionExtensions -static OpenTelemetry.Logs.OpenTelemetryDependencyInjectionLoggerProviderBuilderExtensions.AddInstrumentation(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.OpenTelemetryDependencyInjectionLoggerProviderBuilderExtensions.AddInstrumentation(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, System.Func! instrumentationFactory) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.OpenTelemetryDependencyInjectionLoggerProviderBuilderExtensions.AddInstrumentation(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, System.Func! instrumentationFactory) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.OpenTelemetryDependencyInjectionLoggerProviderBuilderExtensions.AddInstrumentation(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, T! instrumentation) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.OpenTelemetryDependencyInjectionLoggerProviderBuilderExtensions.ConfigureServices(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, System.Action! configure) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.OpenTelemetryDependencyInjectionLoggingServiceCollectionExtensions.ConfigureOpenTelemetryLoggerProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static OpenTelemetry.Logs.OpenTelemetryDependencyInjectionLoggingServiceCollectionExtensions.ConfigureOpenTelemetryLoggerProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! \ No newline at end of file diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/AssemblyInfo.cs b/src/OpenTelemetry.Api.ProviderBuilderExtensions/AssemblyInfo.cs index e1ba813e7aa..c6a15f659eb 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/AssemblyInfo.cs +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/AssemblyInfo.cs @@ -1,21 +1,27 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("OpenTelemetry" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("OpenTelemetry.Tests" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("OpenTelemetry.Api.ProviderBuilderExtensions.Tests" + AssemblyInfo.PublicKey)] + +#if !EXPOSE_EXPERIMENTAL_FEATURES +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Console" + AssemblyInfo.PublicKey)] +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol" + AssemblyInfo.PublicKey)] +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests" + AssemblyInfo.PublicKey)] +[assembly: InternalsVisibleTo("OpenTelemetry.Extensions.Hosting" + AssemblyInfo.PublicKey)] +#endif + +#if SIGNED +file static class AssemblyInfo +{ + public const string PublicKey = ", PublicKey=002400000480000094000000060200000024000052534131000400000100010051C1562A090FB0C9F391012A32198B5E5D9A60E9B80FA2D7B434C9E5CCB7259BD606E66F9660676AFC6692B8CDC6793D190904551D2103B7B22FA636DCBB8208839785BA402EA08FC00C8F1500CCEF28BBF599AA64FFB1E1D5DC1BF3420A3777BADFE697856E9D52070A50C3EA5821C80BEF17CA3ACFFA28F89DD413F096F898"; +} +#else +file static class AssemblyInfo +{ + public const string PublicKey = ""; +} +#endif diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/CHANGELOG.md b/src/OpenTelemetry.Api.ProviderBuilderExtensions/CHANGELOG.md index 37bebb10e77..721930376d4 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/CHANGELOG.md +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/CHANGELOG.md @@ -2,10 +2,44 @@ ## Unreleased -* Added extension methods to support using the [Logs Bridge +* Added `IOpenTelemetryBuilder` interface to support authoring extensions which + can configure multiple OpenTelemetry signals (tracing, metrics, and/or logs). + ([#5265](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5265)) + +## 1.7.0 + +Released 2023-Dec-08 + +## 1.7.0-rc.1 + +Released 2023-Nov-29 + +* Updated `Microsoft.Extensions.DependencyInjection.Abstractions` package + version to `8.0.0`. + ([#5051](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5051)) + +## 1.7.0-alpha.1 + +Released 2023-Oct-16 + +## 1.6.0 + +Released 2023-Sep-05 + +## 1.6.0-rc.1 + +Released 2023-Aug-21 + +## 1.6.0-alpha.1 + +Released 2023-Jul-12 + +* **Experimental (pre-release builds only):** Added extension methods to support + using the [Logs Bridge API](https://github.com/open-telemetry/opentelemetry-specification/blob/976432b74c565e8a84af3570e9b82cb95e1d844c/specification/logs/bridge-api.md) implementation (eg `LoggerProviderBuilder`) with dependency injection. - ([#4433](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4433)) + ([#4433](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4433), + [#4735](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4735)) ## 1.5.1 diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/IOpenTelemetryBuilder.cs b/src/OpenTelemetry.Api.ProviderBuilderExtensions/IOpenTelemetryBuilder.cs new file mode 100644 index 00000000000..872eaf3975f --- /dev/null +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/IOpenTelemetryBuilder.cs @@ -0,0 +1,19 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.DependencyInjection; + +namespace OpenTelemetry; + +/// +/// An interface for configuring OpenTelemetry inside an . +/// +public interface IOpenTelemetryBuilder +{ + /// + /// Gets the where OpenTelemetry services + /// are configured. + /// + IServiceCollection Services { get; } +} diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/IConfigureLoggerProviderBuilder.cs b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/IConfigureLoggerProviderBuilder.cs index 91a43a3ae9e..a8629e7f37d 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/IConfigureLoggerProviderBuilder.cs +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/IConfigureLoggerProviderBuilder.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 namespace OpenTelemetry.Logs; diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/ILoggerProviderBuilder.cs b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/ILoggerProviderBuilder.cs index 0142659bbca..05deee48091 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/ILoggerProviderBuilder.cs +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/ILoggerProviderBuilder.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/LoggerProviderServiceCollectionBuilder.cs b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/LoggerProviderServiceCollectionBuilder.cs index e91d1dd75e1..4f54e125e29 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/LoggerProviderServiceCollectionBuilder.cs +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/LoggerProviderServiceCollectionBuilder.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Internal; @@ -57,7 +44,7 @@ public LoggerProviderBuilder ConfigureBuilder(Action configure) => this.ConfigureBuilderInternal(configure); - private LoggerProviderBuilder ConfigureBuilderInternal(Action configure) + private LoggerProviderServiceCollectionBuilder ConfigureBuilderInternal(Action configure) { var services = this.Services ?? throw new NotSupportedException("Builder cannot be configured during LoggerProvider construction."); @@ -67,7 +54,7 @@ private LoggerProviderBuilder ConfigureBuilderInternal(Action configure) + private LoggerProviderServiceCollectionBuilder ConfigureServicesInternal(Action configure) { Guard.ThrowIfNull(configure); diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/OpenTelemetryDependencyInjectionLoggerProviderBuilderExtensions.cs b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/OpenTelemetryDependencyInjectionLoggerProviderBuilderExtensions.cs index 6c5fd864f9b..257b7332bec 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/OpenTelemetryDependencyInjectionLoggerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/OpenTelemetryDependencyInjectionLoggerProviderBuilderExtensions.cs @@ -1,19 +1,9 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using OpenTelemetry.Internal; @@ -23,19 +13,45 @@ namespace OpenTelemetry.Logs; /// /// Contains extension methods for the class. /// -public static class OpenTelemetryDependencyInjectionLoggerProviderBuilderExtensions +#if EXPOSE_EXPERIMENTAL_FEATURES +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else +internal +#endif +static class OpenTelemetryDependencyInjectionLoggerProviderBuilderExtensions { +#if EXPOSE_EXPERIMENTAL_FEATURES /// /// Adds instrumentation to the provider. /// /// + /// /// Note: The type specified by will be /// registered as a singleton service into application services. /// /// Instrumentation type. /// . /// The supplied for chaining. - public static LoggerProviderBuilder AddInstrumentation(this LoggerProviderBuilder loggerProviderBuilder) +#else + /// + /// Adds instrumentation to the provider. + /// + /// + /// Note: The type specified by will be + /// registered as a singleton service into application services. + /// + /// Instrumentation type. + /// . + /// The supplied for chaining. +#endif + public static LoggerProviderBuilder AddInstrumentation< +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + T>(this LoggerProviderBuilder loggerProviderBuilder) where T : class { loggerProviderBuilder.ConfigureServices(services => services.TryAddSingleton()); @@ -48,13 +64,24 @@ public static LoggerProviderBuilder AddInstrumentation(this LoggerProviderBui return loggerProviderBuilder; } +#if EXPOSE_EXPERIMENTAL_FEATURES /// /// Adds instrumentation to the provider. /// /// Instrumentation type. + /// WARNING: This is an experimental API which might change or be removed in the future. Use at your own risk. /// . /// Instrumentation instance. /// The supplied for chaining. +#else + /// + /// Adds instrumentation to the provider. + /// + /// Instrumentation type. + /// . + /// Instrumentation instance. + /// The supplied for chaining. +#endif public static LoggerProviderBuilder AddInstrumentation(this LoggerProviderBuilder loggerProviderBuilder, T instrumentation) where T : class { @@ -68,6 +95,16 @@ public static LoggerProviderBuilder AddInstrumentation(this LoggerProviderBui return loggerProviderBuilder; } +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds instrumentation to the provider. + /// + /// Instrumentation type. + /// + /// . + /// Instrumentation factory. + /// The supplied for chaining. +#else /// /// Adds instrumentation to the provider. /// @@ -75,6 +112,7 @@ public static LoggerProviderBuilder AddInstrumentation(this LoggerProviderBui /// . /// Instrumentation factory. /// The supplied for chaining. +#endif public static LoggerProviderBuilder AddInstrumentation( this LoggerProviderBuilder loggerProviderBuilder, Func instrumentationFactory) @@ -90,13 +128,24 @@ public static LoggerProviderBuilder AddInstrumentation( return loggerProviderBuilder; } +#if EXPOSE_EXPERIMENTAL_FEATURES /// /// Adds instrumentation to the provider. /// /// Instrumentation type. + /// /// . /// Instrumentation factory. /// The supplied for chaining. +#else + /// + /// Adds instrumentation to the provider. + /// + /// Instrumentation type. + /// . + /// Instrumentation factory. + /// The supplied for chaining. +#endif public static LoggerProviderBuilder AddInstrumentation( this LoggerProviderBuilder loggerProviderBuilder, Func instrumentationFactory) @@ -116,6 +165,20 @@ public static LoggerProviderBuilder AddInstrumentation( return loggerProviderBuilder; } +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Register a callback action to configure the where logging services are configured. + /// + /// + /// + /// Note: Logging services are only available during the application + /// configuration phase. + /// + /// . + /// Configuration callback. + /// The supplied for chaining. +#else /// /// Register a callback action to configure the where logging services are configured. @@ -127,6 +190,7 @@ public static LoggerProviderBuilder AddInstrumentation( /// . /// Configuration callback. /// The supplied for chaining. +#endif public static LoggerProviderBuilder ConfigureServices( this LoggerProviderBuilder loggerProviderBuilder, Action configure) diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/OpenTelemetryDependencyInjectionLoggingServiceCollectionExtensions.cs b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/OpenTelemetryDependencyInjectionLoggingServiceCollectionExtensions.cs index f9679b1e9de..8e6f899b24e 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/OpenTelemetryDependencyInjectionLoggingServiceCollectionExtensions.cs +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Logs/OpenTelemetryDependencyInjectionLoggingServiceCollectionExtensions.cs @@ -1,19 +1,9 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Internal; @@ -22,13 +12,23 @@ namespace OpenTelemetry.Logs; /// /// Extension methods for setting up OpenTelemetry logging services in an . /// -public static class OpenTelemetryDependencyInjectionLoggingServiceCollectionExtensions +#if EXPOSE_EXPERIMENTAL_FEATURES +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else +internal +#endif +static class OpenTelemetryDependencyInjectionLoggingServiceCollectionExtensions { +#if EXPOSE_EXPERIMENTAL_FEATURES /// /// Registers an action used to configure the OpenTelemetry . /// /// + /// /// Notes: /// /// This is safe to be called multiple times and by library authors. @@ -45,6 +45,29 @@ public static class OpenTelemetryDependencyInjectionLoggingServiceCollectionExte /// cref="LoggerProviderBuilder"/>. /// The so that additional calls /// can be chained. +#else + /// + /// Registers an action used to configure the OpenTelemetry . + /// + /// + /// Notes: + /// + /// This is safe to be called multiple times and by library authors. + /// Each registered configuration action will be applied + /// sequentially. + /// A will NOT be created automatically + /// using this method. To begin collecting logs use the + /// IServiceCollection.AddOpenTelemetry extension in the + /// OpenTelemetry.Extensions.Hosting package. + /// + /// + /// . + /// Callback action to configure the . + /// The so that additional calls + /// can be chained. +#endif public static IServiceCollection ConfigureOpenTelemetryLoggerProvider( this IServiceCollection services, Action configure) @@ -57,6 +80,40 @@ public static IServiceCollection ConfigureOpenTelemetryLoggerProvider( return services; } +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Registers an action used to configure the OpenTelemetry once the + /// is available. + /// + /// + /// + /// Notes: + /// + /// This is safe to be called multiple times and by library authors. + /// Each registered configuration action will be applied + /// sequentially. + /// A will NOT be created automatically + /// using this method. To begin collecting logs use the + /// IServiceCollection.AddOpenTelemetry extension in the + /// OpenTelemetry.Extensions.Hosting package. + /// The supplied configuration delegate is called once the is available. Services may NOT be added to a + /// once the has been created. Many helper extensions + /// register services and may throw if invoked inside the configuration + /// delegate. If you don't need access to the + /// call instead which is safe to be used with + /// helper extensions. + /// + /// + /// . + /// Callback action to configure the . + /// The so that additional calls + /// can be chained. +#else /// /// Registers an action used to configure the OpenTelemetry once the @@ -88,6 +145,7 @@ public static IServiceCollection ConfigureOpenTelemetryLoggerProvider( /// cref="LoggerProviderBuilder"/>. /// The so that additional calls /// can be chained. +#endif public static IServiceCollection ConfigureOpenTelemetryLoggerProvider( this IServiceCollection services, Action configure) diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/IConfigureMeterProviderBuilder.cs b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/IConfigureMeterProviderBuilder.cs index 5021c427425..472126e80b7 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/IConfigureMeterProviderBuilder.cs +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/IConfigureMeterProviderBuilder.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 namespace OpenTelemetry.Metrics; diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/IMeterProviderBuilder.cs b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/IMeterProviderBuilder.cs index cbeca74e849..a7f2bf609fd 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/IMeterProviderBuilder.cs +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/IMeterProviderBuilder.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; @@ -36,10 +23,10 @@ internal interface IMeterProviderBuilder : IDeferredMeterProviderBuilder /// /// Register a callback action to configure the where metric services are configured. + /// cref="IServiceCollection"/> where metrics services are configured. /// /// - /// Note: Metric services are only available during the application + /// Note: Metrics services are only available during the application /// configuration phase. This method should throw a if services are configured after the /// application has been created. diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/MeterProviderServiceCollectionBuilder.cs b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/MeterProviderServiceCollectionBuilder.cs index a2ddc2d59a7..1297d4de441 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/MeterProviderServiceCollectionBuilder.cs +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/MeterProviderServiceCollectionBuilder.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Internal; @@ -70,7 +57,7 @@ public MeterProviderBuilder ConfigureBuilder(Action configure) => this.ConfigureBuilderInternal(configure); - private MeterProviderBuilder ConfigureBuilderInternal(Action configure) + private MeterProviderServiceCollectionBuilder ConfigureBuilderInternal(Action configure) { var services = this.Services ?? throw new NotSupportedException("Builder cannot be configured during MeterProvider construction."); @@ -80,7 +67,7 @@ private MeterProviderBuilder ConfigureBuilderInternal(Action configure) + private MeterProviderServiceCollectionBuilder ConfigureServicesInternal(Action configure) { Guard.ThrowIfNull(configure); diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/OpenTelemetryDependencyInjectionMeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/OpenTelemetryDependencyInjectionMeterProviderBuilderExtensions.cs index ec83263fc90..457ddc87663 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/OpenTelemetryDependencyInjectionMeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/OpenTelemetryDependencyInjectionMeterProviderBuilderExtensions.cs @@ -1,19 +1,9 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using OpenTelemetry.Internal; @@ -35,7 +25,11 @@ public static class OpenTelemetryDependencyInjectionMeterProviderBuilderExtensio /// Instrumentation type. /// . /// The supplied for chaining. - public static MeterProviderBuilder AddInstrumentation(this MeterProviderBuilder meterProviderBuilder) + public static MeterProviderBuilder AddInstrumentation< +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + T>(this MeterProviderBuilder meterProviderBuilder) where T : class { meterProviderBuilder.ConfigureServices(services => services.TryAddSingleton()); @@ -118,10 +112,10 @@ public static MeterProviderBuilder AddInstrumentation( /// /// Register a callback action to configure the where tracing services are configured. + /// cref="IServiceCollection"/> where metrics services are configured. /// /// - /// Note: Tracing services are only available during the application + /// Note: Metrics services are only available during the application /// configuration phase. /// /// . diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/OpenTelemetryDependencyInjectionMetricsServiceCollectionExtensions.cs b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/OpenTelemetryDependencyInjectionMetricsServiceCollectionExtensions.cs index e62776c6cad..05da73825f2 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/OpenTelemetryDependencyInjectionMetricsServiceCollectionExtensions.cs +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/OpenTelemetryDependencyInjectionMetricsServiceCollectionExtensions.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Internal; diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/OpenTelemetry.Api.ProviderBuilderExtensions.csproj b/src/OpenTelemetry.Api.ProviderBuilderExtensions/OpenTelemetry.Api.ProviderBuilderExtensions.csproj index d7c70aea5f8..d057d595f43 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/OpenTelemetry.Api.ProviderBuilderExtensions.csproj +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/OpenTelemetry.Api.ProviderBuilderExtensions.csproj @@ -1,8 +1,7 @@ - - netstandard2.0;net462 + $(TargetFrameworksForLibraries) Contains extensions to register OpenTelemetry in applications using Microsoft.Extensions.DependencyInjection OpenTelemetry core- diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/IConfigureTracerProviderBuilder.cs b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/IConfigureTracerProviderBuilder.cs index 5e9f078d8f0..34a54b69b53 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/IConfigureTracerProviderBuilder.cs +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/IConfigureTracerProviderBuilder.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 namespace OpenTelemetry.Trace; diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/ITracerProviderBuilder.cs b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/ITracerProviderBuilder.cs index bd2ae18d190..20c465142f6 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/ITracerProviderBuilder.cs +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/ITracerProviderBuilder.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/OpenTelemetryDependencyInjectionTracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/OpenTelemetryDependencyInjectionTracerProviderBuilderExtensions.cs index 3642e9960a0..35088b2a5ef 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/OpenTelemetryDependencyInjectionTracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/OpenTelemetryDependencyInjectionTracerProviderBuilderExtensions.cs @@ -1,19 +1,9 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using OpenTelemetry.Internal; @@ -35,7 +25,11 @@ public static class OpenTelemetryDependencyInjectionTracerProviderBuilderExtensi /// Instrumentation type. /// . /// The supplied for chaining. - public static TracerProviderBuilder AddInstrumentation(this TracerProviderBuilder tracerProviderBuilder) + public static TracerProviderBuilder AddInstrumentation< +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + T>(this TracerProviderBuilder tracerProviderBuilder) where T : class { tracerProviderBuilder.ConfigureServices(services => services.TryAddSingleton()); diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/OpenTelemetryDependencyInjectionTracingServiceCollectionExtensions.cs b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/OpenTelemetryDependencyInjectionTracingServiceCollectionExtensions.cs index 7d46cb52130..6a7ceb2a3be 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/OpenTelemetryDependencyInjectionTracingServiceCollectionExtensions.cs +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/OpenTelemetryDependencyInjectionTracingServiceCollectionExtensions.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Internal; diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/TracerProviderServiceCollectionBuilder.cs b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/TracerProviderServiceCollectionBuilder.cs index cb2e0d7f0f1..bfc94c051a9 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/TracerProviderServiceCollectionBuilder.cs +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/TracerProviderServiceCollectionBuilder.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Internal; @@ -83,7 +70,7 @@ public TracerProviderBuilder ConfigureBuilder(Action configure) => this.ConfigureBuilderInternal(configure); - private TracerProviderBuilder ConfigureBuilderInternal(Action configure) + private TracerProviderServiceCollectionBuilder ConfigureBuilderInternal(Action configure) { var services = this.Services ?? throw new NotSupportedException("Builder cannot be configured during TracerProvider construction."); @@ -93,7 +80,7 @@ private TracerProviderBuilder ConfigureBuilderInternal(Action configure) + private TracerProviderServiceCollectionBuilder ConfigureServicesInternal(Action configure) { Guard.ThrowIfNull(configure); diff --git a/src/OpenTelemetry.Api/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Api/.publicApi/Experimental/PublicAPI.Unshipped.txt similarity index 98% rename from src/OpenTelemetry.Api/.publicApi/net462/PublicAPI.Unshipped.txt rename to src/OpenTelemetry.Api/.publicApi/Experimental/PublicAPI.Unshipped.txt index 29b616ee92c..2c2dcbb59e2 100644 --- a/src/OpenTelemetry.Api/.publicApi/net462/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Api/.publicApi/Experimental/PublicAPI.Unshipped.txt @@ -1,5 +1,5 @@ abstract OpenTelemetry.Logs.Logger.EmitLog(in OpenTelemetry.Logs.LogRecordData data, in OpenTelemetry.Logs.LogRecordAttributeList attributes) -> void -abstract OpenTelemetry.Logs.LoggerProviderBuilder.AddInstrumentation(System.Func! instrumentationFactory) -> OpenTelemetry.Logs.LoggerProviderBuilder! +abstract OpenTelemetry.Logs.LoggerProviderBuilder.AddInstrumentation(System.Func! instrumentationFactory) -> OpenTelemetry.Logs.LoggerProviderBuilder! OpenTelemetry.Logs.IDeferredLoggerProviderBuilder OpenTelemetry.Logs.IDeferredLoggerProviderBuilder.Configure(System.Action! configure) -> OpenTelemetry.Logs.LoggerProviderBuilder! OpenTelemetry.Logs.Logger diff --git a/src/OpenTelemetry.Api/.publicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Api/.publicApi/Stable/PublicAPI.Shipped.txt similarity index 67% rename from src/OpenTelemetry.Api/.publicApi/netstandard2.0/PublicAPI.Shipped.txt rename to src/OpenTelemetry.Api/.publicApi/Stable/PublicAPI.Shipped.txt index b33cbc15745..a87bb701214 100644 --- a/src/OpenTelemetry.Api/.publicApi/netstandard2.0/PublicAPI.Shipped.txt +++ b/src/OpenTelemetry.Api/.publicApi/Stable/PublicAPI.Shipped.txt @@ -2,44 +2,79 @@ ~abstract OpenTelemetry.Context.Propagation.TextMapPropagator.Extract(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Func> getter) -> OpenTelemetry.Context.Propagation.PropagationContext ~abstract OpenTelemetry.Context.Propagation.TextMapPropagator.Fields.get -> System.Collections.Generic.ISet ~abstract OpenTelemetry.Context.Propagation.TextMapPropagator.Inject(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Action setter) -> void +~OpenTelemetry.Baggage.GetBaggage() -> System.Collections.Generic.IReadOnlyDictionary +~OpenTelemetry.Baggage.GetBaggage(string name) -> string +~OpenTelemetry.Baggage.GetEnumerator() -> System.Collections.Generic.Dictionary.Enumerator +~OpenTelemetry.Baggage.RemoveBaggage(string name) -> OpenTelemetry.Baggage +~OpenTelemetry.Baggage.SetBaggage(params System.Collections.Generic.KeyValuePair[] baggageItems) -> OpenTelemetry.Baggage +~OpenTelemetry.Baggage.SetBaggage(string name, string value) -> OpenTelemetry.Baggage +~OpenTelemetry.Baggage.SetBaggage(System.Collections.Generic.IEnumerable> baggageItems) -> OpenTelemetry.Baggage +~OpenTelemetry.Context.AsyncLocalRuntimeContextSlot.AsyncLocalRuntimeContextSlot(string name) -> void +~OpenTelemetry.Context.AsyncLocalRuntimeContextSlot.Value.get -> object +~OpenTelemetry.Context.AsyncLocalRuntimeContextSlot.Value.set -> void +~OpenTelemetry.Context.IRuntimeContextSlotValueAccessor.Value.get -> object +~OpenTelemetry.Context.IRuntimeContextSlotValueAccessor.Value.set -> void +~OpenTelemetry.Context.Propagation.CompositeTextMapPropagator.CompositeTextMapPropagator(System.Collections.Generic.IEnumerable propagators) -> void +~OpenTelemetry.Context.RuntimeContextSlot.Name.get -> string +~OpenTelemetry.Context.RuntimeContextSlot.RuntimeContextSlot(string name) -> void +~OpenTelemetry.Context.ThreadLocalRuntimeContextSlot.ThreadLocalRuntimeContextSlot(string name) -> void +~OpenTelemetry.Context.ThreadLocalRuntimeContextSlot.Value.get -> object +~OpenTelemetry.Context.ThreadLocalRuntimeContextSlot.Value.set -> void +~override OpenTelemetry.Baggage.Equals(object obj) -> bool +~override OpenTelemetry.Context.Propagation.B3Propagator.Extract(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Func> getter) -> OpenTelemetry.Context.Propagation.PropagationContext +~override OpenTelemetry.Context.Propagation.B3Propagator.Fields.get -> System.Collections.Generic.ISet +~override OpenTelemetry.Context.Propagation.B3Propagator.Inject(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Action setter) -> void +~override OpenTelemetry.Context.Propagation.BaggagePropagator.Extract(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Func> getter) -> OpenTelemetry.Context.Propagation.PropagationContext +~override OpenTelemetry.Context.Propagation.BaggagePropagator.Fields.get -> System.Collections.Generic.ISet +~override OpenTelemetry.Context.Propagation.BaggagePropagator.Inject(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Action setter) -> void +~override OpenTelemetry.Context.Propagation.CompositeTextMapPropagator.Extract(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Func> getter) -> OpenTelemetry.Context.Propagation.PropagationContext +~override OpenTelemetry.Context.Propagation.CompositeTextMapPropagator.Fields.get -> System.Collections.Generic.ISet +~override OpenTelemetry.Context.Propagation.CompositeTextMapPropagator.Inject(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Action setter) -> void +~override OpenTelemetry.Context.Propagation.PropagationContext.Equals(object obj) -> bool +~override OpenTelemetry.Context.Propagation.TraceContextPropagator.Extract(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Func> getter) -> OpenTelemetry.Context.Propagation.PropagationContext +~override OpenTelemetry.Context.Propagation.TraceContextPropagator.Fields.get -> System.Collections.Generic.ISet +~override OpenTelemetry.Context.Propagation.TraceContextPropagator.Inject(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Action setter) -> void +~static OpenTelemetry.Baggage.Create(System.Collections.Generic.Dictionary baggageItems = null) -> OpenTelemetry.Baggage +~static OpenTelemetry.Baggage.GetBaggage(OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> System.Collections.Generic.IReadOnlyDictionary +~static OpenTelemetry.Baggage.GetBaggage(string name, OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> string +~static OpenTelemetry.Baggage.GetEnumerator(OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> System.Collections.Generic.Dictionary.Enumerator +~static OpenTelemetry.Baggage.RemoveBaggage(string name, OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> OpenTelemetry.Baggage +~static OpenTelemetry.Baggage.SetBaggage(string name, string value, OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> OpenTelemetry.Baggage +~static OpenTelemetry.Baggage.SetBaggage(System.Collections.Generic.IEnumerable> baggageItems, OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> OpenTelemetry.Baggage +~static OpenTelemetry.Context.Propagation.Propagators.DefaultTextMapPropagator.get -> OpenTelemetry.Context.Propagation.TextMapPropagator +~static OpenTelemetry.Context.RuntimeContext.ContextSlotType.get -> System.Type +~static OpenTelemetry.Context.RuntimeContext.ContextSlotType.set -> void +~static OpenTelemetry.Context.RuntimeContext.GetSlot(string slotName) -> OpenTelemetry.Context.RuntimeContextSlot +~static OpenTelemetry.Context.RuntimeContext.GetValue(string slotName) -> object +~static OpenTelemetry.Context.RuntimeContext.GetValue(string slotName) -> T +~static OpenTelemetry.Context.RuntimeContext.RegisterSlot(string slotName) -> OpenTelemetry.Context.RuntimeContextSlot +~static OpenTelemetry.Context.RuntimeContext.SetValue(string slotName, object value) -> void +~static OpenTelemetry.Context.RuntimeContext.SetValue(string slotName, T value) -> void abstract OpenTelemetry.Context.RuntimeContextSlot.Get() -> T abstract OpenTelemetry.Context.RuntimeContextSlot.Set(T value) -> void -~abstract OpenTelemetry.Metrics.MeterProviderBuilder.AddInstrumentation(System.Func instrumentationFactory) -> OpenTelemetry.Metrics.MeterProviderBuilder -~abstract OpenTelemetry.Metrics.MeterProviderBuilder.AddMeter(params string[] names) -> OpenTelemetry.Metrics.MeterProviderBuilder -~abstract OpenTelemetry.Trace.TracerProviderBuilder.AddInstrumentation(System.Func instrumentationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder -~abstract OpenTelemetry.Trace.TracerProviderBuilder.AddLegacySource(string operationName) -> OpenTelemetry.Trace.TracerProviderBuilder -~abstract OpenTelemetry.Trace.TracerProviderBuilder.AddSource(params string[] names) -> OpenTelemetry.Trace.TracerProviderBuilder +abstract OpenTelemetry.Metrics.MeterProviderBuilder.AddInstrumentation(System.Func! instrumentationFactory) -> OpenTelemetry.Metrics.MeterProviderBuilder! +abstract OpenTelemetry.Metrics.MeterProviderBuilder.AddMeter(params string![]! names) -> OpenTelemetry.Metrics.MeterProviderBuilder! +abstract OpenTelemetry.Trace.TracerProviderBuilder.AddInstrumentation(System.Func! instrumentationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder! +abstract OpenTelemetry.Trace.TracerProviderBuilder.AddLegacySource(string! operationName) -> OpenTelemetry.Trace.TracerProviderBuilder! +abstract OpenTelemetry.Trace.TracerProviderBuilder.AddSource(params string![]! names) -> OpenTelemetry.Trace.TracerProviderBuilder! OpenTelemetry.ActivityContextExtensions OpenTelemetry.Baggage OpenTelemetry.Baggage.Baggage() -> void OpenTelemetry.Baggage.ClearBaggage() -> OpenTelemetry.Baggage OpenTelemetry.Baggage.Count.get -> int OpenTelemetry.Baggage.Equals(OpenTelemetry.Baggage other) -> bool -~OpenTelemetry.Baggage.GetBaggage() -> System.Collections.Generic.IReadOnlyDictionary -~OpenTelemetry.Baggage.GetBaggage(string name) -> string -~OpenTelemetry.Baggage.GetEnumerator() -> System.Collections.Generic.Dictionary.Enumerator -~OpenTelemetry.Baggage.RemoveBaggage(string name) -> OpenTelemetry.Baggage -~OpenTelemetry.Baggage.SetBaggage(params System.Collections.Generic.KeyValuePair[] baggageItems) -> OpenTelemetry.Baggage -~OpenTelemetry.Baggage.SetBaggage(string name, string value) -> OpenTelemetry.Baggage -~OpenTelemetry.Baggage.SetBaggage(System.Collections.Generic.IEnumerable> baggageItems) -> OpenTelemetry.Baggage OpenTelemetry.BaseProvider OpenTelemetry.BaseProvider.~BaseProvider() -> void OpenTelemetry.BaseProvider.BaseProvider() -> void OpenTelemetry.BaseProvider.Dispose() -> void OpenTelemetry.Context.AsyncLocalRuntimeContextSlot -~OpenTelemetry.Context.AsyncLocalRuntimeContextSlot.AsyncLocalRuntimeContextSlot(string name) -> void -~OpenTelemetry.Context.AsyncLocalRuntimeContextSlot.Value.get -> object -~OpenTelemetry.Context.AsyncLocalRuntimeContextSlot.Value.set -> void OpenTelemetry.Context.IRuntimeContextSlotValueAccessor -~OpenTelemetry.Context.IRuntimeContextSlotValueAccessor.Value.get -> object -~OpenTelemetry.Context.IRuntimeContextSlotValueAccessor.Value.set -> void OpenTelemetry.Context.Propagation.B3Propagator OpenTelemetry.Context.Propagation.B3Propagator.B3Propagator() -> void OpenTelemetry.Context.Propagation.B3Propagator.B3Propagator(bool singleHeader) -> void OpenTelemetry.Context.Propagation.BaggagePropagator OpenTelemetry.Context.Propagation.BaggagePropagator.BaggagePropagator() -> void OpenTelemetry.Context.Propagation.CompositeTextMapPropagator -~OpenTelemetry.Context.Propagation.CompositeTextMapPropagator.CompositeTextMapPropagator(System.Collections.Generic.IEnumerable propagators) -> void OpenTelemetry.Context.Propagation.PropagationContext OpenTelemetry.Context.Propagation.PropagationContext.ActivityContext.get -> System.Diagnostics.ActivityContext OpenTelemetry.Context.Propagation.PropagationContext.Baggage.get -> OpenTelemetry.Baggage @@ -54,50 +89,45 @@ OpenTelemetry.Context.Propagation.TraceContextPropagator.TraceContextPropagator( OpenTelemetry.Context.RuntimeContext OpenTelemetry.Context.RuntimeContextSlot OpenTelemetry.Context.RuntimeContextSlot.Dispose() -> void -~OpenTelemetry.Context.RuntimeContextSlot.Name.get -> string -~OpenTelemetry.Context.RuntimeContextSlot.RuntimeContextSlot(string name) -> void OpenTelemetry.Context.ThreadLocalRuntimeContextSlot -~OpenTelemetry.Context.ThreadLocalRuntimeContextSlot.ThreadLocalRuntimeContextSlot(string name) -> void -~OpenTelemetry.Context.ThreadLocalRuntimeContextSlot.Value.get -> object -~OpenTelemetry.Context.ThreadLocalRuntimeContextSlot.Value.set -> void OpenTelemetry.Metrics.IDeferredMeterProviderBuilder -~OpenTelemetry.Metrics.IDeferredMeterProviderBuilder.Configure(System.Action configure) -> OpenTelemetry.Metrics.MeterProviderBuilder +OpenTelemetry.Metrics.IDeferredMeterProviderBuilder.Configure(System.Action! configure) -> OpenTelemetry.Metrics.MeterProviderBuilder! OpenTelemetry.Metrics.MeterProvider OpenTelemetry.Metrics.MeterProvider.MeterProvider() -> void OpenTelemetry.Metrics.MeterProviderBuilder OpenTelemetry.Metrics.MeterProviderBuilder.MeterProviderBuilder() -> void OpenTelemetry.Trace.ActivityExtensions OpenTelemetry.Trace.IDeferredTracerProviderBuilder -~OpenTelemetry.Trace.IDeferredTracerProviderBuilder.Configure(System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder +OpenTelemetry.Trace.IDeferredTracerProviderBuilder.Configure(System.Action! configure) -> OpenTelemetry.Trace.TracerProviderBuilder! OpenTelemetry.Trace.Link -~OpenTelemetry.Trace.Link.Attributes.get -> System.Collections.Generic.IEnumerable> +OpenTelemetry.Trace.Link.Attributes.get -> System.Collections.Generic.IEnumerable>? OpenTelemetry.Trace.Link.Context.get -> OpenTelemetry.Trace.SpanContext OpenTelemetry.Trace.Link.Equals(OpenTelemetry.Trace.Link other) -> bool OpenTelemetry.Trace.Link.Link() -> void OpenTelemetry.Trace.Link.Link(in OpenTelemetry.Trace.SpanContext spanContext) -> void -~OpenTelemetry.Trace.Link.Link(in OpenTelemetry.Trace.SpanContext spanContext, OpenTelemetry.Trace.SpanAttributes attributes) -> void +OpenTelemetry.Trace.Link.Link(in OpenTelemetry.Trace.SpanContext spanContext, OpenTelemetry.Trace.SpanAttributes? attributes) -> void OpenTelemetry.Trace.SpanAttributes -~OpenTelemetry.Trace.SpanAttributes.Add(string key, bool value) -> void -~OpenTelemetry.Trace.SpanAttributes.Add(string key, bool[] values) -> void -~OpenTelemetry.Trace.SpanAttributes.Add(string key, double value) -> void -~OpenTelemetry.Trace.SpanAttributes.Add(string key, double[] values) -> void -~OpenTelemetry.Trace.SpanAttributes.Add(string key, long value) -> void -~OpenTelemetry.Trace.SpanAttributes.Add(string key, long[] values) -> void -~OpenTelemetry.Trace.SpanAttributes.Add(string key, string value) -> void -~OpenTelemetry.Trace.SpanAttributes.Add(string key, string[] values) -> void +OpenTelemetry.Trace.SpanAttributes.Add(string! key, bool value) -> void +OpenTelemetry.Trace.SpanAttributes.Add(string! key, bool[]? values) -> void +OpenTelemetry.Trace.SpanAttributes.Add(string! key, double value) -> void +OpenTelemetry.Trace.SpanAttributes.Add(string! key, double[]? values) -> void +OpenTelemetry.Trace.SpanAttributes.Add(string! key, long value) -> void +OpenTelemetry.Trace.SpanAttributes.Add(string! key, long[]? values) -> void +OpenTelemetry.Trace.SpanAttributes.Add(string! key, string? value) -> void +OpenTelemetry.Trace.SpanAttributes.Add(string! key, string?[]? values) -> void OpenTelemetry.Trace.SpanAttributes.SpanAttributes() -> void -~OpenTelemetry.Trace.SpanAttributes.SpanAttributes(System.Collections.Generic.IEnumerable> attributes) -> void +OpenTelemetry.Trace.SpanAttributes.SpanAttributes(System.Collections.Generic.IEnumerable>! attributes) -> void OpenTelemetry.Trace.SpanContext OpenTelemetry.Trace.SpanContext.Equals(OpenTelemetry.Trace.SpanContext other) -> bool OpenTelemetry.Trace.SpanContext.IsRemote.get -> bool OpenTelemetry.Trace.SpanContext.IsValid.get -> bool OpenTelemetry.Trace.SpanContext.SpanContext() -> void OpenTelemetry.Trace.SpanContext.SpanContext(in System.Diagnostics.ActivityContext activityContext) -> void -~OpenTelemetry.Trace.SpanContext.SpanContext(in System.Diagnostics.ActivityTraceId traceId, in System.Diagnostics.ActivitySpanId spanId, System.Diagnostics.ActivityTraceFlags traceFlags, bool isRemote = false, System.Collections.Generic.IEnumerable> traceState = null) -> void +OpenTelemetry.Trace.SpanContext.SpanContext(in System.Diagnostics.ActivityTraceId traceId, in System.Diagnostics.ActivitySpanId spanId, System.Diagnostics.ActivityTraceFlags traceFlags, bool isRemote = false, System.Collections.Generic.IEnumerable>? traceState = null) -> void OpenTelemetry.Trace.SpanContext.SpanId.get -> System.Diagnostics.ActivitySpanId OpenTelemetry.Trace.SpanContext.TraceFlags.get -> System.Diagnostics.ActivityTraceFlags OpenTelemetry.Trace.SpanContext.TraceId.get -> System.Diagnostics.ActivityTraceId -~OpenTelemetry.Trace.SpanContext.TraceState.get -> System.Collections.Generic.IEnumerable> +OpenTelemetry.Trace.SpanContext.TraceState.get -> System.Collections.Generic.IEnumerable>! OpenTelemetry.Trace.SpanKind OpenTelemetry.Trace.SpanKind.Client = 3 -> OpenTelemetry.Trace.SpanKind OpenTelemetry.Trace.SpanKind.Consumer = 5 -> OpenTelemetry.Trace.SpanKind @@ -105,105 +135,76 @@ OpenTelemetry.Trace.SpanKind.Internal = 1 -> OpenTelemetry.Trace.SpanKind OpenTelemetry.Trace.SpanKind.Producer = 4 -> OpenTelemetry.Trace.SpanKind OpenTelemetry.Trace.SpanKind.Server = 2 -> OpenTelemetry.Trace.SpanKind OpenTelemetry.Trace.Status -~OpenTelemetry.Trace.Status.Description.get -> string +OpenTelemetry.Trace.Status.Description.get -> string? OpenTelemetry.Trace.Status.Equals(OpenTelemetry.Trace.Status other) -> bool OpenTelemetry.Trace.Status.Status() -> void OpenTelemetry.Trace.Status.StatusCode.get -> OpenTelemetry.Trace.StatusCode -~OpenTelemetry.Trace.Status.WithDescription(string description) -> OpenTelemetry.Trace.Status +OpenTelemetry.Trace.Status.WithDescription(string? description) -> OpenTelemetry.Trace.Status OpenTelemetry.Trace.StatusCode OpenTelemetry.Trace.StatusCode.Error = 2 -> OpenTelemetry.Trace.StatusCode OpenTelemetry.Trace.StatusCode.Ok = 1 -> OpenTelemetry.Trace.StatusCode OpenTelemetry.Trace.StatusCode.Unset = 0 -> OpenTelemetry.Trace.StatusCode OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.AddEvent(string name) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.AddEvent(string name, OpenTelemetry.Trace.SpanAttributes attributes) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.AddEvent(string name, System.DateTimeOffset timestamp) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.AddEvent(string name, System.DateTimeOffset timestamp, OpenTelemetry.Trace.SpanAttributes attributes) -> OpenTelemetry.Trace.TelemetrySpan +OpenTelemetry.Trace.TelemetrySpan.AddEvent(string! name) -> OpenTelemetry.Trace.TelemetrySpan! +OpenTelemetry.Trace.TelemetrySpan.AddEvent(string! name, OpenTelemetry.Trace.SpanAttributes? attributes) -> OpenTelemetry.Trace.TelemetrySpan! +OpenTelemetry.Trace.TelemetrySpan.AddEvent(string! name, System.DateTimeOffset timestamp) -> OpenTelemetry.Trace.TelemetrySpan! +OpenTelemetry.Trace.TelemetrySpan.AddEvent(string! name, System.DateTimeOffset timestamp, OpenTelemetry.Trace.SpanAttributes? attributes) -> OpenTelemetry.Trace.TelemetrySpan! OpenTelemetry.Trace.TelemetrySpan.Context.get -> OpenTelemetry.Trace.SpanContext OpenTelemetry.Trace.TelemetrySpan.Dispose() -> void OpenTelemetry.Trace.TelemetrySpan.End() -> void OpenTelemetry.Trace.TelemetrySpan.End(System.DateTimeOffset endTimestamp) -> void OpenTelemetry.Trace.TelemetrySpan.IsRecording.get -> bool OpenTelemetry.Trace.TelemetrySpan.ParentSpanId.get -> System.Diagnostics.ActivitySpanId -~OpenTelemetry.Trace.TelemetrySpan.RecordException(string type, string message, string stacktrace) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.RecordException(System.Exception ex) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.SetAttribute(string key, bool value) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.SetAttribute(string key, bool[] values) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.SetAttribute(string key, double value) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.SetAttribute(string key, double[] values) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.SetAttribute(string key, int value) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.SetAttribute(string key, int[] values) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.SetAttribute(string key, string value) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.SetAttribute(string key, string[] values) -> OpenTelemetry.Trace.TelemetrySpan +OpenTelemetry.Trace.TelemetrySpan.RecordException(string? type, string? message, string? stacktrace) -> OpenTelemetry.Trace.TelemetrySpan! +OpenTelemetry.Trace.TelemetrySpan.RecordException(System.Exception? ex) -> OpenTelemetry.Trace.TelemetrySpan! +OpenTelemetry.Trace.TelemetrySpan.SetAttribute(string! key, bool value) -> OpenTelemetry.Trace.TelemetrySpan! +OpenTelemetry.Trace.TelemetrySpan.SetAttribute(string! key, bool[]? values) -> OpenTelemetry.Trace.TelemetrySpan! +OpenTelemetry.Trace.TelemetrySpan.SetAttribute(string! key, double value) -> OpenTelemetry.Trace.TelemetrySpan! +OpenTelemetry.Trace.TelemetrySpan.SetAttribute(string! key, double[]? values) -> OpenTelemetry.Trace.TelemetrySpan! +OpenTelemetry.Trace.TelemetrySpan.SetAttribute(string! key, int value) -> OpenTelemetry.Trace.TelemetrySpan! +OpenTelemetry.Trace.TelemetrySpan.SetAttribute(string! key, int[]? values) -> OpenTelemetry.Trace.TelemetrySpan! +OpenTelemetry.Trace.TelemetrySpan.SetAttribute(string! key, string? value) -> OpenTelemetry.Trace.TelemetrySpan! +OpenTelemetry.Trace.TelemetrySpan.SetAttribute(string! key, string?[]? values) -> OpenTelemetry.Trace.TelemetrySpan! OpenTelemetry.Trace.TelemetrySpan.SetStatus(OpenTelemetry.Trace.Status value) -> void -~OpenTelemetry.Trace.TelemetrySpan.UpdateName(string name) -> OpenTelemetry.Trace.TelemetrySpan +OpenTelemetry.Trace.TelemetrySpan.UpdateName(string! name) -> OpenTelemetry.Trace.TelemetrySpan! OpenTelemetry.Trace.Tracer -~OpenTelemetry.Trace.Tracer.StartActiveSpan(string name, OpenTelemetry.Trace.SpanKind kind = OpenTelemetry.Trace.SpanKind.Internal, in OpenTelemetry.Trace.SpanContext parentContext = default(OpenTelemetry.Trace.SpanContext), OpenTelemetry.Trace.SpanAttributes initialAttributes = null, System.Collections.Generic.IEnumerable links = null, System.DateTimeOffset startTime = default(System.DateTimeOffset)) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.Tracer.StartActiveSpan(string name, OpenTelemetry.Trace.SpanKind kind, in OpenTelemetry.Trace.TelemetrySpan parentSpan, OpenTelemetry.Trace.SpanAttributes initialAttributes = null, System.Collections.Generic.IEnumerable links = null, System.DateTimeOffset startTime = default(System.DateTimeOffset)) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.Tracer.StartRootSpan(string name, OpenTelemetry.Trace.SpanKind kind = OpenTelemetry.Trace.SpanKind.Internal, OpenTelemetry.Trace.SpanAttributes initialAttributes = null, System.Collections.Generic.IEnumerable links = null, System.DateTimeOffset startTime = default(System.DateTimeOffset)) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.Tracer.StartSpan(string name, OpenTelemetry.Trace.SpanKind kind = OpenTelemetry.Trace.SpanKind.Internal, in OpenTelemetry.Trace.SpanContext parentContext = default(OpenTelemetry.Trace.SpanContext), OpenTelemetry.Trace.SpanAttributes initialAttributes = null, System.Collections.Generic.IEnumerable links = null, System.DateTimeOffset startTime = default(System.DateTimeOffset)) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.Tracer.StartSpan(string name, OpenTelemetry.Trace.SpanKind kind, in OpenTelemetry.Trace.TelemetrySpan parentSpan, OpenTelemetry.Trace.SpanAttributes initialAttributes = null, System.Collections.Generic.IEnumerable links = null, System.DateTimeOffset startTime = default(System.DateTimeOffset)) -> OpenTelemetry.Trace.TelemetrySpan +OpenTelemetry.Trace.Tracer.StartActiveSpan(string! name, OpenTelemetry.Trace.SpanKind kind = OpenTelemetry.Trace.SpanKind.Internal, in OpenTelemetry.Trace.SpanContext parentContext = default(OpenTelemetry.Trace.SpanContext), OpenTelemetry.Trace.SpanAttributes? initialAttributes = null, System.Collections.Generic.IEnumerable? links = null, System.DateTimeOffset startTime = default(System.DateTimeOffset)) -> OpenTelemetry.Trace.TelemetrySpan! +OpenTelemetry.Trace.Tracer.StartActiveSpan(string! name, OpenTelemetry.Trace.SpanKind kind, in OpenTelemetry.Trace.TelemetrySpan? parentSpan, OpenTelemetry.Trace.SpanAttributes? initialAttributes = null, System.Collections.Generic.IEnumerable? links = null, System.DateTimeOffset startTime = default(System.DateTimeOffset)) -> OpenTelemetry.Trace.TelemetrySpan! +OpenTelemetry.Trace.Tracer.StartRootSpan(string! name, OpenTelemetry.Trace.SpanKind kind = OpenTelemetry.Trace.SpanKind.Internal, OpenTelemetry.Trace.SpanAttributes? initialAttributes = null, System.Collections.Generic.IEnumerable? links = null, System.DateTimeOffset startTime = default(System.DateTimeOffset)) -> OpenTelemetry.Trace.TelemetrySpan! +OpenTelemetry.Trace.Tracer.StartSpan(string! name, OpenTelemetry.Trace.SpanKind kind = OpenTelemetry.Trace.SpanKind.Internal, in OpenTelemetry.Trace.SpanContext parentContext = default(OpenTelemetry.Trace.SpanContext), OpenTelemetry.Trace.SpanAttributes? initialAttributes = null, System.Collections.Generic.IEnumerable? links = null, System.DateTimeOffset startTime = default(System.DateTimeOffset)) -> OpenTelemetry.Trace.TelemetrySpan! +OpenTelemetry.Trace.Tracer.StartSpan(string! name, OpenTelemetry.Trace.SpanKind kind, in OpenTelemetry.Trace.TelemetrySpan? parentSpan, OpenTelemetry.Trace.SpanAttributes? initialAttributes = null, System.Collections.Generic.IEnumerable? links = null, System.DateTimeOffset startTime = default(System.DateTimeOffset)) -> OpenTelemetry.Trace.TelemetrySpan! OpenTelemetry.Trace.TracerProvider -~OpenTelemetry.Trace.TracerProvider.GetTracer(string name, string version = null) -> OpenTelemetry.Trace.Tracer +OpenTelemetry.Trace.TracerProvider.GetTracer(string! name, string? version = null) -> OpenTelemetry.Trace.Tracer! OpenTelemetry.Trace.TracerProvider.TracerProvider() -> void OpenTelemetry.Trace.TracerProviderBuilder OpenTelemetry.Trace.TracerProviderBuilder.TracerProviderBuilder() -> void -~override OpenTelemetry.Baggage.Equals(object obj) -> bool override OpenTelemetry.Baggage.GetHashCode() -> int override OpenTelemetry.Context.AsyncLocalRuntimeContextSlot.Get() -> T override OpenTelemetry.Context.AsyncLocalRuntimeContextSlot.Set(T value) -> void -~override OpenTelemetry.Context.Propagation.B3Propagator.Extract(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Func> getter) -> OpenTelemetry.Context.Propagation.PropagationContext -~override OpenTelemetry.Context.Propagation.B3Propagator.Fields.get -> System.Collections.Generic.ISet -~override OpenTelemetry.Context.Propagation.B3Propagator.Inject(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Action setter) -> void -~override OpenTelemetry.Context.Propagation.BaggagePropagator.Extract(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Func> getter) -> OpenTelemetry.Context.Propagation.PropagationContext -~override OpenTelemetry.Context.Propagation.BaggagePropagator.Fields.get -> System.Collections.Generic.ISet -~override OpenTelemetry.Context.Propagation.BaggagePropagator.Inject(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Action setter) -> void -~override OpenTelemetry.Context.Propagation.CompositeTextMapPropagator.Extract(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Func> getter) -> OpenTelemetry.Context.Propagation.PropagationContext -~override OpenTelemetry.Context.Propagation.CompositeTextMapPropagator.Fields.get -> System.Collections.Generic.ISet -~override OpenTelemetry.Context.Propagation.CompositeTextMapPropagator.Inject(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Action setter) -> void -~override OpenTelemetry.Context.Propagation.PropagationContext.Equals(object obj) -> bool override OpenTelemetry.Context.Propagation.PropagationContext.GetHashCode() -> int -~override OpenTelemetry.Context.Propagation.TraceContextPropagator.Extract(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Func> getter) -> OpenTelemetry.Context.Propagation.PropagationContext -~override OpenTelemetry.Context.Propagation.TraceContextPropagator.Fields.get -> System.Collections.Generic.ISet -~override OpenTelemetry.Context.Propagation.TraceContextPropagator.Inject(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Action setter) -> void override OpenTelemetry.Context.ThreadLocalRuntimeContextSlot.Dispose(bool disposing) -> void override OpenTelemetry.Context.ThreadLocalRuntimeContextSlot.Get() -> T override OpenTelemetry.Context.ThreadLocalRuntimeContextSlot.Set(T value) -> void -~override OpenTelemetry.Trace.Link.Equals(object obj) -> bool +override OpenTelemetry.Trace.Link.Equals(object? obj) -> bool override OpenTelemetry.Trace.Link.GetHashCode() -> int -~override OpenTelemetry.Trace.SpanContext.Equals(object obj) -> bool +override OpenTelemetry.Trace.SpanContext.Equals(object? obj) -> bool override OpenTelemetry.Trace.SpanContext.GetHashCode() -> int -~override OpenTelemetry.Trace.Status.Equals(object obj) -> bool +override OpenTelemetry.Trace.Status.Equals(object? obj) -> bool override OpenTelemetry.Trace.Status.GetHashCode() -> int -~override OpenTelemetry.Trace.Status.ToString() -> string +override OpenTelemetry.Trace.Status.ToString() -> string! +override OpenTelemetry.Trace.TracerProvider.Dispose(bool disposing) -> void static OpenTelemetry.ActivityContextExtensions.IsValid(this System.Diagnostics.ActivityContext ctx) -> bool static OpenTelemetry.Baggage.ClearBaggage(OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> OpenTelemetry.Baggage -~static OpenTelemetry.Baggage.Create(System.Collections.Generic.Dictionary baggageItems = null) -> OpenTelemetry.Baggage static OpenTelemetry.Baggage.Current.get -> OpenTelemetry.Baggage static OpenTelemetry.Baggage.Current.set -> void -~static OpenTelemetry.Baggage.GetBaggage(OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> System.Collections.Generic.IReadOnlyDictionary -~static OpenTelemetry.Baggage.GetBaggage(string name, OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> string -~static OpenTelemetry.Baggage.GetEnumerator(OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> System.Collections.Generic.Dictionary.Enumerator static OpenTelemetry.Baggage.operator !=(OpenTelemetry.Baggage left, OpenTelemetry.Baggage right) -> bool static OpenTelemetry.Baggage.operator ==(OpenTelemetry.Baggage left, OpenTelemetry.Baggage right) -> bool -~static OpenTelemetry.Baggage.RemoveBaggage(string name, OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> OpenTelemetry.Baggage -~static OpenTelemetry.Baggage.SetBaggage(string name, string value, OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> OpenTelemetry.Baggage -~static OpenTelemetry.Baggage.SetBaggage(System.Collections.Generic.IEnumerable> baggageItems, OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> OpenTelemetry.Baggage static OpenTelemetry.Context.Propagation.PropagationContext.operator !=(OpenTelemetry.Context.Propagation.PropagationContext left, OpenTelemetry.Context.Propagation.PropagationContext right) -> bool static OpenTelemetry.Context.Propagation.PropagationContext.operator ==(OpenTelemetry.Context.Propagation.PropagationContext left, OpenTelemetry.Context.Propagation.PropagationContext right) -> bool -~static OpenTelemetry.Context.Propagation.Propagators.DefaultTextMapPropagator.get -> OpenTelemetry.Context.Propagation.TextMapPropagator -~static OpenTelemetry.Context.RuntimeContext.ContextSlotType.get -> System.Type -~static OpenTelemetry.Context.RuntimeContext.ContextSlotType.set -> void -~static OpenTelemetry.Context.RuntimeContext.GetSlot(string slotName) -> OpenTelemetry.Context.RuntimeContextSlot -~static OpenTelemetry.Context.RuntimeContext.GetValue(string slotName) -> object -~static OpenTelemetry.Context.RuntimeContext.GetValue(string slotName) -> T -~static OpenTelemetry.Context.RuntimeContext.RegisterSlot(string slotName) -> OpenTelemetry.Context.RuntimeContextSlot -~static OpenTelemetry.Context.RuntimeContext.SetValue(string slotName, object value) -> void -~static OpenTelemetry.Context.RuntimeContext.SetValue(string slotName, T value) -> void -~static OpenTelemetry.Trace.ActivityExtensions.GetStatus(this System.Diagnostics.Activity activity) -> OpenTelemetry.Trace.Status -~static OpenTelemetry.Trace.ActivityExtensions.RecordException(this System.Diagnostics.Activity activity, System.Exception ex) -> void -~static OpenTelemetry.Trace.ActivityExtensions.RecordException(this System.Diagnostics.Activity activity, System.Exception ex, in System.Diagnostics.TagList tags) -> void -~static OpenTelemetry.Trace.ActivityExtensions.SetStatus(this System.Diagnostics.Activity activity, OpenTelemetry.Trace.Status status) -> void +static OpenTelemetry.Trace.ActivityExtensions.GetStatus(this System.Diagnostics.Activity! activity) -> OpenTelemetry.Trace.Status +static OpenTelemetry.Trace.ActivityExtensions.RecordException(this System.Diagnostics.Activity! activity, System.Exception? ex) -> void +static OpenTelemetry.Trace.ActivityExtensions.RecordException(this System.Diagnostics.Activity! activity, System.Exception? ex, in System.Diagnostics.TagList tags) -> void +static OpenTelemetry.Trace.ActivityExtensions.SetStatus(this System.Diagnostics.Activity! activity, OpenTelemetry.Trace.Status status) -> void static OpenTelemetry.Trace.Link.operator !=(OpenTelemetry.Trace.Link link1, OpenTelemetry.Trace.Link link2) -> bool static OpenTelemetry.Trace.Link.operator ==(OpenTelemetry.Trace.Link link1, OpenTelemetry.Trace.Link link2) -> bool static OpenTelemetry.Trace.SpanContext.implicit operator System.Diagnostics.ActivityContext(OpenTelemetry.Trace.SpanContext spanContext) -> System.Diagnostics.ActivityContext @@ -211,9 +212,9 @@ static OpenTelemetry.Trace.SpanContext.operator !=(OpenTelemetry.Trace.SpanConte static OpenTelemetry.Trace.SpanContext.operator ==(OpenTelemetry.Trace.SpanContext spanContext1, OpenTelemetry.Trace.SpanContext spanContext2) -> bool static OpenTelemetry.Trace.Status.operator !=(OpenTelemetry.Trace.Status status1, OpenTelemetry.Trace.Status status2) -> bool static OpenTelemetry.Trace.Status.operator ==(OpenTelemetry.Trace.Status status1, OpenTelemetry.Trace.Status status2) -> bool -~static OpenTelemetry.Trace.Tracer.CurrentSpan.get -> OpenTelemetry.Trace.TelemetrySpan -~static OpenTelemetry.Trace.Tracer.WithSpan(OpenTelemetry.Trace.TelemetrySpan span) -> OpenTelemetry.Trace.TelemetrySpan -~static OpenTelemetry.Trace.TracerProvider.Default.get -> OpenTelemetry.Trace.TracerProvider +static OpenTelemetry.Trace.Tracer.CurrentSpan.get -> OpenTelemetry.Trace.TelemetrySpan! +static OpenTelemetry.Trace.Tracer.WithSpan(OpenTelemetry.Trace.TelemetrySpan? span) -> OpenTelemetry.Trace.TelemetrySpan? +static OpenTelemetry.Trace.TracerProvider.Default.get -> OpenTelemetry.Trace.TracerProvider! static readonly OpenTelemetry.Trace.Status.Error -> OpenTelemetry.Trace.Status static readonly OpenTelemetry.Trace.Status.Ok -> OpenTelemetry.Trace.Status static readonly OpenTelemetry.Trace.Status.Unset -> OpenTelemetry.Trace.Status diff --git a/src/OpenTelemetry.Exporter.Jaeger/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Api/.publicApi/Stable/PublicAPI.Unshipped.txt similarity index 100% rename from src/OpenTelemetry.Exporter.Jaeger/.publicApi/net462/PublicAPI.Unshipped.txt rename to src/OpenTelemetry.Api/.publicApi/Stable/PublicAPI.Unshipped.txt diff --git a/src/OpenTelemetry.Api/.publicApi/Stable/net462/PublicAPI.Shipped.txt b/src/OpenTelemetry.Api/.publicApi/Stable/net462/PublicAPI.Shipped.txt new file mode 100644 index 00000000000..58383768cd8 --- /dev/null +++ b/src/OpenTelemetry.Api/.publicApi/Stable/net462/PublicAPI.Shipped.txt @@ -0,0 +1,6 @@ +~OpenTelemetry.Context.RemotingRuntimeContextSlot.RemotingRuntimeContextSlot(string name) -> void +~OpenTelemetry.Context.RemotingRuntimeContextSlot.Value.get -> object +~OpenTelemetry.Context.RemotingRuntimeContextSlot.Value.set -> void +OpenTelemetry.Context.RemotingRuntimeContextSlot +override OpenTelemetry.Context.RemotingRuntimeContextSlot.Get() -> T +override OpenTelemetry.Context.RemotingRuntimeContextSlot.Set(T value) -> void diff --git a/src/OpenTelemetry.Exporter.Jaeger/.publicApi/net6.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Api/.publicApi/Stable/net462/PublicAPI.Unshipped.txt similarity index 100% rename from src/OpenTelemetry.Exporter.Jaeger/.publicApi/net6.0/PublicAPI.Unshipped.txt rename to src/OpenTelemetry.Api/.publicApi/Stable/net462/PublicAPI.Unshipped.txt diff --git a/src/OpenTelemetry.Api/.publicApi/net462/PublicAPI.Shipped.txt b/src/OpenTelemetry.Api/.publicApi/net462/PublicAPI.Shipped.txt deleted file mode 100644 index aed363d0ebd..00000000000 --- a/src/OpenTelemetry.Api/.publicApi/net462/PublicAPI.Shipped.txt +++ /dev/null @@ -1,227 +0,0 @@ -#nullable enable -~abstract OpenTelemetry.Context.Propagation.TextMapPropagator.Extract(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Func> getter) -> OpenTelemetry.Context.Propagation.PropagationContext -~abstract OpenTelemetry.Context.Propagation.TextMapPropagator.Fields.get -> System.Collections.Generic.ISet -~abstract OpenTelemetry.Context.Propagation.TextMapPropagator.Inject(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Action setter) -> void -abstract OpenTelemetry.Context.RuntimeContextSlot.Get() -> T -abstract OpenTelemetry.Context.RuntimeContextSlot.Set(T value) -> void -~abstract OpenTelemetry.Metrics.MeterProviderBuilder.AddInstrumentation(System.Func instrumentationFactory) -> OpenTelemetry.Metrics.MeterProviderBuilder -~abstract OpenTelemetry.Metrics.MeterProviderBuilder.AddMeter(params string[] names) -> OpenTelemetry.Metrics.MeterProviderBuilder -~abstract OpenTelemetry.Trace.TracerProviderBuilder.AddInstrumentation(System.Func instrumentationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder -~abstract OpenTelemetry.Trace.TracerProviderBuilder.AddLegacySource(string operationName) -> OpenTelemetry.Trace.TracerProviderBuilder -~abstract OpenTelemetry.Trace.TracerProviderBuilder.AddSource(params string[] names) -> OpenTelemetry.Trace.TracerProviderBuilder -OpenTelemetry.ActivityContextExtensions -OpenTelemetry.Baggage -OpenTelemetry.Baggage.Baggage() -> void -OpenTelemetry.Baggage.ClearBaggage() -> OpenTelemetry.Baggage -OpenTelemetry.Baggage.Count.get -> int -OpenTelemetry.Baggage.Equals(OpenTelemetry.Baggage other) -> bool -~OpenTelemetry.Baggage.GetBaggage() -> System.Collections.Generic.IReadOnlyDictionary -~OpenTelemetry.Baggage.GetBaggage(string name) -> string -~OpenTelemetry.Baggage.GetEnumerator() -> System.Collections.Generic.Dictionary.Enumerator -~OpenTelemetry.Baggage.RemoveBaggage(string name) -> OpenTelemetry.Baggage -~OpenTelemetry.Baggage.SetBaggage(params System.Collections.Generic.KeyValuePair[] baggageItems) -> OpenTelemetry.Baggage -~OpenTelemetry.Baggage.SetBaggage(string name, string value) -> OpenTelemetry.Baggage -~OpenTelemetry.Baggage.SetBaggage(System.Collections.Generic.IEnumerable> baggageItems) -> OpenTelemetry.Baggage -OpenTelemetry.BaseProvider -OpenTelemetry.BaseProvider.~BaseProvider() -> void -OpenTelemetry.BaseProvider.BaseProvider() -> void -OpenTelemetry.BaseProvider.Dispose() -> void -OpenTelemetry.Context.AsyncLocalRuntimeContextSlot -~OpenTelemetry.Context.AsyncLocalRuntimeContextSlot.AsyncLocalRuntimeContextSlot(string name) -> void -~OpenTelemetry.Context.AsyncLocalRuntimeContextSlot.Value.get -> object -~OpenTelemetry.Context.AsyncLocalRuntimeContextSlot.Value.set -> void -OpenTelemetry.Context.IRuntimeContextSlotValueAccessor -~OpenTelemetry.Context.IRuntimeContextSlotValueAccessor.Value.get -> object -~OpenTelemetry.Context.IRuntimeContextSlotValueAccessor.Value.set -> void -OpenTelemetry.Context.Propagation.B3Propagator -OpenTelemetry.Context.Propagation.B3Propagator.B3Propagator() -> void -OpenTelemetry.Context.Propagation.B3Propagator.B3Propagator(bool singleHeader) -> void -OpenTelemetry.Context.Propagation.BaggagePropagator -OpenTelemetry.Context.Propagation.BaggagePropagator.BaggagePropagator() -> void -OpenTelemetry.Context.Propagation.CompositeTextMapPropagator -~OpenTelemetry.Context.Propagation.CompositeTextMapPropagator.CompositeTextMapPropagator(System.Collections.Generic.IEnumerable propagators) -> void -OpenTelemetry.Context.Propagation.PropagationContext -OpenTelemetry.Context.Propagation.PropagationContext.ActivityContext.get -> System.Diagnostics.ActivityContext -OpenTelemetry.Context.Propagation.PropagationContext.Baggage.get -> OpenTelemetry.Baggage -OpenTelemetry.Context.Propagation.PropagationContext.Equals(OpenTelemetry.Context.Propagation.PropagationContext value) -> bool -OpenTelemetry.Context.Propagation.PropagationContext.PropagationContext() -> void -OpenTelemetry.Context.Propagation.PropagationContext.PropagationContext(System.Diagnostics.ActivityContext activityContext, OpenTelemetry.Baggage baggage) -> void -OpenTelemetry.Context.Propagation.Propagators -OpenTelemetry.Context.Propagation.TextMapPropagator -OpenTelemetry.Context.Propagation.TextMapPropagator.TextMapPropagator() -> void -OpenTelemetry.Context.Propagation.TraceContextPropagator -OpenTelemetry.Context.Propagation.TraceContextPropagator.TraceContextPropagator() -> void -OpenTelemetry.Context.RemotingRuntimeContextSlot -~OpenTelemetry.Context.RemotingRuntimeContextSlot.RemotingRuntimeContextSlot(string name) -> void -~OpenTelemetry.Context.RemotingRuntimeContextSlot.Value.get -> object -~OpenTelemetry.Context.RemotingRuntimeContextSlot.Value.set -> void -OpenTelemetry.Context.RuntimeContext -OpenTelemetry.Context.RuntimeContextSlot -OpenTelemetry.Context.RuntimeContextSlot.Dispose() -> void -~OpenTelemetry.Context.RuntimeContextSlot.Name.get -> string -~OpenTelemetry.Context.RuntimeContextSlot.RuntimeContextSlot(string name) -> void -OpenTelemetry.Context.ThreadLocalRuntimeContextSlot -~OpenTelemetry.Context.ThreadLocalRuntimeContextSlot.ThreadLocalRuntimeContextSlot(string name) -> void -~OpenTelemetry.Context.ThreadLocalRuntimeContextSlot.Value.get -> object -~OpenTelemetry.Context.ThreadLocalRuntimeContextSlot.Value.set -> void -OpenTelemetry.Metrics.IDeferredMeterProviderBuilder -~OpenTelemetry.Metrics.IDeferredMeterProviderBuilder.Configure(System.Action configure) -> OpenTelemetry.Metrics.MeterProviderBuilder -OpenTelemetry.Metrics.MeterProvider -OpenTelemetry.Metrics.MeterProvider.MeterProvider() -> void -OpenTelemetry.Metrics.MeterProviderBuilder -OpenTelemetry.Metrics.MeterProviderBuilder.MeterProviderBuilder() -> void -OpenTelemetry.Trace.ActivityExtensions -OpenTelemetry.Trace.IDeferredTracerProviderBuilder -~OpenTelemetry.Trace.IDeferredTracerProviderBuilder.Configure(System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder -OpenTelemetry.Trace.Link -~OpenTelemetry.Trace.Link.Attributes.get -> System.Collections.Generic.IEnumerable> -OpenTelemetry.Trace.Link.Context.get -> OpenTelemetry.Trace.SpanContext -OpenTelemetry.Trace.Link.Equals(OpenTelemetry.Trace.Link other) -> bool -OpenTelemetry.Trace.Link.Link() -> void -OpenTelemetry.Trace.Link.Link(in OpenTelemetry.Trace.SpanContext spanContext) -> void -~OpenTelemetry.Trace.Link.Link(in OpenTelemetry.Trace.SpanContext spanContext, OpenTelemetry.Trace.SpanAttributes attributes) -> void -OpenTelemetry.Trace.SpanAttributes -~OpenTelemetry.Trace.SpanAttributes.Add(string key, bool value) -> void -~OpenTelemetry.Trace.SpanAttributes.Add(string key, bool[] values) -> void -~OpenTelemetry.Trace.SpanAttributes.Add(string key, double value) -> void -~OpenTelemetry.Trace.SpanAttributes.Add(string key, double[] values) -> void -~OpenTelemetry.Trace.SpanAttributes.Add(string key, long value) -> void -~OpenTelemetry.Trace.SpanAttributes.Add(string key, long[] values) -> void -~OpenTelemetry.Trace.SpanAttributes.Add(string key, string value) -> void -~OpenTelemetry.Trace.SpanAttributes.Add(string key, string[] values) -> void -OpenTelemetry.Trace.SpanAttributes.SpanAttributes() -> void -~OpenTelemetry.Trace.SpanAttributes.SpanAttributes(System.Collections.Generic.IEnumerable> attributes) -> void -OpenTelemetry.Trace.SpanContext -OpenTelemetry.Trace.SpanContext.Equals(OpenTelemetry.Trace.SpanContext other) -> bool -OpenTelemetry.Trace.SpanContext.IsRemote.get -> bool -OpenTelemetry.Trace.SpanContext.IsValid.get -> bool -OpenTelemetry.Trace.SpanContext.SpanContext() -> void -OpenTelemetry.Trace.SpanContext.SpanContext(in System.Diagnostics.ActivityContext activityContext) -> void -~OpenTelemetry.Trace.SpanContext.SpanContext(in System.Diagnostics.ActivityTraceId traceId, in System.Diagnostics.ActivitySpanId spanId, System.Diagnostics.ActivityTraceFlags traceFlags, bool isRemote = false, System.Collections.Generic.IEnumerable> traceState = null) -> void -OpenTelemetry.Trace.SpanContext.SpanId.get -> System.Diagnostics.ActivitySpanId -OpenTelemetry.Trace.SpanContext.TraceFlags.get -> System.Diagnostics.ActivityTraceFlags -OpenTelemetry.Trace.SpanContext.TraceId.get -> System.Diagnostics.ActivityTraceId -~OpenTelemetry.Trace.SpanContext.TraceState.get -> System.Collections.Generic.IEnumerable> -OpenTelemetry.Trace.SpanKind -OpenTelemetry.Trace.SpanKind.Client = 3 -> OpenTelemetry.Trace.SpanKind -OpenTelemetry.Trace.SpanKind.Consumer = 5 -> OpenTelemetry.Trace.SpanKind -OpenTelemetry.Trace.SpanKind.Internal = 1 -> OpenTelemetry.Trace.SpanKind -OpenTelemetry.Trace.SpanKind.Producer = 4 -> OpenTelemetry.Trace.SpanKind -OpenTelemetry.Trace.SpanKind.Server = 2 -> OpenTelemetry.Trace.SpanKind -OpenTelemetry.Trace.Status -~OpenTelemetry.Trace.Status.Description.get -> string -OpenTelemetry.Trace.Status.Equals(OpenTelemetry.Trace.Status other) -> bool -OpenTelemetry.Trace.Status.Status() -> void -OpenTelemetry.Trace.Status.StatusCode.get -> OpenTelemetry.Trace.StatusCode -~OpenTelemetry.Trace.Status.WithDescription(string description) -> OpenTelemetry.Trace.Status -OpenTelemetry.Trace.StatusCode -OpenTelemetry.Trace.StatusCode.Error = 2 -> OpenTelemetry.Trace.StatusCode -OpenTelemetry.Trace.StatusCode.Ok = 1 -> OpenTelemetry.Trace.StatusCode -OpenTelemetry.Trace.StatusCode.Unset = 0 -> OpenTelemetry.Trace.StatusCode -OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.AddEvent(string name) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.AddEvent(string name, OpenTelemetry.Trace.SpanAttributes attributes) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.AddEvent(string name, System.DateTimeOffset timestamp) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.AddEvent(string name, System.DateTimeOffset timestamp, OpenTelemetry.Trace.SpanAttributes attributes) -> OpenTelemetry.Trace.TelemetrySpan -OpenTelemetry.Trace.TelemetrySpan.Context.get -> OpenTelemetry.Trace.SpanContext -OpenTelemetry.Trace.TelemetrySpan.Dispose() -> void -OpenTelemetry.Trace.TelemetrySpan.End() -> void -OpenTelemetry.Trace.TelemetrySpan.End(System.DateTimeOffset endTimestamp) -> void -OpenTelemetry.Trace.TelemetrySpan.IsRecording.get -> bool -OpenTelemetry.Trace.TelemetrySpan.ParentSpanId.get -> System.Diagnostics.ActivitySpanId -~OpenTelemetry.Trace.TelemetrySpan.RecordException(string type, string message, string stacktrace) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.RecordException(System.Exception ex) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.SetAttribute(string key, bool value) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.SetAttribute(string key, bool[] values) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.SetAttribute(string key, double value) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.SetAttribute(string key, double[] values) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.SetAttribute(string key, int value) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.SetAttribute(string key, int[] values) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.SetAttribute(string key, string value) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.TelemetrySpan.SetAttribute(string key, string[] values) -> OpenTelemetry.Trace.TelemetrySpan -OpenTelemetry.Trace.TelemetrySpan.SetStatus(OpenTelemetry.Trace.Status value) -> void -~OpenTelemetry.Trace.TelemetrySpan.UpdateName(string name) -> OpenTelemetry.Trace.TelemetrySpan -OpenTelemetry.Trace.Tracer -~OpenTelemetry.Trace.Tracer.StartActiveSpan(string name, OpenTelemetry.Trace.SpanKind kind = OpenTelemetry.Trace.SpanKind.Internal, in OpenTelemetry.Trace.SpanContext parentContext = default(OpenTelemetry.Trace.SpanContext), OpenTelemetry.Trace.SpanAttributes initialAttributes = null, System.Collections.Generic.IEnumerable links = null, System.DateTimeOffset startTime = default(System.DateTimeOffset)) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.Tracer.StartActiveSpan(string name, OpenTelemetry.Trace.SpanKind kind, in OpenTelemetry.Trace.TelemetrySpan parentSpan, OpenTelemetry.Trace.SpanAttributes initialAttributes = null, System.Collections.Generic.IEnumerable links = null, System.DateTimeOffset startTime = default(System.DateTimeOffset)) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.Tracer.StartRootSpan(string name, OpenTelemetry.Trace.SpanKind kind = OpenTelemetry.Trace.SpanKind.Internal, OpenTelemetry.Trace.SpanAttributes initialAttributes = null, System.Collections.Generic.IEnumerable links = null, System.DateTimeOffset startTime = default(System.DateTimeOffset)) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.Tracer.StartSpan(string name, OpenTelemetry.Trace.SpanKind kind = OpenTelemetry.Trace.SpanKind.Internal, in OpenTelemetry.Trace.SpanContext parentContext = default(OpenTelemetry.Trace.SpanContext), OpenTelemetry.Trace.SpanAttributes initialAttributes = null, System.Collections.Generic.IEnumerable links = null, System.DateTimeOffset startTime = default(System.DateTimeOffset)) -> OpenTelemetry.Trace.TelemetrySpan -~OpenTelemetry.Trace.Tracer.StartSpan(string name, OpenTelemetry.Trace.SpanKind kind, in OpenTelemetry.Trace.TelemetrySpan parentSpan, OpenTelemetry.Trace.SpanAttributes initialAttributes = null, System.Collections.Generic.IEnumerable links = null, System.DateTimeOffset startTime = default(System.DateTimeOffset)) -> OpenTelemetry.Trace.TelemetrySpan -OpenTelemetry.Trace.TracerProvider -~OpenTelemetry.Trace.TracerProvider.GetTracer(string name, string version = null) -> OpenTelemetry.Trace.Tracer -OpenTelemetry.Trace.TracerProvider.TracerProvider() -> void -OpenTelemetry.Trace.TracerProviderBuilder -OpenTelemetry.Trace.TracerProviderBuilder.TracerProviderBuilder() -> void -~override OpenTelemetry.Baggage.Equals(object obj) -> bool -override OpenTelemetry.Baggage.GetHashCode() -> int -override OpenTelemetry.Context.AsyncLocalRuntimeContextSlot.Get() -> T -override OpenTelemetry.Context.AsyncLocalRuntimeContextSlot.Set(T value) -> void -~override OpenTelemetry.Context.Propagation.B3Propagator.Extract(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Func> getter) -> OpenTelemetry.Context.Propagation.PropagationContext -~override OpenTelemetry.Context.Propagation.B3Propagator.Fields.get -> System.Collections.Generic.ISet -~override OpenTelemetry.Context.Propagation.B3Propagator.Inject(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Action setter) -> void -~override OpenTelemetry.Context.Propagation.BaggagePropagator.Extract(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Func> getter) -> OpenTelemetry.Context.Propagation.PropagationContext -~override OpenTelemetry.Context.Propagation.BaggagePropagator.Fields.get -> System.Collections.Generic.ISet -~override OpenTelemetry.Context.Propagation.BaggagePropagator.Inject(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Action setter) -> void -~override OpenTelemetry.Context.Propagation.CompositeTextMapPropagator.Extract(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Func> getter) -> OpenTelemetry.Context.Propagation.PropagationContext -~override OpenTelemetry.Context.Propagation.CompositeTextMapPropagator.Fields.get -> System.Collections.Generic.ISet -~override OpenTelemetry.Context.Propagation.CompositeTextMapPropagator.Inject(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Action setter) -> void -~override OpenTelemetry.Context.Propagation.PropagationContext.Equals(object obj) -> bool -override OpenTelemetry.Context.Propagation.PropagationContext.GetHashCode() -> int -~override OpenTelemetry.Context.Propagation.TraceContextPropagator.Extract(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Func> getter) -> OpenTelemetry.Context.Propagation.PropagationContext -~override OpenTelemetry.Context.Propagation.TraceContextPropagator.Fields.get -> System.Collections.Generic.ISet -~override OpenTelemetry.Context.Propagation.TraceContextPropagator.Inject(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Action setter) -> void -override OpenTelemetry.Context.RemotingRuntimeContextSlot.Get() -> T -override OpenTelemetry.Context.RemotingRuntimeContextSlot.Set(T value) -> void -override OpenTelemetry.Context.ThreadLocalRuntimeContextSlot.Dispose(bool disposing) -> void -override OpenTelemetry.Context.ThreadLocalRuntimeContextSlot.Get() -> T -override OpenTelemetry.Context.ThreadLocalRuntimeContextSlot.Set(T value) -> void -~override OpenTelemetry.Trace.Link.Equals(object obj) -> bool -override OpenTelemetry.Trace.Link.GetHashCode() -> int -~override OpenTelemetry.Trace.SpanContext.Equals(object obj) -> bool -override OpenTelemetry.Trace.SpanContext.GetHashCode() -> int -~override OpenTelemetry.Trace.Status.Equals(object obj) -> bool -override OpenTelemetry.Trace.Status.GetHashCode() -> int -~override OpenTelemetry.Trace.Status.ToString() -> string -static OpenTelemetry.ActivityContextExtensions.IsValid(this System.Diagnostics.ActivityContext ctx) -> bool -static OpenTelemetry.Baggage.ClearBaggage(OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> OpenTelemetry.Baggage -~static OpenTelemetry.Baggage.Create(System.Collections.Generic.Dictionary baggageItems = null) -> OpenTelemetry.Baggage -static OpenTelemetry.Baggage.Current.get -> OpenTelemetry.Baggage -static OpenTelemetry.Baggage.Current.set -> void -~static OpenTelemetry.Baggage.GetBaggage(OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> System.Collections.Generic.IReadOnlyDictionary -~static OpenTelemetry.Baggage.GetBaggage(string name, OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> string -~static OpenTelemetry.Baggage.GetEnumerator(OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> System.Collections.Generic.Dictionary.Enumerator -static OpenTelemetry.Baggage.operator !=(OpenTelemetry.Baggage left, OpenTelemetry.Baggage right) -> bool -static OpenTelemetry.Baggage.operator ==(OpenTelemetry.Baggage left, OpenTelemetry.Baggage right) -> bool -~static OpenTelemetry.Baggage.RemoveBaggage(string name, OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> OpenTelemetry.Baggage -~static OpenTelemetry.Baggage.SetBaggage(string name, string value, OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> OpenTelemetry.Baggage -~static OpenTelemetry.Baggage.SetBaggage(System.Collections.Generic.IEnumerable> baggageItems, OpenTelemetry.Baggage baggage = default(OpenTelemetry.Baggage)) -> OpenTelemetry.Baggage -static OpenTelemetry.Context.Propagation.PropagationContext.operator !=(OpenTelemetry.Context.Propagation.PropagationContext left, OpenTelemetry.Context.Propagation.PropagationContext right) -> bool -static OpenTelemetry.Context.Propagation.PropagationContext.operator ==(OpenTelemetry.Context.Propagation.PropagationContext left, OpenTelemetry.Context.Propagation.PropagationContext right) -> bool -~static OpenTelemetry.Context.Propagation.Propagators.DefaultTextMapPropagator.get -> OpenTelemetry.Context.Propagation.TextMapPropagator -~static OpenTelemetry.Context.RuntimeContext.ContextSlotType.get -> System.Type -~static OpenTelemetry.Context.RuntimeContext.ContextSlotType.set -> void -~static OpenTelemetry.Context.RuntimeContext.GetSlot(string slotName) -> OpenTelemetry.Context.RuntimeContextSlot -~static OpenTelemetry.Context.RuntimeContext.GetValue(string slotName) -> object -~static OpenTelemetry.Context.RuntimeContext.GetValue(string slotName) -> T -~static OpenTelemetry.Context.RuntimeContext.RegisterSlot(string slotName) -> OpenTelemetry.Context.RuntimeContextSlot -~static OpenTelemetry.Context.RuntimeContext.SetValue(string slotName, object value) -> void -~static OpenTelemetry.Context.RuntimeContext.SetValue(string slotName, T value) -> void -~static OpenTelemetry.Trace.ActivityExtensions.GetStatus(this System.Diagnostics.Activity activity) -> OpenTelemetry.Trace.Status -~static OpenTelemetry.Trace.ActivityExtensions.RecordException(this System.Diagnostics.Activity activity, System.Exception ex) -> void -~static OpenTelemetry.Trace.ActivityExtensions.RecordException(this System.Diagnostics.Activity activity, System.Exception ex, in System.Diagnostics.TagList tags) -> void -~static OpenTelemetry.Trace.ActivityExtensions.SetStatus(this System.Diagnostics.Activity activity, OpenTelemetry.Trace.Status status) -> void -static OpenTelemetry.Trace.Link.operator !=(OpenTelemetry.Trace.Link link1, OpenTelemetry.Trace.Link link2) -> bool -static OpenTelemetry.Trace.Link.operator ==(OpenTelemetry.Trace.Link link1, OpenTelemetry.Trace.Link link2) -> bool -static OpenTelemetry.Trace.SpanContext.implicit operator System.Diagnostics.ActivityContext(OpenTelemetry.Trace.SpanContext spanContext) -> System.Diagnostics.ActivityContext -static OpenTelemetry.Trace.SpanContext.operator !=(OpenTelemetry.Trace.SpanContext spanContext1, OpenTelemetry.Trace.SpanContext spanContext2) -> bool -static OpenTelemetry.Trace.SpanContext.operator ==(OpenTelemetry.Trace.SpanContext spanContext1, OpenTelemetry.Trace.SpanContext spanContext2) -> bool -static OpenTelemetry.Trace.Status.operator !=(OpenTelemetry.Trace.Status status1, OpenTelemetry.Trace.Status status2) -> bool -static OpenTelemetry.Trace.Status.operator ==(OpenTelemetry.Trace.Status status1, OpenTelemetry.Trace.Status status2) -> bool -~static OpenTelemetry.Trace.Tracer.CurrentSpan.get -> OpenTelemetry.Trace.TelemetrySpan -~static OpenTelemetry.Trace.Tracer.WithSpan(OpenTelemetry.Trace.TelemetrySpan span) -> OpenTelemetry.Trace.TelemetrySpan -~static OpenTelemetry.Trace.TracerProvider.Default.get -> OpenTelemetry.Trace.TracerProvider -static readonly OpenTelemetry.Trace.Status.Error -> OpenTelemetry.Trace.Status -static readonly OpenTelemetry.Trace.Status.Ok -> OpenTelemetry.Trace.Status -static readonly OpenTelemetry.Trace.Status.Unset -> OpenTelemetry.Trace.Status -virtual OpenTelemetry.BaseProvider.Dispose(bool disposing) -> void -virtual OpenTelemetry.Context.RuntimeContextSlot.Dispose(bool disposing) -> void diff --git a/src/OpenTelemetry.Api/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Api/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt deleted file mode 100644 index 29b616ee92c..00000000000 --- a/src/OpenTelemetry.Api/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,80 +0,0 @@ -abstract OpenTelemetry.Logs.Logger.EmitLog(in OpenTelemetry.Logs.LogRecordData data, in OpenTelemetry.Logs.LogRecordAttributeList attributes) -> void -abstract OpenTelemetry.Logs.LoggerProviderBuilder.AddInstrumentation(System.Func! instrumentationFactory) -> OpenTelemetry.Logs.LoggerProviderBuilder! -OpenTelemetry.Logs.IDeferredLoggerProviderBuilder -OpenTelemetry.Logs.IDeferredLoggerProviderBuilder.Configure(System.Action! configure) -> OpenTelemetry.Logs.LoggerProviderBuilder! -OpenTelemetry.Logs.Logger -OpenTelemetry.Logs.Logger.EmitLog(in OpenTelemetry.Logs.LogRecordData data) -> void -OpenTelemetry.Logs.Logger.Logger(string? name) -> void -OpenTelemetry.Logs.Logger.Name.get -> string! -OpenTelemetry.Logs.Logger.Version.get -> string? -OpenTelemetry.Logs.LoggerProvider -OpenTelemetry.Logs.LoggerProvider.GetLogger() -> OpenTelemetry.Logs.Logger! -OpenTelemetry.Logs.LoggerProvider.GetLogger(string? name) -> OpenTelemetry.Logs.Logger! -OpenTelemetry.Logs.LoggerProvider.GetLogger(string? name, string? version) -> OpenTelemetry.Logs.Logger! -OpenTelemetry.Logs.LoggerProvider.LoggerProvider() -> void -OpenTelemetry.Logs.LoggerProviderBuilder -OpenTelemetry.Logs.LoggerProviderBuilder.LoggerProviderBuilder() -> void -OpenTelemetry.Logs.LogRecordAttributeList -OpenTelemetry.Logs.LogRecordAttributeList.Add(string! key, object? value) -> void -OpenTelemetry.Logs.LogRecordAttributeList.Add(System.Collections.Generic.KeyValuePair attribute) -> void -OpenTelemetry.Logs.LogRecordAttributeList.Clear() -> void -OpenTelemetry.Logs.LogRecordAttributeList.Count.get -> int -OpenTelemetry.Logs.LogRecordAttributeList.Enumerator -OpenTelemetry.Logs.LogRecordAttributeList.Enumerator.Current.get -> System.Collections.Generic.KeyValuePair -OpenTelemetry.Logs.LogRecordAttributeList.Enumerator.Dispose() -> void -OpenTelemetry.Logs.LogRecordAttributeList.Enumerator.Enumerator() -> void -OpenTelemetry.Logs.LogRecordAttributeList.Enumerator.MoveNext() -> bool -OpenTelemetry.Logs.LogRecordAttributeList.GetEnumerator() -> OpenTelemetry.Logs.LogRecordAttributeList.Enumerator -OpenTelemetry.Logs.LogRecordAttributeList.LogRecordAttributeList() -> void -OpenTelemetry.Logs.LogRecordAttributeList.RecordException(System.Exception! exception) -> void -OpenTelemetry.Logs.LogRecordAttributeList.this[int index].get -> System.Collections.Generic.KeyValuePair -OpenTelemetry.Logs.LogRecordAttributeList.this[int index].set -> void -OpenTelemetry.Logs.LogRecordAttributeList.this[string! key].set -> void -OpenTelemetry.Logs.LogRecordData -OpenTelemetry.Logs.LogRecordData.Body.get -> string? -OpenTelemetry.Logs.LogRecordData.Body.set -> void -OpenTelemetry.Logs.LogRecordData.LogRecordData() -> void -OpenTelemetry.Logs.LogRecordData.LogRecordData(in System.Diagnostics.ActivityContext activityContext) -> void -OpenTelemetry.Logs.LogRecordData.LogRecordData(System.Diagnostics.Activity? activity) -> void -OpenTelemetry.Logs.LogRecordData.Severity.get -> OpenTelemetry.Logs.LogRecordSeverity? -OpenTelemetry.Logs.LogRecordData.Severity.set -> void -OpenTelemetry.Logs.LogRecordData.SeverityText.get -> string? -OpenTelemetry.Logs.LogRecordData.SeverityText.set -> void -OpenTelemetry.Logs.LogRecordData.SpanId.get -> System.Diagnostics.ActivitySpanId -OpenTelemetry.Logs.LogRecordData.SpanId.set -> void -OpenTelemetry.Logs.LogRecordData.Timestamp.get -> System.DateTime -OpenTelemetry.Logs.LogRecordData.Timestamp.set -> void -OpenTelemetry.Logs.LogRecordData.TraceFlags.get -> System.Diagnostics.ActivityTraceFlags -OpenTelemetry.Logs.LogRecordData.TraceFlags.set -> void -OpenTelemetry.Logs.LogRecordData.TraceId.get -> System.Diagnostics.ActivityTraceId -OpenTelemetry.Logs.LogRecordData.TraceId.set -> void -OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Debug = 5 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Debug2 = 6 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Debug3 = 7 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Debug4 = 8 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Error = 17 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Error2 = 18 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Error3 = 19 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Error4 = 20 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Fatal = 21 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Fatal2 = 22 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Fatal3 = 23 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Fatal4 = 24 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Info = 9 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Info2 = 10 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Info3 = 11 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Info4 = 12 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Trace = 1 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Trace2 = 2 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Trace3 = 3 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Trace4 = 4 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Unspecified = 0 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Warn = 13 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Warn2 = 14 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Warn3 = 15 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverity.Warn4 = 16 -> OpenTelemetry.Logs.LogRecordSeverity -OpenTelemetry.Logs.LogRecordSeverityExtensions -static OpenTelemetry.Logs.LogRecordAttributeList.CreateFromEnumerable(System.Collections.Generic.IEnumerable>! attributes) -> OpenTelemetry.Logs.LogRecordAttributeList -static OpenTelemetry.Logs.LogRecordSeverityExtensions.ToShortName(this OpenTelemetry.Logs.LogRecordSeverity logRecordSeverity) -> string! -virtual OpenTelemetry.Logs.LoggerProvider.TryCreateLogger(string? name, out OpenTelemetry.Logs.Logger? logger) -> bool diff --git a/src/OpenTelemetry.Api/ActivityContextExtensions.cs b/src/OpenTelemetry.Api/ActivityContextExtensions.cs index 408d0bea562..f50a9af8fee 100644 --- a/src/OpenTelemetry.Api/ActivityContextExtensions.cs +++ b/src/OpenTelemetry.Api/ActivityContextExtensions.cs @@ -1,18 +1,7 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable using System.Diagnostics; @@ -21,21 +10,20 @@ // same namespace as Activity to prevent name collisions in the future. // The OpenTelemetry namespace is used because ActivityContext applies to all types // of telemetry data - i.e. traces, metrics, and logs. -namespace OpenTelemetry +namespace OpenTelemetry; + +/// +/// Extension methods on ActivityContext. +/// +public static class ActivityContextExtensions { /// - /// Extension methods on ActivityContext. + /// Returns a bool indicating if a ActivityContext is valid or not. /// - public static class ActivityContextExtensions + /// ActivityContext. + /// whether the context is a valid one or not. + public static bool IsValid(this ActivityContext ctx) { - /// - /// Returns a bool indicating if a ActivityContext is valid or not. - /// - /// ActivityContext. - /// whether the context is a valid one or not. - public static bool IsValid(this ActivityContext ctx) - { - return ctx != default; - } + return ctx != default; } } diff --git a/src/OpenTelemetry.Api/AssemblyInfo.cs b/src/OpenTelemetry.Api/AssemblyInfo.cs index 2af154f989e..47bca9e5f57 100644 --- a/src/OpenTelemetry.Api/AssemblyInfo.cs +++ b/src/OpenTelemetry.Api/AssemblyInfo.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Runtime.CompilerServices; @@ -21,18 +8,25 @@ [assembly: InternalsVisibleTo("OpenTelemetry.Api.Tests" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("OpenTelemetry.Shims.OpenTracing.Tests" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("OpenTelemetry.Tests" + AssemblyInfo.PublicKey)] -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2" + AssemblyInfo.MoqPublicKey)] + +#if !EXPOSE_EXPERIMENTAL_FEATURES +[assembly: InternalsVisibleTo("OpenTelemetry.Api.ProviderBuilderExtensions.Tests" + AssemblyInfo.PublicKey)] +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Console" + AssemblyInfo.PublicKey)] +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.InMemory" + AssemblyInfo.PublicKey)] +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol" + AssemblyInfo.PublicKey)] +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests" + AssemblyInfo.PublicKey)] +[assembly: InternalsVisibleTo("OpenTelemetry.Extensions.Hosting" + AssemblyInfo.PublicKey)] +[assembly: InternalsVisibleTo("OpenTelemetry.Extensions.Hosting.Tests" + AssemblyInfo.PublicKey)] +#endif #if SIGNED -internal static class AssemblyInfo +file static class AssemblyInfo { public const string PublicKey = ", PublicKey=002400000480000094000000060200000024000052534131000400000100010051C1562A090FB0C9F391012A32198B5E5D9A60E9B80FA2D7B434C9E5CCB7259BD606E66F9660676AFC6692B8CDC6793D190904551D2103B7B22FA636DCBB8208839785BA402EA08FC00C8F1500CCEF28BBF599AA64FFB1E1D5DC1BF3420A3777BADFE697856E9D52070A50C3EA5821C80BEF17CA3ACFFA28F89DD413F096F898"; - public const string MoqPublicKey = ", PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7"; } #else -internal static class AssemblyInfo +file static class AssemblyInfo { public const string PublicKey = ""; - public const string MoqPublicKey = ""; } #endif diff --git a/src/OpenTelemetry.Api/Baggage.cs b/src/OpenTelemetry.Api/Baggage.cs index de4b979352c..09e19a6ecf7 100644 --- a/src/OpenTelemetry.Api/Baggage.cs +++ b/src/OpenTelemetry.Api/Baggage.cs @@ -1,378 +1,369 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics.CodeAnalysis; using OpenTelemetry.Context; using OpenTelemetry.Internal; -namespace OpenTelemetry +namespace OpenTelemetry; + +/// +/// Baggage implementation. +/// +/// +/// Spec reference: Baggage API. +/// +public readonly struct Baggage : IEquatable { + private static readonly RuntimeContextSlot RuntimeContextSlot = RuntimeContext.RegisterSlot("otel.baggage"); + private static readonly Dictionary EmptyBaggage = new(); + + private readonly Dictionary baggage; + + /// + /// Initializes a new instance of the struct. + /// + /// Baggage key/value pairs. + internal Baggage(Dictionary baggage) + { + this.baggage = baggage; + } + /// - /// Baggage implementation. + /// Gets or sets the current . /// /// - /// Spec reference: Baggage API. + /// Note: returns a forked version of the current + /// Baggage. Changes to the forked version will not automatically be + /// reflected back on . To update either use one of the static methods that target + /// as the default source or set to a new instance of . + /// Examples: + /// + /// Baggage.SetBaggage("newKey1", "newValue1"); // Updates Baggage.Current with 'newKey1' + /// Baggage.SetBaggage("newKey2", "newValue2"); // Updates Baggage.Current with 'newKey2' + /// + /// Or: + /// + /// var baggageCopy = Baggage.Current; + /// Baggage.SetBaggage("newKey1", "newValue1"); // Updates Baggage.Current with 'newKey1' + /// var newBaggage = baggageCopy + /// .SetBaggage("newKey2", "newValue2"); + /// .SetBaggage("newKey3", "newValue3"); + /// // Sets Baggage.Current to 'newBaggage' which will override any + /// // changes made to Baggage.Current after the copy was made. For example + /// // the 'newKey1' change is lost. + /// Baggage.Current = newBaggage; + /// /// - public readonly struct Baggage : IEquatable + public static Baggage Current { - private static readonly RuntimeContextSlot RuntimeContextSlot = RuntimeContext.RegisterSlot("otel.baggage"); - private static readonly Dictionary EmptyBaggage = new(); + get => RuntimeContextSlot.Get()?.Baggage ?? default; + set => EnsureBaggageHolder().Baggage = value; + } - private readonly Dictionary baggage; + /// + /// Gets the number of key/value pairs in the baggage. + /// + public int Count => this.baggage?.Count ?? 0; - /// - /// Initializes a new instance of the struct. - /// - /// Baggage key/value pairs. - internal Baggage(Dictionary baggage) - { - this.baggage = baggage; - } + /// + /// Compare two entries of for equality. + /// + /// First Entry to compare. + /// Second Entry to compare. + public static bool operator ==(Baggage left, Baggage right) => left.Equals(right); - /// - /// Gets or sets the current . - /// - /// - /// Note: returns a forked version of the current - /// Baggage. Changes to the forked version will not automatically be - /// reflected back on . To update either use one of the static methods that target - /// as the default source or set to a new instance of . - /// Examples: - /// - /// Baggage.SetBaggage("newKey1", "newValue1"); // Updates Baggage.Current with 'newKey1' - /// Baggage.SetBaggage("newKey2", "newValue2"); // Updates Baggage.Current with 'newKey2' - /// - /// Or: - /// - /// var baggageCopy = Baggage.Current; - /// Baggage.SetBaggage("newKey1", "newValue1"); // Updates Baggage.Current with 'newKey1' - /// var newBaggage = baggageCopy - /// .SetBaggage("newKey2", "newValue2"); - /// .SetBaggage("newKey3", "newValue3"); - /// // Sets Baggage.Current to 'newBaggage' which will override any - /// // changes made to Baggage.Current after the copy was made. For example - /// // the 'newKey1' change is lost. - /// Baggage.Current = newBaggage; - /// - /// - public static Baggage Current + /// + /// Compare two entries of for not equality. + /// + /// First Entry to compare. + /// Second Entry to compare. + public static bool operator !=(Baggage left, Baggage right) => !(left == right); + + /// + /// Create a instance from dictionary of baggage key/value pairs. + /// + /// Baggage key/value pairs. + /// . + public static Baggage Create(Dictionary baggageItems = null) + { + if (baggageItems == null) { - get => RuntimeContextSlot.Get()?.Baggage ?? default; - set => EnsureBaggageHolder().Baggage = value; + return default; } - /// - /// Gets the number of key/value pairs in the baggage. - /// - public int Count => this.baggage?.Count ?? 0; - - /// - /// Compare two entries of for equality. - /// - /// First Entry to compare. - /// Second Entry to compare. - public static bool operator ==(Baggage left, Baggage right) => left.Equals(right); - - /// - /// Compare two entries of for not equality. - /// - /// First Entry to compare. - /// Second Entry to compare. - public static bool operator !=(Baggage left, Baggage right) => !(left == right); - - /// - /// Create a instance from dictionary of baggage key/value pairs. - /// - /// Baggage key/value pairs. - /// . - public static Baggage Create(Dictionary baggageItems = null) + Dictionary baggageCopy = new Dictionary(baggageItems.Count, StringComparer.OrdinalIgnoreCase); + foreach (KeyValuePair baggageItem in baggageItems) { - if (baggageItems == null) + if (string.IsNullOrEmpty(baggageItem.Value)) { - return default; + baggageCopy.Remove(baggageItem.Key); + continue; } - Dictionary baggageCopy = new Dictionary(baggageItems.Count, StringComparer.OrdinalIgnoreCase); - foreach (KeyValuePair baggageItem in baggageItems) - { - if (string.IsNullOrEmpty(baggageItem.Value)) - { - baggageCopy.Remove(baggageItem.Key); - continue; - } + baggageCopy[baggageItem.Key] = baggageItem.Value; + } - baggageCopy[baggageItem.Key] = baggageItem.Value; - } + return new Baggage(baggageCopy); + } - return new Baggage(baggageCopy); - } + /// + /// Returns the name/value pairs in the . + /// + /// Optional . is used if not specified. + /// Baggage key/value pairs. + [SuppressMessage("roslyn", "RS0026", Justification = "TODO: fix APIs that violate the backcompt requirement - multiple overloads with optional parameters: https://github.com/dotnet/roslyn/blob/main/docs/Adding%20Optional%20Parameters%20in%20Public%20API.md.")] + public static IReadOnlyDictionary GetBaggage(Baggage baggage = default) + => baggage == default ? Current.GetBaggage() : baggage.GetBaggage(); + + /// + /// Returns an enumerator that iterates through the . + /// + /// Optional . is used if not specified. + /// . + public static Dictionary.Enumerator GetEnumerator(Baggage baggage = default) + => baggage == default ? Current.GetEnumerator() : baggage.GetEnumerator(); + + /// + /// Returns the value associated with the given name, or if the given name is not present. + /// + /// Baggage item name. + /// Optional . is used if not specified. + /// Baggage item or if nothing was found. + [SuppressMessage("roslyn", "RS0026", Justification = "TODO: fix APIs that violate the backcompt requirement - multiple overloads with optional parameters: https://github.com/dotnet/roslyn/blob/main/docs/Adding%20Optional%20Parameters%20in%20Public%20API.md.")] + public static string GetBaggage(string name, Baggage baggage = default) + => baggage == default ? Current.GetBaggage(name) : baggage.GetBaggage(name); - /// - /// Returns the name/value pairs in the . - /// - /// Optional . is used if not specified. - /// Baggage key/value pairs. - public static IReadOnlyDictionary GetBaggage(Baggage baggage = default) - => baggage == default ? Current.GetBaggage() : baggage.GetBaggage(); - - /// - /// Returns an enumerator that iterates through the . - /// - /// Optional . is used if not specified. - /// . - public static Dictionary.Enumerator GetEnumerator(Baggage baggage = default) - => baggage == default ? Current.GetEnumerator() : baggage.GetEnumerator(); - - /// - /// Returns the value associated with the given name, or if the given name is not present. - /// - /// Baggage item name. - /// Optional . is used if not specified. - /// Baggage item or if nothing was found. - public static string GetBaggage(string name, Baggage baggage = default) - => baggage == default ? Current.GetBaggage(name) : baggage.GetBaggage(name); - - /// - /// Returns a new which contains the new key/value pair. - /// - /// Baggage item name. - /// Baggage item value. - /// Optional . is used if not specified. - /// New containing the key/value pair. - /// Note: The returned will be set as the new instance. - public static Baggage SetBaggage(string name, string value, Baggage baggage = default) + /// + /// Returns a new which contains the new key/value pair. + /// + /// Baggage item name. + /// Baggage item value. + /// Optional . is used if not specified. + /// New containing the key/value pair. + /// Note: The returned will be set as the new instance. + [SuppressMessage("roslyn", "RS0026", Justification = "TODO: fix APIs that violate the backcompt requirement - multiple overloads with optional parameters: https://github.com/dotnet/roslyn/blob/main/docs/Adding%20Optional%20Parameters%20in%20Public%20API.md.")] + public static Baggage SetBaggage(string name, string value, Baggage baggage = default) + { + var baggageHolder = EnsureBaggageHolder(); + lock (baggageHolder) { - var baggageHolder = EnsureBaggageHolder(); - lock (baggageHolder) - { - return baggageHolder.Baggage = baggage == default - ? baggageHolder.Baggage.SetBaggage(name, value) - : baggage.SetBaggage(name, value); - } + return baggageHolder.Baggage = baggage == default + ? baggageHolder.Baggage.SetBaggage(name, value) + : baggage.SetBaggage(name, value); } + } - /// - /// Returns a new which contains the new key/value pair. - /// - /// Baggage key/value pairs. - /// Optional . is used if not specified. - /// New containing the key/value pair. - /// Note: The returned will be set as the new instance. - public static Baggage SetBaggage(IEnumerable> baggageItems, Baggage baggage = default) + /// + /// Returns a new which contains the new key/value pair. + /// + /// Baggage key/value pairs. + /// Optional . is used if not specified. + /// New containing the key/value pair. + /// Note: The returned will be set as the new instance. + [SuppressMessage("roslyn", "RS0026", Justification = "TODO: fix APIs that violate the backcompt requirement - multiple overloads with optional parameters: https://github.com/dotnet/roslyn/blob/main/docs/Adding%20Optional%20Parameters%20in%20Public%20API.md.")] + public static Baggage SetBaggage(IEnumerable> baggageItems, Baggage baggage = default) + { + var baggageHolder = EnsureBaggageHolder(); + lock (baggageHolder) { - var baggageHolder = EnsureBaggageHolder(); - lock (baggageHolder) - { - return baggageHolder.Baggage = baggage == default - ? baggageHolder.Baggage.SetBaggage(baggageItems) - : baggage.SetBaggage(baggageItems); - } + return baggageHolder.Baggage = baggage == default + ? baggageHolder.Baggage.SetBaggage(baggageItems) + : baggage.SetBaggage(baggageItems); } + } - /// - /// Returns a new with the key/value pair removed. - /// - /// Baggage item name. - /// Optional . is used if not specified. - /// New containing the key/value pair. - /// Note: The returned will be set as the new instance. - public static Baggage RemoveBaggage(string name, Baggage baggage = default) + /// + /// Returns a new with the key/value pair removed. + /// + /// Baggage item name. + /// Optional . is used if not specified. + /// New containing the key/value pair. + /// Note: The returned will be set as the new instance. + public static Baggage RemoveBaggage(string name, Baggage baggage = default) + { + var baggageHolder = EnsureBaggageHolder(); + lock (baggageHolder) { - var baggageHolder = EnsureBaggageHolder(); - lock (baggageHolder) - { - return baggageHolder.Baggage = baggage == default - ? baggageHolder.Baggage.RemoveBaggage(name) - : baggage.RemoveBaggage(name); - } + return baggageHolder.Baggage = baggage == default + ? baggageHolder.Baggage.RemoveBaggage(name) + : baggage.RemoveBaggage(name); } + } - /// - /// Returns a new with all the key/value pairs removed. - /// - /// Optional . is used if not specified. - /// New containing the key/value pair. - /// Note: The returned will be set as the new instance. - public static Baggage ClearBaggage(Baggage baggage = default) + /// + /// Returns a new with all the key/value pairs removed. + /// + /// Optional . is used if not specified. + /// New containing the key/value pair. + /// Note: The returned will be set as the new instance. + public static Baggage ClearBaggage(Baggage baggage = default) + { + var baggageHolder = EnsureBaggageHolder(); + lock (baggageHolder) { - var baggageHolder = EnsureBaggageHolder(); - lock (baggageHolder) - { - return baggageHolder.Baggage = baggage == default - ? baggageHolder.Baggage.ClearBaggage() - : baggage.ClearBaggage(); - } + return baggageHolder.Baggage = baggage == default + ? baggageHolder.Baggage.ClearBaggage() + : baggage.ClearBaggage(); } + } - /// - /// Returns the name/value pairs in the . - /// - /// Baggage key/value pairs. - public IReadOnlyDictionary GetBaggage() - => this.baggage ?? EmptyBaggage; - - /// - /// Returns the value associated with the given name, or if the given name is not present. - /// - /// Baggage item name. - /// Baggage item or if nothing was found. - public string GetBaggage(string name) - { - Guard.ThrowIfNullOrEmpty(name); + /// + /// Returns the name/value pairs in the . + /// + /// Baggage key/value pairs. + public IReadOnlyDictionary GetBaggage() + => this.baggage ?? EmptyBaggage; - return this.baggage != null && this.baggage.TryGetValue(name, out string value) - ? value - : null; - } + /// + /// Returns the value associated with the given name, or if the given name is not present. + /// + /// Baggage item name. + /// Baggage item or if nothing was found. + public string GetBaggage(string name) + { + Guard.ThrowIfNullOrEmpty(name); - /// - /// Returns a new which contains the new key/value pair. - /// - /// Baggage item name. - /// Baggage item value. - /// New containing the key/value pair. - public Baggage SetBaggage(string name, string value) + return this.baggage != null && this.baggage.TryGetValue(name, out string value) + ? value + : null; + } + + /// + /// Returns a new which contains the new key/value pair. + /// + /// Baggage item name. + /// Baggage item value. + /// New containing the key/value pair. + public Baggage SetBaggage(string name, string value) + { + if (string.IsNullOrEmpty(value)) { - if (string.IsNullOrEmpty(value)) + return this.RemoveBaggage(name); + } + + return new Baggage( + new Dictionary(this.baggage ?? EmptyBaggage, StringComparer.OrdinalIgnoreCase) { - return this.RemoveBaggage(name); - } + [name] = value, + }); + } + + /// + /// Returns a new which contains the new key/value pair. + /// + /// Baggage key/value pairs. + /// New containing the key/value pair. + public Baggage SetBaggage(params KeyValuePair[] baggageItems) + => this.SetBaggage((IEnumerable>)baggageItems); - return new Baggage( - new Dictionary(this.baggage ?? EmptyBaggage, StringComparer.OrdinalIgnoreCase) - { - [name] = value, - }); + /// + /// Returns a new which contains the new key/value pair. + /// + /// Baggage key/value pairs. + /// New containing the key/value pair. + public Baggage SetBaggage(IEnumerable> baggageItems) + { + if (baggageItems?.Any() != true) + { + return this; } - /// - /// Returns a new which contains the new key/value pair. - /// - /// Baggage key/value pairs. - /// New containing the key/value pair. - public Baggage SetBaggage(params KeyValuePair[] baggageItems) - => this.SetBaggage((IEnumerable>)baggageItems); - - /// - /// Returns a new which contains the new key/value pair. - /// - /// Baggage key/value pairs. - /// New containing the key/value pair. - public Baggage SetBaggage(IEnumerable> baggageItems) + var newBaggage = new Dictionary(this.baggage ?? EmptyBaggage, StringComparer.OrdinalIgnoreCase); + + foreach (var item in baggageItems) { - if (baggageItems?.Any() != true) + if (string.IsNullOrEmpty(item.Value)) { - return this; + newBaggage.Remove(item.Key); } - - var newBaggage = new Dictionary(this.baggage ?? EmptyBaggage, StringComparer.OrdinalIgnoreCase); - - foreach (var item in baggageItems) + else { - if (string.IsNullOrEmpty(item.Value)) - { - newBaggage.Remove(item.Key); - } - else - { - newBaggage[item.Key] = item.Value; - } + newBaggage[item.Key] = item.Value; } - - return new Baggage(newBaggage); } - /// - /// Returns a new with the key/value pair removed. - /// - /// Baggage item name. - /// New containing the key/value pair. - public Baggage RemoveBaggage(string name) - { - var baggage = new Dictionary(this.baggage ?? EmptyBaggage, StringComparer.OrdinalIgnoreCase); - baggage.Remove(name); + return new Baggage(newBaggage); + } - return new Baggage(baggage); - } + /// + /// Returns a new with the key/value pair removed. + /// + /// Baggage item name. + /// New containing the key/value pair. + public Baggage RemoveBaggage(string name) + { + var baggage = new Dictionary(this.baggage ?? EmptyBaggage, StringComparer.OrdinalIgnoreCase); + baggage.Remove(name); - /// - /// Returns a new with all the key/value pairs removed. - /// - /// New containing the key/value pair. - public Baggage ClearBaggage() - => default; - - /// - /// Returns an enumerator that iterates through the . - /// - /// . - public Dictionary.Enumerator GetEnumerator() - => (this.baggage ?? EmptyBaggage).GetEnumerator(); - - /// - public bool Equals(Baggage other) - { - bool baggageIsNullOrEmpty = this.baggage == null || this.baggage.Count <= 0; + return new Baggage(baggage); + } - if (baggageIsNullOrEmpty != (other.baggage == null || other.baggage.Count <= 0)) - { - return false; - } + /// + /// Returns a new with all the key/value pairs removed. + /// + /// New containing the key/value pair. + public Baggage ClearBaggage() + => default; - return baggageIsNullOrEmpty || this.baggage.SequenceEqual(other.baggage); - } + /// + /// Returns an enumerator that iterates through the . + /// + /// . + public Dictionary.Enumerator GetEnumerator() + => (this.baggage ?? EmptyBaggage).GetEnumerator(); - /// - public override bool Equals(object obj) - => (obj is Baggage baggage) && this.Equals(baggage); + /// + public bool Equals(Baggage other) + { + bool baggageIsNullOrEmpty = this.baggage == null || this.baggage.Count <= 0; - /// - public override int GetHashCode() + if (baggageIsNullOrEmpty != (other.baggage == null || other.baggage.Count <= 0)) { - var baggage = this.baggage ?? EmptyBaggage; + return false; + } - var hash = 17; - foreach (var item in baggage) - { - unchecked - { - hash = (hash * 23) + baggage.Comparer.GetHashCode(item.Key); - hash = (hash * 23) + item.Value.GetHashCode(); - } - } + return baggageIsNullOrEmpty || this.baggage.SequenceEqual(other.baggage); + } - return hash; - } + /// + public override bool Equals(object obj) + => (obj is Baggage baggage) && this.Equals(baggage); - private static BaggageHolder EnsureBaggageHolder() + /// + public override int GetHashCode() + { + var baggage = this.baggage ?? EmptyBaggage; + + var hash = 17; + foreach (var item in baggage) { - var baggageHolder = RuntimeContextSlot.Get(); - if (baggageHolder == null) + unchecked { - baggageHolder = new BaggageHolder(); - RuntimeContextSlot.Set(baggageHolder); + hash = (hash * 23) + baggage.Comparer.GetHashCode(item.Key); + hash = (hash * 23) + item.Value.GetHashCode(); } - - return baggageHolder; } - private sealed class BaggageHolder + return hash; + } + + private static BaggageHolder EnsureBaggageHolder() + { + var baggageHolder = RuntimeContextSlot.Get(); + if (baggageHolder == null) { - public Baggage Baggage; + baggageHolder = new BaggageHolder(); + RuntimeContextSlot.Set(baggageHolder); } + + return baggageHolder; + } + + private sealed class BaggageHolder + { + public Baggage Baggage; } } diff --git a/src/OpenTelemetry.Api/BaseProvider.cs b/src/OpenTelemetry.Api/BaseProvider.cs index 93aa680a331..ce8e30a46b7 100644 --- a/src/OpenTelemetry.Api/BaseProvider.cs +++ b/src/OpenTelemetry.Api/BaseProvider.cs @@ -1,47 +1,35 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry +#nullable enable + +namespace OpenTelemetry; + +/// +/// Contains logic shared by all OpenTelemetry providers. +/// +public abstract class BaseProvider : IDisposable { /// - /// Contains logic shared by all OpenTelemetry providers. + /// Finalizes an instance of the class. /// - public abstract class BaseProvider : IDisposable + ~BaseProvider() { - /// - /// Finalizes an instance of the class. - /// - ~BaseProvider() - { - this.Dispose(false); - } + this.Dispose(false); + } - /// - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } - /// - /// Releases the unmanaged resources used by this class and optionally releases the managed resources. - /// - /// to release both managed and unmanaged resources; to release only unmanaged resources. - protected virtual void Dispose(bool disposing) - { - } + /// + /// Releases the unmanaged resources used by this class and optionally releases the managed resources. + /// + /// to release both managed and unmanaged resources; to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { } } diff --git a/src/OpenTelemetry.Api/CHANGELOG.md b/src/OpenTelemetry.Api/CHANGELOG.md index fcc99613615..033331f895e 100644 --- a/src/OpenTelemetry.Api/CHANGELOG.md +++ b/src/OpenTelemetry.Api/CHANGELOG.md @@ -2,22 +2,62 @@ ## Unreleased +## 1.7.0 + +Released 2023-Dec-08 + +## 1.7.0-rc.1 + +Released 2023-Nov-29 + +* Updated `System.Diagnostics.DiagnosticSource` package version to + `8.0.0`. + ([#5051](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5051)) + +## 1.7.0-alpha.1 + +Released 2023-Oct-16 + +* Fixed a bug which caused `Tracer.StartRootSpan` to generate a child span if a + trace was running (`Activity.Current != null`). + ([#4890](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4890)) + +* Added a `Tracer` cache inside of `TracerProvider` to prevent repeated calls to + `GetTracer` from leaking memory. + ([#4906](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4906)) + +* Fix `TraceContextPropagator` by validating the first digit of the hex-encoded + `trace-flags` field of the `traceparent` header. + ([#4893](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4893)) + +## 1.6.0 + +Released 2023-Sep-05 + +## 1.6.0-rc.1 + +Released 2023-Aug-21 + +## 1.6.0-alpha.1 + +Released 2023-Jul-12 + * Updated `System.Diagnostics.DiagnosticSource` package version to `7.0.2`. ([#4576](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4576)) -* **Breaking change** In order to make `RuntimeContext` compatible with - ahead-of-time compilation (AOT), - `RuntimeContext.ContextSlotType` can only be assigned one - of the following types: `AsyncLocalRuntimeContextSlot<>`, +* **Breaking change:** In order to make `RuntimeContext` compatible with + ahead-of-time compilation (AOT), `RuntimeContext.ContextSlotType` can only be + assigned one of the following types: `AsyncLocalRuntimeContextSlot<>`, `ThreadLocalRuntimeContextSlot<>`, and `RemotingRuntimeContextSlot<>`. A `System.NotSupportedException` will be thrown if you try to assign any type other than the three types mentioned. ([#4542](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4542)) -* Added [Logs Bridge +* **Experimental (pre-release builds only):** Added [Logs Bridge API](https://github.com/open-telemetry/opentelemetry-specification/blob/976432b74c565e8a84af3570e9b82cb95e1d844c/specification/logs/bridge-api.md) implementation (`LoggerProviderBuilder`, `LoggerProvider`, `Logger`, etc.). - ([#4433](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4433)) + ([#4433](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4433), + [#4735](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4735)) ## 1.5.1 diff --git a/src/OpenTelemetry.Api/Context/AsyncLocalRuntimeContextSlot.cs b/src/OpenTelemetry.Api/Context/AsyncLocalRuntimeContextSlot.cs index 90c1e221e1d..15eb31d08c2 100644 --- a/src/OpenTelemetry.Api/Context/AsyncLocalRuntimeContextSlot.cs +++ b/src/OpenTelemetry.Api/Context/AsyncLocalRuntimeContextSlot.cs @@ -1,60 +1,46 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Runtime.CompilerServices; -namespace OpenTelemetry.Context +namespace OpenTelemetry.Context; + +/// +/// The async local implementation of context slot. +/// +/// The type of the underlying value. +public class AsyncLocalRuntimeContextSlot : RuntimeContextSlot, IRuntimeContextSlotValueAccessor { + private readonly AsyncLocal slot; + /// - /// The async local implementation of context slot. + /// Initializes a new instance of the class. /// - /// The type of the underlying value. - public class AsyncLocalRuntimeContextSlot : RuntimeContextSlot, IRuntimeContextSlotValueAccessor + /// The name of the context slot. + public AsyncLocalRuntimeContextSlot(string name) + : base(name) { - private readonly AsyncLocal slot; - - /// - /// Initializes a new instance of the class. - /// - /// The name of the context slot. - public AsyncLocalRuntimeContextSlot(string name) - : base(name) - { - this.slot = new AsyncLocal(); - } + this.slot = new AsyncLocal(); + } - /// - public object Value - { - get => this.slot.Value; - set => this.slot.Value = (T)value; - } + /// + public object Value + { + get => this.slot.Value; + set => this.slot.Value = (T)value; + } - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override T Get() - { - return this.slot.Value; - } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override T Get() + { + return this.slot.Value; + } - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void Set(T value) - { - this.slot.Value = value; - } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Set(T value) + { + this.slot.Value = value; } } diff --git a/src/OpenTelemetry.Api/Context/IRuntimeContextSlotValueAccessor.cs b/src/OpenTelemetry.Api/Context/IRuntimeContextSlotValueAccessor.cs index eab76adfca1..1e16ea42601 100644 --- a/src/OpenTelemetry.Api/Context/IRuntimeContextSlotValueAccessor.cs +++ b/src/OpenTelemetry.Api/Context/IRuntimeContextSlotValueAccessor.cs @@ -1,29 +1,15 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Context +namespace OpenTelemetry.Context; + +/// +/// Describes a type of which can expose its value as an . +/// +public interface IRuntimeContextSlotValueAccessor { /// - /// Describes a type of which can expose its value as an . + /// Gets or sets the value of the slot as an . /// - public interface IRuntimeContextSlotValueAccessor - { - /// - /// Gets or sets the value of the slot as an . - /// - object Value { get; set; } - } + object Value { get; set; } } diff --git a/src/OpenTelemetry.Api/Context/Propagation/B3Propagator.cs b/src/OpenTelemetry.Api/Context/Propagation/B3Propagator.cs index dd749eb856f..10b4aee1d81 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/B3Propagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/B3Propagator.cs @@ -1,272 +1,258 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Text; using OpenTelemetry.Internal; -namespace OpenTelemetry.Context.Propagation +namespace OpenTelemetry.Context.Propagation; + +/// +/// A text map propagator for B3. See https://github.com/openzipkin/b3-propagation. +/// This class has been deprecated in favour of OpenTelemetry.Extensions.Propagators package. +/// +[Obsolete("Use B3Propagator class from OpenTelemetry.Extensions.Propagators namespace, shipped as part of OpenTelemetry.Extensions.Propagators package.")] +public sealed class B3Propagator : TextMapPropagator { + internal const string XB3TraceId = "X-B3-TraceId"; + internal const string XB3SpanId = "X-B3-SpanId"; + internal const string XB3ParentSpanId = "X-B3-ParentSpanId"; + internal const string XB3Sampled = "X-B3-Sampled"; + internal const string XB3Flags = "X-B3-Flags"; + internal const string XB3Combined = "b3"; + internal const char XB3CombinedDelimiter = '-'; + + // Used as the upper ActivityTraceId.SIZE hex characters of the traceID. B3-propagation used to send + // ActivityTraceId.SIZE hex characters (8-bytes traceId) in the past. + internal const string UpperTraceId = "0000000000000000"; + + // Sampled values via the X_B3_SAMPLED header. + internal const string SampledValue = "1"; + + // Some old zipkin implementations may send true/false for the sampled header. Only use this for checking incoming values. + internal const string LegacySampledValue = "true"; + + // "Debug" sampled value. + internal const string FlagsValue = "1"; + + private static readonly HashSet AllFields = new() { XB3TraceId, XB3SpanId, XB3ParentSpanId, XB3Sampled, XB3Flags }; + + private static readonly HashSet SampledValues = new(StringComparer.Ordinal) { SampledValue, LegacySampledValue }; + + private readonly bool singleHeader; + /// - /// A text map propagator for B3. See https://github.com/openzipkin/b3-propagation. - /// This class has been deprecated in favour of OpenTelemetry.Extensions.Propagators package. + /// Initializes a new instance of the class. /// [Obsolete("Use B3Propagator class from OpenTelemetry.Extensions.Propagators namespace, shipped as part of OpenTelemetry.Extensions.Propagators package.")] - public sealed class B3Propagator : TextMapPropagator + public B3Propagator() + : this(false) { - internal const string XB3TraceId = "X-B3-TraceId"; - internal const string XB3SpanId = "X-B3-SpanId"; - internal const string XB3ParentSpanId = "X-B3-ParentSpanId"; - internal const string XB3Sampled = "X-B3-Sampled"; - internal const string XB3Flags = "X-B3-Flags"; - internal const string XB3Combined = "b3"; - internal const char XB3CombinedDelimiter = '-'; - - // Used as the upper ActivityTraceId.SIZE hex characters of the traceID. B3-propagation used to send - // ActivityTraceId.SIZE hex characters (8-bytes traceId) in the past. - internal const string UpperTraceId = "0000000000000000"; + } - // Sampled values via the X_B3_SAMPLED header. - internal const string SampledValue = "1"; + /// + /// Initializes a new instance of the class. + /// + /// Determines whether to use single or multiple headers when extracting or injecting span context. + [Obsolete("Use B3Propagator class from OpenTelemetry.Extensions.Propagators namespace, shipped as part of OpenTelemetry.Extensions.Propagators package.")] + public B3Propagator(bool singleHeader) + { + this.singleHeader = singleHeader; + } - // Some old zipkin implementations may send true/false for the sampled header. Only use this for checking incoming values. - internal const string LegacySampledValue = "true"; + /// + public override ISet Fields => AllFields; - // "Debug" sampled value. - internal const string FlagsValue = "1"; + /// + [Obsolete("Use B3Propagator class from OpenTelemetry.Extensions.Propagators namespace, shipped as part of OpenTelemetry.Extensions.Propagators package.")] +#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member + public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) +#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member + { + if (context.ActivityContext.IsValid()) + { + // If a valid context has already been extracted, perform a noop. + return context; + } - private static readonly HashSet AllFields = new() { XB3TraceId, XB3SpanId, XB3ParentSpanId, XB3Sampled, XB3Flags }; + if (carrier == null) + { + OpenTelemetryApiEventSource.Log.FailedToExtractActivityContext(nameof(B3Propagator), "null carrier"); + return context; + } - private static readonly HashSet SampledValues = new(StringComparer.Ordinal) { SampledValue, LegacySampledValue }; + if (getter == null) + { + OpenTelemetryApiEventSource.Log.FailedToExtractActivityContext(nameof(B3Propagator), "null getter"); + return context; + } - private readonly bool singleHeader; + if (this.singleHeader) + { + return ExtractFromSingleHeader(context, carrier, getter); + } + else + { + return ExtractFromMultipleHeaders(context, carrier, getter); + } + } - /// - /// Initializes a new instance of the class. - /// - [Obsolete("Use B3Propagator class from OpenTelemetry.Extensions.Propagators namespace, shipped as part of OpenTelemetry.Extensions.Propagators package.")] - public B3Propagator() - : this(false) + /// + [Obsolete("Use B3Propagator class from OpenTelemetry.Extensions.Propagators namespace, shipped as part of OpenTelemetry.Extensions.Propagators package.")] +#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member + public override void Inject(PropagationContext context, T carrier, Action setter) +#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member + { + if (context.ActivityContext.TraceId == default || context.ActivityContext.SpanId == default) { + OpenTelemetryApiEventSource.Log.FailedToInjectActivityContext(nameof(B3Propagator), "invalid context"); + return; } - /// - /// Initializes a new instance of the class. - /// - /// Determines whether to use single or multiple headers when extracting or injecting span context. - [Obsolete("Use B3Propagator class from OpenTelemetry.Extensions.Propagators namespace, shipped as part of OpenTelemetry.Extensions.Propagators package.")] - public B3Propagator(bool singleHeader) + if (carrier == null) { - this.singleHeader = singleHeader; + OpenTelemetryApiEventSource.Log.FailedToInjectActivityContext(nameof(B3Propagator), "null carrier"); + return; } - /// - public override ISet Fields => AllFields; + if (setter == null) + { + OpenTelemetryApiEventSource.Log.FailedToInjectActivityContext(nameof(B3Propagator), "null setter"); + return; + } - /// - [Obsolete("Use B3Propagator class from OpenTelemetry.Extensions.Propagators namespace, shipped as part of OpenTelemetry.Extensions.Propagators package.")] -#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member - public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) -#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member + if (this.singleHeader) { - if (context.ActivityContext.IsValid()) + var sb = new StringBuilder(); + sb.Append(context.ActivityContext.TraceId.ToHexString()); + sb.Append(XB3CombinedDelimiter); + sb.Append(context.ActivityContext.SpanId.ToHexString()); + if ((context.ActivityContext.TraceFlags & ActivityTraceFlags.Recorded) != 0) { - // If a valid context has already been extracted, perform a noop. - return context; + sb.Append(XB3CombinedDelimiter); + sb.Append(SampledValue); } - if (carrier == null) + setter(carrier, XB3Combined, sb.ToString()); + } + else + { + setter(carrier, XB3TraceId, context.ActivityContext.TraceId.ToHexString()); + setter(carrier, XB3SpanId, context.ActivityContext.SpanId.ToHexString()); + if ((context.ActivityContext.TraceFlags & ActivityTraceFlags.Recorded) != 0) { - OpenTelemetryApiEventSource.Log.FailedToExtractActivityContext(nameof(B3Propagator), "null carrier"); - return context; + setter(carrier, XB3Sampled, SampledValue); } + } + } - if (getter == null) + private static PropagationContext ExtractFromMultipleHeaders(PropagationContext context, T carrier, Func> getter) + { + try + { + ActivityTraceId traceId; + var traceIdStr = getter(carrier, XB3TraceId)?.FirstOrDefault(); + if (traceIdStr != null) { - OpenTelemetryApiEventSource.Log.FailedToExtractActivityContext(nameof(B3Propagator), "null getter"); - return context; - } + if (traceIdStr.Length == 16) + { + // This is an 8-byte traceID. + traceIdStr = UpperTraceId + traceIdStr; + } - if (this.singleHeader) - { - return ExtractFromSingleHeader(context, carrier, getter); + traceId = ActivityTraceId.CreateFromString(traceIdStr.AsSpan()); } else { - return ExtractFromMultipleHeaders(context, carrier, getter); + return context; } - } - /// - [Obsolete("Use B3Propagator class from OpenTelemetry.Extensions.Propagators namespace, shipped as part of OpenTelemetry.Extensions.Propagators package.")] -#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member - public override void Inject(PropagationContext context, T carrier, Action setter) -#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member - { - if (context.ActivityContext.TraceId == default || context.ActivityContext.SpanId == default) + ActivitySpanId spanId; + var spanIdStr = getter(carrier, XB3SpanId)?.FirstOrDefault(); + if (spanIdStr != null) { - OpenTelemetryApiEventSource.Log.FailedToInjectActivityContext(nameof(B3Propagator), "invalid context"); - return; + spanId = ActivitySpanId.CreateFromString(spanIdStr.AsSpan()); } - - if (carrier == null) + else { - OpenTelemetryApiEventSource.Log.FailedToInjectActivityContext(nameof(B3Propagator), "null carrier"); - return; + return context; } - if (setter == null) + var traceOptions = ActivityTraceFlags.None; + if (SampledValues.Contains(getter(carrier, XB3Sampled)?.FirstOrDefault()) + || FlagsValue.Equals(getter(carrier, XB3Flags)?.FirstOrDefault(), StringComparison.Ordinal)) { - OpenTelemetryApiEventSource.Log.FailedToInjectActivityContext(nameof(B3Propagator), "null setter"); - return; + traceOptions |= ActivityTraceFlags.Recorded; } - if (this.singleHeader) - { - var sb = new StringBuilder(); - sb.Append(context.ActivityContext.TraceId.ToHexString()); - sb.Append(XB3CombinedDelimiter); - sb.Append(context.ActivityContext.SpanId.ToHexString()); - if ((context.ActivityContext.TraceFlags & ActivityTraceFlags.Recorded) != 0) - { - sb.Append(XB3CombinedDelimiter); - sb.Append(SampledValue); - } - - setter(carrier, XB3Combined, sb.ToString()); - } - else - { - setter(carrier, XB3TraceId, context.ActivityContext.TraceId.ToHexString()); - setter(carrier, XB3SpanId, context.ActivityContext.SpanId.ToHexString()); - if ((context.ActivityContext.TraceFlags & ActivityTraceFlags.Recorded) != 0) - { - setter(carrier, XB3Sampled, SampledValue); - } - } + return new PropagationContext( + new ActivityContext(traceId, spanId, traceOptions, isRemote: true), + context.Baggage); + } + catch (Exception e) + { + OpenTelemetryApiEventSource.Log.ActivityContextExtractException(nameof(B3Propagator), e); + return context; } + } - private static PropagationContext ExtractFromMultipleHeaders(PropagationContext context, T carrier, Func> getter) + private static PropagationContext ExtractFromSingleHeader(PropagationContext context, T carrier, Func> getter) + { + try { - try + var header = getter(carrier, XB3Combined)?.FirstOrDefault(); + if (string.IsNullOrWhiteSpace(header)) { - ActivityTraceId traceId; - var traceIdStr = getter(carrier, XB3TraceId)?.FirstOrDefault(); - if (traceIdStr != null) - { - if (traceIdStr.Length == 16) - { - // This is an 8-byte traceID. - traceIdStr = UpperTraceId + traceIdStr; - } - - traceId = ActivityTraceId.CreateFromString(traceIdStr.AsSpan()); - } - else - { - return context; - } - - ActivitySpanId spanId; - var spanIdStr = getter(carrier, XB3SpanId)?.FirstOrDefault(); - if (spanIdStr != null) - { - spanId = ActivitySpanId.CreateFromString(spanIdStr.AsSpan()); - } - else - { - return context; - } - - var traceOptions = ActivityTraceFlags.None; - if (SampledValues.Contains(getter(carrier, XB3Sampled)?.FirstOrDefault()) - || FlagsValue.Equals(getter(carrier, XB3Flags)?.FirstOrDefault(), StringComparison.Ordinal)) - { - traceOptions |= ActivityTraceFlags.Recorded; - } - - return new PropagationContext( - new ActivityContext(traceId, spanId, traceOptions, isRemote: true), - context.Baggage); + return context; } - catch (Exception e) + + var parts = header.Split(XB3CombinedDelimiter); + if (parts.Length < 2 || parts.Length > 4) { - OpenTelemetryApiEventSource.Log.ActivityContextExtractException(nameof(B3Propagator), e); return context; } - } - private static PropagationContext ExtractFromSingleHeader(PropagationContext context, T carrier, Func> getter) - { - try + var traceIdStr = parts[0]; + if (string.IsNullOrWhiteSpace(traceIdStr)) { - var header = getter(carrier, XB3Combined)?.FirstOrDefault(); - if (string.IsNullOrWhiteSpace(header)) - { - return context; - } - - var parts = header.Split(XB3CombinedDelimiter); - if (parts.Length < 2 || parts.Length > 4) - { - return context; - } - - var traceIdStr = parts[0]; - if (string.IsNullOrWhiteSpace(traceIdStr)) - { - return context; - } + return context; + } - if (traceIdStr.Length == 16) - { - // This is an 8-byte traceID. - traceIdStr = UpperTraceId + traceIdStr; - } + if (traceIdStr.Length == 16) + { + // This is an 8-byte traceID. + traceIdStr = UpperTraceId + traceIdStr; + } - var traceId = ActivityTraceId.CreateFromString(traceIdStr.AsSpan()); + var traceId = ActivityTraceId.CreateFromString(traceIdStr.AsSpan()); - var spanIdStr = parts[1]; - if (string.IsNullOrWhiteSpace(spanIdStr)) - { - return context; - } + var spanIdStr = parts[1]; + if (string.IsNullOrWhiteSpace(spanIdStr)) + { + return context; + } - var spanId = ActivitySpanId.CreateFromString(spanIdStr.AsSpan()); + var spanId = ActivitySpanId.CreateFromString(spanIdStr.AsSpan()); - var traceOptions = ActivityTraceFlags.None; - if (parts.Length > 2) + var traceOptions = ActivityTraceFlags.None; + if (parts.Length > 2) + { + var traceFlagsStr = parts[2]; + if (SampledValues.Contains(traceFlagsStr) + || FlagsValue.Equals(traceFlagsStr, StringComparison.Ordinal)) { - var traceFlagsStr = parts[2]; - if (SampledValues.Contains(traceFlagsStr) - || FlagsValue.Equals(traceFlagsStr, StringComparison.Ordinal)) - { - traceOptions |= ActivityTraceFlags.Recorded; - } + traceOptions |= ActivityTraceFlags.Recorded; } - - return new PropagationContext( - new ActivityContext(traceId, spanId, traceOptions, isRemote: true), - context.Baggage); - } - catch (Exception e) - { - OpenTelemetryApiEventSource.Log.ActivityContextExtractException(nameof(B3Propagator), e); - return context; } + + return new PropagationContext( + new ActivityContext(traceId, spanId, traceOptions, isRemote: true), + context.Baggage); + } + catch (Exception e) + { + OpenTelemetryApiEventSource.Log.ActivityContextExtractException(nameof(B3Propagator), e); + return context; } } } diff --git a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs index 30e4388a056..57bdb453b8f 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs @@ -1,178 +1,164 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Net; using System.Text; using OpenTelemetry.Internal; -namespace OpenTelemetry.Context.Propagation +namespace OpenTelemetry.Context.Propagation; + +/// +/// A text map propagator for W3C Baggage. See https://w3c.github.io/baggage/. +/// +public class BaggagePropagator : TextMapPropagator { - /// - /// A text map propagator for W3C Baggage. See https://w3c.github.io/baggage/. - /// - public class BaggagePropagator : TextMapPropagator - { - internal const string BaggageHeaderName = "baggage"; + internal const string BaggageHeaderName = "baggage"; - private const int MaxBaggageLength = 8192; - private const int MaxBaggageItems = 180; + private const int MaxBaggageLength = 8192; + private const int MaxBaggageItems = 180; - private static readonly char[] EqualSignSeparator = new[] { '=' }; - private static readonly char[] CommaSignSeparator = new[] { ',' }; + private static readonly char[] EqualSignSeparator = new[] { '=' }; + private static readonly char[] CommaSignSeparator = new[] { ',' }; - /// - public override ISet Fields => new HashSet { BaggageHeaderName }; + /// + public override ISet Fields => new HashSet { BaggageHeaderName }; - /// - public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) + /// + public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) + { + if (context.Baggage != default) { - if (context.Baggage != default) - { - // If baggage has already been extracted, perform a noop. - return context; - } + // If baggage has already been extracted, perform a noop. + return context; + } - if (carrier == null) - { - OpenTelemetryApiEventSource.Log.FailedToExtractBaggage(nameof(BaggagePropagator), "null carrier"); - return context; - } + if (carrier == null) + { + OpenTelemetryApiEventSource.Log.FailedToExtractBaggage(nameof(BaggagePropagator), "null carrier"); + return context; + } - if (getter == null) + if (getter == null) + { + OpenTelemetryApiEventSource.Log.FailedToExtractBaggage(nameof(BaggagePropagator), "null getter"); + return context; + } + + try + { + Dictionary baggage = null; + var baggageCollection = getter(carrier, BaggageHeaderName); + if (baggageCollection?.Any() ?? false) { - OpenTelemetryApiEventSource.Log.FailedToExtractBaggage(nameof(BaggagePropagator), "null getter"); - return context; + TryExtractBaggage(baggageCollection.ToArray(), out baggage); } - try + return new PropagationContext( + context.ActivityContext, + baggage == null ? context.Baggage : new Baggage(baggage)); + } + catch (Exception ex) + { + OpenTelemetryApiEventSource.Log.BaggageExtractException(nameof(BaggagePropagator), ex); + } + + return context; + } + + /// + public override void Inject(PropagationContext context, T carrier, Action setter) + { + if (carrier == null) + { + OpenTelemetryApiEventSource.Log.FailedToInjectBaggage(nameof(BaggagePropagator), "null carrier"); + return; + } + + if (setter == null) + { + OpenTelemetryApiEventSource.Log.FailedToInjectBaggage(nameof(BaggagePropagator), "null setter"); + return; + } + + using var e = context.Baggage.GetEnumerator(); + + if (e.MoveNext() == true) + { + int itemCount = 0; + StringBuilder baggage = new StringBuilder(); + do { - Dictionary baggage = null; - var baggageCollection = getter(carrier, BaggageHeaderName); - if (baggageCollection?.Any() ?? false) + KeyValuePair item = e.Current; + if (string.IsNullOrEmpty(item.Value)) { - TryExtractBaggage(baggageCollection.ToArray(), out baggage); + continue; } - return new PropagationContext( - context.ActivityContext, - baggage == null ? context.Baggage : new Baggage(baggage)); + baggage.Append(WebUtility.UrlEncode(item.Key)).Append('=').Append(WebUtility.UrlEncode(item.Value)).Append(','); } - catch (Exception ex) - { - OpenTelemetryApiEventSource.Log.BaggageExtractException(nameof(BaggagePropagator), ex); - } - - return context; + while (e.MoveNext() && ++itemCount < MaxBaggageItems && baggage.Length < MaxBaggageLength); + baggage.Remove(baggage.Length - 1, 1); + setter(carrier, BaggageHeaderName, baggage.ToString()); } + } + + internal static bool TryExtractBaggage(string[] baggageCollection, out Dictionary baggage) + { + int baggageLength = -1; + bool done = false; + Dictionary baggageDictionary = null; - /// - public override void Inject(PropagationContext context, T carrier, Action setter) + foreach (var item in baggageCollection) { - if (carrier == null) + if (done) { - OpenTelemetryApiEventSource.Log.FailedToInjectBaggage(nameof(BaggagePropagator), "null carrier"); - return; + break; } - if (setter == null) + if (string.IsNullOrEmpty(item)) { - OpenTelemetryApiEventSource.Log.FailedToInjectBaggage(nameof(BaggagePropagator), "null setter"); - return; + continue; } - using var e = context.Baggage.GetEnumerator(); - - if (e.MoveNext() == true) + foreach (var pair in item.Split(CommaSignSeparator)) { - int itemCount = 0; - StringBuilder baggage = new StringBuilder(); - do - { - KeyValuePair item = e.Current; - if (string.IsNullOrEmpty(item.Value)) - { - continue; - } + baggageLength += pair.Length + 1; // pair and comma - baggage.Append(WebUtility.UrlEncode(item.Key)).Append('=').Append(WebUtility.UrlEncode(item.Value)).Append(','); + if (baggageLength >= MaxBaggageLength || baggageDictionary?.Count >= MaxBaggageItems) + { + done = true; + break; } - while (e.MoveNext() && ++itemCount < MaxBaggageItems && baggage.Length < MaxBaggageLength); - baggage.Remove(baggage.Length - 1, 1); - setter(carrier, BaggageHeaderName, baggage.ToString()); - } - } - internal static bool TryExtractBaggage(string[] baggageCollection, out Dictionary baggage) - { - int baggageLength = -1; - bool done = false; - Dictionary baggageDictionary = null; + if (pair.IndexOf('=') < 0) + { + continue; + } - foreach (var item in baggageCollection) - { - if (done) + var parts = pair.Split(EqualSignSeparator, 2); + if (parts.Length != 2) { - break; + continue; } - if (string.IsNullOrEmpty(item)) + var key = WebUtility.UrlDecode(parts[0]); + var value = WebUtility.UrlDecode(parts[1]); + + if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value)) { continue; } - foreach (var pair in item.Split(CommaSignSeparator)) + if (baggageDictionary == null) { - baggageLength += pair.Length + 1; // pair and comma - - if (baggageLength >= MaxBaggageLength || baggageDictionary?.Count >= MaxBaggageItems) - { - done = true; - break; - } - - if (pair.IndexOf('=') < 0) - { - continue; - } - - var parts = pair.Split(EqualSignSeparator, 2); - if (parts.Length != 2) - { - continue; - } - - var key = WebUtility.UrlDecode(parts[0]); - var value = WebUtility.UrlDecode(parts[1]); - - if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value)) - { - continue; - } - - if (baggageDictionary == null) - { - baggageDictionary = new Dictionary(); - } - - baggageDictionary[key] = value; + baggageDictionary = new Dictionary(); } - } - baggage = baggageDictionary; - return baggageDictionary != null; + baggageDictionary[key] = value; + } } + + baggage = baggageDictionary; + return baggageDictionary != null; } } diff --git a/src/OpenTelemetry.Api/Context/Propagation/CompositeTextMapPropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/CompositeTextMapPropagator.cs index 18c1633ca82..a6b12c3cd1b 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/CompositeTextMapPropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/CompositeTextMapPropagator.cs @@ -1,64 +1,50 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Internal; -namespace OpenTelemetry.Context.Propagation +namespace OpenTelemetry.Context.Propagation; + +/// +/// CompositeTextMapPropagator provides a mechanism for combining multiple +/// textmap propagators into a single one. +/// +public class CompositeTextMapPropagator : TextMapPropagator { + private static readonly ISet EmptyFields = new HashSet(); + private readonly List propagators; + /// - /// CompositeTextMapPropagator provides a mechanism for combining multiple - /// textmap propagators into a single one. + /// Initializes a new instance of the class. /// - public class CompositeTextMapPropagator : TextMapPropagator + /// List of wire context propagator. + public CompositeTextMapPropagator(IEnumerable propagators) { - private static readonly ISet EmptyFields = new HashSet(); - private readonly List propagators; - - /// - /// Initializes a new instance of the class. - /// - /// List of wire context propagator. - public CompositeTextMapPropagator(IEnumerable propagators) - { - Guard.ThrowIfNull(propagators); + Guard.ThrowIfNull(propagators); - this.propagators = new List(propagators); - } + this.propagators = new List(propagators); + } - /// - public override ISet Fields => EmptyFields; + /// + public override ISet Fields => EmptyFields; - /// - public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) + /// + public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) + { + foreach (var propagator in this.propagators) { - foreach (var propagator in this.propagators) - { - context = propagator.Extract(context, carrier, getter); - } - - return context; + context = propagator.Extract(context, carrier, getter); } - /// - public override void Inject(PropagationContext context, T carrier, Action setter) + return context; + } + + /// + public override void Inject(PropagationContext context, T carrier, Action setter) + { + foreach (var propagator in this.propagators) { - foreach (var propagator in this.propagators) - { - propagator.Inject(context, carrier, setter); - } + propagator.Inject(context, carrier, setter); } } } diff --git a/src/OpenTelemetry.Api/Context/Propagation/NoopTextMapPropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/NoopTextMapPropagator.cs index a63d896e983..03dd45785d4 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/NoopTextMapPropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/NoopTextMapPropagator.cs @@ -1,34 +1,20 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Context.Propagation +namespace OpenTelemetry.Context.Propagation; + +internal sealed class NoopTextMapPropagator : TextMapPropagator { - internal sealed class NoopTextMapPropagator : TextMapPropagator - { - private static readonly PropagationContext DefaultPropagationContext = default; + private static readonly PropagationContext DefaultPropagationContext = default; - public override ISet Fields => null; + public override ISet Fields => null; - public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) - { - return DefaultPropagationContext; - } + public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) + { + return DefaultPropagationContext; + } - public override void Inject(PropagationContext context, T carrier, Action setter) - { - } + public override void Inject(PropagationContext context, T carrier, Action setter) + { } } diff --git a/src/OpenTelemetry.Api/Context/Propagation/PropagationContext.cs b/src/OpenTelemetry.Api/Context/Propagation/PropagationContext.cs index 0f0ded1b3b3..77b6c71e77a 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/PropagationContext.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/PropagationContext.cs @@ -1,84 +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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -namespace OpenTelemetry.Context.Propagation +namespace OpenTelemetry.Context.Propagation; + +/// +/// Stores propagation data. +/// +public readonly struct PropagationContext : IEquatable { /// - /// Stores propagation data. + /// Initializes a new instance of the struct. /// - public readonly struct PropagationContext : IEquatable + /// . + /// . + public PropagationContext(ActivityContext activityContext, Baggage baggage) { - /// - /// Initializes a new instance of the struct. - /// - /// . - /// . - public PropagationContext(ActivityContext activityContext, Baggage baggage) - { - this.ActivityContext = activityContext; - this.Baggage = baggage; - } + this.ActivityContext = activityContext; + this.Baggage = baggage; + } - /// - /// Gets . - /// - public ActivityContext ActivityContext { get; } + /// + /// Gets . + /// + public ActivityContext ActivityContext { get; } - /// - /// Gets . - /// - public Baggage Baggage { get; } + /// + /// Gets . + /// + public Baggage Baggage { get; } - /// - /// Compare two entries of for equality. - /// - /// First Entry to compare. - /// Second Entry to compare. - public static bool operator ==(PropagationContext left, PropagationContext right) => left.Equals(right); + /// + /// Compare two entries of for equality. + /// + /// First Entry to compare. + /// Second Entry to compare. + public static bool operator ==(PropagationContext left, PropagationContext right) => left.Equals(right); - /// - /// Compare two entries of for not equality. - /// - /// First Entry to compare. - /// Second Entry to compare. - public static bool operator !=(PropagationContext left, PropagationContext right) => !(left == right); + /// + /// Compare two entries of for not equality. + /// + /// First Entry to compare. + /// Second Entry to compare. + public static bool operator !=(PropagationContext left, PropagationContext right) => !(left == right); - /// - public bool Equals(PropagationContext value) - { - return this.ActivityContext == value.ActivityContext - && this.Baggage == value.Baggage; - } + /// + public bool Equals(PropagationContext value) + { + return this.ActivityContext == value.ActivityContext + && this.Baggage == value.Baggage; + } - /// - public override bool Equals(object obj) => (obj is PropagationContext context) && this.Equals(context); + /// + public override bool Equals(object obj) => (obj is PropagationContext context) && this.Equals(context); - /// - public override int GetHashCode() + /// + public override int GetHashCode() + { + var hash = 323591981; + unchecked { - var hash = 323591981; - unchecked - { - hash = (hash * -1521134295) + this.ActivityContext.GetHashCode(); - hash = (hash * -1521134295) + this.Baggage.GetHashCode(); - } - - return hash; + hash = (hash * -1521134295) + this.ActivityContext.GetHashCode(); + hash = (hash * -1521134295) + this.Baggage.GetHashCode(); } + + return hash; } } diff --git a/src/OpenTelemetry.Api/Context/Propagation/Propagators.cs b/src/OpenTelemetry.Api/Context/Propagation/Propagators.cs index 645cd600bce..c1dab62400d 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/Propagators.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/Propagators.cs @@ -1,39 +1,25 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Context.Propagation +namespace OpenTelemetry.Context.Propagation; + +/// +/// Propagators allow setting the global default Propagators. +/// +public static class Propagators { + private static readonly TextMapPropagator Noop = new NoopTextMapPropagator(); + /// - /// Propagators allow setting the global default Propagators. + /// Gets the Default TextMapPropagator to be used. /// - public static class Propagators - { - private static readonly TextMapPropagator Noop = new NoopTextMapPropagator(); + /// + /// Setting this can be done only from Sdk. + /// + public static TextMapPropagator DefaultTextMapPropagator { get; internal set; } = Noop; - /// - /// Gets the Default TextMapPropagator to be used. - /// - /// - /// Setting this can be done only from Sdk. - /// - public static TextMapPropagator DefaultTextMapPropagator { get; internal set; } = Noop; - - internal static void Reset() - { - DefaultTextMapPropagator = Noop; - } + internal static void Reset() + { + DefaultTextMapPropagator = Noop; } } diff --git a/src/OpenTelemetry.Api/Context/Propagation/TextMapPropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/TextMapPropagator.cs index 0516e9c1ab6..1000b317bf5 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/TextMapPropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/TextMapPropagator.cs @@ -1,52 +1,38 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Context.Propagation +namespace OpenTelemetry.Context.Propagation; + +/// +/// Defines an interface for a Propagator of TextMap type, +/// which uses string key/value pairs to inject and extract +/// propagation data. +/// +public abstract class TextMapPropagator { /// - /// Defines an interface for a Propagator of TextMap type, - /// which uses string key/value pairs to inject and extract - /// propagation data. + /// Gets the list of headers used by propagator. The use cases of this are: + /// * allow pre-allocation of fields, especially in systems like gRPC Metadata + /// * allow a single-pass over an iterator (ex OpenTracing has no getter in TextMap). /// - public abstract class TextMapPropagator - { - /// - /// Gets the list of headers used by propagator. The use cases of this are: - /// * allow pre-allocation of fields, especially in systems like gRPC Metadata - /// * allow a single-pass over an iterator (ex OpenTracing has no getter in TextMap). - /// - public abstract ISet Fields { get; } + public abstract ISet Fields { get; } - /// - /// Injects the context into a carrier. - /// - /// Type of an object to set context on. Typically HttpRequest or similar. - /// The default context to transmit over the wire. - /// Object to set context on. Instance of this object will be passed to setter. - /// Action that will set name and value pair on the object. - public abstract void Inject(PropagationContext context, T carrier, Action setter); + /// + /// Injects the context into a carrier. + /// + /// Type of an object to set context on. Typically HttpRequest or similar. + /// The default context to transmit over the wire. + /// Object to set context on. Instance of this object will be passed to setter. + /// Action that will set name and value pair on the object. + public abstract void Inject(PropagationContext context, T carrier, Action setter); - /// - /// Extracts the context from a carrier. - /// - /// Type of object to extract context from. Typically HttpRequest or similar. - /// The default context to be used if Extract fails. - /// Object to extract context from. Instance of this object will be passed to the getter. - /// Function that will return string value of a key with the specified name. - /// Context from it's text representation. - public abstract PropagationContext Extract(PropagationContext context, T carrier, Func> getter); - } + /// + /// Extracts the context from a carrier. + /// + /// Type of object to extract context from. Typically HttpRequest or similar. + /// The default context to be used if Extract fails. + /// Object to extract context from. Instance of this object will be passed to the getter. + /// Function that will return string value of a key with the specified name. + /// Context from it's text representation. + public abstract PropagationContext Extract(PropagationContext context, T carrier, Func> getter); } diff --git a/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs index 4e65a6436c7..de711295428 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs @@ -1,441 +1,450 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Runtime.CompilerServices; using System.Text; using OpenTelemetry.Internal; -namespace OpenTelemetry.Context.Propagation +namespace OpenTelemetry.Context.Propagation; + +/// +/// A text map propagator for W3C trace context. See https://w3c.github.io/trace-context/. +/// +public class TraceContextPropagator : TextMapPropagator { - /// - /// A text map propagator for W3C trace context. See https://w3c.github.io/trace-context/. - /// - public class TraceContextPropagator : TextMapPropagator + private const string TraceParent = "traceparent"; + private const string TraceState = "tracestate"; + + private static readonly int VersionPrefixIdLength = "00-".Length; + private static readonly int TraceIdLength = "0af7651916cd43dd8448eb211c80319c".Length; + private static readonly int VersionAndTraceIdLength = "00-0af7651916cd43dd8448eb211c80319c-".Length; + private static readonly int SpanIdLength = "00f067aa0ba902b7".Length; + private static readonly int VersionAndTraceIdAndSpanIdLength = "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-".Length; + private static readonly int OptionsLength = "00".Length; + private static readonly int TraceparentLengthV0 = "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-00".Length; + + // The following length limits are from Trace Context v1 https://www.w3.org/TR/trace-context-1/#key + private static readonly int TraceStateKeyMaxLength = 256; + private static readonly int TraceStateKeyTenantMaxLength = 241; + private static readonly int TraceStateKeyVendorMaxLength = 14; + private static readonly int TraceStateValueMaxLength = 256; + + /// + public override ISet Fields => new HashSet { TraceState, TraceParent }; + + /// + public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) { - private const string TraceParent = "traceparent"; - private const string TraceState = "tracestate"; - - private static readonly int VersionPrefixIdLength = "00-".Length; - private static readonly int TraceIdLength = "0af7651916cd43dd8448eb211c80319c".Length; - private static readonly int VersionAndTraceIdLength = "00-0af7651916cd43dd8448eb211c80319c-".Length; - private static readonly int SpanIdLength = "00f067aa0ba902b7".Length; - private static readonly int VersionAndTraceIdAndSpanIdLength = "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-".Length; - private static readonly int OptionsLength = "00".Length; - private static readonly int TraceparentLengthV0 = "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-00".Length; - - // The following length limits are from Trace Context v1 https://www.w3.org/TR/trace-context-1/#key - private static readonly int TraceStateKeyMaxLength = 256; - private static readonly int TraceStateKeyTenantMaxLength = 241; - private static readonly int TraceStateKeyVendorMaxLength = 14; - private static readonly int TraceStateValueMaxLength = 256; - - /// - public override ISet Fields => new HashSet { TraceState, TraceParent }; - - /// - public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) - { - if (context.ActivityContext.IsValid()) + if (context.ActivityContext.IsValid()) + { + // If a valid context has already been extracted, perform a noop. + return context; + } + + if (carrier == null) + { + OpenTelemetryApiEventSource.Log.FailedToExtractActivityContext(nameof(TraceContextPropagator), "null carrier"); + return context; + } + + if (getter == null) + { + OpenTelemetryApiEventSource.Log.FailedToExtractActivityContext(nameof(TraceContextPropagator), "null getter"); + return context; + } + + try + { + var traceparentCollection = getter(carrier, TraceParent); + + // There must be a single traceparent + if (traceparentCollection == null || traceparentCollection.Count() != 1) { - // If a valid context has already been extracted, perform a noop. return context; } - if (carrier == null) + var traceparent = traceparentCollection.First(); + var traceparentParsed = TryExtractTraceparent(traceparent, out var traceId, out var spanId, out var traceoptions); + + if (!traceparentParsed) { - OpenTelemetryApiEventSource.Log.FailedToExtractActivityContext(nameof(TraceContextPropagator), "null carrier"); return context; } - if (getter == null) + string tracestate = null; + var tracestateCollection = getter(carrier, TraceState); + if (tracestateCollection?.Any() ?? false) { - OpenTelemetryApiEventSource.Log.FailedToExtractActivityContext(nameof(TraceContextPropagator), "null getter"); - return context; + TryExtractTracestate(tracestateCollection.ToArray(), out tracestate); } - try - { - var traceparentCollection = getter(carrier, TraceParent); + return new PropagationContext( + new ActivityContext(traceId, spanId, traceoptions, tracestate, isRemote: true), + context.Baggage); + } + catch (Exception ex) + { + OpenTelemetryApiEventSource.Log.ActivityContextExtractException(nameof(TraceContextPropagator), ex); + } - // There must be a single traceparent - if (traceparentCollection == null || traceparentCollection.Count() != 1) - { - return context; - } + // in case of exception indicate to upstream that there is no parseable context from the top + return context; + } - var traceparent = traceparentCollection.First(); - var traceparentParsed = TryExtractTraceparent(traceparent, out var traceId, out var spanId, out var traceoptions); + /// + public override void Inject(PropagationContext context, T carrier, Action setter) + { + if (context.ActivityContext.TraceId == default || context.ActivityContext.SpanId == default) + { + OpenTelemetryApiEventSource.Log.FailedToInjectActivityContext(nameof(TraceContextPropagator), "Invalid context"); + return; + } - if (!traceparentParsed) - { - return context; - } + if (carrier == null) + { + OpenTelemetryApiEventSource.Log.FailedToInjectActivityContext(nameof(TraceContextPropagator), "null carrier"); + return; + } - string tracestate = null; - var tracestateCollection = getter(carrier, TraceState); - if (tracestateCollection?.Any() ?? false) - { - TryExtractTracestate(tracestateCollection.ToArray(), out tracestate); - } + if (setter == null) + { + OpenTelemetryApiEventSource.Log.FailedToInjectActivityContext(nameof(TraceContextPropagator), "null setter"); + return; + } - return new PropagationContext( - new ActivityContext(traceId, spanId, traceoptions, tracestate, isRemote: true), - context.Baggage); - } - catch (Exception ex) - { - OpenTelemetryApiEventSource.Log.ActivityContextExtractException(nameof(TraceContextPropagator), ex); - } +#if NET6_0_OR_GREATER + var traceparent = string.Create(55, context.ActivityContext, WriteTraceParentIntoSpan); +#else + var traceparent = string.Concat("00-", context.ActivityContext.TraceId.ToHexString(), "-", context.ActivityContext.SpanId.ToHexString()); + traceparent = string.Concat(traceparent, (context.ActivityContext.TraceFlags & ActivityTraceFlags.Recorded) != 0 ? "-01" : "-00"); +#endif - // in case of exception indicate to upstream that there is no parseable context from the top - return context; - } + setter(carrier, TraceParent, traceparent); - /// - public override void Inject(PropagationContext context, T carrier, Action setter) + string tracestateStr = context.ActivityContext.TraceState; + if (tracestateStr?.Length > 0) { - if (context.ActivityContext.TraceId == default || context.ActivityContext.SpanId == default) - { - OpenTelemetryApiEventSource.Log.FailedToInjectActivityContext(nameof(TraceContextPropagator), "Invalid context"); - return; - } + setter(carrier, TraceState, tracestateStr); + } + } - if (carrier == null) - { - OpenTelemetryApiEventSource.Log.FailedToInjectActivityContext(nameof(TraceContextPropagator), "null carrier"); - return; - } + internal static bool TryExtractTraceparent(string traceparent, out ActivityTraceId traceId, out ActivitySpanId spanId, out ActivityTraceFlags traceOptions) + { + // from https://github.com/w3c/distributed-tracing/blob/master/trace_context/HTTP_HEADER_FORMAT.md + // traceparent: 00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01 - if (setter == null) - { - OpenTelemetryApiEventSource.Log.FailedToInjectActivityContext(nameof(TraceContextPropagator), "null setter"); - return; - } + traceId = default; + spanId = default; + traceOptions = default; + var bestAttempt = false; - var traceparent = string.Concat("00-", context.ActivityContext.TraceId.ToHexString(), "-", context.ActivityContext.SpanId.ToHexString()); - traceparent = string.Concat(traceparent, (context.ActivityContext.TraceFlags & ActivityTraceFlags.Recorded) != 0 ? "-01" : "-00"); + if (string.IsNullOrWhiteSpace(traceparent) || traceparent.Length < TraceparentLengthV0) + { + return false; + } - setter(carrier, TraceParent, traceparent); + // if version does not end with delimiter + if (traceparent[VersionPrefixIdLength - 1] != '-') + { + return false; + } - string tracestateStr = context.ActivityContext.TraceState; - if (tracestateStr?.Length > 0) - { - setter(carrier, TraceState, tracestateStr); - } + // or version is not a hex (will throw) + var version0 = HexCharToByte(traceparent[0]); + var version1 = HexCharToByte(traceparent[1]); + + if (version0 == 0xf && version1 == 0xf) + { + return false; } - internal static bool TryExtractTraceparent(string traceparent, out ActivityTraceId traceId, out ActivitySpanId spanId, out ActivityTraceFlags traceOptions) + if (version0 > 0) { - // from https://github.com/w3c/distributed-tracing/blob/master/trace_context/HTTP_HEADER_FORMAT.md - // traceparent: 00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01 + // expected version is 00 + // for higher versions - best attempt parsing of trace id, span id, etc. + bestAttempt = true; + } - traceId = default; - spanId = default; - traceOptions = default; - var bestAttempt = false; + if (traceparent[VersionAndTraceIdLength - 1] != '-') + { + return false; + } - if (string.IsNullOrWhiteSpace(traceparent) || traceparent.Length < TraceparentLengthV0) - { - return false; - } + try + { + traceId = ActivityTraceId.CreateFromString(traceparent.AsSpan().Slice(VersionPrefixIdLength, TraceIdLength)); + } + catch (ArgumentOutOfRangeException) + { + // it's ok to still parse tracestate + return false; + } - // if version does not end with delimiter - if (traceparent[VersionPrefixIdLength - 1] != '-') - { - return false; - } + if (traceparent[VersionAndTraceIdAndSpanIdLength - 1] != '-') + { + return false; + } - // or version is not a hex (will throw) - var version0 = HexCharToByte(traceparent[0]); - var version1 = HexCharToByte(traceparent[1]); + byte optionsLowByte; + try + { + spanId = ActivitySpanId.CreateFromString(traceparent.AsSpan().Slice(VersionAndTraceIdLength, SpanIdLength)); + _ = HexCharToByte(traceparent[VersionAndTraceIdAndSpanIdLength]); // to verify if there is no bad chars on options position + optionsLowByte = HexCharToByte(traceparent[VersionAndTraceIdAndSpanIdLength + 1]); + } + catch (ArgumentOutOfRangeException) + { + // it's ok to still parse tracestate + return false; + } - if (version0 == 0xf && version1 == 0xf) - { - return false; - } + if ((optionsLowByte & 1) == 1) + { + traceOptions |= ActivityTraceFlags.Recorded; + } - if (version0 > 0) - { - // expected version is 00 - // for higher versions - best attempt parsing of trace id, span id, etc. - bestAttempt = true; - } + if ((!bestAttempt) && (traceparent.Length != VersionAndTraceIdAndSpanIdLength + OptionsLength)) + { + return false; + } - if (traceparent[VersionAndTraceIdLength - 1] != '-') + if (bestAttempt) + { + if ((traceparent.Length > TraceparentLengthV0) && (traceparent[TraceparentLengthV0] != '-')) { return false; } + } - try - { - traceId = ActivityTraceId.CreateFromString(traceparent.AsSpan().Slice(VersionPrefixIdLength, TraceIdLength)); - } - catch (ArgumentOutOfRangeException) - { - // it's ok to still parse tracestate - return false; - } + return true; + } - if (traceparent[VersionAndTraceIdAndSpanIdLength - 1] != '-') - { - return false; - } + internal static bool TryExtractTracestate(string[] tracestateCollection, out string tracestateResult) + { + tracestateResult = string.Empty; - byte options1; - try - { - spanId = ActivitySpanId.CreateFromString(traceparent.AsSpan().Slice(VersionAndTraceIdLength, SpanIdLength)); - options1 = HexCharToByte(traceparent[VersionAndTraceIdAndSpanIdLength + 1]); - } - catch (ArgumentOutOfRangeException) + if (tracestateCollection != null) + { + var keySet = new HashSet(); + var result = new StringBuilder(); + for (int i = 0; i < tracestateCollection.Length; ++i) { - // it's ok to still parse tracestate - return false; - } + var tracestate = tracestateCollection[i].AsSpan(); + int begin = 0; + while (begin < tracestate.Length) + { + int length = tracestate.Slice(begin).IndexOf(','); + ReadOnlySpan listMember; + if (length != -1) + { + listMember = tracestate.Slice(begin, length).Trim(); + begin += length + 1; + } + else + { + listMember = tracestate.Slice(begin).Trim(); + begin = tracestate.Length; + } - if ((options1 & 1) == 1) - { - traceOptions |= ActivityTraceFlags.Recorded; - } + // https://github.com/w3c/trace-context/blob/master/spec/20-http_request_header_format.md#tracestate-header-field-values + if (listMember.IsEmpty) + { + // Empty and whitespace - only list members are allowed. + // Vendors MUST accept empty tracestate headers but SHOULD avoid sending them. + continue; + } - if ((!bestAttempt) && (traceparent.Length != VersionAndTraceIdAndSpanIdLength + OptionsLength)) - { - return false; - } + if (keySet.Count >= 32) + { + // https://github.com/w3c/trace-context/blob/master/spec/20-http_request_header_format.md#list + // test_tracestate_member_count_limit + return false; + } - if (bestAttempt) - { - if ((traceparent.Length > TraceparentLengthV0) && (traceparent[TraceparentLengthV0] != '-')) - { - return false; - } - } + int keyLength = listMember.IndexOf('='); + if (keyLength == listMember.Length || keyLength == -1) + { + // Missing key or value in tracestate + return false; + } - return true; - } + var key = listMember.Slice(0, keyLength); + if (!ValidateKey(key)) + { + // test_tracestate_key_illegal_characters in https://github.com/w3c/trace-context/blob/master/test/test.py + // test_tracestate_key_length_limit + // test_tracestate_key_illegal_vendor_format + return false; + } - internal static bool TryExtractTracestate(string[] tracestateCollection, out string tracestateResult) - { - tracestateResult = string.Empty; + var value = listMember.Slice(keyLength + 1); + if (!ValidateValue(value)) + { + // test_tracestate_value_illegal_characters + return false; + } - if (tracestateCollection != null) - { - var keySet = new HashSet(); - var result = new StringBuilder(); - for (int i = 0; i < tracestateCollection.Length; ++i) - { - var tracestate = tracestateCollection[i].AsSpan(); - int begin = 0; - while (begin < tracestate.Length) + // ValidateKey() call above has ensured the key does not contain upper case letters. + if (!keySet.Add(key.ToString())) { - int length = tracestate.Slice(begin).IndexOf(','); - ReadOnlySpan listMember; - if (length != -1) - { - listMember = tracestate.Slice(begin, length).Trim(); - begin += length + 1; - } - else - { - listMember = tracestate.Slice(begin).Trim(); - begin = tracestate.Length; - } - - // https://github.com/w3c/trace-context/blob/master/spec/20-http_request_header_format.md#tracestate-header-field-values - if (listMember.IsEmpty) - { - // Empty and whitespace - only list members are allowed. - // Vendors MUST accept empty tracestate headers but SHOULD avoid sending them. - continue; - } - - if (keySet.Count >= 32) - { - // https://github.com/w3c/trace-context/blob/master/spec/20-http_request_header_format.md#list - // test_tracestate_member_count_limit - return false; - } - - int keyLength = listMember.IndexOf('='); - if (keyLength == listMember.Length || keyLength == -1) - { - // Missing key or value in tracestate - return false; - } - - var key = listMember.Slice(0, keyLength); - if (!ValidateKey(key)) - { - // test_tracestate_key_illegal_characters in https://github.com/w3c/trace-context/blob/master/test/test.py - // test_tracestate_key_length_limit - // test_tracestate_key_illegal_vendor_format - return false; - } - - var value = listMember.Slice(keyLength + 1); - if (!ValidateValue(value)) - { - // test_tracestate_value_illegal_characters - return false; - } - - // ValidateKey() call above has ensured the key does not contain upper case letters. - if (!keySet.Add(key.ToString())) - { - // test_tracestate_duplicated_keys - return false; - } - - if (result.Length > 0) - { - result.Append(','); - } - - result.Append(listMember.ToString()); + // test_tracestate_duplicated_keys + return false; + } + + if (result.Length > 0) + { + result.Append(','); } - } - tracestateResult = result.ToString(); + result.Append(listMember.ToString()); + } } - return true; + tracestateResult = result.ToString(); } - private static byte HexCharToByte(char c) + return true; + } + + private static byte HexCharToByte(char c) + { + if ((c >= '0') && (c <= '9')) { - if ((c >= '0') && (c <= '9')) - { - return (byte)(c - '0'); - } + return (byte)(c - '0'); + } - if ((c >= 'a') && (c <= 'f')) - { - return (byte)(c - 'a' + 10); - } + if ((c >= 'a') && (c <= 'f')) + { + return (byte)(c - 'a' + 10); + } + + throw new ArgumentOutOfRangeException(nameof(c), c, "Must be within: [0-9] or [a-f]"); + } + + private static bool ValidateKey(ReadOnlySpan key) + { + // This implementation follows Trace Context v1 which has W3C Recommendation. + // https://www.w3.org/TR/trace-context-1/#key + // It will be slightly differently from the next version of specification in GitHub repository. - throw new ArgumentOutOfRangeException(nameof(c), c, "Must be within: [0-9] or [a-f]"); + // There are two format for the key. The length rule applies to both. + if (key.Length <= 0 || key.Length > TraceStateKeyMaxLength) + { + return false; } - private static bool ValidateKey(ReadOnlySpan key) + // The first format: + // key = lcalpha 0*255( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) + // lcalpha = % x61 - 7A; a - z + // (There is an inconsistency in the expression above and the description in note. + // Here is following the description in note: + // "Identifiers MUST begin with a lowercase letter or a digit.") + if (!IsLowerAlphaDigit(key[0])) { - // This implementation follows Trace Context v1 which has W3C Recommendation. - // https://www.w3.org/TR/trace-context-1/#key - // It will be slightly differently from the next version of specification in GitHub repository. + return false; + } - // There are two format for the key. The length rule applies to both. - if (key.Length <= 0 || key.Length > TraceStateKeyMaxLength) + int tenantLength = -1; + for (int i = 1; i < key.Length; ++i) + { + char ch = key[i]; + if (ch == '@') { - return false; + tenantLength = i; + break; } - // The first format: - // key = lcalpha 0*255( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) - // lcalpha = % x61 - 7A; a - z - // (There is an inconsistency in the expression above and the description in note. - // Here is following the description in note: - // "Identifiers MUST begin with a lowercase letter or a digit.") - if (!IsLowerAlphaDigit(key[0])) + if (!(IsLowerAlphaDigit(ch) + || ch == '_' + || ch == '-' + || ch == '*' + || ch == '/')) { return false; } + } - int tenantLength = -1; - for (int i = 1; i < key.Length; ++i) - { - char ch = key[i]; - if (ch == '@') - { - tenantLength = i; - break; - } - - if (!(IsLowerAlphaDigit(ch) - || ch == '_' - || ch == '-' - || ch == '*' - || ch == '/')) - { - return false; - } - } + if (tenantLength == -1) + { + // There is no "@" sign. The key follow the first format. + return true; + } - if (tenantLength == -1) - { - // There is no "@" sign. The key follow the first format. - return true; - } + // The second format: + // key = (lcalpha / DIGIT) 0 * 240(lcalpha / DIGIT / "_" / "-" / "*" / "/") "@" lcalpha 0 * 13(lcalpha / DIGIT / "_" / "-" / "*" / "/") + if (tenantLength == 0 || tenantLength > TraceStateKeyTenantMaxLength) + { + return false; + } - // The second format: - // key = (lcalpha / DIGIT) 0 * 240(lcalpha / DIGIT / "_" / "-" / "*" / "/") "@" lcalpha 0 * 13(lcalpha / DIGIT / "_" / "-" / "*" / "/") - if (tenantLength == 0 || tenantLength > TraceStateKeyTenantMaxLength) - { - return false; - } + int vendorLength = key.Length - tenantLength - 1; + if (vendorLength == 0 || vendorLength > TraceStateKeyVendorMaxLength) + { + return false; + } - int vendorLength = key.Length - tenantLength - 1; - if (vendorLength == 0 || vendorLength > TraceStateKeyVendorMaxLength) + for (int i = tenantLength + 1; i < key.Length; ++i) + { + char ch = key[i]; + if (!(IsLowerAlphaDigit(ch) + || ch == '_' + || ch == '-' + || ch == '*' + || ch == '/')) { return false; } + } - for (int i = tenantLength + 1; i < key.Length; ++i) - { - char ch = key[i]; - if (!(IsLowerAlphaDigit(ch) - || ch == '_' - || ch == '-' - || ch == '*' - || ch == '/')) - { - return false; - } - } + return true; + } - return true; + private static bool ValidateValue(ReadOnlySpan value) + { + // https://github.com/w3c/trace-context/blob/master/spec/20-http_request_header_format.md#value + // value = 0*255(chr) nblk-chr + // nblk - chr = % x21 - 2B / % x2D - 3C / % x3E - 7E + // chr = % x20 / nblk - chr + if (value.Length <= 0 || value.Length > TraceStateValueMaxLength) + { + return false; } - private static bool ValidateValue(ReadOnlySpan value) + for (int i = 0; i < value.Length - 1; ++i) { - // https://github.com/w3c/trace-context/blob/master/spec/20-http_request_header_format.md#value - // value = 0*255(chr) nblk-chr - // nblk - chr = % x21 - 2B / % x2D - 3C / % x3E - 7E - // chr = % x20 / nblk - chr - if (value.Length <= 0 || value.Length > TraceStateValueMaxLength) + char c = value[i]; + if (!(c >= 0x20 && c <= 0x7E && c != 0x2C && c != 0x3D)) { return false; } + } - for (int i = 0; i < value.Length - 1; ++i) - { - char c = value[i]; - if (!(c >= 0x20 && c <= 0x7E && c != 0x2C && c != 0x3D)) - { - return false; - } - } + char last = value[value.Length - 1]; + return last >= 0x21 && last <= 0x7E && last != 0x2C && last != 0x3D; + } - char last = value[value.Length - 1]; - return last >= 0x21 && last <= 0x7E && last != 0x2C && last != 0x3D; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsLowerAlphaDigit(char c) + { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z'); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsLowerAlphaDigit(char c) +#if NET6_0_OR_GREATER + private static void WriteTraceParentIntoSpan(Span destination, ActivityContext context) + { + "00-".CopyTo(destination); + context.TraceId.ToHexString().CopyTo(destination.Slice(3)); + destination[35] = '-'; + context.SpanId.ToHexString().CopyTo(destination.Slice(36)); + if ((context.TraceFlags & ActivityTraceFlags.Recorded) != 0) + { + "-01".CopyTo(destination.Slice(52)); + } + else { - return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z'); + "-00".CopyTo(destination.Slice(52)); } } +#endif } diff --git a/src/OpenTelemetry.Api/Context/Propagation/TraceStateUtilsNew.cs b/src/OpenTelemetry.Api/Context/Propagation/TraceStateUtilsNew.cs index 19bd96f8196..717e1a94584 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/TraceStateUtilsNew.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/TraceStateUtilsNew.cs @@ -1,270 +1,256 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Text; using OpenTelemetry.Internal; -namespace OpenTelemetry.Context.Propagation +namespace OpenTelemetry.Context.Propagation; + +/// +/// Extension methods to extract TraceState from string. +/// +internal static class TraceStateUtilsNew { + private const int KeyMaxSize = 256; + private const int ValueMaxSize = 256; + private const int MaxKeyValuePairsCount = 32; + /// - /// Extension methods to extract TraceState from string. + /// Extracts tracestate pairs from the given string and appends it to provided tracestate list. /// - internal static class TraceStateUtilsNew + /// String with comma separated tracestate key value pairs. + /// to set tracestate pairs on. + /// True if string was parsed successfully and tracestate was recognized, false otherwise. + internal static bool AppendTraceState(string traceStateString, List> tracestate) { - private const int KeyMaxSize = 256; - private const int ValueMaxSize = 256; - private const int MaxKeyValuePairsCount = 32; - - /// - /// Extracts tracestate pairs from the given string and appends it to provided tracestate list. - /// - /// String with comma separated tracestate key value pairs. - /// to set tracestate pairs on. - /// True if string was parsed successfully and tracestate was recognized, false otherwise. - internal static bool AppendTraceState(string traceStateString, List> tracestate) + Debug.Assert(tracestate != null, "tracestate list cannot be null"); + + if (string.IsNullOrEmpty(traceStateString)) { - Debug.Assert(tracestate != null, "tracestate list cannot be null"); + return false; + } - if (string.IsNullOrEmpty(traceStateString)) - { - return false; - } + bool isValid = true; + try + { + var names = new HashSet(); - bool isValid = true; - try + var traceStateSpan = traceStateString.AsSpan().Trim(' ').Trim(','); + do { - var names = new HashSet(); + // tracestate: rojo=00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01,congo=BleGNlZWRzIHRohbCBwbGVhc3VyZS4 - var traceStateSpan = traceStateString.AsSpan().Trim(' ').Trim(','); - do + int pairEnd = traceStateSpan.IndexOf(','); + if (pairEnd < 0) { - // tracestate: rojo=00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01,congo=BleGNlZWRzIHRohbCBwbGVhc3VyZS4 - - int pairEnd = traceStateSpan.IndexOf(','); - if (pairEnd < 0) - { - pairEnd = traceStateSpan.Length; - } - - if (!TryParseKeyValue(traceStateSpan.Slice(0, pairEnd), out var key, out var value)) - { - isValid = false; - break; - } - - var keyStr = key.ToString(); - if (names.Add(keyStr)) - { - tracestate.Add(new KeyValuePair(keyStr, value.ToString())); - } - else - { - isValid = false; - break; - } - - if (tracestate.Count > MaxKeyValuePairsCount) - { - OpenTelemetryApiEventSource.Log.TooManyItemsInTracestate(); - isValid = false; - break; - } - - if (pairEnd == traceStateSpan.Length) - { - break; - } - - traceStateSpan = traceStateSpan.Slice(pairEnd + 1); + pairEnd = traceStateSpan.Length; } - while (!traceStateSpan.IsEmpty); - if (!isValid) + if (!TryParseKeyValue(traceStateSpan.Slice(0, pairEnd), out var key, out var value)) { - tracestate.Clear(); - return false; + isValid = false; + break; } - return true; - } - catch (Exception ex) - { - OpenTelemetryApiEventSource.Log.TracestateExtractException(ex); - } + var keyStr = key.ToString(); + if (names.Add(keyStr)) + { + tracestate.Add(new KeyValuePair(keyStr, value.ToString())); + } + else + { + isValid = false; + break; + } - return false; - } + if (tracestate.Count > MaxKeyValuePairsCount) + { + OpenTelemetryApiEventSource.Log.TooManyItemsInTracestate(); + isValid = false; + break; + } - internal static string GetString(IEnumerable> traceState) - { - if (traceState == null || !traceState.Any()) - { - return string.Empty; + if (pairEnd == traceStateSpan.Length) + { + break; + } + + traceStateSpan = traceStateSpan.Slice(pairEnd + 1); } + while (!traceStateSpan.IsEmpty); - // it's supposedly cheaper to iterate over very short collection a couple of times - // than to convert it to array. - var pairsCount = traceState.Count(); - if (pairsCount > MaxKeyValuePairsCount) + if (!isValid) { - OpenTelemetryApiEventSource.Log.TooManyItemsInTracestate(); + tracestate.Clear(); + return false; } - var sb = new StringBuilder(); + return true; + } + catch (Exception ex) + { + OpenTelemetryApiEventSource.Log.TracestateExtractException(ex); + } - int ind = 0; - foreach (var entry in traceState) - { - if (ind++ < MaxKeyValuePairsCount) - { - // take last MaxKeyValuePairsCount pairs, ignore last (oldest) pairs - sb.Append(entry.Key) - .Append('=') - .Append(entry.Value) - .Append(','); - } - } + return false; + } - return sb.Remove(sb.Length - 1, 1).ToString(); + internal static string GetString(IEnumerable> traceState) + { + if (traceState == null || !traceState.Any()) + { + return string.Empty; } - private static bool TryParseKeyValue(ReadOnlySpan pair, out ReadOnlySpan key, out ReadOnlySpan value) + // it's supposedly cheaper to iterate over very short collection a couple of times + // than to convert it to array. + var pairsCount = traceState.Count(); + if (pairsCount > MaxKeyValuePairsCount) { - key = default; - value = default; + OpenTelemetryApiEventSource.Log.TooManyItemsInTracestate(); + } - var keyEndIdx = pair.IndexOf('='); - if (keyEndIdx <= 0) - { - return false; - } + var sb = new StringBuilder(); - var valueStartIdx = keyEndIdx + 1; - if (valueStartIdx >= pair.Length) + int ind = 0; + foreach (var entry in traceState) + { + if (ind++ < MaxKeyValuePairsCount) { - return false; + // take last MaxKeyValuePairsCount pairs, ignore last (oldest) pairs + sb.Append(entry.Key) + .Append('=') + .Append(entry.Value) + .Append(','); } + } - key = pair.Slice(0, keyEndIdx).TrimStart(); - if (!ValidateKey(key)) - { - OpenTelemetryApiEventSource.Log.TracestateKeyIsInvalid(key); - return false; - } + return sb.Remove(sb.Length - 1, 1).ToString(); + } - value = pair.Slice(valueStartIdx, pair.Length - valueStartIdx).Trim(); - if (!ValidateValue(value)) - { - OpenTelemetryApiEventSource.Log.TracestateValueIsInvalid(value); - return false; - } + private static bool TryParseKeyValue(ReadOnlySpan pair, out ReadOnlySpan key, out ReadOnlySpan value) + { + key = default; + value = default; - return true; + var keyEndIdx = pair.IndexOf('='); + if (keyEndIdx <= 0) + { + return false; } - private static bool ValidateKey(ReadOnlySpan key) + var valueStartIdx = keyEndIdx + 1; + if (valueStartIdx >= pair.Length) { - // Key is opaque string up to 256 characters printable. It MUST begin with a lowercase letter, and - // can only contain lowercase letters a-z, digits 0-9, underscores _, dashes -, asterisks *, - // forward slashes / and @ + return false; + } - var i = 0; + key = pair.Slice(0, keyEndIdx).TrimStart(); + if (!ValidateKey(key)) + { + OpenTelemetryApiEventSource.Log.TracestateKeyIsInvalid(key); + return false; + } - if (key.IsEmpty - || key.Length > KeyMaxSize - || ((!(key[i] >= 'a' && key[i] <= 'z')) && (!(key[i] >= '0' && key[i] <= '9')))) - { - return false; - } + value = pair.Slice(valueStartIdx, pair.Length - valueStartIdx).Trim(); + if (!ValidateValue(value)) + { + OpenTelemetryApiEventSource.Log.TracestateValueIsInvalid(value); + return false; + } - // before - for (i = 1; i < key.Length; i++) - { - var c = key[i]; + return true; + } - if (c == '@') - { - // vendor follows - break; - } + private static bool ValidateKey(ReadOnlySpan key) + { + // Key is opaque string up to 256 characters printable. It MUST begin with a lowercase letter, and + // can only contain lowercase letters a-z, digits 0-9, underscores _, dashes -, asterisks *, + // forward slashes / and @ - if (!(c >= 'a' && c <= 'z') - && !(c >= '0' && c <= '9') - && c != '_' - && c != '-' - && c != '*' - && c != '/') - { - return false; - } - } + var i = 0; - i++; // skip @ or increment further than key.Length + if (key.IsEmpty + || key.Length > KeyMaxSize + || ((!(key[i] >= 'a' && key[i] <= 'z')) && (!(key[i] >= '0' && key[i] <= '9')))) + { + return false; + } + + // before + for (i = 1; i < key.Length; i++) + { + var c = key[i]; - var vendorLength = key.Length - i; - if (vendorLength == 0 || vendorLength > 14) + if (c == '@') { - // vendor name should be at least 1 to 14 character long - return false; + // vendor follows + break; } - if (vendorLength > 0 && i > 242) + if (!(c >= 'a' && c <= 'z') + && !(c >= '0' && c <= '9') + && c != '_' + && c != '-' + && c != '*' + && c != '/') { - // tenant section should be less than 241 characters long return false; } + } - for (; i < key.Length; i++) - { - var c = key[i]; - - if (!(c >= 'a' && c <= 'z') - && !(c >= '0' && c <= '9') - && c != '_' - && c != '-' - && c != '*' - && c != '/') - { - return false; - } - } + i++; // skip @ or increment further than key.Length - return true; + var vendorLength = key.Length - i; + if (vendorLength == 0 || vendorLength > 14) + { + // vendor name should be at least 1 to 14 character long + return false; } - private static bool ValidateValue(ReadOnlySpan value) + if (vendorLength > 0 && i > 242) { - // Value is opaque string up to 256 characters printable ASCII RFC0020 characters (i.e., the range - // 0x20 to 0x7E) except comma , and =. + // tenant section should be less than 241 characters long + return false; + } - if (value.Length > ValueMaxSize || value[value.Length - 1] == ' ' /* '\u0020' */) + for (; i < key.Length; i++) + { + var c = key[i]; + + if (!(c >= 'a' && c <= 'z') + && !(c >= '0' && c <= '9') + && c != '_' + && c != '-' + && c != '*' + && c != '/') { return false; } + } + + return true; + } + + private static bool ValidateValue(ReadOnlySpan value) + { + // Value is opaque string up to 256 characters printable ASCII RFC0020 characters (i.e., the range + // 0x20 to 0x7E) except comma , and =. + + if (value.Length > ValueMaxSize || value[value.Length - 1] == ' ' /* '\u0020' */) + { + return false; + } - foreach (var c in value) + foreach (var c in value) + { + if (c == ',' || c == '=' || c < ' ' /* '\u0020' */ || c > '~' /* '\u007E' */) { - if (c == ',' || c == '=' || c < ' ' /* '\u0020' */ || c > '~' /* '\u007E' */) - { - return false; - } + return false; } - - return true; } + + return true; } } diff --git a/src/OpenTelemetry.Api/Context/RemotingRuntimeContextSlot.cs b/src/OpenTelemetry.Api/Context/RemotingRuntimeContextSlot.cs index 55c7804215e..61d2722653b 100644 --- a/src/OpenTelemetry.Api/Context/RemotingRuntimeContextSlot.cs +++ b/src/OpenTelemetry.Api/Context/RemotingRuntimeContextSlot.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #if NETFRAMEWORK using System.Collections; @@ -20,66 +7,65 @@ using System.Runtime.CompilerServices; using System.Runtime.Remoting.Messaging; -namespace OpenTelemetry.Context +namespace OpenTelemetry.Context; + +/// +/// The .NET Remoting implementation of context slot. +/// +/// The type of the underlying value. +public class RemotingRuntimeContextSlot : RuntimeContextSlot, IRuntimeContextSlotValueAccessor { + // A special workaround to suppress context propagation cross AppDomains. + // + // By default the value added to System.Runtime.Remoting.Messaging.CallContext + // will be marshalled/unmarshalled across AppDomain boundary. This will cause + // serious issue if the destination AppDomain doesn't have the corresponding type + // to unmarshal data. + // The worst case is AppDomain crash with ReflectionLoadTypeException. + // + // The workaround is to use a well known type that exists in all AppDomains, and + // put the actual payload as a non-public field so the field is ignored during + // marshalling. + private static readonly FieldInfo WrapperField = typeof(BitArray).GetField("_syncRoot", BindingFlags.Instance | BindingFlags.NonPublic); + /// - /// The .NET Remoting implementation of context slot. + /// Initializes a new instance of the class. /// - /// The type of the underlying value. - public class RemotingRuntimeContextSlot : RuntimeContextSlot, IRuntimeContextSlotValueAccessor + /// The name of the context slot. + public RemotingRuntimeContextSlot(string name) + : base(name) { - // A special workaround to suppress context propagation cross AppDomains. - // - // By default the value added to System.Runtime.Remoting.Messaging.CallContext - // will be marshalled/unmarshalled across AppDomain boundary. This will cause - // serious issue if the destination AppDomain doesn't have the corresponding type - // to unmarshal data. - // The worst case is AppDomain crash with ReflectionLoadTypeException. - // - // The workaround is to use a well known type that exists in all AppDomains, and - // put the actual payload as a non-public field so the field is ignored during - // marshalling. - private static readonly FieldInfo WrapperField = typeof(BitArray).GetField("_syncRoot", BindingFlags.Instance | BindingFlags.NonPublic); + } - /// - /// Initializes a new instance of the class. - /// - /// The name of the context slot. - public RemotingRuntimeContextSlot(string name) - : base(name) - { - } + /// + public object Value + { + get => this.Get(); + set => this.Set((T)value); + } - /// - public object Value + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override T Get() + { + if (!(CallContext.LogicalGetData(this.Name) is BitArray wrapper)) { - get => this.Get(); - set => this.Set((T)value); + return default; } - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override T Get() - { - if (!(CallContext.LogicalGetData(this.Name) is BitArray wrapper)) - { - return default; - } - - var value = WrapperField.GetValue(wrapper); - return value is T t - ? t - : default; - } + var value = WrapperField.GetValue(wrapper); + return value is T t + ? t + : default; + } - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void Set(T value) - { - var wrapper = new BitArray(0); - WrapperField.SetValue(wrapper, value); - CallContext.LogicalSetData(this.Name, wrapper); - } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Set(T value) + { + var wrapper = new BitArray(0); + WrapperField.SetValue(wrapper, value); + CallContext.LogicalSetData(this.Name, wrapper); } } #endif diff --git a/src/OpenTelemetry.Api/Context/RuntimeContext.cs b/src/OpenTelemetry.Api/Context/RuntimeContext.cs index a8f45c4052e..93c96299d2f 100644 --- a/src/OpenTelemetry.Api/Context/RuntimeContext.cs +++ b/src/OpenTelemetry.Api/Context/RuntimeContext.cs @@ -1,206 +1,192 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Collections.Concurrent; using System.Runtime.CompilerServices; using OpenTelemetry.Internal; -namespace OpenTelemetry.Context +namespace OpenTelemetry.Context; + +/// +/// Generic runtime context management API. +/// +public static class RuntimeContext { + private static readonly ConcurrentDictionary Slots = new(); + + private static Type contextSlotType = typeof(AsyncLocalRuntimeContextSlot<>); + /// - /// Generic runtime context management API. + /// Gets or sets the actual context carrier implementation. /// - public static class RuntimeContext + public static Type ContextSlotType { - private static readonly ConcurrentDictionary Slots = new(); - - private static Type contextSlotType = typeof(AsyncLocalRuntimeContextSlot<>); - - /// - /// Gets or sets the actual context carrier implementation. - /// - public static Type ContextSlotType + get => contextSlotType; + set { - get => contextSlotType; - set + Guard.ThrowIfNull(value, nameof(value)); + + if (value == typeof(AsyncLocalRuntimeContextSlot<>)) + { + contextSlotType = value; + } + else if (value == typeof(ThreadLocalRuntimeContextSlot<>)) { - Guard.ThrowIfNull(value, nameof(value)); - - if (value == typeof(AsyncLocalRuntimeContextSlot<>)) - { - contextSlotType = value; - } - else if (value == typeof(ThreadLocalRuntimeContextSlot<>)) - { - contextSlotType = value; - } + contextSlotType = value; + } #if NETFRAMEWORK - else if (value == typeof(RemotingRuntimeContextSlot<>)) - { - contextSlotType = value; - } + else if (value == typeof(RemotingRuntimeContextSlot<>)) + { + contextSlotType = value; + } #endif - else - { - throw new NotSupportedException($"{value} is not a supported type."); - } + else + { + throw new NotSupportedException($"{value} is not a supported type."); } } + } - /// - /// Register a named context slot. - /// - /// The name of the context slot. - /// The type of the underlying value. - /// The slot registered. - public static RuntimeContextSlot RegisterSlot(string slotName) + /// + /// Register a named context slot. + /// + /// The name of the context slot. + /// The type of the underlying value. + /// The slot registered. + public static RuntimeContextSlot RegisterSlot(string slotName) + { + Guard.ThrowIfNullOrEmpty(slotName); + RuntimeContextSlot slot = null; + + lock (Slots) { - Guard.ThrowIfNullOrEmpty(slotName); - RuntimeContextSlot slot = null; + if (Slots.ContainsKey(slotName)) + { + throw new InvalidOperationException($"Context slot already registered: '{slotName}'"); + } - lock (Slots) + if (ContextSlotType == typeof(AsyncLocalRuntimeContextSlot<>)) { - if (Slots.ContainsKey(slotName)) - { - throw new InvalidOperationException($"Context slot already registered: '{slotName}'"); - } - - if (ContextSlotType == typeof(AsyncLocalRuntimeContextSlot<>)) - { - slot = new AsyncLocalRuntimeContextSlot(slotName); - } - else if (ContextSlotType == typeof(ThreadLocalRuntimeContextSlot<>)) - { - slot = new ThreadLocalRuntimeContextSlot(slotName); - } + slot = new AsyncLocalRuntimeContextSlot(slotName); + } + else if (ContextSlotType == typeof(ThreadLocalRuntimeContextSlot<>)) + { + slot = new ThreadLocalRuntimeContextSlot(slotName); + } #if NETFRAMEWORK - else if (ContextSlotType == typeof(RemotingRuntimeContextSlot<>)) - { - slot = new RemotingRuntimeContextSlot(slotName); - } + else if (ContextSlotType == typeof(RemotingRuntimeContextSlot<>)) + { + slot = new RemotingRuntimeContextSlot(slotName); + } #endif - Slots[slotName] = slot; - return slot; - } + Slots[slotName] = slot; + return slot; } + } - /// - /// Get a registered slot from a given name. - /// - /// The name of the context slot. - /// The type of the underlying value. - /// The slot previously registered. - public static RuntimeContextSlot GetSlot(string slotName) - { - Guard.ThrowIfNullOrEmpty(slotName); - var slot = GuardNotFound(slotName); - var contextSlot = Guard.ThrowIfNotOfType>(slot); - return contextSlot; - } + /// + /// Get a registered slot from a given name. + /// + /// The name of the context slot. + /// The type of the underlying value. + /// The slot previously registered. + public static RuntimeContextSlot GetSlot(string slotName) + { + Guard.ThrowIfNullOrEmpty(slotName); + var slot = GuardNotFound(slotName); + var contextSlot = Guard.ThrowIfNotOfType>(slot); + return contextSlot; + } - /* - public static void Apply(IDictionary snapshot) + /* + public static void Apply(IDictionary snapshot) + { + foreach (var entry in snapshot) { - foreach (var entry in snapshot) - { - // TODO: revisit this part if we want Snapshot() to be used on critical paths - dynamic value = entry.Value; - SetValue(entry.Key, value); - } + // TODO: revisit this part if we want Snapshot() to be used on critical paths + dynamic value = entry.Value; + SetValue(entry.Key, value); } + } - public static IDictionary Snapshot() - { - var retval = new Dictionary(); - foreach (var entry in Slots) - { - // TODO: revisit this part if we want Snapshot() to be used on critical paths - dynamic slot = entry.Value; - retval[entry.Key] = slot.Get(); - } - return retval; - } - */ - - /// - /// Sets the value to a registered slot. - /// - /// The name of the context slot. - /// The value to be set. - /// The type of the value. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(string slotName, T value) + public static IDictionary Snapshot() + { + var retval = new Dictionary(); + foreach (var entry in Slots) { - GetSlot(slotName).Set(value); + // TODO: revisit this part if we want Snapshot() to be used on critical paths + dynamic slot = entry.Value; + retval[entry.Key] = slot.Get(); } + return retval; + } + */ - /// - /// Gets the value from a registered slot. - /// - /// The name of the context slot. - /// The type of the value. - /// The value retrieved from the context slot. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static T GetValue(string slotName) - { - return GetSlot(slotName).Get(); - } + /// + /// Sets the value to a registered slot. + /// + /// The name of the context slot. + /// The value to be set. + /// The type of the value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetValue(string slotName, T value) + { + GetSlot(slotName).Set(value); + } - /// - /// Sets the value to a registered slot. - /// - /// The name of the context slot. - /// The value to be set. - public static void SetValue(string slotName, object value) - { - Guard.ThrowIfNullOrEmpty(slotName); - var slot = GuardNotFound(slotName); - var runtimeContextSlotValueAccessor = Guard.ThrowIfNotOfType(slot); - runtimeContextSlotValueAccessor.Value = value; - } + /// + /// Gets the value from a registered slot. + /// + /// The name of the context slot. + /// The type of the value. + /// The value retrieved from the context slot. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T GetValue(string slotName) + { + return GetSlot(slotName).Get(); + } - /// - /// Gets the value from a registered slot. - /// - /// The name of the context slot. - /// The value retrieved from the context slot. - public static object GetValue(string slotName) - { - Guard.ThrowIfNullOrEmpty(slotName); - var slot = GuardNotFound(slotName); - var runtimeContextSlotValueAccessor = Guard.ThrowIfNotOfType(slot); - return runtimeContextSlotValueAccessor.Value; - } + /// + /// Sets the value to a registered slot. + /// + /// The name of the context slot. + /// The value to be set. + public static void SetValue(string slotName, object value) + { + Guard.ThrowIfNullOrEmpty(slotName); + var slot = GuardNotFound(slotName); + var runtimeContextSlotValueAccessor = Guard.ThrowIfNotOfType(slot); + runtimeContextSlotValueAccessor.Value = value; + } - // For testing purpose - internal static void Clear() - { - Slots.Clear(); - } + /// + /// Gets the value from a registered slot. + /// + /// The name of the context slot. + /// The value retrieved from the context slot. + public static object GetValue(string slotName) + { + Guard.ThrowIfNullOrEmpty(slotName); + var slot = GuardNotFound(slotName); + var runtimeContextSlotValueAccessor = Guard.ThrowIfNotOfType(slot); + return runtimeContextSlotValueAccessor.Value; + } - private static object GuardNotFound(string slotName) - { - if (!Slots.TryGetValue(slotName, out var slot)) - { - throw new ArgumentException($"Context slot not found: '{slotName}'"); - } + // For testing purpose + internal static void Clear() + { + Slots.Clear(); + } - return slot; + private static object GuardNotFound(string slotName) + { + if (!Slots.TryGetValue(slotName, out var slot)) + { + throw new ArgumentException($"Context slot not found: '{slotName}'"); } + + return slot; } } diff --git a/src/OpenTelemetry.Api/Context/RuntimeContextSlot.cs b/src/OpenTelemetry.Api/Context/RuntimeContextSlot.cs index 9864634f7c2..918e4b79424 100644 --- a/src/OpenTelemetry.Api/Context/RuntimeContextSlot.cs +++ b/src/OpenTelemetry.Api/Context/RuntimeContextSlot.cs @@ -1,66 +1,52 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Context +namespace OpenTelemetry.Context; + +/// +/// The abstract context slot. +/// +/// The type of the underlying value. +public abstract class RuntimeContextSlot : IDisposable { /// - /// The abstract context slot. + /// Initializes a new instance of the class. /// - /// The type of the underlying value. - public abstract class RuntimeContextSlot : IDisposable + /// The name of the context slot. + protected RuntimeContextSlot(string name) { - /// - /// Initializes a new instance of the class. - /// - /// The name of the context slot. - protected RuntimeContextSlot(string name) - { - this.Name = name; - } + this.Name = name; + } - /// - /// Gets the name of the context slot. - /// - public string Name { get; private set; } + /// + /// Gets the name of the context slot. + /// + public string Name { get; private set; } - /// - /// Get the value from the context slot. - /// - /// The value retrieved from the context slot. - public abstract T Get(); + /// + /// Get the value from the context slot. + /// + /// The value retrieved from the context slot. + public abstract T Get(); - /// - /// Set the value to the context slot. - /// - /// The value to be set. - public abstract void Set(T value); + /// + /// Set the value to the context slot. + /// + /// The value to be set. + public abstract void Set(T value); - /// - public void Dispose() - { - this.Dispose(disposing: true); - GC.SuppressFinalize(this); - } + /// + public void Dispose() + { + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } - /// - /// Releases the unmanaged resources used by this class and optionally releases the managed resources. - /// - /// to release both managed and unmanaged resources; to release only unmanaged resources. - protected virtual void Dispose(bool disposing) - { - } + /// + /// Releases the unmanaged resources used by this class and optionally releases the managed resources. + /// + /// to release both managed and unmanaged resources; to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { } } diff --git a/src/OpenTelemetry.Api/Context/ThreadLocalRuntimeContextSlot.cs b/src/OpenTelemetry.Api/Context/ThreadLocalRuntimeContextSlot.cs index cc312e99469..c7724a842b4 100644 --- a/src/OpenTelemetry.Api/Context/ThreadLocalRuntimeContextSlot.cs +++ b/src/OpenTelemetry.Api/Context/ThreadLocalRuntimeContextSlot.cs @@ -1,77 +1,63 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Runtime.CompilerServices; -namespace OpenTelemetry.Context +namespace OpenTelemetry.Context; + +/// +/// The thread local (TLS) implementation of context slot. +/// +/// The type of the underlying value. +public class ThreadLocalRuntimeContextSlot : RuntimeContextSlot, IRuntimeContextSlotValueAccessor { + private readonly ThreadLocal slot; + private bool disposed; + /// - /// The thread local (TLS) implementation of context slot. + /// Initializes a new instance of the class. /// - /// The type of the underlying value. - public class ThreadLocalRuntimeContextSlot : RuntimeContextSlot, IRuntimeContextSlotValueAccessor + /// The name of the context slot. + public ThreadLocalRuntimeContextSlot(string name) + : base(name) { - private readonly ThreadLocal slot; - private bool disposed; - - /// - /// Initializes a new instance of the class. - /// - /// The name of the context slot. - public ThreadLocalRuntimeContextSlot(string name) - : base(name) - { - this.slot = new ThreadLocal(); - } + this.slot = new ThreadLocal(); + } - /// - public object Value - { - get => this.slot.Value; - set => this.slot.Value = (T)value; - } + /// + public object Value + { + get => this.slot.Value; + set => this.slot.Value = (T)value; + } - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override T Get() - { - return this.slot.Value; - } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override T Get() + { + return this.slot.Value; + } - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void Set(T value) - { - this.slot.Value = value; - } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Set(T value) + { + this.slot.Value = value; + } - /// - protected override void Dispose(bool disposing) + /// + protected override void Dispose(bool disposing) + { + if (!this.disposed) { - if (!this.disposed) + if (disposing) { - if (disposing) - { - this.slot.Dispose(); - } - - this.disposed = true; + this.slot.Dispose(); } - base.Dispose(disposing); + this.disposed = true; } + + base.Dispose(disposing); } } diff --git a/src/OpenTelemetry.Api/Internal/ActivityHelperExtensions.cs b/src/OpenTelemetry.Api/Internal/ActivityHelperExtensions.cs deleted file mode 100644 index ec844f8c5ad..00000000000 --- a/src/OpenTelemetry.Api/Internal/ActivityHelperExtensions.cs +++ /dev/null @@ -1,127 +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 System.Runtime.CompilerServices; -using OpenTelemetry.Internal; - -namespace OpenTelemetry.Trace -{ - /// - /// Extension methods on Activity. - /// - internal static class ActivityHelperExtensions - { - /// - /// Gets the status of activity execution. - /// Activity class in .NET does not support 'Status'. - /// This extension provides a workaround to retrieve Status from special tags with key name otel.status_code and otel.status_description. - /// - /// Activity instance. - /// . - /// Status description. - /// if was found on the supplied Activity. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool TryGetStatus(this Activity activity, out StatusCode statusCode, out string statusDescription) - { - Debug.Assert(activity != null, "Activity should not be null"); - - bool foundStatusCode = false; - statusCode = default; - statusDescription = null; - - foreach (ref readonly var tag in activity.EnumerateTagObjects()) - { - switch (tag.Key) - { - case SpanAttributeConstants.StatusCodeKey: - foundStatusCode = StatusHelper.TryGetStatusCodeForTagValue(tag.Value as string, out statusCode); - if (!foundStatusCode) - { - // If status code was found but turned out to be invalid give up immediately. - return false; - } - - break; - case SpanAttributeConstants.StatusDescriptionKey: - statusDescription = tag.Value as string; - break; - default: - continue; - } - - if (foundStatusCode && statusDescription != null) - { - // If we found a status code and a description we break enumeration because our work is done. - break; - } - } - - return foundStatusCode; - } - - /// - /// Gets the value of a specific tag on an . - /// - /// Activity instance. - /// Case-sensitive tag name to retrieve. - /// Tag value or null if a match was not found. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static object GetTagValue(this Activity activity, string tagName) - { - Debug.Assert(activity != null, "Activity should not be null"); - - foreach (ref readonly var tag in activity.EnumerateTagObjects()) - { - if (tag.Key == tagName) - { - return tag.Value; - } - } - - return null; - } - - /// - /// Checks if the user provided tag name is the first tag of the and retrieves the tag value. - /// - /// Activity instance. - /// Tag name. - /// Tag value. - /// if the first tag of the supplied Activity matches the user provide tag name. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool TryCheckFirstTag(this Activity activity, string tagName, out object tagValue) - { - Debug.Assert(activity != null, "Activity should not be null"); - - var enumerator = activity.EnumerateTagObjects(); - - if (enumerator.MoveNext()) - { - ref readonly var tag = ref enumerator.Current; - - if (tag.Key == tagName) - { - tagValue = tag.Value; - return true; - } - } - - tagValue = null; - return false; - } - } -} diff --git a/src/OpenTelemetry.Api/Internal/ExceptionExtensions.cs b/src/OpenTelemetry.Api/Internal/ExceptionExtensions.cs deleted file mode 100644 index f27767c9e16..00000000000 --- a/src/OpenTelemetry.Api/Internal/ExceptionExtensions.cs +++ /dev/null @@ -1,46 +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. -// - -#nullable enable - -using System.Globalization; - -namespace OpenTelemetry.Internal -{ - internal static class ExceptionExtensions - { - /// - /// Returns a culture-independent string representation of the given object, - /// appropriate for diagnostics tracing. - /// - /// Exception to convert to string. - /// Exception as string with no culture. - public static string ToInvariantString(this Exception exception) - { - var originalUICulture = Thread.CurrentThread.CurrentUICulture; - - try - { - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; - return exception.ToString(); - } - finally - { - Thread.CurrentThread.CurrentUICulture = originalUICulture; - } - } - } -} diff --git a/src/OpenTelemetry.Api/Internal/HttpSemanticConventionHelper.cs b/src/OpenTelemetry.Api/Internal/HttpSemanticConventionHelper.cs deleted file mode 100644 index 1774cbd2069..00000000000 --- a/src/OpenTelemetry.Api/Internal/HttpSemanticConventionHelper.cs +++ /dev/null @@ -1,66 +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. -// - -namespace OpenTelemetry.Internal; - -/// -/// Helper class for Http Semantic Conventions. -/// -/// -/// Due to a breaking change in the semantic convention, affected instrumentation libraries -/// must inspect an environment variable to determine which attributes to emit. -/// This is expected to be removed when the instrumentation libraries reach Stable. -/// . -/// -internal static class HttpSemanticConventionHelper -{ - [Flags] - internal enum HttpSemanticConvention - { - /// - /// Instructs an instrumentation library to emit the old experimental HTTP attributes. - /// - Old = 0x1, - - /// - /// Instructs an instrumentation library to emit the new, stable Http attributes. - /// - New = 0x2, - - /// - /// Instructs an instrumentation library to emit both the old and new attributes. - /// - Dupe = Old | New, - } - - public static HttpSemanticConvention GetSemanticConventionOptIn() - { - try - { - var envVarValue = Environment.GetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN"); - return envVarValue?.ToLowerInvariant() switch - { - "http" => HttpSemanticConvention.New, - "http/dup" => HttpSemanticConvention.Dupe, - _ => HttpSemanticConvention.Old, - }; - } - catch - { - return HttpSemanticConvention.Old; - } - } -} diff --git a/src/OpenTelemetry.Api/Internal/OpenTelemetryApiEventSource.cs b/src/OpenTelemetry.Api/Internal/OpenTelemetryApiEventSource.cs index 7d114db7839..ea802a6ed15 100644 --- a/src/OpenTelemetry.Api/Internal/OpenTelemetryApiEventSource.cs +++ b/src/OpenTelemetry.Api/Internal/OpenTelemetryApiEventSource.cs @@ -1,123 +1,111 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable using System.Diagnostics.Tracing; -namespace OpenTelemetry.Internal +namespace OpenTelemetry.Internal; + +/// +/// EventSource implementation for OpenTelemetry API. +/// This is used for internal logging of this library. +/// +[EventSource(Name = "OpenTelemetry-Api")] +internal sealed class OpenTelemetryApiEventSource : EventSource { - /// - /// EventSource implementation for OpenTelemetry API. - /// This is used for internal logging of this library. - /// - [EventSource(Name = "OpenTelemetry-Api")] - internal sealed class OpenTelemetryApiEventSource : EventSource - { - public static OpenTelemetryApiEventSource Log = new(); + public static OpenTelemetryApiEventSource Log = new(); - [NonEvent] - public void ActivityContextExtractException(string format, Exception ex) + [NonEvent] + public void ActivityContextExtractException(string format, Exception ex) + { + if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) - { - this.FailedToExtractActivityContext(format, ex.ToInvariantString()); - } + this.FailedToExtractActivityContext(format, ex.ToInvariantString()); } + } - [NonEvent] - public void BaggageExtractException(string format, Exception ex) + [NonEvent] + public void BaggageExtractException(string format, Exception ex) + { + if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) - { - this.FailedToExtractBaggage(format, ex.ToInvariantString()); - } + this.FailedToExtractBaggage(format, ex.ToInvariantString()); } + } - [NonEvent] - public void TracestateExtractException(Exception ex) + [NonEvent] + public void TracestateExtractException(Exception ex) + { + if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) - { - this.TracestateExtractError(ex.ToInvariantString()); - } + this.TracestateExtractError(ex.ToInvariantString()); } + } - [NonEvent] - public void TracestateKeyIsInvalid(ReadOnlySpan key) + [NonEvent] + public void TracestateKeyIsInvalid(ReadOnlySpan key) + { + if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) - { - this.TracestateKeyIsInvalid(key.ToString()); - } + this.TracestateKeyIsInvalid(key.ToString()); } + } - [NonEvent] - public void TracestateValueIsInvalid(ReadOnlySpan value) + [NonEvent] + public void TracestateValueIsInvalid(ReadOnlySpan value) + { + if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) - { - this.TracestateValueIsInvalid(value.ToString()); - } + this.TracestateValueIsInvalid(value.ToString()); } + } - [Event(3, Message = "Failed to parse tracestate: too many items", Level = EventLevel.Warning)] - public void TooManyItemsInTracestate() - { - this.WriteEvent(3); - } + [Event(3, Message = "Failed to parse tracestate: too many items", Level = EventLevel.Warning)] + public void TooManyItemsInTracestate() + { + this.WriteEvent(3); + } - [Event(4, Message = "Tracestate key is invalid, key = '{0}'", Level = EventLevel.Warning)] - public void TracestateKeyIsInvalid(string key) - { - this.WriteEvent(4, key); - } + [Event(4, Message = "Tracestate key is invalid, key = '{0}'", Level = EventLevel.Warning)] + public void TracestateKeyIsInvalid(string key) + { + this.WriteEvent(4, key); + } - [Event(5, Message = "Tracestate value is invalid, value = '{0}'", Level = EventLevel.Warning)] - public void TracestateValueIsInvalid(string value) - { - this.WriteEvent(5, value); - } + [Event(5, Message = "Tracestate value is invalid, value = '{0}'", Level = EventLevel.Warning)] + public void TracestateValueIsInvalid(string value) + { + this.WriteEvent(5, value); + } - [Event(6, Message = "Tracestate parse error: '{0}'", Level = EventLevel.Warning)] - public void TracestateExtractError(string error) - { - this.WriteEvent(6, error); - } + [Event(6, Message = "Tracestate parse error: '{0}'", Level = EventLevel.Warning)] + public void TracestateExtractError(string error) + { + this.WriteEvent(6, error); + } - [Event(8, Message = "Failed to extract activity context in format: '{0}', context: '{1}'.", Level = EventLevel.Warning)] - public void FailedToExtractActivityContext(string format, string exception) - { - this.WriteEvent(8, format, exception); - } + [Event(8, Message = "Failed to extract activity context in format: '{0}', context: '{1}'.", Level = EventLevel.Warning)] + public void FailedToExtractActivityContext(string format, string exception) + { + this.WriteEvent(8, format, exception); + } - [Event(9, Message = "Failed to inject activity context in format: '{0}', context: '{1}'.", Level = EventLevel.Warning)] - public void FailedToInjectActivityContext(string format, string error) - { - this.WriteEvent(9, format, error); - } + [Event(9, Message = "Failed to inject activity context in format: '{0}', context: '{1}'.", Level = EventLevel.Warning)] + public void FailedToInjectActivityContext(string format, string error) + { + this.WriteEvent(9, format, error); + } - [Event(10, Message = "Failed to extract baggage in format: '{0}', baggage: '{1}'.", Level = EventLevel.Warning)] - public void FailedToExtractBaggage(string format, string exception) - { - this.WriteEvent(10, format, exception); - } + [Event(10, Message = "Failed to extract baggage in format: '{0}', baggage: '{1}'.", Level = EventLevel.Warning)] + public void FailedToExtractBaggage(string format, string exception) + { + this.WriteEvent(10, format, exception); + } - [Event(11, Message = "Failed to inject baggage in format: '{0}', baggage: '{1}'.", Level = EventLevel.Warning)] - public void FailedToInjectBaggage(string format, string error) - { - this.WriteEvent(11, format, error); - } + [Event(11, Message = "Failed to inject baggage in format: '{0}', baggage: '{1}'.", Level = EventLevel.Warning)] + public void FailedToInjectBaggage(string format, string error) + { + this.WriteEvent(11, format, error); } } diff --git a/src/OpenTelemetry.Api/Internal/SemanticConventions.cs b/src/OpenTelemetry.Api/Internal/SemanticConventions.cs deleted file mode 100644 index d92e3502518..00000000000 --- a/src/OpenTelemetry.Api/Internal/SemanticConventions.cs +++ /dev/null @@ -1,126 +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. -// - -#nullable enable - -namespace OpenTelemetry.Trace -{ - /// - /// Constants for semantic attribute names outlined by the OpenTelemetry specifications. - /// and - /// . - /// - internal static class SemanticConventions - { - // The set of constants matches the specification as of this commit. - // https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/trace/semantic_conventions - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/exceptions.md - public const string AttributeNetTransport = "net.transport"; - public const string AttributeNetPeerIp = "net.peer.ip"; - public const string AttributeNetPeerPort = "net.peer.port"; - public const string AttributeNetPeerName = "net.peer.name"; - public const string AttributeNetHostIp = "net.host.ip"; - public const string AttributeNetHostPort = "net.host.port"; - public const string AttributeNetHostName = "net.host.name"; - - public const string AttributeEnduserId = "enduser.id"; - public const string AttributeEnduserRole = "enduser.role"; - public const string AttributeEnduserScope = "enduser.scope"; - - public const string AttributePeerService = "peer.service"; - - public const string AttributeHttpMethod = "http.method"; - public const string AttributeHttpUrl = "http.url"; - public const string AttributeHttpTarget = "http.target"; - public const string AttributeHttpHost = "http.host"; - public const string AttributeHttpScheme = "http.scheme"; - public const string AttributeHttpStatusCode = "http.status_code"; - public const string AttributeHttpStatusText = "http.status_text"; - public const string AttributeHttpFlavor = "http.flavor"; - public const string AttributeHttpServerName = "http.server_name"; - public const string AttributeHttpRoute = "http.route"; - public const string AttributeHttpClientIP = "http.client_ip"; - public const string AttributeHttpUserAgent = "http.user_agent"; - public const string AttributeHttpRequestContentLength = "http.request_content_length"; - public const string AttributeHttpRequestContentLengthUncompressed = "http.request_content_length_uncompressed"; - public const string AttributeHttpResponseContentLength = "http.response_content_length"; - public const string AttributeHttpResponseContentLengthUncompressed = "http.response_content_length_uncompressed"; - - public const string AttributeDbSystem = "db.system"; - public const string AttributeDbConnectionString = "db.connection_string"; - public const string AttributeDbUser = "db.user"; - public const string AttributeDbMsSqlInstanceName = "db.mssql.instance_name"; - public const string AttributeDbJdbcDriverClassName = "db.jdbc.driver_classname"; - public const string AttributeDbName = "db.name"; - public const string AttributeDbStatement = "db.statement"; - public const string AttributeDbOperation = "db.operation"; - public const string AttributeDbInstance = "db.instance"; - public const string AttributeDbUrl = "db.url"; - public const string AttributeDbCassandraKeyspace = "db.cassandra.keyspace"; - public const string AttributeDbHBaseNamespace = "db.hbase.namespace"; - public const string AttributeDbRedisDatabaseIndex = "db.redis.database_index"; - public const string AttributeDbMongoDbCollection = "db.mongodb.collection"; - - public const string AttributeRpcSystem = "rpc.system"; - public const string AttributeRpcService = "rpc.service"; - public const string AttributeRpcMethod = "rpc.method"; - public const string AttributeRpcGrpcStatusCode = "rpc.grpc.status_code"; - - public const string AttributeMessageType = "message.type"; - public const string AttributeMessageId = "message.id"; - public const string AttributeMessageCompressedSize = "message.compressed_size"; - public const string AttributeMessageUncompressedSize = "message.uncompressed_size"; - - public const string AttributeFaasTrigger = "faas.trigger"; - public const string AttributeFaasExecution = "faas.execution"; - public const string AttributeFaasDocumentCollection = "faas.document.collection"; - public const string AttributeFaasDocumentOperation = "faas.document.operation"; - public const string AttributeFaasDocumentTime = "faas.document.time"; - public const string AttributeFaasDocumentName = "faas.document.name"; - public const string AttributeFaasTime = "faas.time"; - public const string AttributeFaasCron = "faas.cron"; - - public const string AttributeMessagingSystem = "messaging.system"; - public const string AttributeMessagingDestination = "messaging.destination"; - public const string AttributeMessagingDestinationKind = "messaging.destination_kind"; - public const string AttributeMessagingTempDestination = "messaging.temp_destination"; - public const string AttributeMessagingProtocol = "messaging.protocol"; - public const string AttributeMessagingProtocolVersion = "messaging.protocol_version"; - public const string AttributeMessagingUrl = "messaging.url"; - public const string AttributeMessagingMessageId = "messaging.message_id"; - public const string AttributeMessagingConversationId = "messaging.conversation_id"; - public const string AttributeMessagingPayloadSize = "messaging.message_payload_size_bytes"; - public const string AttributeMessagingPayloadCompressedSize = "messaging.message_payload_compressed_size_bytes"; - public const string AttributeMessagingOperation = "messaging.operation"; - - public const string AttributeExceptionEventName = "exception"; - public const string AttributeExceptionType = "exception.type"; - public const string AttributeExceptionMessage = "exception.message"; - public const string AttributeExceptionStacktrace = "exception.stacktrace"; - - // Http v1.21.0 https://github.com/open-telemetry/opentelemetry-specification/blob/v1.21.0/specification/trace/semantic_conventions/http.md - public const string AttributeHttpRequestMethod = "http.request.method"; // replaces: "http.method" (AttributeHttpMethod) - public const string AttributeHttpResponseStatusCode = "http.response.status_code"; // replaces: "http.status_code" (AttributeHttpStatusCode) - public const string AttributeNetworkProtocolVersion = "network.protocol.version"; // replaces: "http.flavor" (AttributeHttpFlavor) - public const string AttributeServerAddress = "server.address"; // replaces: "net.host.name" (AttributeNetHostName) - public const string AttributeServerPort = "server.port"; // replaces: "net.host.port" (AttributeNetHostPort) and "net.peer.port" (AttributeNetPeerPort) - public const string AttributeUrlFull = "url.full"; // replaces: "http.url" (AttributeHttpUrl) - public const string AttributeUrlPath = "url.path"; // replaces: "http.target" (AttributeHttpTarget) - public const string AttributeUrlScheme = "url.scheme"; // replaces: "http.scheme" (AttributeHttpScheme) - public const string AttributeUrlQuery = "url.query"; - public const string AttributeUserAgentOriginal = "user_agent.original"; // replaces: "http.user_agent" (AttributeHttpUserAgent) - } -} diff --git a/src/OpenTelemetry.Api/Internal/SpanAttributeConstants.cs b/src/OpenTelemetry.Api/Internal/SpanAttributeConstants.cs deleted file mode 100644 index 3bb7809cd2f..00000000000 --- a/src/OpenTelemetry.Api/Internal/SpanAttributeConstants.cs +++ /dev/null @@ -1,30 +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. -// - -#nullable enable - -namespace OpenTelemetry.Trace -{ - /// - /// Defines well-known span attribute keys. - /// - internal static class SpanAttributeConstants - { - public const string StatusCodeKey = "otel.status_code"; - public const string StatusDescriptionKey = "otel.status_description"; - public const string DatabaseStatementTypeKey = "db.statement_type"; - } -} diff --git a/src/OpenTelemetry.Api/Internal/SpanHelper.cs b/src/OpenTelemetry.Api/Internal/SpanHelper.cs deleted file mode 100644 index 6f909db58c0..00000000000 --- a/src/OpenTelemetry.Api/Internal/SpanHelper.cs +++ /dev/null @@ -1,44 +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; - -namespace OpenTelemetry.Trace -{ - /// - /// A collection of helper methods to be used when building spans. - /// - internal static class SpanHelper - { - /// - /// Helper method that populates span properties from http status code according - /// to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status. - /// - /// The span kind. - /// Http status code. - /// 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) - { - return ActivityStatusCode.Unset; - } - - return ActivityStatusCode.Error; - } - } -} diff --git a/src/OpenTelemetry.Api/Internal/StatusHelper.cs b/src/OpenTelemetry.Api/Internal/StatusHelper.cs deleted file mode 100644 index 1b3b7358f59..00000000000 --- a/src/OpenTelemetry.Api/Internal/StatusHelper.cs +++ /dev/null @@ -1,74 +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. -// - -#nullable enable - -using System.Runtime.CompilerServices; -using OpenTelemetry.Trace; - -namespace OpenTelemetry.Internal -{ - internal static class StatusHelper - { - public const string UnsetStatusCodeTagValue = "UNSET"; - public const string OkStatusCodeTagValue = "OK"; - public const string ErrorStatusCodeTagValue = "ERROR"; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string? GetTagValueForStatusCode(StatusCode statusCode) - { - return statusCode switch - { - /* - * Note: Order here does matter for perf. Unset is - * first because assumption is most spans will be - * Unset, then Error. Ok is not set by the SDK. - */ - StatusCode.Unset => UnsetStatusCodeTagValue, - StatusCode.Error => ErrorStatusCodeTagValue, - StatusCode.Ok => OkStatusCodeTagValue, - _ => null, - }; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static StatusCode? GetStatusCodeForTagValue(string? statusCodeTagValue) - { - return statusCodeTagValue switch - { - /* - * Note: Order here does matter for perf. Unset is - * first because assumption is most spans will be - * Unset, then Error. Ok is not set by the SDK. - */ - not null when UnsetStatusCodeTagValue.Equals(statusCodeTagValue, StringComparison.OrdinalIgnoreCase) => StatusCode.Unset, - not null when ErrorStatusCodeTagValue.Equals(statusCodeTagValue, StringComparison.OrdinalIgnoreCase) => StatusCode.Error, - not null when OkStatusCodeTagValue.Equals(statusCodeTagValue, StringComparison.OrdinalIgnoreCase) => StatusCode.Ok, - _ => null, - }; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool TryGetStatusCodeForTagValue(string? statusCodeTagValue, out StatusCode statusCode) - { - StatusCode? tempStatusCode = GetStatusCodeForTagValue(statusCodeTagValue); - - statusCode = tempStatusCode ?? default; - - return tempStatusCode.HasValue; - } - } -} diff --git a/src/OpenTelemetry.Api/Logs/IDeferredLoggerProviderBuilder.cs b/src/OpenTelemetry.Api/Logs/IDeferredLoggerProviderBuilder.cs index 5c8585b799e..70528fe6cb7 100644 --- a/src/OpenTelemetry.Api/Logs/IDeferredLoggerProviderBuilder.cs +++ b/src/OpenTelemetry.Api/Logs/IDeferredLoggerProviderBuilder.cs @@ -1,29 +1,35 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #nullable enable +#if NET8_0_OR_GREATER && EXPOSE_EXPERIMENTAL_FEATURES +using System.Diagnostics.CodeAnalysis; +using OpenTelemetry.Internal; +#endif + namespace OpenTelemetry.Logs; +#if EXPOSE_EXPERIMENTAL_FEATURES +/// +/// Describes a logger provider builder that supports deferred +/// initialization using an to perform +/// dependency injection. +/// +/// +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else /// /// Describes a logger provider builder that supports deferred /// initialization using an to perform /// dependency injection. /// -public interface IDeferredLoggerProviderBuilder +internal +#endif +interface IDeferredLoggerProviderBuilder { /// /// Register a callback action to configure the // 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. -// +// SPDX-License-Identifier: Apache-2.0 #nullable enable using System.Collections; using System.ComponentModel; using System.Diagnostics; +#if NET8_0_OR_GREATER && EXPOSE_EXPERIMENTAL_FEATURES +using System.Diagnostics.CodeAnalysis; +#endif using OpenTelemetry.Internal; using OpenTelemetry.Trace; namespace OpenTelemetry.Logs; +#if EXPOSE_EXPERIMENTAL_FEATURES /// /// Stores attributes to be added to a log message. /// -public struct LogRecordAttributeList : IReadOnlyList> +/// +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.LogsBridgeExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else +/// +/// Stores attributes to be added to a log message. +/// +internal +#endif + struct LogRecordAttributeList : IReadOnlyList> { internal const int OverflowMaxCount = 8; internal const int OverflowAdditionalCapacity = 16; diff --git a/src/OpenTelemetry.Api/Logs/LogRecordData.cs b/src/OpenTelemetry.Api/Logs/LogRecordData.cs index 6daefdcee74..cb3c49292af 100644 --- a/src/OpenTelemetry.Api/Logs/LogRecordData.cs +++ b/src/OpenTelemetry.Api/Logs/LogRecordData.cs @@ -1,29 +1,32 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #nullable enable using System.Diagnostics; +#if NET8_0_OR_GREATER && EXPOSE_EXPERIMENTAL_FEATURES +using System.Diagnostics.CodeAnalysis; +using OpenTelemetry.Internal; +#endif namespace OpenTelemetry.Logs; +#if EXPOSE_EXPERIMENTAL_FEATURES /// /// Stores details about a log message. /// -public struct LogRecordData +/// +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.LogsBridgeExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else +/// +/// Stores details about a log message. +/// +internal +#endif + struct LogRecordData { internal DateTime TimestampBacking = DateTime.UtcNow; diff --git a/src/OpenTelemetry.Api/Logs/LogRecordSeverity.cs b/src/OpenTelemetry.Api/Logs/LogRecordSeverity.cs index 076cd2545c0..9f48e71e854 100644 --- a/src/OpenTelemetry.Api/Logs/LogRecordSeverity.cs +++ b/src/OpenTelemetry.Api/Logs/LogRecordSeverity.cs @@ -1,27 +1,31 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #nullable enable +#if NET8_0_OR_GREATER && EXPOSE_EXPERIMENTAL_FEATURES +using System.Diagnostics.CodeAnalysis; +using OpenTelemetry.Internal; +#endif + namespace OpenTelemetry.Logs; +#if EXPOSE_EXPERIMENTAL_FEATURES +/// +/// Describes the severity level of a log record. +/// +/// +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.LogsBridgeExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else /// /// Describes the severity level of a log record. /// -public enum LogRecordSeverity +internal +#endif + enum LogRecordSeverity { /// Unspecified severity (0). Unspecified = 0, diff --git a/src/OpenTelemetry.Api/Logs/LogRecordSeverityExtensions.cs b/src/OpenTelemetry.Api/Logs/LogRecordSeverityExtensions.cs index f86892c3181..f171edbc9ca 100644 --- a/src/OpenTelemetry.Api/Logs/LogRecordSeverityExtensions.cs +++ b/src/OpenTelemetry.Api/Logs/LogRecordSeverityExtensions.cs @@ -1,27 +1,31 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #nullable enable +#if NET8_0_OR_GREATER && EXPOSE_EXPERIMENTAL_FEATURES +using System.Diagnostics.CodeAnalysis; +using OpenTelemetry.Internal; +#endif + namespace OpenTelemetry.Logs; +#if EXPOSE_EXPERIMENTAL_FEATURES +/// +/// Contains extension methods for the enum. +/// +/// +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.LogsBridgeExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else /// /// Contains extension methods for the enum. /// -public static class LogRecordSeverityExtensions +internal +#endif + static class LogRecordSeverityExtensions { internal const string UnspecifiedShortName = "UNSPECIFIED"; diff --git a/src/OpenTelemetry.Api/Logs/Logger.cs b/src/OpenTelemetry.Api/Logs/Logger.cs index 25bd741f113..71baf7d611e 100644 --- a/src/OpenTelemetry.Api/Logs/Logger.cs +++ b/src/OpenTelemetry.Api/Logs/Logger.cs @@ -1,27 +1,31 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #nullable enable +#if NET8_0_OR_GREATER && EXPOSE_EXPERIMENTAL_FEATURES +using System.Diagnostics.CodeAnalysis; +using OpenTelemetry.Internal; +#endif + namespace OpenTelemetry.Logs; +#if EXPOSE_EXPERIMENTAL_FEATURES +/// +/// Logger is the class responsible for creating log records. +/// +/// WARNING: This is an experimental API which might change or be removed in the future. Use at your own risk. +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.LogsBridgeExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else /// /// Logger is the class responsible for creating log records. /// -public abstract class Logger +internal +#endif +abstract class Logger { /// /// Initializes a new instance of the class. diff --git a/src/OpenTelemetry.Api/Logs/LoggerProvider.cs b/src/OpenTelemetry.Api/Logs/LoggerProvider.cs index e0d71637664..c7993dc664a 100644 --- a/src/OpenTelemetry.Api/Logs/LoggerProvider.cs +++ b/src/OpenTelemetry.Api/Logs/LoggerProvider.cs @@ -1,38 +1,50 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #nullable enable #if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER using System.Diagnostics.CodeAnalysis; #endif +#if NET8_0_OR_GREATER +using OpenTelemetry.Internal; +#endif namespace OpenTelemetry.Logs; +#if EXPOSE_EXPERIMENTAL_FEATURES +/// +/// LoggerProvider is the entry point of the OpenTelemetry API. It provides access to . +/// +/// +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else /// /// LoggerProvider is the entry point of the OpenTelemetry API. It provides access to . /// -public class LoggerProvider : BaseProvider +internal +#endif + class LoggerProvider : BaseProvider { private static readonly NoopLogger NoopLogger = new(); + /// + /// Initializes a new instance of the class. + /// + protected LoggerProvider() + { + } + /// /// Gets a logger. /// /// instance. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LogsBridgeExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif public Logger GetLogger() => this.GetLogger(name: null, version: null); @@ -41,6 +53,9 @@ public Logger GetLogger() /// /// Optional name identifying the instrumentation library. /// instance. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LogsBridgeExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif public Logger GetLogger(string? name) => this.GetLogger(name, version: null); @@ -50,6 +65,9 @@ public Logger GetLogger(string? name) /// Optional name identifying the instrumentation library. /// Optional version of the instrumentation library. /// instance. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LogsBridgeExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif public Logger GetLogger(string? name, string? version) { if (!this.TryCreateLogger(name, out var logger)) @@ -68,6 +86,9 @@ public Logger GetLogger(string? name, string? version) /// Optional name identifying the instrumentation library. /// . /// if the logger was created. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LogsBridgeExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif protected virtual bool TryCreateLogger( string? name, #if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER diff --git a/src/OpenTelemetry.Api/Logs/LoggerProviderBuilder.cs b/src/OpenTelemetry.Api/Logs/LoggerProviderBuilder.cs index c8379f46fb1..3fa9c6fc743 100644 --- a/src/OpenTelemetry.Api/Logs/LoggerProviderBuilder.cs +++ b/src/OpenTelemetry.Api/Logs/LoggerProviderBuilder.cs @@ -1,27 +1,31 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #nullable enable +#if NET8_0_OR_GREATER && EXPOSE_EXPERIMENTAL_FEATURES +using System.Diagnostics.CodeAnalysis; +using OpenTelemetry.Internal; +#endif + namespace OpenTelemetry.Logs; +#if EXPOSE_EXPERIMENTAL_FEATURES +/// +/// LoggerProviderBuilder base class. +/// +/// +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else /// /// LoggerProviderBuilder base class. /// -public abstract class LoggerProviderBuilder +internal +#endif + abstract class LoggerProviderBuilder { /// /// Initializes a new instance of the class. @@ -38,5 +42,5 @@ protected LoggerProviderBuilder() /// Returns for chaining. public abstract LoggerProviderBuilder AddInstrumentation( Func instrumentationFactory) - where TInstrumentation : class; + where TInstrumentation : class?; } diff --git a/src/OpenTelemetry.Api/Logs/NoopLogger.cs b/src/OpenTelemetry.Api/Logs/NoopLogger.cs index 51ba1d81042..f33ec668aca 100644 --- a/src/OpenTelemetry.Api/Logs/NoopLogger.cs +++ b/src/OpenTelemetry.Api/Logs/NoopLogger.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #nullable enable diff --git a/src/OpenTelemetry.Api/Metrics/IDeferredMeterProviderBuilder.cs b/src/OpenTelemetry.Api/Metrics/IDeferredMeterProviderBuilder.cs index b33ee69575b..c283219c431 100644 --- a/src/OpenTelemetry.Api/Metrics/IDeferredMeterProviderBuilder.cs +++ b/src/OpenTelemetry.Api/Metrics/IDeferredMeterProviderBuilder.cs @@ -1,34 +1,22 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Metrics +#nullable enable + +namespace OpenTelemetry.Metrics; + +/// +/// Describes a meter provider builder that supports deferred initialization +/// using an to perform dependency injection. +/// +public interface IDeferredMeterProviderBuilder { /// - /// Describes a meter provider builder that supports deferred initialization - /// using an to perform dependency injection. + /// Register a callback action to configure the once the application is available. /// - public interface IDeferredMeterProviderBuilder - { - /// - /// Register a callback action to configure the once the application is available. - /// - /// Configuration callback. - /// The supplied for chaining. - MeterProviderBuilder Configure(Action configure); - } + /// Configuration callback. + /// The supplied for chaining. + MeterProviderBuilder Configure(Action configure); } diff --git a/src/OpenTelemetry.Api/Metrics/MeterProvider.cs b/src/OpenTelemetry.Api/Metrics/MeterProvider.cs index 80859e56cb5..a16fd88df93 100644 --- a/src/OpenTelemetry.Api/Metrics/MeterProvider.cs +++ b/src/OpenTelemetry.Api/Metrics/MeterProvider.cs @@ -1,31 +1,19 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Metrics +#nullable enable + +namespace OpenTelemetry.Metrics; + +/// +/// MeterProvider base class. +/// +public class MeterProvider : BaseProvider { /// - /// MeterProvider base class. + /// Initializes a new instance of the class. /// - public class MeterProvider : BaseProvider + protected MeterProvider() { - /// - /// Initializes a new instance of the class. - /// - protected MeterProvider() - { - } } } diff --git a/src/OpenTelemetry.Api/Metrics/MeterProviderBuilder.cs b/src/OpenTelemetry.Api/Metrics/MeterProviderBuilder.cs index af79f1cc237..95075dbe703 100644 --- a/src/OpenTelemetry.Api/Metrics/MeterProviderBuilder.cs +++ b/src/OpenTelemetry.Api/Metrics/MeterProviderBuilder.cs @@ -1,48 +1,36 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Metrics +#nullable enable + +namespace OpenTelemetry.Metrics; + +/// +/// MeterProviderBuilder base class. +/// +public abstract class MeterProviderBuilder { /// - /// MeterProviderBuilder base class. + /// Initializes a new instance of the class. /// - public abstract class MeterProviderBuilder + protected MeterProviderBuilder() { - /// - /// Initializes a new instance of the class. - /// - protected MeterProviderBuilder() - { - } + } - /// - /// Adds instrumentation to the provider. - /// - /// Type of instrumentation class. - /// Function that builds instrumentation. - /// Returns for chaining. - public abstract MeterProviderBuilder AddInstrumentation( - Func instrumentationFactory) - where TInstrumentation : class; + /// + /// Adds instrumentation to the provider. + /// + /// Type of instrumentation class. + /// Function that builds instrumentation. + /// Returns for chaining. + public abstract MeterProviderBuilder AddInstrumentation( + Func instrumentationFactory) + where TInstrumentation : class?; - /// - /// Adds given Meter names to the list of subscribed meters. - /// - /// Meter names. - /// Returns for chaining. - public abstract MeterProviderBuilder AddMeter(params string[] names); - } + /// + /// Adds given Meter names to the list of subscribed meters. + /// + /// Meter names. + /// Returns for chaining. + public abstract MeterProviderBuilder AddMeter(params string[] names); } diff --git a/src/OpenTelemetry.Api/OpenTelemetry.Api.csproj b/src/OpenTelemetry.Api/OpenTelemetry.Api.csproj index ba06279041c..0a781a94173 100644 --- a/src/OpenTelemetry.Api/OpenTelemetry.Api.csproj +++ b/src/OpenTelemetry.Api/OpenTelemetry.Api.csproj @@ -1,11 +1,8 @@ - - netstandard2.0;net462 + $(TargetFrameworksForLibraries) OpenTelemetry .NET API OpenTelemetry - - $(NoWarn),CS0618 core- @@ -15,4 +12,14 @@ + + + + + + + + + + diff --git a/src/OpenTelemetry.Api/README.md b/src/OpenTelemetry.Api/README.md index 9a412ee3c6b..f297a63c086 100644 --- a/src/OpenTelemetry.Api/README.md +++ b/src/OpenTelemetry.Api/README.md @@ -37,13 +37,13 @@ telemetry is exported to a specific telemetry backend, how to sample the telemetry, etc. The API consists of [Tracing API](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md), [Logging -API](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/overview.md), +API](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/README.md), [Metrics API](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md), [Context and Propagation API](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/context), and a set of [semantic -conventions](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/trace/semantic_conventions). +conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/general/trace.md). ### Tracing API @@ -185,7 +185,7 @@ here as well. ```csharp static ActivitySource activitySource = new ActivitySource( - "companyname.product.instrumentationlibrary", + "MyCompany.MyProduct.MyLibrary", "1.0.0"); ``` @@ -206,8 +206,10 @@ here as well. this activity are protected with a null check. 4. Populate activity with tags following the [OpenTelemetry semantic - conventions](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/trace/semantic_conventions). - It is highly recommended to check `activity.IsAllDataRequested`, before + conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/general/trace.md), + using the + [SetTag](https://learn.microsoft.com/dotnet/api/system.diagnostics.activity.settag) + API. It is highly recommended to check `activity.IsAllDataRequested`, before populating any tags which are not readily available. `IsAllDataRequested` is the same as [Span.IsRecording](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#isrecording) @@ -222,11 +224,6 @@ here as well. } ``` - The recommended way to [set span - attributes](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#set-attributes) - in `Activity` class is by using `SetTag()`. OpenTelemetry users should not - use other methods like `AddTag`, `SetCustomProperty` on `Activity`. - 5. Perform application/library logic. 6. Stop the activity when done. @@ -311,7 +308,7 @@ chose not to sample this activity. Attributes](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#set-attributes). Earlier sample showed the usage of `SetTag` method of `Activity` to add tags. Refer to the - [specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/common.md#attribute-and-label-naming) + [specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/attribute-naming.md) for best practices on naming tags. It is also possible to provide an initial set of tags during activity creation, as shown below. It is recommended to provide all available `Tags` during activity creation itself, as @@ -452,8 +449,9 @@ and [extract](../../examples/MicroserviceExample/Utils/Messaging/MessageReceiver.cs) context. -**Note on instrumentation libraries**: If you are using the instrumentation -libraries shipped from this repo [e.g. [ASP.NET +> [!NOTE] +> If you are using the instrumentation libraries shipped from this repo [e.g. +[ASP.NET Core](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src/OpenTelemetry.Instrumentation.AspNetCore) or [HttpClient](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src/OpenTelemetry.Instrumentation.Http)], @@ -503,7 +501,7 @@ Windows-based .NET implementation). The above requires import of the `System.Diagnostics.Metrics` namespace. - > **Note** + > [!NOTE] > It is important to note that `Meter` instances are created by using its constructor, and *not* by calling a `GetMeter` method on the `MeterProvider`. This is an important distinction from the [OpenTelemetry diff --git a/src/OpenTelemetry.Api/Trace/ActivityExtensions.cs b/src/OpenTelemetry.Api/Trace/ActivityExtensions.cs index 1234328f4f2..d8ac769b9c9 100644 --- a/src/OpenTelemetry.Api/Trace/ActivityExtensions.cs +++ b/src/OpenTelemetry.Api/Trace/ActivityExtensions.cs @@ -1,18 +1,7 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable using System.Diagnostics; using System.Runtime.CompilerServices; @@ -23,90 +12,95 @@ // same namespace as Activity to prevent name collisions in the future. // The OpenTelemetry.Trace namespace is used because Activity is analogous // to Span in OpenTelemetry. -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +/// +/// Extension methods on Activity. +/// +public static class ActivityExtensions { /// - /// Extension methods on Activity. + /// Sets the status of activity execution. + /// Activity class in .NET does not support 'Status'. + /// This extension provides a workaround to store Status as special tags with key name of otel.status_code and otel.status_description. + /// Read more about SetStatus here https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#set-status. /// - public static class ActivityExtensions + /// Activity instance. + /// Activity execution status. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetStatus(this Activity activity, Status status) { - /// - /// Sets the status of activity execution. - /// Activity class in .NET does not support 'Status'. - /// This extension provides a workaround to store Status as special tags with key name of otel.status_code and otel.status_description. - /// Read more about SetStatus here https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#set-status. - /// - /// Activity instance. - /// Activity execution status. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetStatus(this Activity activity, Status status) + if (activity != null) { - Debug.Assert(activity != null, "Activity should not be null"); - activity.SetTag(SpanAttributeConstants.StatusCodeKey, StatusHelper.GetTagValueForStatusCode(status.StatusCode)); activity.SetTag(SpanAttributeConstants.StatusDescriptionKey, status.Description); } + } - /// - /// Gets the status of activity execution. - /// Activity class in .NET does not support 'Status'. - /// This extension provides a workaround to retrieve Status from special tags with key name otel.status_code and otel.status_description. - /// - /// Activity instance. - /// Activity execution status. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Status GetStatus(this Activity activity) + /// + /// Gets the status of activity execution. + /// Activity class in .NET does not support 'Status'. + /// This extension provides a workaround to retrieve Status from special tags with key name otel.status_code and otel.status_description. + /// + /// Activity instance. + /// Activity execution status. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Status GetStatus(this Activity activity) + { + if (activity == null + || !activity.TryGetStatus(out var statusCode, out var statusDescription)) { - if (!activity.TryGetStatus(out StatusCode statusCode, out string statusDescription)) - { - return Status.Unset; - } - - return new Status(statusCode, statusDescription); + return Status.Unset; } - /// - /// Adds an activity event containing information from the specified exception. - /// - /// Activity instance. - /// Exception to be recorded. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void RecordException(this Activity activity, Exception ex) + return new Status(statusCode, statusDescription); + } + + /// + /// Adds an containing information from the specified exception. + /// + /// Activity instance. + /// Exception to be recorded. + /// The exception is recorded as per specification. + /// "exception.stacktrace" is represented using the value of Exception.ToString. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void RecordException(this Activity activity, Exception? ex) + => RecordException(activity, ex, default); + + /// + /// Adds an containing information from the specified exception and additional tags. + /// + /// Activity instance. + /// Exception to be recorded. + /// Additional tags to record on the event. + /// The exception is recorded as per specification. + /// "exception.stacktrace" is represented using the value of Exception.ToString. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void RecordException(this Activity activity, Exception? ex, in TagList tags) + { + if (ex == null || activity == null) { - activity?.RecordException(ex, default); + return; } - /// - /// Adds an activity event containing information from the specified exception and additional tags. - /// - /// Activity instance. - /// Exception to be recorded. - /// Additional tags to record on the event. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void RecordException(this Activity activity, Exception ex, in TagList tags) + var tagsCollection = new ActivityTagsCollection { - if (ex == null || activity == null) - { - return; - } - - var tagsCollection = new ActivityTagsCollection - { - { SemanticConventions.AttributeExceptionType, ex.GetType().FullName }, - { SemanticConventions.AttributeExceptionStacktrace, ex.ToInvariantString() }, - }; + { SemanticConventions.AttributeExceptionType, ex.GetType().FullName }, + { SemanticConventions.AttributeExceptionStacktrace, ex.ToInvariantString() }, + }; - if (!string.IsNullOrWhiteSpace(ex.Message)) - { - tagsCollection.Add(SemanticConventions.AttributeExceptionMessage, ex.Message); - } - - foreach (var tag in tags) - { - tagsCollection[tag.Key] = tag.Value; - } + if (!string.IsNullOrWhiteSpace(ex.Message)) + { + tagsCollection.Add(SemanticConventions.AttributeExceptionMessage, ex.Message); + } - activity.AddEvent(new ActivityEvent(SemanticConventions.AttributeExceptionEventName, default, tagsCollection)); + foreach (var tag in tags) + { + tagsCollection[tag.Key] = tag.Value; } + + activity.AddEvent(new ActivityEvent(SemanticConventions.AttributeExceptionEventName, default, tagsCollection)); } } diff --git a/src/OpenTelemetry.Api/Trace/IDeferredTracerProviderBuilder.cs b/src/OpenTelemetry.Api/Trace/IDeferredTracerProviderBuilder.cs index 9ca1f3b51cf..8be46ae4432 100644 --- a/src/OpenTelemetry.Api/Trace/IDeferredTracerProviderBuilder.cs +++ b/src/OpenTelemetry.Api/Trace/IDeferredTracerProviderBuilder.cs @@ -1,35 +1,23 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Trace +#nullable enable + +namespace OpenTelemetry.Trace; + +/// +/// Describes a tracer provider builder that supports deferred +/// initialization using an to perform +/// dependency injection. +/// +public interface IDeferredTracerProviderBuilder { /// - /// Describes a tracer provider builder that supports deferred - /// initialization using an to perform - /// dependency injection. + /// Register a callback action to configure the once the application is available. /// - public interface IDeferredTracerProviderBuilder - { - /// - /// Register a callback action to configure the once the application is available. - /// - /// Configuration callback. - /// The supplied for chaining. - TracerProviderBuilder Configure(Action configure); - } + /// Configuration callback. + /// The supplied for chaining. + TracerProviderBuilder Configure(Action configure); } diff --git a/src/OpenTelemetry.Api/Trace/Link.cs b/src/OpenTelemetry.Api/Trace/Link.cs index 46feae2adc4..45af8791625 100644 --- a/src/OpenTelemetry.Api/Trace/Link.cs +++ b/src/OpenTelemetry.Api/Trace/Link.cs @@ -1,101 +1,75 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable using System.Diagnostics; -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +/// +/// Link associated with the span. +/// +public readonly struct Link : IEquatable { + internal readonly ActivityLink ActivityLink; + /// - /// Link associated with the span. + /// Initializes a new instance of the struct. /// - public readonly struct Link : System.IEquatable + /// Span context of a linked span. + public Link(in SpanContext spanContext) + : this(in spanContext, attributes: null) { - internal readonly ActivityLink ActivityLink; - - /// - /// Initializes a new instance of the struct. - /// - /// Span context of a linked span. - public Link(in SpanContext spanContext) - { - this.ActivityLink = new ActivityLink(spanContext.ActivityContext); - } + } - /// - /// Initializes a new instance of the struct. - /// - /// Span context of a linked span. - /// Link attributes. - public Link(in SpanContext spanContext, SpanAttributes attributes) - { - this.ActivityLink = new ActivityLink(spanContext.ActivityContext, attributes?.Attributes); - } + /// + /// Initializes a new instance of the struct. + /// + /// Span context of a linked span. + /// Link attributes. + public Link(in SpanContext spanContext, SpanAttributes? attributes) + { + this.ActivityLink = new ActivityLink(spanContext.ActivityContext, attributes?.Attributes); + } - /// - /// Gets the span context of a linked span. - /// - public SpanContext Context - { - get - { - return new SpanContext(this.ActivityLink.Context); - } - } + /// + /// Gets the span context of a linked span. + /// + public SpanContext Context + => new(this.ActivityLink.Context); - /// - /// Gets the collection of attributes associated with the link. - /// - public IEnumerable> Attributes - { - get - { - return this.ActivityLink.Tags; - } - } + /// + /// Gets the collection of attributes associated with the link. + /// + public IEnumerable>? Attributes + => this.ActivityLink.Tags; - /// - /// Compare two for equality. - /// - /// First link to compare. - /// Second link to compare. - public static bool operator ==(Link link1, Link link2) => link1.Equals(link2); + /// + /// Compare two for equality. + /// + /// First link to compare. + /// Second link to compare. + public static bool operator ==(Link link1, Link link2) + => link1.Equals(link2); - /// - /// Compare two for not equality. - /// - /// First link to compare. - /// Second link to compare. - public static bool operator !=(Link link1, Link link2) => !link1.Equals(link2); + /// + /// Compare two for not equality. + /// + /// First link to compare. + /// Second link to compare. + public static bool operator !=(Link link1, Link link2) + => !link1.Equals(link2); - /// - public override bool Equals(object obj) - { - return (obj is Link link) && this.ActivityLink.Equals(link.ActivityLink); - } + /// + public override bool Equals(object? obj) + => obj is Link link && this.ActivityLink.Equals(link.ActivityLink); - /// - public override int GetHashCode() - { - return this.ActivityLink.GetHashCode(); - } + /// + public override int GetHashCode() + => this.ActivityLink.GetHashCode(); - /// - public bool Equals(Link other) - { - return this.ActivityLink.Equals(other.ActivityLink); - } - } + /// + public bool Equals(Link other) + => this.ActivityLink.Equals(other.ActivityLink); } diff --git a/src/OpenTelemetry.Api/Trace/SpanAttributes.cs b/src/OpenTelemetry.Api/Trace/SpanAttributes.cs index 02d74c9ff2c..84850ae048d 100644 --- a/src/OpenTelemetry.Api/Trace/SpanAttributes.cs +++ b/src/OpenTelemetry.Api/Trace/SpanAttributes.cs @@ -1,140 +1,128 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable using System.Diagnostics; using OpenTelemetry.Internal; -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +/// +/// A class that represents the span attributes. Read more here https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/README.md#attribute. +/// +/// SpanAttributes is a wrapper around class. +public class SpanAttributes { /// - /// A class that represents the span attributes. Read more here https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/common.md#attributes. + /// Initializes a new instance of the class. /// - /// SpanAttributes is a wrapper around class. - public class SpanAttributes + public SpanAttributes() { - /// - /// Initializes a new instance of the class. - /// - public SpanAttributes() - { - this.Attributes = new ActivityTagsCollection(); - } + this.Attributes = new ActivityTagsCollection(); + } - /// - /// Initializes a new instance of the class. - /// - /// Initial attributes to store in the collection. - public SpanAttributes(IEnumerable> attributes) - : this() - { - Guard.ThrowIfNull(attributes); + /// + /// Initializes a new instance of the class. + /// + /// Initial attributes to store in the collection. + public SpanAttributes(IEnumerable> attributes) + : this() + { + Guard.ThrowIfNull(attributes); - foreach (KeyValuePair kvp in attributes) - { - this.AddInternal(kvp.Key, kvp.Value); - } + foreach (KeyValuePair kvp in attributes) + { + this.AddInternal(kvp.Key, kvp.Value); } + } - internal ActivityTagsCollection Attributes { get; } + internal ActivityTagsCollection Attributes { get; } - /// - /// Add entry to the attributes. - /// - /// Entry key. - /// Entry value. - public void Add(string key, long value) - { - this.AddInternal(key, value); - } + /// + /// Add entry to the attributes. + /// + /// Entry key. + /// Entry value. + public void Add(string key, long value) + { + this.AddInternal(key, value); + } - /// - /// Add entry to the attributes. - /// - /// Entry key. - /// Entry value. - public void Add(string key, string value) - { - this.AddInternal(key, value); - } + /// + /// Add entry to the attributes. + /// + /// Entry key. + /// Entry value. + public void Add(string key, string? value) + { + this.AddInternal(key, value); + } - /// - /// Add entry to the attributes. - /// - /// Entry key. - /// Entry value. - public void Add(string key, bool value) - { - this.AddInternal(key, value); - } + /// + /// Add entry to the attributes. + /// + /// Entry key. + /// Entry value. + public void Add(string key, bool value) + { + this.AddInternal(key, value); + } - /// - /// Add entry to the attributes. - /// - /// Entry key. - /// Entry value. - public void Add(string key, double value) - { - this.AddInternal(key, value); - } + /// + /// Add entry to the attributes. + /// + /// Entry key. + /// Entry value. + public void Add(string key, double value) + { + this.AddInternal(key, value); + } - /// - /// Add entry to the attributes. - /// - /// Entry key. - /// Entry value. - public void Add(string key, long[] values) - { - this.AddInternal(key, values); - } + /// + /// Add entry to the attributes. + /// + /// Entry key. + /// Entry value. + public void Add(string key, long[]? values) + { + this.AddInternal(key, values); + } - /// - /// Add entry to the attributes. - /// - /// Entry key. - /// Entry value. - public void Add(string key, string[] values) - { - this.AddInternal(key, values); - } + /// + /// Add entry to the attributes. + /// + /// Entry key. + /// Entry value. + public void Add(string key, string?[]? values) + { + this.AddInternal(key, values); + } - /// - /// Add entry to the attributes. - /// - /// Entry key. - /// Entry value. - public void Add(string key, bool[] values) - { - this.AddInternal(key, values); - } + /// + /// Add entry to the attributes. + /// + /// Entry key. + /// Entry value. + public void Add(string key, bool[]? values) + { + this.AddInternal(key, values); + } - /// - /// Add entry to the attributes. - /// - /// Entry key. - /// Entry value. - public void Add(string key, double[] values) - { - this.AddInternal(key, values); - } + /// + /// Add entry to the attributes. + /// + /// Entry key. + /// Entry value. + public void Add(string key, double[]? values) + { + this.AddInternal(key, values); + } - private void AddInternal(string key, object value) - { - Guard.ThrowIfNull(key); + private void AddInternal(string key, object? value) + { + Guard.ThrowIfNull(key); - this.Attributes[key] = value; - } + this.Attributes[key] = value; } } diff --git a/src/OpenTelemetry.Api/Trace/SpanContext.cs b/src/OpenTelemetry.Api/Trace/SpanContext.cs index a2915454236..b94f50cc5f6 100644 --- a/src/OpenTelemetry.Api/Trace/SpanContext.cs +++ b/src/OpenTelemetry.Api/Trace/SpanContext.cs @@ -1,172 +1,139 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + using System.Diagnostics; using OpenTelemetry.Context.Propagation; -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +/// +/// A struct that represents a span context. A span context contains the portion of a span +/// that must propagate to child and across process boundaries. +/// It contains the identifiers and +/// associated with the along with a set of +/// common and system-specific values>. +/// +/// SpanContext is a wrapper around . +public readonly struct SpanContext : IEquatable { + internal readonly ActivityContext ActivityContext; + /// - /// A struct that represents a span context. A span context contains the portion of a span - /// that must propagate to child and across process boundaries. - /// It contains the identifiers and - /// associated with the along with a set of - /// common and system-specific values>. + /// Initializes a new instance of the struct with the given identifiers and options. /// - /// SpanContext is a wrapper around . - public readonly struct SpanContext : System.IEquatable + /// The to associate with the . + /// The to associate with the . + /// The to + /// associate with the . + /// The value indicating whether this was propagated from the remote parent. + /// The traceState to associate with the . + public SpanContext( + in ActivityTraceId traceId, + in ActivitySpanId spanId, + ActivityTraceFlags traceFlags, + bool isRemote = false, + IEnumerable>? traceState = null) { - internal readonly ActivityContext ActivityContext; - - /// - /// Initializes a new instance of the struct with the given identifiers and options. - /// - /// The to associate with the . - /// The to associate with the . - /// The to - /// associate with the . - /// The value indicating whether this was propagated from the remote parent. - /// The traceState to associate with the . - public SpanContext(in ActivityTraceId traceId, in ActivitySpanId spanId, ActivityTraceFlags traceFlags, bool isRemote = false, IEnumerable> traceState = null) - { - this.ActivityContext = new ActivityContext(traceId, spanId, traceFlags, TraceStateUtilsNew.GetString(traceState), isRemote); - } + this.ActivityContext = new ActivityContext(traceId, spanId, traceFlags, TraceStateUtilsNew.GetString(traceState), isRemote); + } - /// - /// Initializes a new instance of the struct with the given identifiers and options. - /// - /// The activity context. - public SpanContext(in ActivityContext activityContext) - { - this.ActivityContext = activityContext; - } + /// + /// Initializes a new instance of the struct with the given identifiers and options. + /// + /// The activity context. + public SpanContext(in ActivityContext activityContext) + { + this.ActivityContext = activityContext; + } - /// - /// Gets the associated with this . - /// - public ActivityTraceId TraceId - { - get - { - return this.ActivityContext.TraceId; - } - } + /// + /// Gets the associated with this . + /// + public ActivityTraceId TraceId + => this.ActivityContext.TraceId; - /// - /// Gets the associated with this . - /// - public ActivitySpanId SpanId - { - get - { - return this.ActivityContext.SpanId; - } - } + /// + /// Gets the associated with this . + /// + public ActivitySpanId SpanId + => this.ActivityContext.SpanId; - /// - /// Gets the associated with this . - /// - public ActivityTraceFlags TraceFlags - { - get - { - return this.ActivityContext.TraceFlags; - } - } + /// + /// Gets the associated with this . + /// + public ActivityTraceFlags TraceFlags + => this.ActivityContext.TraceFlags; - /// - /// Gets a value indicating whether this - /// was propagated from a remote parent. - /// - public bool IsRemote - { - get - { - return this.ActivityContext.IsRemote; - } - } + /// + /// Gets a value indicating whether this + /// was propagated from a remote parent. + /// + public bool IsRemote + => this.ActivityContext.IsRemote; - /// - /// Gets a value indicating whether this is valid. - /// - public bool IsValid => IsTraceIdValid(this.TraceId) && IsSpanIdValid(this.SpanId); + /// + /// Gets a value indicating whether this is valid. + /// + public bool IsValid => IsTraceIdValid(this.TraceId) && IsSpanIdValid(this.SpanId); - /// - /// Gets the associated with this . - /// - public IEnumerable> TraceState + /// + /// Gets the associated with this . + /// + public IEnumerable> TraceState + { + get { - get + if (string.IsNullOrEmpty(this.ActivityContext.TraceState)) { - if (string.IsNullOrEmpty(this.ActivityContext.TraceState)) - { - return Enumerable.Empty>(); - } - - var traceStateResult = new List>(); - TraceStateUtilsNew.AppendTraceState(this.ActivityContext.TraceState, traceStateResult); - return traceStateResult; + return Enumerable.Empty>(); } - } - /// - /// Converts a into an . - /// - /// source. - public static implicit operator ActivityContext(SpanContext spanContext) => spanContext.ActivityContext; - - /// - /// Compare two for equality. - /// - /// First SpanContext to compare. - /// Second SpanContext to compare. - public static bool operator ==(SpanContext spanContext1, SpanContext spanContext2) => spanContext1.Equals(spanContext2); - - /// - /// Compare two for not equality. - /// - /// First SpanContext to compare. - /// Second SpanContext to compare. - public static bool operator !=(SpanContext spanContext1, SpanContext spanContext2) => !spanContext1.Equals(spanContext2); - - /// - public override int GetHashCode() - { - return this.ActivityContext.GetHashCode(); + var traceStateResult = new List>(); + TraceStateUtilsNew.AppendTraceState(this.ActivityContext.TraceState, traceStateResult); + return traceStateResult; } + } - /// - public override bool Equals(object obj) - { - return (obj is SpanContext ctx) && this.ActivityContext.Equals(ctx.ActivityContext); - } + /// + /// Converts a into an . + /// + /// source. + public static implicit operator ActivityContext(SpanContext spanContext) + => spanContext.ActivityContext; - /// - public bool Equals(SpanContext other) - { - return this.ActivityContext.Equals(other.ActivityContext); - } + /// + /// Compare two for equality. + /// + /// First SpanContext to compare. + /// Second SpanContext to compare. + public static bool operator ==(SpanContext spanContext1, SpanContext spanContext2) + => spanContext1.Equals(spanContext2); - private static bool IsTraceIdValid(ActivityTraceId traceId) - { - return traceId != default; - } + /// + /// Compare two for not equality. + /// + /// First SpanContext to compare. + /// Second SpanContext to compare. + public static bool operator !=(SpanContext spanContext1, SpanContext spanContext2) + => !spanContext1.Equals(spanContext2); - private static bool IsSpanIdValid(ActivitySpanId spanId) - { - return spanId != default; - } - } + /// + public override int GetHashCode() + => this.ActivityContext.GetHashCode(); + + /// + public override bool Equals(object? obj) + => obj is SpanContext ctx && this.Equals(ctx); + + /// + public bool Equals(SpanContext other) + => this.ActivityContext.Equals(other.ActivityContext); + + private static bool IsTraceIdValid(ActivityTraceId traceId) + => traceId != default; + + private static bool IsSpanIdValid(ActivitySpanId spanId) + => spanId != default; } diff --git a/src/OpenTelemetry.Api/Trace/SpanKind.cs b/src/OpenTelemetry.Api/Trace/SpanKind.cs index da273805dfc..f3237a6bd4a 100644 --- a/src/OpenTelemetry.Api/Trace/SpanKind.cs +++ b/src/OpenTelemetry.Api/Trace/SpanKind.cs @@ -1,53 +1,41 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Trace +#nullable enable + +namespace OpenTelemetry.Trace; + +/// +/// Span kind. +/// +public enum SpanKind { /// - /// Span kind. + /// Span kind was not specified. /// - public enum SpanKind - { - /// - /// Span kind was not specified. - /// - Internal = 1, + Internal = 1, - /// - /// Server span represents request incoming from external component. - /// - Server = 2, + /// + /// Server span represents request incoming from external component. + /// + Server = 2, - /// - /// Client span represents outgoing request to the external component. - /// - Client = 3, + /// + /// Client span represents outgoing request to the external component. + /// + Client = 3, - /// - /// Producer span represents output provided to external components. Unlike client and - /// server, there is no direct critical path latency relationship between producer and consumer - /// spans. - /// - Producer = 4, + /// + /// Producer span represents output provided to external components. Unlike client and + /// server, there is no direct critical path latency relationship between producer and consumer + /// spans. + /// + Producer = 4, - /// - /// Consumer span represents output received from an external component. Unlike client and - /// server, there is no direct critical path latency relationship between producer and consumer - /// spans. - /// - Consumer = 5, - } + /// + /// Consumer span represents output received from an external component. Unlike client and + /// server, there is no direct critical path latency relationship between producer and consumer + /// spans. + /// + Consumer = 5, } diff --git a/src/OpenTelemetry.Api/Trace/Status.cs b/src/OpenTelemetry.Api/Trace/Status.cs index a8585c25ddd..b7d0eb35c32 100644 --- a/src/OpenTelemetry.Api/Trace/Status.cs +++ b/src/OpenTelemetry.Api/Trace/Status.cs @@ -1,132 +1,110 @@ -// // 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. -// - -namespace OpenTelemetry.Trace +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +namespace OpenTelemetry.Trace; + +/// +/// Span execution status. +/// +public readonly struct Status : IEquatable { /// - /// Span execution status. + /// The operation completed successfully. + /// + public static readonly Status Ok = new(StatusCode.Ok); + + /// + /// The default status. /// - public readonly struct Status : System.IEquatable + public static readonly Status Unset = new(StatusCode.Unset); + + /// + /// The operation contains an error. + /// + public static readonly Status Error = new(StatusCode.Error); + + internal Status(StatusCode statusCode, string? description = null) { - /// - /// The operation completed successfully. - /// - public static readonly Status Ok = new(StatusCode.Ok); - - /// - /// The default status. - /// - public static readonly Status Unset = new(StatusCode.Unset); - - /// - /// The operation contains an error. - /// - public static readonly Status Error = new(StatusCode.Error); - - internal Status(StatusCode statusCode, string description = null) - { - this.StatusCode = statusCode; - this.Description = description; - } + this.StatusCode = statusCode; + this.Description = description; + } - /// - /// Gets the canonical code from this status. - /// - public StatusCode StatusCode { get; } - - /// - /// Gets the status description. - /// - public string Description { get; } - - /// - /// Compare two for equality. - /// - /// First Status to compare. - /// Second Status to compare. - public static bool operator ==(Status status1, Status status2) => status1.Equals(status2); - - /// - /// Compare two for not equality. - /// - /// First Status to compare. - /// Second Status to compare. - public static bool operator !=(Status status1, Status status2) => !status1.Equals(status2); - - /// - /// Returns a new instance of a status with the description populated. - /// - /// - /// Note: Status Description is only valid for Status and will be ignored for all other - /// values. See the Status - /// API for details. - /// - /// Description of the status. - /// New instance of the status class with the description populated. - public Status WithDescription(string description) - { - if (this.StatusCode != StatusCode.Error || this.Description == description) - { - return this; - } + /// + /// Gets the canonical code from this status. + /// + public StatusCode StatusCode { get; } - return new Status(this.StatusCode, description); - } + /// + /// Gets the status description. + /// + public string? Description { get; } - /// - public override bool Equals(object obj) - { - if (obj is not Status) - { - return false; - } + /// + /// Compare two for equality. + /// + /// First Status to compare. + /// Second Status to compare. + public static bool operator ==(Status status1, Status status2) => status1.Equals(status2); - var that = (Status)obj; - return this.StatusCode == that.StatusCode && this.Description == that.Description; - } + /// + /// Compare two for not equality. + /// + /// First Status to compare. + /// Second Status to compare. + public static bool operator !=(Status status1, Status status2) => !status1.Equals(status2); - /// - public override int GetHashCode() + /// + /// Returns a new instance of a status with the description populated. + /// + /// + /// Note: Status Description is only valid for Status and will be ignored for all other + /// values. See the Status + /// API for details. + /// + /// Description of the status. + /// New instance of the status class with the description populated. + public Status WithDescription(string? description) + { + if (this.StatusCode != StatusCode.Error || this.Description == description) { - var hash = 17; - unchecked - { - hash = (31 * hash) + this.StatusCode.GetHashCode(); - hash = (31 * hash) + (this.Description?.GetHashCode() ?? 0); - } - - return hash; + return this; } - /// - public override string ToString() - { - return nameof(Status) - + "{" - + nameof(this.StatusCode) + "=" + this.StatusCode + ", " - + nameof(this.Description) + "=" + this.Description - + "}"; - } + return new Status(this.StatusCode, description); + } + + /// + public override bool Equals(object? obj) + => obj is Status status && this.Equals(status); - /// - public bool Equals(Status other) + /// + public override int GetHashCode() + { + var hash = 17; + unchecked { - return this.StatusCode == other.StatusCode && this.Description == other.Description; + hash = (31 * hash) + this.StatusCode.GetHashCode(); + hash = (31 * hash) + (this.Description?.GetHashCode() ?? 0); } + + return hash; } + + /// + public override string ToString() + { + return nameof(Status) + + "{" + + nameof(this.StatusCode) + "=" + this.StatusCode + ", " + + nameof(this.Description) + "=" + this.Description + + "}"; + } + + /// + public bool Equals(Status other) + => this.StatusCode == other.StatusCode && this.Description == other.Description; } diff --git a/src/OpenTelemetry.Api/Trace/StatusCode.cs b/src/OpenTelemetry.Api/Trace/StatusCode.cs index bcae40c096b..9332d708d06 100644 --- a/src/OpenTelemetry.Api/Trace/StatusCode.cs +++ b/src/OpenTelemetry.Api/Trace/StatusCode.cs @@ -1,39 +1,27 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Trace +#nullable enable + +namespace OpenTelemetry.Trace; + +/// +/// Canonical result code of span execution. +/// +public enum StatusCode { /// - /// Canonical result code of span execution. + /// The default status. /// - public enum StatusCode - { - /// - /// The default status. - /// - Unset = 0, + Unset = 0, - /// - /// The operation completed successfully. - /// - Ok = 1, + /// + /// The operation completed successfully. + /// + Ok = 1, - /// - /// The operation contains an error. - /// - Error = 2, - } + /// + /// The operation contains an error. + /// + Error = 2, } diff --git a/src/OpenTelemetry.Api/Trace/TelemetrySpan.cs b/src/OpenTelemetry.Api/Trace/TelemetrySpan.cs index 1b4a69ba009..f70598d078b 100644 --- a/src/OpenTelemetry.Api/Trace/TelemetrySpan.cs +++ b/src/OpenTelemetry.Api/Trace/TelemetrySpan.cs @@ -1,380 +1,345 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable using System.Diagnostics; using System.Runtime.CompilerServices; using OpenTelemetry.Internal; -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +/// +/// Span represents a single operation within a trace. +/// +/// TelemetrySpan is a wrapper around class. +public class TelemetrySpan : IDisposable { + internal static readonly TelemetrySpan NoopInstance = new(null); + internal readonly Activity? Activity; + + internal TelemetrySpan(Activity? activity) + { + this.Activity = activity; + } + /// - /// Span represents a single operation within a trace. + /// Gets the span context. /// - /// TelemetrySpan is a wrapper around class. - public class TelemetrySpan : IDisposable + public SpanContext Context + => this.Activity == null ? default : new SpanContext(this.Activity.Context); + + /// + /// Gets a value indicating whether this span will be recorded. + /// + public bool IsRecording + => this.Activity?.IsAllDataRequested == true; + + /// + /// Gets the identity of the parent span id, if any. + /// + public ActivitySpanId ParentSpanId + => this.Activity?.ParentSpanId ?? default; + + /// + /// Sets the status of the span execution. + /// + /// Status to be set. + public void SetStatus(Status value) { - internal static readonly TelemetrySpan NoopInstance = new(null); - internal readonly Activity Activity; + this.Activity?.SetStatus(value); + } - internal TelemetrySpan(Activity activity) + /// + /// Updates the name. + /// + /// If used, this will override the name provided via StartSpan method overload. + /// Upon this update, any sampling behavior based on name will depend on the + /// implementation. + /// + /// Name of the span. + /// The instance for chaining. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TelemetrySpan UpdateName(string name) + { + if (this.Activity != null) { - this.Activity = activity; + this.Activity.DisplayName = name; } - /// - /// Gets the span context. - /// - public SpanContext Context - { - get - { - if (this.Activity == null) - { - return default; - } - else - { - return new SpanContext(this.Activity.Context); - } - } - } + return this; + } - /// - /// Gets a value indicating whether this span will be recorded. - /// - public bool IsRecording - { - get - { - return this.Activity != null && this.Activity.IsAllDataRequested; - } - } + /// + /// Sets a new attribute on the span. + /// + /// Attribute key. + /// Attribute value. + /// The instance for chaining. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TelemetrySpan SetAttribute(string key, string? value) + { + this.SetAttributeInternal(key, value); + return this; + } - /// - /// Gets the identity of the parent span id, if any. - /// - public ActivitySpanId ParentSpanId - { - get - { - if (this.Activity == null) - { - return default; - } - else - { - return this.Activity.ParentSpanId; - } - } - } + /// + /// Sets a new attribute on the span. + /// + /// Attribute key. + /// Attribute value. + /// The instance for chaining. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TelemetrySpan SetAttribute(string key, int value) + { + this.SetAttributeInternal(key, value); + return this; + } - /// - /// Sets the status of the span execution. - /// - /// Status to be set. - public void SetStatus(Status value) - { - this.Activity?.SetStatus(value); - } + /// + /// Sets a new attribute on the span. + /// + /// Attribute key. + /// Attribute value. + /// The instance for chaining. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TelemetrySpan SetAttribute(string key, bool value) + { + this.SetAttributeInternal(key, value); + return this; + } - /// - /// Updates the name. - /// - /// If used, this will override the name provided via StartSpan method overload. - /// Upon this update, any sampling behavior based on name will depend on the - /// implementation. - /// - /// Name of the span. - /// The instance for chaining. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TelemetrySpan UpdateName(string name) - { - if (this.Activity != null) - { - this.Activity.DisplayName = name; - } + /// + /// Sets a new attribute on the span. + /// + /// Attribute key. + /// Attribute value. + /// The instance for chaining. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TelemetrySpan SetAttribute(string key, double value) + { + this.SetAttributeInternal(key, value); + return this; + } - return this; - } + /// + /// Sets a new attribute on the span. + /// + /// Attribute key. + /// Attribute values. + /// The instance for chaining. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TelemetrySpan SetAttribute(string key, string?[]? values) + { + this.SetAttributeInternal(key, values); + return this; + } - /// - /// Sets a new attribute on the span. - /// - /// Attribute key. - /// Attribute value. - /// The instance for chaining. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TelemetrySpan SetAttribute(string key, string value) - { - this.SetAttributeInternal(key, value); - return this; - } + /// + /// Sets a new attribute on the span. + /// + /// Attribute key. + /// Attribute values. + /// The instance for chaining. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TelemetrySpan SetAttribute(string key, int[]? values) + { + this.SetAttributeInternal(key, values); + return this; + } - /// - /// Sets a new attribute on the span. - /// - /// Attribute key. - /// Attribute value. - /// The instance for chaining. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TelemetrySpan SetAttribute(string key, int value) - { - this.SetAttributeInternal(key, value); - return this; - } + /// + /// Sets a new attribute on the span. + /// + /// Attribute key. + /// Attribute values. + /// The instance for chaining. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TelemetrySpan SetAttribute(string key, bool[]? values) + { + this.SetAttributeInternal(key, values); + return this; + } - /// - /// Sets a new attribute on the span. - /// - /// Attribute key. - /// Attribute value. - /// The instance for chaining. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TelemetrySpan SetAttribute(string key, bool value) - { - this.SetAttributeInternal(key, value); - return this; - } + /// + /// Sets a new attribute on the span. + /// + /// Attribute key. + /// Attribute values. + /// The instance for chaining. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TelemetrySpan SetAttribute(string key, double[]? values) + { + this.SetAttributeInternal(key, values); + return this; + } - /// - /// Sets a new attribute on the span. - /// - /// Attribute key. - /// Attribute value. - /// The instance for chaining. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TelemetrySpan SetAttribute(string key, double value) - { - this.SetAttributeInternal(key, value); - return this; - } + /// + /// Adds a single Event to the . + /// + /// Name of the event. + /// The instance for chaining. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TelemetrySpan AddEvent(string name) + { + this.AddEventInternal(name); + return this; + } - /// - /// Sets a new attribute on the span. - /// - /// Attribute key. - /// Attribute values. - /// The instance for chaining. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TelemetrySpan SetAttribute(string key, string[] values) - { - this.SetAttributeInternal(key, values); - return this; - } + /// + /// Adds a single Event to the . + /// + /// Name of the event. + /// Timestamp of the event. + /// The instance for chaining. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TelemetrySpan AddEvent(string name, DateTimeOffset timestamp) + { + this.AddEventInternal(name, timestamp); + return this; + } - /// - /// Sets a new attribute on the span. - /// - /// Attribute key. - /// Attribute values. - /// The instance for chaining. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TelemetrySpan SetAttribute(string key, int[] values) - { - this.SetAttributeInternal(key, values); - return this; - } + /// + /// Adds a single Event to the . + /// + /// Name of the event. + /// Attributes for the event. + /// The instance for chaining. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TelemetrySpan AddEvent(string name, SpanAttributes? attributes) + { + this.AddEventInternal(name, default, attributes?.Attributes); + return this; + } - /// - /// Sets a new attribute on the span. - /// - /// Attribute key. - /// Attribute values. - /// The instance for chaining. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TelemetrySpan SetAttribute(string key, bool[] values) - { - this.SetAttributeInternal(key, values); - return this; - } + /// + /// Adds a single Event to the . + /// + /// Name of the event. + /// Timestamp of the event. + /// Attributes for the event. + /// The instance for chaining. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TelemetrySpan AddEvent(string name, DateTimeOffset timestamp, SpanAttributes? attributes) + { + this.AddEventInternal(name, timestamp, attributes?.Attributes); + return this; + } - /// - /// Sets a new attribute on the span. - /// - /// Attribute key. - /// Attribute values. - /// The instance for chaining. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TelemetrySpan SetAttribute(string key, double[] values) - { - this.SetAttributeInternal(key, values); - return this; - } + /// + /// End the span. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void End() + { + this.Activity?.Stop(); + } - /// - /// Adds a single Event to the . - /// - /// Name of the event. - /// The instance for chaining. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TelemetrySpan AddEvent(string name) + /// + /// End the span. + /// + /// End timestamp. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void End(DateTimeOffset endTimestamp) + { + var activity = this.Activity; + if (activity != null) { - this.AddEventInternal(name); - return this; + activity.SetEndTime(endTimestamp.UtcDateTime); + activity.Stop(); } + } - /// - /// Adds a single Event to the . - /// - /// Name of the event. - /// Timestamp of the event. - /// The instance for chaining. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TelemetrySpan AddEvent(string name, DateTimeOffset timestamp) + /// + /// Record Exception. + /// + /// Exception to be recorded. + /// The instance for chaining. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TelemetrySpan RecordException(Exception? ex) + { + if (ex == null) { - this.AddEventInternal(name, timestamp); return this; } - /// - /// Adds a single Event to the . - /// - /// Name of the event. - /// Attributes for the event. - /// The instance for chaining. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TelemetrySpan AddEvent(string name, SpanAttributes attributes) - { - this.AddEventInternal(name, default, attributes?.Attributes); - return this; - } + return this.RecordException(ex.GetType().Name, ex.Message, ex.ToInvariantString()); + } - /// - /// Adds a single Event to the . - /// - /// Name of the event. - /// Timestamp of the event. - /// Attributes for the event. - /// The instance for chaining. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TelemetrySpan AddEvent(string name, DateTimeOffset timestamp, SpanAttributes attributes) - { - this.AddEventInternal(name, timestamp, attributes?.Attributes); - return this; - } + /// + /// Record Exception. + /// + /// Type of the exception to be recorded. + /// Message of the exception to be recorded. + /// Stacktrace of the exception to be recorded. + /// The instance for chaining. + public TelemetrySpan RecordException(string? type, string? message, string? stacktrace) + { + SpanAttributes attributes = new SpanAttributes(); - /// - /// End the span. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void End() + if (!string.IsNullOrWhiteSpace(type)) { - this.Activity?.Stop(); + attributes.Add(SemanticConventions.AttributeExceptionType, type); } - /// - /// End the span. - /// - /// End timestamp. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void End(DateTimeOffset endTimestamp) + if (!string.IsNullOrWhiteSpace(stacktrace)) { - this.Activity?.SetEndTime(endTimestamp.UtcDateTime); - this.Activity?.Stop(); + attributes.Add(SemanticConventions.AttributeExceptionStacktrace, stacktrace); } - /// - /// Record Exception. - /// - /// Exception to be recorded. - /// The instance for chaining. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TelemetrySpan RecordException(Exception ex) + if (!string.IsNullOrWhiteSpace(message)) { - if (ex == null) - { - return this; - } - - return this.RecordException(ex.GetType().Name, ex.Message, ex.ToInvariantString()); + attributes.Add(SemanticConventions.AttributeExceptionMessage, message); } - /// - /// Record Exception. - /// - /// Type of the exception to be recorded. - /// Message of the exception to be recorded. - /// Stacktrace of the exception to be recorded. - /// The instance for chaining. - public TelemetrySpan RecordException(string type, string message, string stacktrace) + if (attributes.Attributes.Count != 0) { - SpanAttributes attributes = new SpanAttributes(); - if (!string.IsNullOrWhiteSpace(type)) - { - attributes.Add(SemanticConventions.AttributeExceptionType, type); - } - - if (!string.IsNullOrWhiteSpace(stacktrace)) - { - attributes.Add(SemanticConventions.AttributeExceptionStacktrace, stacktrace); - } - - if (!string.IsNullOrWhiteSpace(message)) - { - attributes.Add(SemanticConventions.AttributeExceptionMessage, message); - } - - if (attributes.Attributes.Count != 0) - { - this.AddEvent(SemanticConventions.AttributeExceptionEventName, attributes); - } - - return this; + this.AddEvent(SemanticConventions.AttributeExceptionEventName, attributes); } - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } + return this; + } - /// - /// Marks the span as current. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void Activate() - { - Activity.Current = this.Activity; - } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Marks the span as current. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void Activate() + => Activity.Current = this.Activity; - /// - /// Releases the unmanaged resources used by this class and optionally releases the managed resources. - /// - /// to release both managed and unmanaged resources; to release only unmanaged resources. - protected virtual void Dispose(bool disposing) + /// + /// Releases the unmanaged resources used by this class and optionally releases the managed resources. + /// + /// to release both managed and unmanaged resources; to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (disposing) { this.Activity?.Dispose(); } + } - private void SetAttributeInternal(string key, object value) + private void SetAttributeInternal(string key, object? value) + { + if (this.IsRecording) { - if (this.IsRecording) - { - this.Activity.SetTag(key, value); - } + this.Activity!.SetTag(key, value); } + } - private void AddEventInternal(string name, DateTimeOffset timestamp = default, ActivityTagsCollection tags = null) + private void AddEventInternal(string name, DateTimeOffset timestamp = default, ActivityTagsCollection? tags = null) + { + if (this.IsRecording) { - if (this.IsRecording) - { - this.Activity.AddEvent(new ActivityEvent(name, timestamp, tags)); - } + this.Activity!.AddEvent(new ActivityEvent(name, timestamp, tags)); } } } diff --git a/src/OpenTelemetry.Api/Trace/Tracer.cs b/src/OpenTelemetry.Api/Trace/Tracer.cs index 4a656bc9700..44bf17e1f5c 100644 --- a/src/OpenTelemetry.Api/Trace/Tracer.cs +++ b/src/OpenTelemetry.Api/Trace/Tracer.cs @@ -1,190 +1,236 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +/// +/// Tracer is the class responsible for creating . +/// +/// Tracer is a wrapper around class. +public class Tracer { - /// - /// Tracer is the class responsible for creating . - /// - /// Tracer is a wrapper around class. - public class Tracer + internal ActivitySource? ActivitySource; + + internal Tracer(ActivitySource? activitySource) { - internal readonly ActivitySource ActivitySource; + this.ActivitySource = activitySource; + } - internal Tracer(ActivitySource activitySource) - { - this.ActivitySource = activitySource; - } + [Flags] + private enum StartSpanBehaviors + { + ActivateNewSpan = 0b1, + DeactivateNewSpan = 0b10, + NewSpanAsRoot = 0b100, + } - /// - /// Gets the current span from the context. - /// - public static TelemetrySpan CurrentSpan + /// + /// Gets the current span from the context. + /// + public static TelemetrySpan CurrentSpan + { + get { - get + var currentActivity = Activity.Current; + if (currentActivity == null) { - var currentActivity = Activity.Current; - if (currentActivity == null) - { - return TelemetrySpan.NoopInstance; - } - else - { - return new TelemetrySpan(currentActivity); - } + return TelemetrySpan.NoopInstance; + } + else + { + return new TelemetrySpan(currentActivity); } } + } - /// - /// Makes the given span as the current one. - /// - /// The span to be made current. - /// The current span. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static TelemetrySpan WithSpan(TelemetrySpan span) - { - span?.Activate(); - return span; - } + /// + /// Sets the given span as the current one in the context. + /// + /// The span to be made current. + /// The supplied span for call chaining. +#if NET6_0_OR_GREATER + [return: NotNullIfNotNull(nameof(span))] +#endif + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TelemetrySpan? WithSpan(TelemetrySpan? span) + { + span?.Activate(); + return span; + } - /// - /// Starts root span. - /// - /// Span name. - /// Kind. - /// Initial attributes for the span. - /// for the span. - /// Start time for the span. - /// Span instance. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TelemetrySpan StartRootSpan(string name, SpanKind kind = SpanKind.Internal, SpanAttributes initialAttributes = null, IEnumerable links = null, DateTimeOffset startTime = default) - { - return this.StartSpanHelper(false, name, kind, default, initialAttributes, links, startTime); - } + /// + /// Starts root span. + /// + /// Span name. + /// Kind. + /// Initial attributes for the span. + /// for the span. + /// Start time for the span. + /// Span instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TelemetrySpan StartRootSpan( + string name, + SpanKind kind = SpanKind.Internal, + SpanAttributes? initialAttributes = null, + IEnumerable? links = null, + DateTimeOffset startTime = default) + { + return this.StartSpanHelper(StartSpanBehaviors.NewSpanAsRoot | StartSpanBehaviors.DeactivateNewSpan, name, kind, default, initialAttributes, links, startTime); + } - /// - /// Starts a span and does not make it as current span. - /// - /// Span name. - /// Kind. - /// Parent for new span. - /// Initial attributes for the span. - /// for the span. - /// Start time for the span. - /// Span instance. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TelemetrySpan StartSpan(string name, SpanKind kind, in TelemetrySpan parentSpan, SpanAttributes initialAttributes = null, IEnumerable links = null, DateTimeOffset startTime = default) - { - return this.StartSpan(name, kind, parentSpan?.Context ?? default, initialAttributes, links, startTime); - } + /// + /// Starts a span and does not make it as current span. + /// + /// Span name. + /// Kind. + /// Parent for new span. + /// Initial attributes for the span. + /// for the span. + /// Start time for the span. + /// Span instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("roslyn", "RS0026", Justification = "TODO: fix APIs that violate the backcompt requirement - multiple overloads with optional parameters: https://github.com/dotnet/roslyn/blob/main/docs/Adding%20Optional%20Parameters%20in%20Public%20API.md.")] + public TelemetrySpan StartSpan( + string name, + SpanKind kind, + in TelemetrySpan? parentSpan, // <- TODO: Clean this up if we ever do a major release w/ breaking changes. The "in" here was probably a copy/paste mistake. Removing would be source compatible but binary breaking. + SpanAttributes? initialAttributes = null, + IEnumerable? links = null, + DateTimeOffset startTime = default) + { + return this.StartSpan(name, kind, parentSpan?.Context ?? default, initialAttributes, links, startTime); + } - /// - /// Starts a span and does not make it as current span. - /// - /// Span name. - /// Kind. - /// Parent Context for new span. - /// Initial attributes for the span. - /// for the span. - /// Start time for the span. - /// Span instance. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TelemetrySpan StartSpan(string name, SpanKind kind = SpanKind.Internal, in SpanContext parentContext = default, SpanAttributes initialAttributes = null, IEnumerable links = null, DateTimeOffset startTime = default) + /// + /// Starts a span and does not make it as current span. + /// + /// Span name. + /// Kind. + /// Parent Context for new span. + /// Initial attributes for the span. + /// for the span. + /// Start time for the span. + /// Span instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("roslyn", "RS0026", Justification = "TODO: fix APIs that violate the backcompt requirement - multiple overloads with optional parameters: https://github.com/dotnet/roslyn/blob/main/docs/Adding%20Optional%20Parameters%20in%20Public%20API.md.")] + public TelemetrySpan StartSpan( + string name, + SpanKind kind = SpanKind.Internal, + in SpanContext parentContext = default, + SpanAttributes? initialAttributes = null, + IEnumerable? links = null, + DateTimeOffset startTime = default) + { + return this.StartSpanHelper(StartSpanBehaviors.DeactivateNewSpan, name, kind, in parentContext, initialAttributes, links, startTime); + } + + /// + /// Starts a span and make it the current active span. + /// + /// Span name. + /// Kind. + /// Parent for new span. + /// Initial attributes for the span. + /// for the span. + /// Start time for the span. + /// Span instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("roslyn", "RS0026", Justification = "TODO: fix APIs that violate the backcompt requirement - multiple overloads with optional parameters: https://github.com/dotnet/roslyn/blob/main/docs/Adding%20Optional%20Parameters%20in%20Public%20API.md.")] + public TelemetrySpan StartActiveSpan( + string name, + SpanKind kind, + in TelemetrySpan? parentSpan, // <- TODO: Clean this up if we ever do a major release w/ breaking changes. The "in" here was probably a copy/paste mistake. Removing would be source compatible but binary breaking. + SpanAttributes? initialAttributes = null, + IEnumerable? links = null, + DateTimeOffset startTime = default) + { + return this.StartActiveSpan(name, kind, parentSpan?.Context ?? default, initialAttributes, links, startTime); + } + + /// + /// Starts a span and make it the current active span. + /// + /// Span name. + /// Kind. + /// Parent Context for new span. + /// Initial attributes for the span. + /// for the span. + /// Start time for the span. + /// Span instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("roslyn", "RS0026", Justification = "TODO: fix APIs that violate the backcompt requirement - multiple overloads with optional parameters: https://github.com/dotnet/roslyn/blob/main/docs/Adding%20Optional%20Parameters%20in%20Public%20API.md.")] + public TelemetrySpan StartActiveSpan( + string name, + SpanKind kind = SpanKind.Internal, + in SpanContext parentContext = default, + SpanAttributes? initialAttributes = null, + IEnumerable? links = null, + DateTimeOffset startTime = default) + { + return this.StartSpanHelper(StartSpanBehaviors.ActivateNewSpan, name, kind, in parentContext, initialAttributes, links, startTime); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ActivityKind ConvertToActivityKind(SpanKind kind) + { + return kind switch { - return this.StartSpanHelper(false, name, kind, parentContext, initialAttributes, links, startTime); - } + SpanKind.Client => ActivityKind.Client, + SpanKind.Consumer => ActivityKind.Consumer, + SpanKind.Internal => ActivityKind.Internal, + SpanKind.Producer => ActivityKind.Producer, + SpanKind.Server => ActivityKind.Server, + _ => ActivityKind.Internal, + }; + } - /// - /// Starts a span and make it the current active span. - /// - /// Span name. - /// Kind. - /// Parent for new span. - /// Initial attributes for the span. - /// for the span. - /// Start time for the span. - /// Span instance. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TelemetrySpan StartActiveSpan(string name, SpanKind kind, in TelemetrySpan parentSpan, SpanAttributes initialAttributes = null, IEnumerable links = null, DateTimeOffset startTime = default) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private TelemetrySpan StartSpanHelper( + StartSpanBehaviors startSpanBehavior, + string name, + SpanKind kind, + in SpanContext parentContext = default, + SpanAttributes? initialAttributes = null, + IEnumerable? links = null, + DateTimeOffset startTime = default) + { + var activitySource = this.ActivitySource; + + if (!(activitySource?.HasListeners() ?? false)) { - return this.StartActiveSpan(name, kind, parentSpan?.Context ?? default, initialAttributes, links, startTime); + return TelemetrySpan.NoopInstance; } - /// - /// Starts a span and make it the current active span. - /// - /// Span name. - /// Kind. - /// Parent Context for new span. - /// Initial attributes for the span. - /// for the span. - /// Start time for the span. - /// Span instance. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TelemetrySpan StartActiveSpan(string name, SpanKind kind = SpanKind.Internal, in SpanContext parentContext = default, SpanAttributes initialAttributes = null, IEnumerable links = null, DateTimeOffset startTime = default) + var activityKind = ConvertToActivityKind(kind); + var activityLinks = links?.Select(l => l.ActivityLink); + var previousActivity = Activity.Current; + + if (startSpanBehavior.HasFlag(StartSpanBehaviors.NewSpanAsRoot) + && previousActivity != null) { - return this.StartSpanHelper(true, name, kind, parentContext, initialAttributes, links, startTime); + Activity.Current = null; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ActivityKind ConvertToActivityKind(SpanKind kind) + try { - return kind switch - { - SpanKind.Client => ActivityKind.Client, - SpanKind.Consumer => ActivityKind.Consumer, - SpanKind.Internal => ActivityKind.Internal, - SpanKind.Producer => ActivityKind.Producer, - SpanKind.Server => ActivityKind.Server, - _ => ActivityKind.Internal, - }; + var activity = activitySource.StartActivity(name, activityKind, parentContext.ActivityContext, initialAttributes?.Attributes ?? null, activityLinks, startTime); + return activity == null + ? TelemetrySpan.NoopInstance + : new TelemetrySpan(activity); } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private TelemetrySpan StartSpanHelper(bool isActiveSpan, string name, SpanKind kind, in SpanContext parentContext = default, SpanAttributes initialAttributes = null, IEnumerable links = null, DateTimeOffset startTime = default) + finally { - if (!this.ActivitySource.HasListeners()) - { - return TelemetrySpan.NoopInstance; - } - - var activityKind = ConvertToActivityKind(kind); - var activityLinks = links?.Select(l => l.ActivityLink); - - Activity previousActivity = null; - if (!isActiveSpan) - { - previousActivity = Activity.Current; - } - - var activity = this.ActivitySource.StartActivity(name, activityKind, parentContext.ActivityContext, initialAttributes?.Attributes ?? null, activityLinks, startTime); - if (activity == null) - { - return TelemetrySpan.NoopInstance; - } - - if (!isActiveSpan) + if (startSpanBehavior.HasFlag(StartSpanBehaviors.DeactivateNewSpan) + && Activity.Current != previousActivity) { Activity.Current = previousActivity; } - - return new TelemetrySpan(activity); } } } diff --git a/src/OpenTelemetry.Api/Trace/TracerProvider.cs b/src/OpenTelemetry.Api/Trace/TracerProvider.cs index cad797c4011..1dc0a5f2936 100644 --- a/src/OpenTelemetry.Api/Trace/TracerProvider.cs +++ b/src/OpenTelemetry.Api/Trace/TracerProvider.cs @@ -1,54 +1,115 @@ -// // 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; - -namespace OpenTelemetry.Trace +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Collections.Concurrent; +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif + +namespace OpenTelemetry.Trace; + +/// +/// TracerProvider is the entry point of the OpenTelemetry API. It provides access to . +/// +public class TracerProvider : BaseProvider { + internal ConcurrentDictionary? Tracers = new(); + /// - /// TracerProvider is the entry point of the OpenTelemetry API. It provides access to . + /// Initializes a new instance of the class. /// - public class TracerProvider : BaseProvider + protected TracerProvider() { - /// - /// Initializes a new instance of the class. - /// - protected TracerProvider() + } + + /// + /// Gets the default . + /// + public static TracerProvider Default { get; } = new TracerProvider(); + + /// + /// Gets a tracer with given name and version. + /// + /// Name identifying the instrumentation library. + /// Version of the instrumentation library. + /// Tracer instance. + public Tracer GetTracer( +#if NET6_0_OR_GREATER + [AllowNull] +#endif + string name, + string? version = null) + { + var tracers = this.Tracers; + if (tracers == null) + { + // Note: Returns a no-op Tracer once dispose has been called. + return new(activitySource: null); + } + + var key = new TracerKey(name, version); + + if (!tracers.TryGetValue(key, out var tracer)) { + lock (tracers) + { + if (this.Tracers == null) + { + // Note: We check here for a race with Dispose and return a + // no-op Tracer in that case. + return new(activitySource: null); + } + + tracer = new(new(key.Name, key.Version)); +#if DEBUG + bool result = tracers.TryAdd(key, tracer); + System.Diagnostics.Debug.Assert(result, "Write into tracers cache failed"); +#else + tracers.TryAdd(key, tracer); +#endif + } } - /// - /// Gets the default . - /// - public static TracerProvider Default { get; } = new TracerProvider(); - - /// - /// Gets a tracer with given name and version. - /// - /// Name identifying the instrumentation library. - /// Version of the instrumentation library. - /// Tracer instance. - public Tracer GetTracer(string name, string version = null) + return tracer; + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) { - if (name == null) + var tracers = Interlocked.CompareExchange(ref this.Tracers, null, this.Tracers); + if (tracers != null) { - name = string.Empty; + lock (tracers) + { + foreach (var kvp in tracers) + { + var tracer = kvp.Value; + var activitySource = tracer.ActivitySource; + tracer.ActivitySource = null; + activitySource?.Dispose(); + } + + tracers.Clear(); + } } + } + + base.Dispose(disposing); + } - return new Tracer(new ActivitySource(name, version)); + internal readonly record struct TracerKey + { + public readonly string Name; + public readonly string? Version; + + public TracerKey(string? name, string? version) + { + this.Name = name ?? string.Empty; + this.Version = version; } } } diff --git a/src/OpenTelemetry.Api/Trace/TracerProviderBuilder.cs b/src/OpenTelemetry.Api/Trace/TracerProviderBuilder.cs index 3c773895630..0a8c3dae3da 100644 --- a/src/OpenTelemetry.Api/Trace/TracerProviderBuilder.cs +++ b/src/OpenTelemetry.Api/Trace/TracerProviderBuilder.cs @@ -1,59 +1,48 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + using System.Diagnostics; -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +/// +/// TracerProviderBuilder base class. +/// +public abstract class TracerProviderBuilder { /// - /// TracerProviderBuilder base class. + /// Initializes a new instance of the class. /// - public abstract class TracerProviderBuilder + protected TracerProviderBuilder() { - /// - /// Initializes a new instance of the class. - /// - protected TracerProviderBuilder() - { - } + } - /// - /// Adds instrumentation to the provider. - /// - /// Type of instrumentation class. - /// Function that builds instrumentation. - /// Returns for chaining. - public abstract TracerProviderBuilder AddInstrumentation( - Func instrumentationFactory) - where TInstrumentation : class; + /// + /// Adds instrumentation to the provider. + /// + /// Type of instrumentation class. + /// Function that builds instrumentation. + /// Returns for chaining. + public abstract TracerProviderBuilder AddInstrumentation( + Func instrumentationFactory) + where TInstrumentation : class?; - /// - /// Adds given activitysource names to the list of subscribed sources. - /// - /// Activity source names. - /// Returns for chaining. - public abstract TracerProviderBuilder AddSource(params string[] names); + /// + /// Adds the given names to the list of subscribed sources. + /// + /// Activity source names. + /// Returns for chaining. + public abstract TracerProviderBuilder AddSource(params string[] names); - /// - /// Adds a listener for objects created with the given operation name to the . - /// - /// - /// This is provided to capture legacy objects created without using the API. - /// - /// Operation name of the objects to capture. - /// Returns for chaining. - public abstract TracerProviderBuilder AddLegacySource(string operationName); - } + /// + /// Adds a listener for objects created with the given operation name to the . + /// + /// + /// This is provided to capture legacy objects created without using the API. + /// + /// Operation name of the objects to capture. + /// Returns for chaining. + public abstract TracerProviderBuilder AddLegacySource(string operationName); } diff --git a/src/OpenTelemetry.Exporter.Console/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Console/.publicApi/Experimental/PublicAPI.Unshipped.txt similarity index 99% rename from src/OpenTelemetry.Exporter.Console/.publicApi/net462/PublicAPI.Unshipped.txt rename to src/OpenTelemetry.Exporter.Console/.publicApi/Experimental/PublicAPI.Unshipped.txt index e1fbd2dc2c6..be114835ccd 100644 --- a/src/OpenTelemetry.Exporter.Console/.publicApi/net462/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.Console/.publicApi/Experimental/PublicAPI.Unshipped.txt @@ -1,3 +1,3 @@ static OpenTelemetry.Logs.ConsoleExporterLoggingExtensions.AddConsoleExporter(this OpenTelemetry.Logs.LoggerProviderBuilder loggerProviderBuilder) -> OpenTelemetry.Logs.LoggerProviderBuilder static OpenTelemetry.Logs.ConsoleExporterLoggingExtensions.AddConsoleExporter(this OpenTelemetry.Logs.LoggerProviderBuilder loggerProviderBuilder, string name, System.Action configure) -> OpenTelemetry.Logs.LoggerProviderBuilder -static OpenTelemetry.Logs.ConsoleExporterLoggingExtensions.AddConsoleExporter(this OpenTelemetry.Logs.LoggerProviderBuilder loggerProviderBuilder, System.Action configure) -> OpenTelemetry.Logs.LoggerProviderBuilder \ No newline at end of file +static OpenTelemetry.Logs.ConsoleExporterLoggingExtensions.AddConsoleExporter(this OpenTelemetry.Logs.LoggerProviderBuilder loggerProviderBuilder, System.Action configure) -> OpenTelemetry.Logs.LoggerProviderBuilder diff --git a/src/OpenTelemetry.Exporter.Console/.publicApi/net462/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.Console/.publicApi/Stable/PublicAPI.Shipped.txt similarity index 100% rename from src/OpenTelemetry.Exporter.Console/.publicApi/net462/PublicAPI.Shipped.txt rename to src/OpenTelemetry.Exporter.Console/.publicApi/Stable/PublicAPI.Shipped.txt diff --git a/src/OpenTelemetry.Exporter.Jaeger/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Console/.publicApi/Stable/PublicAPI.Unshipped.txt similarity index 100% rename from src/OpenTelemetry.Exporter.Jaeger/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt rename to src/OpenTelemetry.Exporter.Console/.publicApi/Stable/PublicAPI.Unshipped.txt diff --git a/src/OpenTelemetry.Exporter.Console/.publicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.Console/.publicApi/netstandard2.0/PublicAPI.Shipped.txt deleted file mode 100644 index 6f900a1f6f6..00000000000 --- a/src/OpenTelemetry.Exporter.Console/.publicApi/netstandard2.0/PublicAPI.Shipped.txt +++ /dev/null @@ -1,33 +0,0 @@ -OpenTelemetry.Exporter.ConsoleActivityExporter -OpenTelemetry.Exporter.ConsoleActivityExporter.ConsoleActivityExporter(OpenTelemetry.Exporter.ConsoleExporterOptions options) -> void -OpenTelemetry.Exporter.ConsoleExporter -OpenTelemetry.Exporter.ConsoleExporter.ConsoleExporter(OpenTelemetry.Exporter.ConsoleExporterOptions options) -> void -OpenTelemetry.Exporter.ConsoleExporter.WriteLine(string message) -> void -OpenTelemetry.Exporter.ConsoleExporterOptions -OpenTelemetry.Exporter.ConsoleExporterOptions.ConsoleExporterOptions() -> void -OpenTelemetry.Exporter.ConsoleExporterOptions.Targets.get -> OpenTelemetry.Exporter.ConsoleExporterOutputTargets -OpenTelemetry.Exporter.ConsoleExporterOptions.Targets.set -> void -OpenTelemetry.Exporter.ConsoleExporterOutputTargets -OpenTelemetry.Exporter.ConsoleExporterOutputTargets.Console = 1 -> OpenTelemetry.Exporter.ConsoleExporterOutputTargets -OpenTelemetry.Exporter.ConsoleExporterOutputTargets.Debug = 2 -> OpenTelemetry.Exporter.ConsoleExporterOutputTargets -OpenTelemetry.Exporter.ConsoleLogRecordExporter -OpenTelemetry.Exporter.ConsoleLogRecordExporter.ConsoleLogRecordExporter(OpenTelemetry.Exporter.ConsoleExporterOptions options) -> void -OpenTelemetry.Exporter.ConsoleMetricExporter -OpenTelemetry.Exporter.ConsoleMetricExporter.ConsoleMetricExporter(OpenTelemetry.Exporter.ConsoleExporterOptions options) -> void -OpenTelemetry.Logs.ConsoleExporterLoggingExtensions -OpenTelemetry.Metrics.ConsoleExporterMetricsExtensions -OpenTelemetry.Trace.ConsoleExporterHelperExtensions -override OpenTelemetry.Exporter.ConsoleActivityExporter.Export(in OpenTelemetry.Batch batch) -> OpenTelemetry.ExportResult -override OpenTelemetry.Exporter.ConsoleLogRecordExporter.Dispose(bool disposing) -> void -override OpenTelemetry.Exporter.ConsoleLogRecordExporter.Export(in OpenTelemetry.Batch batch) -> OpenTelemetry.ExportResult -override OpenTelemetry.Exporter.ConsoleMetricExporter.Export(in OpenTelemetry.Batch batch) -> OpenTelemetry.ExportResult -static OpenTelemetry.Logs.ConsoleExporterLoggingExtensions.AddConsoleExporter(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions loggerOptions) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions -static OpenTelemetry.Logs.ConsoleExporterLoggingExtensions.AddConsoleExporter(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions loggerOptions, System.Action configure) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions -static OpenTelemetry.Metrics.ConsoleExporterMetricsExtensions.AddConsoleExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.ConsoleExporterMetricsExtensions.AddConsoleExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, string name, System.Action configureExporterAndMetricReader) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.ConsoleExporterMetricsExtensions.AddConsoleExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, string name, System.Action configureExporter) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.ConsoleExporterMetricsExtensions.AddConsoleExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action configureExporterAndMetricReader) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.ConsoleExporterMetricsExtensions.AddConsoleExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action configureExporter) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Trace.ConsoleExporterHelperExtensions.AddConsoleExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.ConsoleExporterHelperExtensions.AddConsoleExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.ConsoleExporterHelperExtensions.AddConsoleExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Exporter.Console/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Console/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt deleted file mode 100644 index e1fbd2dc2c6..00000000000 --- a/src/OpenTelemetry.Exporter.Console/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,3 +0,0 @@ -static OpenTelemetry.Logs.ConsoleExporterLoggingExtensions.AddConsoleExporter(this OpenTelemetry.Logs.LoggerProviderBuilder loggerProviderBuilder) -> OpenTelemetry.Logs.LoggerProviderBuilder -static OpenTelemetry.Logs.ConsoleExporterLoggingExtensions.AddConsoleExporter(this OpenTelemetry.Logs.LoggerProviderBuilder loggerProviderBuilder, string name, System.Action configure) -> OpenTelemetry.Logs.LoggerProviderBuilder -static OpenTelemetry.Logs.ConsoleExporterLoggingExtensions.AddConsoleExporter(this OpenTelemetry.Logs.LoggerProviderBuilder loggerProviderBuilder, System.Action configure) -> OpenTelemetry.Logs.LoggerProviderBuilder \ No newline at end of file diff --git a/src/OpenTelemetry.Exporter.Console/CHANGELOG.md b/src/OpenTelemetry.Exporter.Console/CHANGELOG.md index c13379b6420..043a395cc58 100644 --- a/src/OpenTelemetry.Exporter.Console/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Console/CHANGELOG.md @@ -2,16 +2,53 @@ ## Unreleased -* Add back support for Exemplars. See [exemplars](../../docs/metrics/customizing-the-sdk/README.md#exemplars) - for instructions to enable exemplars. - ([#4553](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4553)) +## 1.7.0 + +Released 2023-Dec-08 + +## 1.7.0-rc.1 + +Released 2023-Nov-29 + +* Add support for Instrumentation Scope Attributes (i.e [Meter + Tags](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.meter.tags)), + fixing issue + [#4563](https://github.com/open-telemetry/opentelemetry-dotnet/issues/4563). + ([#5089](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5089)) + +## 1.7.0-alpha.1 + +Released 2023-Oct-16 + +## 1.6.0 + +Released 2023-Sep-05 + +## 1.6.0-rc.1 + +Released 2023-Aug-21 + +## 1.6.0-alpha.1 + +Released 2023-Jul-12 + +* **Experimental (pre-release builds only):** + + * Note: See + [#4735](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4735) + for the introduction of experimental api support. + + * Add back support for Exemplars. See + [exemplars](../../docs/metrics/customizing-the-sdk/README.md#exemplars) for + instructions to enable exemplars. + ([#4553](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4553)) -* Updated to display `Severity` and `SeverityText` text instead of - `ILogger.LogLevel` when exporting `LogRecord` instances. - ([#4568](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4568)) + * Updated to display `Severity` and `SeverityText` text instead of + `ILogger.LogLevel` when exporting `LogRecord` instances. + ([#4568](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4568)) -* Added `LoggerProviderBuilder.AddConsoleExporter` registration extension. - ([#4583](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4583)) + * Added `LoggerProviderBuilder.AddConsoleExporter` registration extension. + ([#4583](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4583)) ## 1.5.1 diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleActivityExporter.cs b/src/OpenTelemetry.Exporter.Console/ConsoleActivityExporter.cs index f08fb6b6735..c87a184d7ec 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleActivityExporter.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleActivityExporter.cs @@ -1,147 +1,133 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; -namespace OpenTelemetry.Exporter +namespace OpenTelemetry.Exporter; + +public class ConsoleActivityExporter : ConsoleExporter { - public class ConsoleActivityExporter : ConsoleExporter + public ConsoleActivityExporter(ConsoleExporterOptions options) + : base(options) { - public ConsoleActivityExporter(ConsoleExporterOptions options) - : base(options) - { - } + } - public override ExportResult Export(in Batch batch) + public override ExportResult Export(in Batch batch) + { + foreach (var activity in batch) { - foreach (var activity in batch) + this.WriteLine($"Activity.TraceId: {activity.TraceId}"); + this.WriteLine($"Activity.SpanId: {activity.SpanId}"); + this.WriteLine($"Activity.TraceFlags: {activity.ActivityTraceFlags}"); + if (!string.IsNullOrEmpty(activity.TraceStateString)) { - this.WriteLine($"Activity.TraceId: {activity.TraceId}"); - this.WriteLine($"Activity.SpanId: {activity.SpanId}"); - this.WriteLine($"Activity.TraceFlags: {activity.ActivityTraceFlags}"); - if (!string.IsNullOrEmpty(activity.TraceStateString)) - { - this.WriteLine($"Activity.TraceState: {activity.TraceStateString}"); - } + this.WriteLine($"Activity.TraceState: {activity.TraceStateString}"); + } - if (activity.ParentSpanId != default) - { - this.WriteLine($"Activity.ParentSpanId: {activity.ParentSpanId}"); - } + if (activity.ParentSpanId != default) + { + this.WriteLine($"Activity.ParentSpanId: {activity.ParentSpanId}"); + } - this.WriteLine($"Activity.ActivitySourceName: {activity.Source.Name}"); - this.WriteLine($"Activity.DisplayName: {activity.DisplayName}"); - this.WriteLine($"Activity.Kind: {activity.Kind}"); - this.WriteLine($"Activity.StartTime: {activity.StartTimeUtc:yyyy-MM-ddTHH:mm:ss.fffffffZ}"); - this.WriteLine($"Activity.Duration: {activity.Duration}"); - var statusCode = string.Empty; - var statusDesc = string.Empty; + this.WriteLine($"Activity.ActivitySourceName: {activity.Source.Name}"); + this.WriteLine($"Activity.DisplayName: {activity.DisplayName}"); + this.WriteLine($"Activity.Kind: {activity.Kind}"); + this.WriteLine($"Activity.StartTime: {activity.StartTimeUtc:yyyy-MM-ddTHH:mm:ss.fffffffZ}"); + this.WriteLine($"Activity.Duration: {activity.Duration}"); + var statusCode = string.Empty; + var statusDesc = string.Empty; - if (activity.TagObjects.Any()) + if (activity.TagObjects.Any()) + { + this.WriteLine("Activity.Tags:"); + foreach (ref readonly var tag in activity.EnumerateTagObjects()) { - this.WriteLine("Activity.Tags:"); - foreach (ref readonly var tag in activity.EnumerateTagObjects()) + if (tag.Key == SpanAttributeConstants.StatusCodeKey) { - if (tag.Key == SpanAttributeConstants.StatusCodeKey) - { - statusCode = tag.Value as string; - continue; - } + statusCode = tag.Value as string; + continue; + } - if (tag.Key == SpanAttributeConstants.StatusDescriptionKey) - { - statusDesc = tag.Value as string; - continue; - } + if (tag.Key == SpanAttributeConstants.StatusDescriptionKey) + { + statusDesc = tag.Value as string; + continue; + } - if (ConsoleTagTransformer.Instance.TryTransformTag(tag, out var result)) - { - this.WriteLine($" {result}"); - } + if (ConsoleTagTransformer.Instance.TryTransformTag(tag, out var result)) + { + this.WriteLine($" {result}"); } } + } - if (activity.Status != ActivityStatusCode.Unset) + if (activity.Status != ActivityStatusCode.Unset) + { + this.WriteLine($"StatusCode: {activity.Status}"); + if (!string.IsNullOrEmpty(activity.StatusDescription)) { - this.WriteLine($"StatusCode: {activity.Status}"); - if (!string.IsNullOrEmpty(activity.StatusDescription)) - { - this.WriteLine($"Activity.StatusDescription: {activity.StatusDescription}"); - } + this.WriteLine($"Activity.StatusDescription: {activity.StatusDescription}"); } - else if (!string.IsNullOrEmpty(statusCode)) + } + else if (!string.IsNullOrEmpty(statusCode)) + { + this.WriteLine($" StatusCode: {statusCode}"); + if (!string.IsNullOrEmpty(statusDesc)) { - this.WriteLine($" StatusCode: {statusCode}"); - if (!string.IsNullOrEmpty(statusDesc)) - { - this.WriteLine($" Activity.StatusDescription: {statusDesc}"); - } + this.WriteLine($" Activity.StatusDescription: {statusDesc}"); } + } - if (activity.Events.Any()) + if (activity.Events.Any()) + { + this.WriteLine("Activity.Events:"); + foreach (ref readonly var activityEvent in activity.EnumerateEvents()) { - this.WriteLine("Activity.Events:"); - foreach (ref readonly var activityEvent in activity.EnumerateEvents()) + this.WriteLine($" {activityEvent.Name} [{activityEvent.Timestamp}]"); + foreach (ref readonly var attribute in activityEvent.EnumerateTagObjects()) { - this.WriteLine($" {activityEvent.Name} [{activityEvent.Timestamp}]"); - foreach (ref readonly var attribute in activityEvent.EnumerateTagObjects()) + if (ConsoleTagTransformer.Instance.TryTransformTag(attribute, out var result)) { - if (ConsoleTagTransformer.Instance.TryTransformTag(attribute, out var result)) - { - this.WriteLine($" {result}"); - } + this.WriteLine($" {result}"); } } } + } - if (activity.Links.Any()) + if (activity.Links.Any()) + { + this.WriteLine("Activity.Links:"); + foreach (ref readonly var activityLink in activity.EnumerateLinks()) { - this.WriteLine("Activity.Links:"); - foreach (ref readonly var activityLink in activity.EnumerateLinks()) + this.WriteLine($" {activityLink.Context.TraceId} {activityLink.Context.SpanId}"); + foreach (ref readonly var attribute in activityLink.EnumerateTagObjects()) { - this.WriteLine($" {activityLink.Context.TraceId} {activityLink.Context.SpanId}"); - foreach (ref readonly var attribute in activityLink.EnumerateTagObjects()) + if (ConsoleTagTransformer.Instance.TryTransformTag(attribute, out var result)) { - if (ConsoleTagTransformer.Instance.TryTransformTag(attribute, out var result)) - { - this.WriteLine($" {result}"); - } + this.WriteLine($" {result}"); } } } + } - var resource = this.ParentProvider.GetResource(); - if (resource != Resource.Empty) + var resource = this.ParentProvider.GetResource(); + if (resource != Resource.Empty) + { + this.WriteLine("Resource associated with Activity:"); + foreach (var resourceAttribute in resource.Attributes) { - this.WriteLine("Resource associated with Activity:"); - foreach (var resourceAttribute in resource.Attributes) + if (ConsoleTagTransformer.Instance.TryTransformTag(resourceAttribute, out var result)) { - if (ConsoleTagTransformer.Instance.TryTransformTag(resourceAttribute, out var result)) - { - this.WriteLine($" {result}"); - } + this.WriteLine($" {result}"); } } - - this.WriteLine(string.Empty); } - return ExportResult.Success; + this.WriteLine(string.Empty); } + + return ExportResult.Success; } } diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleExporter.cs b/src/OpenTelemetry.Exporter.Console/ConsoleExporter.cs index 0926d24d5dc..4806670a009 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleExporter.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleExporter.cs @@ -1,46 +1,32 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Exporter +namespace OpenTelemetry.Exporter; + +public abstract class ConsoleExporter : BaseExporter + where T : class { - public abstract class ConsoleExporter : BaseExporter - where T : class + private readonly ConsoleExporterOptions options; + + protected ConsoleExporter(ConsoleExporterOptions options) { - private readonly ConsoleExporterOptions options; + this.options = options ?? new ConsoleExporterOptions(); + ConsoleTagTransformer.LogUnsupportedAttributeType = (string tagValueType, string tagKey) => + { + this.WriteLine($"Unsupported attribute type {tagValueType} for {tagKey}."); + }; + } - protected ConsoleExporter(ConsoleExporterOptions options) + protected void WriteLine(string message) + { + if (this.options.Targets.HasFlag(ConsoleExporterOutputTargets.Console)) { - this.options = options ?? new ConsoleExporterOptions(); - ConsoleTagTransformer.LogUnsupportedAttributeType = (string tagValueType, string tagKey) => - { - this.WriteLine($"Unsupported attribute type {tagValueType} for {tagKey}."); - }; + Console.WriteLine(message); } - protected void WriteLine(string message) + if (this.options.Targets.HasFlag(ConsoleExporterOutputTargets.Debug)) { - if (this.options.Targets.HasFlag(ConsoleExporterOutputTargets.Console)) - { - Console.WriteLine(message); - } - - if (this.options.Targets.HasFlag(ConsoleExporterOutputTargets.Debug)) - { - System.Diagnostics.Trace.WriteLine(message); - } + System.Diagnostics.Trace.WriteLine(message); } } } diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.Console/ConsoleExporterHelperExtensions.cs index 08330a0a9d4..b79aa0d9ed9 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleExporterHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleExporterHelperExtensions.cs @@ -1,72 +1,58 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using OpenTelemetry.Exporter; using OpenTelemetry.Internal; -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +public static class ConsoleExporterHelperExtensions { - public static class ConsoleExporterHelperExtensions + /// + /// Adds Console exporter to the TracerProvider. + /// + /// builder to use. + /// The instance of to chain the calls. + public static TracerProviderBuilder AddConsoleExporter(this TracerProviderBuilder builder) + => AddConsoleExporter(builder, name: null, configure: null); + + /// + /// Adds Console exporter to the TracerProvider. + /// + /// builder to use. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static TracerProviderBuilder AddConsoleExporter(this TracerProviderBuilder builder, Action configure) + => AddConsoleExporter(builder, name: null, configure); + + /// + /// Adds Console exporter to the TracerProvider. + /// + /// builder to use. + /// Name which is used when retrieving options. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static TracerProviderBuilder AddConsoleExporter( + this TracerProviderBuilder builder, + string name, + Action configure) { - /// - /// Adds Console exporter to the TracerProvider. - /// - /// builder to use. - /// The instance of to chain the calls. - public static TracerProviderBuilder AddConsoleExporter(this TracerProviderBuilder builder) - => AddConsoleExporter(builder, name: null, configure: null); + Guard.ThrowIfNull(builder); - /// - /// Adds Console exporter to the TracerProvider. - /// - /// builder to use. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static TracerProviderBuilder AddConsoleExporter(this TracerProviderBuilder builder, Action configure) - => AddConsoleExporter(builder, name: null, configure); + name ??= Options.DefaultName; - /// - /// Adds Console exporter to the TracerProvider. - /// - /// builder to use. - /// Name which is used when retrieving options. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static TracerProviderBuilder AddConsoleExporter( - this TracerProviderBuilder builder, - string name, - Action configure) + if (configure != null) { - Guard.ThrowIfNull(builder); - - name ??= Options.DefaultName; - - if (configure != null) - { - builder.ConfigureServices(services => services.Configure(name, configure)); - } + builder.ConfigureServices(services => services.Configure(name, configure)); + } - return builder.AddProcessor(sp => - { - var options = sp.GetRequiredService>().Get(name); + return builder.AddProcessor(sp => + { + var options = sp.GetRequiredService>().Get(name); - return new SimpleActivityExportProcessor(new ConsoleActivityExporter(options)); - }); - } + return new SimpleActivityExportProcessor(new ConsoleActivityExporter(options)); + }); } } diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleExporterLoggingExtensions.cs b/src/OpenTelemetry.Exporter.Console/ConsoleExporterLoggingExtensions.cs index 98f85e6f0ef..6b024498cb7 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleExporterLoggingExtensions.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleExporterLoggingExtensions.cs @@ -1,100 +1,134 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using OpenTelemetry.Exporter; using OpenTelemetry.Internal; -namespace OpenTelemetry.Logs +namespace OpenTelemetry.Logs; + +public static class ConsoleExporterLoggingExtensions { - public static class ConsoleExporterLoggingExtensions - { - /// - /// Adds Console exporter with OpenTelemetryLoggerOptions. - /// - /// options to use. - /// The instance of to chain the calls. - /// todo: [Obsolete("Call LoggerProviderBuilder.AddConsoleExporter instead this method will be removed in a future version.")] - public static OpenTelemetryLoggerOptions AddConsoleExporter(this OpenTelemetryLoggerOptions loggerOptions) - => AddConsoleExporter(loggerOptions, configure: null); + /// + /// Adds Console exporter with OpenTelemetryLoggerOptions. + /// + /// options to use. + /// The instance of to chain the calls. + /// todo: [Obsolete("Call LoggerProviderBuilder.AddConsoleExporter instead this method will be removed in a future version.")] + public static OpenTelemetryLoggerOptions AddConsoleExporter(this OpenTelemetryLoggerOptions loggerOptions) + => AddConsoleExporter(loggerOptions, configure: null); - /// - /// Adds Console exporter with OpenTelemetryLoggerOptions. - /// - /// options to use. - /// Callback action for configuring . - /// The instance of to chain the calls. - /// todo: [Obsolete("Call LoggerProviderBuilder.AddConsoleExporter instead this method will be removed in a future version.")] - public static OpenTelemetryLoggerOptions AddConsoleExporter(this OpenTelemetryLoggerOptions loggerOptions, Action configure) - { - Guard.ThrowIfNull(loggerOptions); + /// + /// Adds Console exporter with OpenTelemetryLoggerOptions. + /// + /// options to use. + /// Callback action for configuring . + /// The instance of to chain the calls. + /// todo: [Obsolete("Call LoggerProviderBuilder.AddConsoleExporter instead this method will be removed in a future version.")] + public static OpenTelemetryLoggerOptions AddConsoleExporter(this OpenTelemetryLoggerOptions loggerOptions, Action configure) + { + Guard.ThrowIfNull(loggerOptions); - var options = new ConsoleExporterOptions(); - configure?.Invoke(options); - return loggerOptions.AddProcessor(new SimpleLogRecordExportProcessor(new ConsoleLogRecordExporter(options))); - } + var options = new ConsoleExporterOptions(); + configure?.Invoke(options); + return loggerOptions.AddProcessor(new SimpleLogRecordExportProcessor(new ConsoleLogRecordExporter(options))); + } - /// - /// Adds Console exporter with LoggerProviderBuilder. - /// - /// . - /// The supplied instance of to chain the calls. - public static LoggerProviderBuilder AddConsoleExporter( - this LoggerProviderBuilder loggerProviderBuilder) - => AddConsoleExporter(loggerProviderBuilder, name: null, configure: null); +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds Console exporter with LoggerProviderBuilder. + /// + /// WARNING: This is an experimental API which might change or be removed in the future. Use at your own risk. + /// . + /// The supplied instance of to chain the calls. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + /// + /// Adds Console exporter with LoggerProviderBuilder. + /// + /// . + /// The supplied instance of to chain the calls. + internal +#endif + static LoggerProviderBuilder AddConsoleExporter( + this LoggerProviderBuilder loggerProviderBuilder) + => AddConsoleExporter(loggerProviderBuilder, name: null, configure: null); - /// - /// Adds Console exporter with LoggerProviderBuilder. - /// - /// . - /// Callback action for configuring . - /// The supplied instance of to chain the calls. - public static LoggerProviderBuilder AddConsoleExporter( - this LoggerProviderBuilder loggerProviderBuilder, - Action configure) - => AddConsoleExporter(loggerProviderBuilder, name: null, configure); +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds Console exporter with LoggerProviderBuilder. + /// + /// + /// . + /// Callback action for configuring . + /// The supplied instance of to chain the calls. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + /// + /// Adds Console exporter with LoggerProviderBuilder. + /// + /// . + /// Callback action for configuring . + /// The supplied instance of to chain the calls. + internal +#endif + static LoggerProviderBuilder AddConsoleExporter( + this LoggerProviderBuilder loggerProviderBuilder, + Action configure) + => AddConsoleExporter(loggerProviderBuilder, name: null, configure); - /// - /// Adds Console exporter with LoggerProviderBuilder. - /// - /// . - /// Name which is used when retrieving options. - /// Callback action for configuring . - /// The supplied instance of to chain the calls. - public static LoggerProviderBuilder AddConsoleExporter( - this LoggerProviderBuilder loggerProviderBuilder, - string name, - Action configure) - { - Guard.ThrowIfNull(loggerProviderBuilder); +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds Console exporter with LoggerProviderBuilder. + /// + /// + /// . + /// Name which is used when retrieving options. + /// Callback action for configuring . + /// The supplied instance of to chain the calls. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + /// + /// Adds Console exporter with LoggerProviderBuilder. + /// + /// . + /// Name which is used when retrieving options. + /// Callback action for configuring . + /// The supplied instance of to chain the calls. + internal +#endif + static LoggerProviderBuilder AddConsoleExporter( + this LoggerProviderBuilder loggerProviderBuilder, + string name, + Action configure) + { + Guard.ThrowIfNull(loggerProviderBuilder); - name ??= Options.DefaultName; + name ??= Options.DefaultName; - if (configure != null) - { - loggerProviderBuilder.ConfigureServices(services => services.Configure(name, configure)); - } + if (configure != null) + { + loggerProviderBuilder.ConfigureServices(services => services.Configure(name, configure)); + } - return loggerProviderBuilder.AddProcessor(sp => - { - var options = sp.GetRequiredService>().Get(name); + return loggerProviderBuilder.AddProcessor(sp => + { + var options = sp.GetRequiredService>().Get(name); - return new SimpleLogRecordExportProcessor(new ConsoleLogRecordExporter(options)); - }); - } + return new SimpleLogRecordExportProcessor(new ConsoleLogRecordExporter(options)); + }); } } diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleExporterMetricsExtensions.cs b/src/OpenTelemetry.Exporter.Console/ConsoleExporterMetricsExtensions.cs index c4c2dabf4f5..103593efddd 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleExporterMetricsExtensions.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleExporterMetricsExtensions.cs @@ -1,133 +1,119 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using OpenTelemetry.Exporter; using OpenTelemetry.Internal; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// Extension methods to simplify registering of the Console exporter. +/// +public static class ConsoleExporterMetricsExtensions { + private const int DefaultExportIntervalMilliseconds = 10000; + private const int DefaultExportTimeoutMilliseconds = Timeout.Infinite; + /// - /// Extension methods to simplify registering of the Console exporter. + /// Adds to the using default options. /// - public static class ConsoleExporterMetricsExtensions - { - private const int DefaultExportIntervalMilliseconds = 10000; - private const int DefaultExportTimeoutMilliseconds = Timeout.Infinite; - - /// - /// Adds to the using default options. - /// - /// builder to use. - /// The instance of to chain the calls. - public static MeterProviderBuilder AddConsoleExporter(this MeterProviderBuilder builder) - => AddConsoleExporter(builder, name: null, configureExporter: null); - - /// - /// Adds to the . - /// - /// builder to use. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static MeterProviderBuilder AddConsoleExporter(this MeterProviderBuilder builder, Action configureExporter) - => AddConsoleExporter(builder, name: null, configureExporter); - - /// - /// Adds to the . - /// - /// builder to use. - /// Name which is used when retrieving options. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static MeterProviderBuilder AddConsoleExporter( - this MeterProviderBuilder builder, - string name, - Action configureExporter) - { - Guard.ThrowIfNull(builder); + /// builder to use. + /// The instance of to chain the calls. + public static MeterProviderBuilder AddConsoleExporter(this MeterProviderBuilder builder) + => AddConsoleExporter(builder, name: null, configureExporter: null); - name ??= Options.DefaultName; + /// + /// Adds to the . + /// + /// builder to use. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static MeterProviderBuilder AddConsoleExporter(this MeterProviderBuilder builder, Action configureExporter) + => AddConsoleExporter(builder, name: null, configureExporter); - if (configureExporter != null) - { - builder.ConfigureServices(services => services.Configure(name, configureExporter)); - } + /// + /// Adds to the . + /// + /// builder to use. + /// Name which is used when retrieving options. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static MeterProviderBuilder AddConsoleExporter( + this MeterProviderBuilder builder, + string name, + Action configureExporter) + { + Guard.ThrowIfNull(builder); - return builder.AddReader(sp => - { - return BuildConsoleExporterMetricReader( - sp.GetRequiredService>().Get(name), - sp.GetRequiredService>().Get(name)); - }); - } + name ??= Options.DefaultName; - /// - /// Adds to the . - /// - /// builder to use. - /// Callback action for - /// configuring and . - /// The instance of to chain the calls. - public static MeterProviderBuilder AddConsoleExporter( - this MeterProviderBuilder builder, - Action configureExporterAndMetricReader) - => AddConsoleExporter(builder, name: null, configureExporterAndMetricReader); - - /// - /// Adds to the . - /// - /// builder to use. - /// Name which is used when retrieving options. - /// Callback action for - /// configuring and . - /// The instance of to chain the calls. - public static MeterProviderBuilder AddConsoleExporter( - this MeterProviderBuilder builder, - string name, - Action configureExporterAndMetricReader) + if (configureExporter != null) { - Guard.ThrowIfNull(builder); + builder.ConfigureServices(services => services.Configure(name, configureExporter)); + } - name ??= Options.DefaultName; + return builder.AddReader(sp => + { + return BuildConsoleExporterMetricReader( + sp.GetRequiredService>().Get(name), + sp.GetRequiredService>().Get(name)); + }); + } - return builder.AddReader(sp => - { - var exporterOptions = sp.GetRequiredService>().Get(name); - var metricReaderOptions = sp.GetRequiredService>().Get(name); + /// + /// Adds to the . + /// + /// builder to use. + /// Callback action for + /// configuring and . + /// The instance of to chain the calls. + public static MeterProviderBuilder AddConsoleExporter( + this MeterProviderBuilder builder, + Action configureExporterAndMetricReader) + => AddConsoleExporter(builder, name: null, configureExporterAndMetricReader); - configureExporterAndMetricReader?.Invoke(exporterOptions, metricReaderOptions); + /// + /// Adds to the . + /// + /// builder to use. + /// Name which is used when retrieving options. + /// Callback action for + /// configuring and . + /// The instance of to chain the calls. + public static MeterProviderBuilder AddConsoleExporter( + this MeterProviderBuilder builder, + string name, + Action configureExporterAndMetricReader) + { + Guard.ThrowIfNull(builder); - return BuildConsoleExporterMetricReader(exporterOptions, metricReaderOptions); - }); - } + name ??= Options.DefaultName; - private static MetricReader BuildConsoleExporterMetricReader( - ConsoleExporterOptions exporterOptions, - MetricReaderOptions metricReaderOptions) + return builder.AddReader(sp => { - var metricExporter = new ConsoleMetricExporter(exporterOptions); + var exporterOptions = sp.GetRequiredService>().Get(name); + var metricReaderOptions = sp.GetRequiredService>().Get(name); - return PeriodicExportingMetricReaderHelper.CreatePeriodicExportingMetricReader( - metricExporter, - metricReaderOptions, - DefaultExportIntervalMilliseconds, - DefaultExportTimeoutMilliseconds); - } + configureExporterAndMetricReader?.Invoke(exporterOptions, metricReaderOptions); + + return BuildConsoleExporterMetricReader(exporterOptions, metricReaderOptions); + }); + } + + private static MetricReader BuildConsoleExporterMetricReader( + ConsoleExporterOptions exporterOptions, + MetricReaderOptions metricReaderOptions) + { + var metricExporter = new ConsoleMetricExporter(exporterOptions); + + return PeriodicExportingMetricReaderHelper.CreatePeriodicExportingMetricReader( + metricExporter, + metricReaderOptions, + DefaultExportIntervalMilliseconds, + DefaultExportTimeoutMilliseconds); } } diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleExporterOptions.cs b/src/OpenTelemetry.Exporter.Console/ConsoleExporterOptions.cs index a7183ea489f..ca20795508a 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleExporterOptions.cs @@ -1,26 +1,12 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Exporter +namespace OpenTelemetry.Exporter; + +public class ConsoleExporterOptions { - public class ConsoleExporterOptions - { - /// - /// Gets or sets the output targets for the console exporter. - /// - public ConsoleExporterOutputTargets Targets { get; set; } = ConsoleExporterOutputTargets.Console; - } + /// + /// Gets or sets the output targets for the console exporter. + /// + public ConsoleExporterOutputTargets Targets { get; set; } = ConsoleExporterOutputTargets.Console; } diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleExporterOutputTargets.cs b/src/OpenTelemetry.Exporter.Console/ConsoleExporterOutputTargets.cs index fcb184d383c..83bda21b869 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleExporterOutputTargets.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleExporterOutputTargets.cs @@ -1,32 +1,18 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Exporter +namespace OpenTelemetry.Exporter; + +[Flags] +public enum ConsoleExporterOutputTargets { - [Flags] - public enum ConsoleExporterOutputTargets - { - /// - /// Output to the Console (stdout). - /// - Console = 0b1, + /// + /// Output to the Console (stdout). + /// + Console = 0b1, - /// - /// Output to the Debug trace. - /// - Debug = 0b10, - } + /// + /// Output to the Debug trace. + /// + Debug = 0b10, } diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleLogRecordExporter.cs b/src/OpenTelemetry.Exporter.Console/ConsoleLogRecordExporter.cs index 0cac9f0ecd8..1baf208c811 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleLogRecordExporter.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleLogRecordExporter.cs @@ -1,180 +1,166 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Internal; using OpenTelemetry.Logs; using OpenTelemetry.Resources; -namespace OpenTelemetry.Exporter +namespace OpenTelemetry.Exporter; + +public class ConsoleLogRecordExporter : ConsoleExporter { - public class ConsoleLogRecordExporter : ConsoleExporter + private const int RightPaddingLength = 35; + private readonly object syncObject = new(); + private bool disposed; + private string disposedStackTrace; + private bool isDisposeMessageSent; + + public ConsoleLogRecordExporter(ConsoleExporterOptions options) + : base(options) { - private const int RightPaddingLength = 35; - private readonly object syncObject = new(); - private bool disposed; - private string disposedStackTrace; - private bool isDisposeMessageSent; - - public ConsoleLogRecordExporter(ConsoleExporterOptions options) - : base(options) - { - } + } - public override ExportResult Export(in Batch batch) + public override ExportResult Export(in Batch batch) + { + if (this.disposed) { - if (this.disposed) + if (!this.isDisposeMessageSent) { - if (!this.isDisposeMessageSent) + lock (this.syncObject) { - lock (this.syncObject) + if (this.isDisposeMessageSent) { - if (this.isDisposeMessageSent) - { - return ExportResult.Failure; - } - - this.isDisposeMessageSent = true; + return ExportResult.Failure; } - this.WriteLine("The console exporter is still being invoked after it has been disposed. This could be due to the application's incorrect lifecycle management of the LoggerFactory/OpenTelemetry .NET SDK."); - this.WriteLine(Environment.StackTrace); - this.WriteLine(Environment.NewLine + "Dispose was called on the following stack trace:"); - this.WriteLine(this.disposedStackTrace); + this.isDisposeMessageSent = true; } - return ExportResult.Failure; + this.WriteLine("The console exporter is still being invoked after it has been disposed. This could be due to the application's incorrect lifecycle management of the LoggerFactory/OpenTelemetry .NET SDK."); + this.WriteLine(Environment.StackTrace); + this.WriteLine(Environment.NewLine + "Dispose was called on the following stack trace:"); + this.WriteLine(this.disposedStackTrace); } - foreach (var logRecord in batch) - { - this.WriteLine($"{"LogRecord.Timestamp:",-RightPaddingLength}{logRecord.Timestamp:yyyy-MM-ddTHH:mm:ss.fffffffZ}"); + return ExportResult.Failure; + } - if (logRecord.TraceId != default) - { - this.WriteLine($"{"LogRecord.TraceId:",-RightPaddingLength}{logRecord.TraceId}"); - this.WriteLine($"{"LogRecord.SpanId:",-RightPaddingLength}{logRecord.SpanId}"); - this.WriteLine($"{"LogRecord.TraceFlags:",-RightPaddingLength}{logRecord.TraceFlags}"); - } + foreach (var logRecord in batch) + { + this.WriteLine($"{"LogRecord.Timestamp:",-RightPaddingLength}{logRecord.Timestamp:yyyy-MM-ddTHH:mm:ss.fffffffZ}"); - if (logRecord.CategoryName != null) - { - this.WriteLine($"{"LogRecord.CategoryName:",-RightPaddingLength}{logRecord.CategoryName}"); - } + if (logRecord.TraceId != default) + { + this.WriteLine($"{"LogRecord.TraceId:",-RightPaddingLength}{logRecord.TraceId}"); + this.WriteLine($"{"LogRecord.SpanId:",-RightPaddingLength}{logRecord.SpanId}"); + this.WriteLine($"{"LogRecord.TraceFlags:",-RightPaddingLength}{logRecord.TraceFlags}"); + } - if (logRecord.Severity.HasValue) - { - this.WriteLine($"{"LogRecord.Severity:",-RightPaddingLength}{logRecord.Severity}"); - } + if (logRecord.CategoryName != null) + { + this.WriteLine($"{"LogRecord.CategoryName:",-RightPaddingLength}{logRecord.CategoryName}"); + } - if (logRecord.SeverityText != null) - { - this.WriteLine($"{"LogRecord.SeverityText:",-RightPaddingLength}{logRecord.SeverityText}"); - } + if (logRecord.Severity.HasValue) + { + this.WriteLine($"{"LogRecord.Severity:",-RightPaddingLength}{logRecord.Severity}"); + } - if (logRecord.FormattedMessage != null) - { - this.WriteLine($"{"LogRecord.FormattedMessage:",-RightPaddingLength}{logRecord.FormattedMessage}"); - } + if (logRecord.SeverityText != null) + { + this.WriteLine($"{"LogRecord.SeverityText:",-RightPaddingLength}{logRecord.SeverityText}"); + } - if (logRecord.Body != null) - { - this.WriteLine($"{"LogRecord.Body:",-RightPaddingLength}{logRecord.Body}"); - } + if (logRecord.FormattedMessage != null) + { + this.WriteLine($"{"LogRecord.FormattedMessage:",-RightPaddingLength}{logRecord.FormattedMessage}"); + } - if (logRecord.Attributes != null) - { - this.WriteLine("LogRecord.Attributes (Key:Value):"); - for (int i = 0; i < logRecord.Attributes.Count; i++) - { - // Special casing {OriginalFormat} - // See https://github.com/open-telemetry/opentelemetry-dotnet/pull/3182 - // for explanation. - var valueToTransform = logRecord.Attributes[i].Key.Equals("{OriginalFormat}") - ? new KeyValuePair("OriginalFormat (a.k.a Body)", logRecord.Attributes[i].Value) - : logRecord.Attributes[i]; - - if (ConsoleTagTransformer.Instance.TryTransformTag(valueToTransform, out var result)) - { - this.WriteLine($"{string.Empty,-4}{result}"); - } - } - } + if (logRecord.Body != null) + { + this.WriteLine($"{"LogRecord.Body:",-RightPaddingLength}{logRecord.Body}"); + } - if (logRecord.EventId != default) + if (logRecord.Attributes != null) + { + this.WriteLine("LogRecord.Attributes (Key:Value):"); + for (int i = 0; i < logRecord.Attributes.Count; i++) { - this.WriteLine($"{"LogRecord.EventId:",-RightPaddingLength}{logRecord.EventId.Id}"); - if (!string.IsNullOrEmpty(logRecord.EventId.Name)) + // Special casing {OriginalFormat} + // See https://github.com/open-telemetry/opentelemetry-dotnet/pull/3182 + // for explanation. + var valueToTransform = logRecord.Attributes[i].Key.Equals("{OriginalFormat}") + ? new KeyValuePair("OriginalFormat (a.k.a Body)", logRecord.Attributes[i].Value) + : logRecord.Attributes[i]; + + if (ConsoleTagTransformer.Instance.TryTransformTag(valueToTransform, out var result)) { - this.WriteLine($"{"LogRecord.EventName:",-RightPaddingLength}{logRecord.EventId.Name}"); + this.WriteLine($"{string.Empty,-4}{result}"); } } + } - if (logRecord.Exception != null) + if (logRecord.EventId != default) + { + this.WriteLine($"{"LogRecord.EventId:",-RightPaddingLength}{logRecord.EventId.Id}"); + if (!string.IsNullOrEmpty(logRecord.EventId.Name)) { - this.WriteLine($"{"LogRecord.Exception:",-RightPaddingLength}{logRecord.Exception.ToInvariantString()}"); + this.WriteLine($"{"LogRecord.EventName:",-RightPaddingLength}{logRecord.EventId.Name}"); } + } - int scopeDepth = -1; + if (logRecord.Exception != null) + { + this.WriteLine($"{"LogRecord.Exception:",-RightPaddingLength}{logRecord.Exception.ToInvariantString()}"); + } + + int scopeDepth = -1; - logRecord.ForEachScope(ProcessScope, this); + logRecord.ForEachScope(ProcessScope, this); - void ProcessScope(LogRecordScope scope, ConsoleLogRecordExporter exporter) + void ProcessScope(LogRecordScope scope, ConsoleLogRecordExporter exporter) + { + if (++scopeDepth == 0) { - if (++scopeDepth == 0) - { - exporter.WriteLine("LogRecord.ScopeValues (Key:Value):"); - } + exporter.WriteLine("LogRecord.ScopeValues (Key:Value):"); + } - foreach (KeyValuePair scopeItem in scope) + foreach (KeyValuePair scopeItem in scope) + { + if (ConsoleTagTransformer.Instance.TryTransformTag(scopeItem, out var result)) { - if (ConsoleTagTransformer.Instance.TryTransformTag(scopeItem, out var result)) - { - exporter.WriteLine($"[Scope.{scopeDepth}]:{result}"); - } + exporter.WriteLine($"[Scope.{scopeDepth}]:{result}"); } } + } - var resource = this.ParentProvider.GetResource(); - if (resource != Resource.Empty) + var resource = this.ParentProvider.GetResource(); + if (resource != Resource.Empty) + { + this.WriteLine("\nResource associated with LogRecord:"); + foreach (var resourceAttribute in resource.Attributes) { - this.WriteLine("\nResource associated with LogRecord:"); - foreach (var resourceAttribute in resource.Attributes) + if (ConsoleTagTransformer.Instance.TryTransformTag(resourceAttribute, out var result)) { - if (ConsoleTagTransformer.Instance.TryTransformTag(resourceAttribute, out var result)) - { - this.WriteLine(result); - } + this.WriteLine(result); } } - - this.WriteLine(string.Empty); } - return ExportResult.Success; + this.WriteLine(string.Empty); } - protected override void Dispose(bool disposing) - { - if (!this.disposed) - { - this.disposed = true; - this.disposedStackTrace = Environment.StackTrace; - } + return ExportResult.Success; + } - base.Dispose(disposing); + protected override void Dispose(bool disposing) + { + if (!this.disposed) + { + this.disposed = true; + this.disposedStackTrace = Environment.StackTrace; } + + base.Dispose(disposing); } } diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs b/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs index 25654d7122b..f3f4b0ba5f9 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs @@ -1,253 +1,265 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Globalization; using System.Text; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; -namespace OpenTelemetry.Exporter +namespace OpenTelemetry.Exporter; + +public class ConsoleMetricExporter : ConsoleExporter { - public class ConsoleMetricExporter : ConsoleExporter - { - private Resource resource; + private Resource resource; - public ConsoleMetricExporter(ConsoleExporterOptions options) - : base(options) - { - } + public ConsoleMetricExporter(ConsoleExporterOptions options) + : base(options) + { + } - public override ExportResult Export(in Batch batch) + public override ExportResult Export(in Batch batch) + { + if (this.resource == null) { - if (this.resource == null) + this.resource = this.ParentProvider.GetResource(); + if (this.resource != Resource.Empty) { - this.resource = this.ParentProvider.GetResource(); - if (this.resource != Resource.Empty) + this.WriteLine("Resource associated with Metric:"); + foreach (var resourceAttribute in this.resource.Attributes) { - this.WriteLine("Resource associated with Metric:"); - foreach (var resourceAttribute in this.resource.Attributes) + if (ConsoleTagTransformer.Instance.TryTransformTag(resourceAttribute, out var result)) { - if (ConsoleTagTransformer.Instance.TryTransformTag(resourceAttribute, out var result)) - { - this.WriteLine($" {result}"); - } + this.WriteLine($" {result}"); } } } + } + + foreach (var metric in batch) + { + var msg = new StringBuilder($"\n"); + msg.Append($"Metric Name: {metric.Name}"); + if (metric.Description != string.Empty) + { + msg.Append(", "); + msg.Append(metric.Description); + } - foreach (var metric in batch) + if (metric.Unit != string.Empty) { - var msg = new StringBuilder($"\nExport "); - msg.Append(metric.Name); - if (metric.Description != string.Empty) - { - msg.Append(", "); - msg.Append(metric.Description); - } + msg.Append($", Unit: {metric.Unit}"); + } - if (metric.Unit != string.Empty) + if (!string.IsNullOrEmpty(metric.MeterName)) + { + msg.Append($", Meter: {metric.MeterName}"); + + if (!string.IsNullOrEmpty(metric.MeterVersion)) { - msg.Append($", Unit: {metric.Unit}"); + msg.Append($"/{metric.MeterVersion}"); } + } - if (!string.IsNullOrEmpty(metric.MeterName)) - { - msg.Append($", Meter: {metric.MeterName}"); + this.WriteLine(msg.ToString()); - if (!string.IsNullOrEmpty(metric.MeterVersion)) + if (metric.MeterTags != null) + { + foreach (var meterTag in metric.MeterTags) + { + this.WriteLine("\tMeter Tags:"); + if (ConsoleTagTransformer.Instance.TryTransformTag(meterTag, out var result)) { - msg.Append($"/{metric.MeterVersion}"); + this.WriteLine($"\t\t{result}"); } } + } - this.WriteLine(msg.ToString()); - - foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + { + string valueDisplay = string.Empty; + StringBuilder tagsBuilder = new StringBuilder(); + foreach (var tag in metricPoint.Tags) { - string valueDisplay = string.Empty; - StringBuilder tagsBuilder = new StringBuilder(); - foreach (var tag in metricPoint.Tags) + if (ConsoleTagTransformer.Instance.TryTransformTag(tag, out var result)) { - if (ConsoleTagTransformer.Instance.TryTransformTag(tag, out var result)) - { - tagsBuilder.Append(result); - tagsBuilder.Append(' '); - } + tagsBuilder.Append(result); + tagsBuilder.Append(' '); } + } - var tags = tagsBuilder.ToString().TrimEnd(); + var tags = tagsBuilder.ToString().TrimEnd(); - var metricType = metric.MetricType; + var metricType = metric.MetricType; - if (metricType == MetricType.Histogram || metricType == MetricType.ExponentialHistogram) + if (metricType == MetricType.Histogram || metricType == MetricType.ExponentialHistogram) + { + var bucketsBuilder = new StringBuilder(); + var sum = metricPoint.GetHistogramSum(); + var count = metricPoint.GetHistogramCount(); + bucketsBuilder.Append($"Sum: {sum} Count: {count} "); + if (metricPoint.TryGetHistogramMinMaxValues(out double min, out double max)) { - var bucketsBuilder = new StringBuilder(); - var sum = metricPoint.GetHistogramSum(); - var count = metricPoint.GetHistogramCount(); - bucketsBuilder.Append($"Sum: {sum} Count: {count} "); - if (metricPoint.TryGetHistogramMinMaxValues(out double min, out double max)) - { - bucketsBuilder.Append($"Min: {min} Max: {max} "); - } + bucketsBuilder.Append($"Min: {min} Max: {max} "); + } - bucketsBuilder.AppendLine(); + bucketsBuilder.AppendLine(); - if (metricType == MetricType.Histogram) + if (metricType == MetricType.Histogram) + { + bool isFirstIteration = true; + double previousExplicitBound = default; + foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets()) { - bool isFirstIteration = true; - double previousExplicitBound = default; - foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets()) + if (isFirstIteration) { - if (isFirstIteration) + bucketsBuilder.Append("(-Infinity,"); + bucketsBuilder.Append(histogramMeasurement.ExplicitBound); + bucketsBuilder.Append(']'); + bucketsBuilder.Append(':'); + bucketsBuilder.Append(histogramMeasurement.BucketCount); + previousExplicitBound = histogramMeasurement.ExplicitBound; + isFirstIteration = false; + } + else + { + bucketsBuilder.Append('('); + bucketsBuilder.Append(previousExplicitBound); + bucketsBuilder.Append(','); + if (histogramMeasurement.ExplicitBound != double.PositiveInfinity) { - bucketsBuilder.Append("(-Infinity,"); bucketsBuilder.Append(histogramMeasurement.ExplicitBound); - bucketsBuilder.Append(']'); - bucketsBuilder.Append(':'); - bucketsBuilder.Append(histogramMeasurement.BucketCount); previousExplicitBound = histogramMeasurement.ExplicitBound; - isFirstIteration = false; } else { - bucketsBuilder.Append('('); - bucketsBuilder.Append(previousExplicitBound); - bucketsBuilder.Append(','); - if (histogramMeasurement.ExplicitBound != double.PositiveInfinity) - { - bucketsBuilder.Append(histogramMeasurement.ExplicitBound); - previousExplicitBound = histogramMeasurement.ExplicitBound; - } - else - { - bucketsBuilder.Append("+Infinity"); - } - - bucketsBuilder.Append(']'); - bucketsBuilder.Append(':'); - bucketsBuilder.Append(histogramMeasurement.BucketCount); + bucketsBuilder.Append("+Infinity"); } - bucketsBuilder.AppendLine(); - } - } - else - { - var exponentialHistogramData = metricPoint.GetExponentialHistogramData(); - var scale = exponentialHistogramData.Scale; - - if (exponentialHistogramData.ZeroCount != 0) - { - bucketsBuilder.AppendLine($"Zero Bucket:{exponentialHistogramData.ZeroCount}"); + bucketsBuilder.Append(']'); + bucketsBuilder.Append(':'); + bucketsBuilder.Append(histogramMeasurement.BucketCount); } - var offset = exponentialHistogramData.PositiveBuckets.Offset; - foreach (var bucketCount in exponentialHistogramData.PositiveBuckets) - { - var lowerBound = Base2ExponentialBucketHistogram.LowerBoundary(offset, scale).ToString(CultureInfo.InvariantCulture); - var upperBound = Base2ExponentialBucketHistogram.LowerBoundary(++offset, scale).ToString(CultureInfo.InvariantCulture); - bucketsBuilder.AppendLine($"({lowerBound}, {upperBound}]:{bucketCount}"); - } + bucketsBuilder.AppendLine(); } - - valueDisplay = bucketsBuilder.ToString(); } - else if (metricType.IsDouble()) + else { - if (metricType.IsSum()) + var exponentialHistogramData = metricPoint.GetExponentialHistogramData(); + var scale = exponentialHistogramData.Scale; + + if (exponentialHistogramData.ZeroCount != 0) { - valueDisplay = metricPoint.GetSumDouble().ToString(CultureInfo.InvariantCulture); + bucketsBuilder.AppendLine($"Zero Bucket:{exponentialHistogramData.ZeroCount}"); } - else + + var offset = exponentialHistogramData.PositiveBuckets.Offset; + foreach (var bucketCount in exponentialHistogramData.PositiveBuckets) { - valueDisplay = metricPoint.GetGaugeLastValueDouble().ToString(CultureInfo.InvariantCulture); + var lowerBound = Base2ExponentialBucketHistogramHelper.CalculateLowerBoundary(offset, scale).ToString(CultureInfo.InvariantCulture); + var upperBound = Base2ExponentialBucketHistogramHelper.CalculateLowerBoundary(++offset, scale).ToString(CultureInfo.InvariantCulture); + bucketsBuilder.AppendLine($"({lowerBound}, {upperBound}]:{bucketCount}"); } } - else if (metricType.IsLong()) + + valueDisplay = bucketsBuilder.ToString(); + } + else if (metricType.IsDouble()) + { + if (metricType.IsSum()) + { + valueDisplay = metricPoint.GetSumDouble().ToString(CultureInfo.InvariantCulture); + } + else + { + valueDisplay = metricPoint.GetGaugeLastValueDouble().ToString(CultureInfo.InvariantCulture); + } + } + else if (metricType.IsLong()) + { + if (metricType.IsSum()) + { + valueDisplay = metricPoint.GetSumLong().ToString(CultureInfo.InvariantCulture); + } + else { - if (metricType.IsSum()) + valueDisplay = metricPoint.GetGaugeLastValueLong().ToString(CultureInfo.InvariantCulture); + } + } + + var exemplarString = new StringBuilder(); + if (metricPoint.TryGetExemplars(out var exemplars)) + { + foreach (ref readonly var exemplar in exemplars) + { + exemplarString.Append("Timestamp: "); + exemplarString.Append(exemplar.Timestamp.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ", CultureInfo.InvariantCulture)); + if (metricType.IsDouble()) { - valueDisplay = metricPoint.GetSumLong().ToString(CultureInfo.InvariantCulture); + exemplarString.Append(" Value: "); + exemplarString.Append(exemplar.DoubleValue); } - else + else if (metricType.IsLong()) { - valueDisplay = metricPoint.GetGaugeLastValueLong().ToString(CultureInfo.InvariantCulture); + exemplarString.Append(" Value: "); + exemplarString.Append(exemplar.LongValue); } - } - var exemplarString = new StringBuilder(); - foreach (var exemplar in metricPoint.GetExemplars()) - { - if (exemplar.Timestamp != default) + if (exemplar.TraceId != default) { - exemplarString.Append("Value: "); - exemplarString.Append(exemplar.DoubleValue); - exemplarString.Append(" Timestamp: "); - exemplarString.Append(exemplar.Timestamp.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ", CultureInfo.InvariantCulture)); exemplarString.Append(" TraceId: "); - exemplarString.Append(exemplar.TraceId); + exemplarString.Append(exemplar.TraceId.ToHexString()); exemplarString.Append(" SpanId: "); - exemplarString.Append(exemplar.SpanId); + exemplarString.Append(exemplar.SpanId.ToHexString()); + } - if (exemplar.FilteredTags != null && exemplar.FilteredTags.Count > 0) + bool appendedTagString = false; + foreach (var tag in exemplar.FilteredTags) + { + if (ConsoleTagTransformer.Instance.TryTransformTag(tag, out var result)) { - exemplarString.Append(" Filtered Tags : "); - - foreach (var tag in exemplar.FilteredTags) + if (!appendedTagString) { - if (ConsoleTagTransformer.Instance.TryTransformTag(tag, out var result)) - { - exemplarString.Append(result); - exemplarString.Append(' '); - } + exemplarString.Append(" Filtered Tags : "); + appendedTagString = true; } - } - exemplarString.AppendLine(); + exemplarString.Append(result); + exemplarString.Append(' '); + } } - } - msg = new StringBuilder(); - msg.Append('('); - msg.Append(metricPoint.StartTime.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ", CultureInfo.InvariantCulture)); - msg.Append(", "); - msg.Append(metricPoint.EndTime.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ", CultureInfo.InvariantCulture)); - msg.Append("] "); - msg.Append(tags); - if (tags != string.Empty) - { - msg.Append(' '); + exemplarString.AppendLine(); } + } - msg.Append(metric.MetricType); - msg.AppendLine(); - msg.Append($"Value: {valueDisplay}"); + msg = new StringBuilder(); + msg.Append('('); + msg.Append(metricPoint.StartTime.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ", CultureInfo.InvariantCulture)); + msg.Append(", "); + msg.Append(metricPoint.EndTime.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ", CultureInfo.InvariantCulture)); + msg.Append("] "); + msg.Append(tags); + if (tags != string.Empty) + { + msg.Append(' '); + } - if (exemplarString.Length > 0) - { - msg.AppendLine(); - msg.AppendLine("Exemplars"); - msg.Append(exemplarString.ToString()); - } + msg.Append(metric.MetricType); + msg.AppendLine(); + msg.Append($"Value: {valueDisplay}"); - this.WriteLine(msg.ToString()); + if (exemplarString.Length > 0) + { + msg.AppendLine(); + msg.AppendLine("Exemplars"); + msg.Append(exemplarString.ToString()); } - } - return ExportResult.Success; + this.WriteLine(msg.ToString()); + } } + + return ExportResult.Success; } } diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleTagTransformer.cs b/src/OpenTelemetry.Exporter.Console/ConsoleTagTransformer.cs index a5a29588bc5..308d2da6e1e 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleTagTransformer.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleTagTransformer.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Internal; @@ -35,5 +22,5 @@ private ConsoleTagTransformer() protected override string TransformStringTag(string key, string value) => $"{key}: {value}"; protected override string TransformArrayTag(string key, Array array) - => this.TransformStringTag(key, System.Text.Json.JsonSerializer.Serialize(array)); + => this.TransformStringTag(key, TagTransformerJsonHelper.JsonSerializeArrayTag(array)); } diff --git a/src/OpenTelemetry.Exporter.Console/OpenTelemetry.Exporter.Console.csproj b/src/OpenTelemetry.Exporter.Console/OpenTelemetry.Exporter.Console.csproj index 9c0715d8ccf..46fec7d5e99 100644 --- a/src/OpenTelemetry.Exporter.Console/OpenTelemetry.Exporter.Console.csproj +++ b/src/OpenTelemetry.Exporter.Console/OpenTelemetry.Exporter.Console.csproj @@ -1,8 +1,7 @@ - - netstandard2.0;net462 + $(TargetFrameworksForLibraries) Console exporter for OpenTelemetry .NET $(PackageTags);Console;distributed-tracing core- @@ -25,13 +24,17 @@ - - - - - - - + + + + + + + + + + diff --git a/src/OpenTelemetry.Exporter.Console/README.md b/src/OpenTelemetry.Exporter.Console/README.md index 1394cb255a7..3fe06ddc458 100644 --- a/src/OpenTelemetry.Exporter.Console/README.md +++ b/src/OpenTelemetry.Exporter.Console/README.md @@ -6,7 +6,7 @@ The console exporter prints data to the Console window. ConsoleExporter supports exporting logs, metrics and traces. -> **Note** +> [!WARNING] > This exporter is intended to be used during learning how telemetry data are created and exported. It is not recommended for any production environment. @@ -20,7 +20,7 @@ dotnet add package OpenTelemetry.Exporter.Console See the individual "getting started" examples depending on the signal being used: -* [Logs](../../docs/logs/getting-started/Program.cs) +* Logs: [Console](../../docs/logs/getting-started-console/README.md) * Metrics: [ASP.NET Core](../../docs/metrics/getting-started-aspnetcore/README.md) | [Console](../../docs/metrics/getting-started-console/README.md) * Traces: [ASP.NET Core](../../docs/trace/getting-started-aspnetcore/README.md) diff --git a/src/OpenTelemetry.Exporter.InMemory/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.InMemory/.publicApi/Experimental/PublicAPI.Unshipped.txt similarity index 92% rename from src/OpenTelemetry.Exporter.InMemory/.publicApi/net462/PublicAPI.Unshipped.txt rename to src/OpenTelemetry.Exporter.InMemory/.publicApi/Experimental/PublicAPI.Unshipped.txt index 2faa5d718d0..f7378ee36cc 100644 --- a/src/OpenTelemetry.Exporter.InMemory/.publicApi/net462/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.InMemory/.publicApi/Experimental/PublicAPI.Unshipped.txt @@ -1 +1 @@ -static OpenTelemetry.Logs.InMemoryExporterLoggingExtensions.AddInMemoryExporter(this OpenTelemetry.Logs.LoggerProviderBuilder loggerProviderBuilder, System.Collections.Generic.ICollection exportedItems) -> OpenTelemetry.Logs.LoggerProviderBuilder \ No newline at end of file +static OpenTelemetry.Logs.InMemoryExporterLoggingExtensions.AddInMemoryExporter(this OpenTelemetry.Logs.LoggerProviderBuilder loggerProviderBuilder, System.Collections.Generic.ICollection exportedItems) -> OpenTelemetry.Logs.LoggerProviderBuilder diff --git a/src/OpenTelemetry.Exporter.InMemory/.publicApi/net462/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.InMemory/.publicApi/Stable/PublicAPI.Shipped.txt similarity index 100% rename from src/OpenTelemetry.Exporter.InMemory/.publicApi/net462/PublicAPI.Shipped.txt rename to src/OpenTelemetry.Exporter.InMemory/.publicApi/Stable/PublicAPI.Shipped.txt diff --git a/src/OpenTelemetry.Exporter.Jaeger/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.InMemory/.publicApi/Stable/PublicAPI.Unshipped.txt similarity index 100% rename from src/OpenTelemetry.Exporter.Jaeger/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt rename to src/OpenTelemetry.Exporter.InMemory/.publicApi/Stable/PublicAPI.Unshipped.txt diff --git a/src/OpenTelemetry.Exporter.InMemory/.publicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.InMemory/.publicApi/netstandard2.0/PublicAPI.Shipped.txt deleted file mode 100644 index 499065e2d09..00000000000 --- a/src/OpenTelemetry.Exporter.InMemory/.publicApi/netstandard2.0/PublicAPI.Shipped.txt +++ /dev/null @@ -1,24 +0,0 @@ -OpenTelemetry.Exporter.InMemoryExporter -OpenTelemetry.Exporter.InMemoryExporter.InMemoryExporter(System.Collections.Generic.ICollection exportedItems) -> void -OpenTelemetry.Logs.InMemoryExporterLoggingExtensions -OpenTelemetry.Metrics.InMemoryExporterMetricsExtensions -OpenTelemetry.Metrics.MetricSnapshot -OpenTelemetry.Metrics.MetricSnapshot.Description.get -> string -OpenTelemetry.Metrics.MetricSnapshot.MeterName.get -> string -OpenTelemetry.Metrics.MetricSnapshot.MeterVersion.get -> string -OpenTelemetry.Metrics.MetricSnapshot.MetricPoints.get -> System.Collections.Generic.IReadOnlyList -OpenTelemetry.Metrics.MetricSnapshot.MetricSnapshot(OpenTelemetry.Metrics.Metric metric) -> void -OpenTelemetry.Metrics.MetricSnapshot.MetricType.get -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricSnapshot.Name.get -> string -OpenTelemetry.Metrics.MetricSnapshot.Unit.get -> string -OpenTelemetry.Trace.InMemoryExporterHelperExtensions -override OpenTelemetry.Exporter.InMemoryExporter.Dispose(bool disposing) -> void -override OpenTelemetry.Exporter.InMemoryExporter.Export(in OpenTelemetry.Batch batch) -> OpenTelemetry.ExportResult -static OpenTelemetry.Logs.InMemoryExporterLoggingExtensions.AddInMemoryExporter(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions loggerOptions, System.Collections.Generic.ICollection exportedItems) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions -static OpenTelemetry.Metrics.InMemoryExporterMetricsExtensions.AddInMemoryExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, string name, System.Collections.Generic.ICollection exportedItems, System.Action configureMetricReader) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.InMemoryExporterMetricsExtensions.AddInMemoryExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, string name, System.Collections.Generic.ICollection exportedItems, System.Action configureMetricReader) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.InMemoryExporterMetricsExtensions.AddInMemoryExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Collections.Generic.ICollection exportedItems) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.InMemoryExporterMetricsExtensions.AddInMemoryExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Collections.Generic.ICollection exportedItems, System.Action configureMetricReader) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.InMemoryExporterMetricsExtensions.AddInMemoryExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Collections.Generic.ICollection exportedItems) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.InMemoryExporterMetricsExtensions.AddInMemoryExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Collections.Generic.ICollection exportedItems, System.Action configureMetricReader) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Trace.InMemoryExporterHelperExtensions.AddInMemoryExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Collections.Generic.ICollection exportedItems) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Exporter.InMemory/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.InMemory/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt deleted file mode 100644 index 2faa5d718d0..00000000000 --- a/src/OpenTelemetry.Exporter.InMemory/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1 +0,0 @@ -static OpenTelemetry.Logs.InMemoryExporterLoggingExtensions.AddInMemoryExporter(this OpenTelemetry.Logs.LoggerProviderBuilder loggerProviderBuilder, System.Collections.Generic.ICollection exportedItems) -> OpenTelemetry.Logs.LoggerProviderBuilder \ No newline at end of file diff --git a/src/OpenTelemetry.Exporter.InMemory/AssemblyInfo.cs b/src/OpenTelemetry.Exporter.InMemory/AssemblyInfo.cs new file mode 100644 index 00000000000..f7775c1b9b8 --- /dev/null +++ b/src/OpenTelemetry.Exporter.InMemory/AssemblyInfo.cs @@ -0,0 +1,20 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if !EXPOSE_EXPERIMENTAL_FEATURES +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests" + AssemblyInfo.PublicKey)] +#endif + +#if SIGNED +file static class AssemblyInfo +{ + public const string PublicKey = ", PublicKey=002400000480000094000000060200000024000052534131000400000100010051C1562A090FB0C9F391012A32198B5E5D9A60E9B80FA2D7B434C9E5CCB7259BD606E66F9660676AFC6692B8CDC6793D190904551D2103B7B22FA636DCBB8208839785BA402EA08FC00C8F1500CCEF28BBF599AA64FFB1E1D5DC1BF3420A3777BADFE697856E9D52070A50C3EA5821C80BEF17CA3ACFFA28F89DD413F096F898"; +} +#else +file static class AssemblyInfo +{ + public const string PublicKey = ""; +} +#endif diff --git a/src/OpenTelemetry.Exporter.InMemory/CHANGELOG.md b/src/OpenTelemetry.Exporter.InMemory/CHANGELOG.md index 77d52dfbf3c..42ad0c3b524 100644 --- a/src/OpenTelemetry.Exporter.InMemory/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.InMemory/CHANGELOG.md @@ -2,8 +2,34 @@ ## Unreleased -* Added `LoggerProviderBuilder.AddInMemoryExporter` registration extension. - ([#4584](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4584)) +## 1.7.0 + +Released 2023-Dec-08 + +## 1.7.0-rc.1 + +Released 2023-Nov-29 + +## 1.7.0-alpha.1 + +Released 2023-Oct-16 + +## 1.6.0 + +Released 2023-Sep-05 + +## 1.6.0-rc.1 + +Released 2023-Aug-21 + +## 1.6.0-alpha.1 + +Released 2023-Jul-12 + +* **Experimental (pre-release builds only):** Added + `LoggerProviderBuilder.AddInMemoryExporter` registration extension. + ([#4584](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4584), + [#4735](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4735)) ## 1.5.1 diff --git a/src/OpenTelemetry.Exporter.InMemory/InMemoryExporter.cs b/src/OpenTelemetry.Exporter.InMemory/InMemoryExporter.cs index 2f940c7d65d..dedcf65b37c 100644 --- a/src/OpenTelemetry.Exporter.InMemory/InMemoryExporter.cs +++ b/src/OpenTelemetry.Exporter.InMemory/InMemoryExporter.cs @@ -1,80 +1,66 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Exporter +namespace OpenTelemetry.Exporter; + +public class InMemoryExporter : BaseExporter + where T : class { - public class InMemoryExporter : BaseExporter - where T : class + private readonly ICollection exportedItems; + private readonly ExportFunc onExport; + private bool disposed; + private string disposedStackTrace; + + public InMemoryExporter(ICollection exportedItems) { - private readonly ICollection exportedItems; - private readonly ExportFunc onExport; - private bool disposed; - private string disposedStackTrace; + this.exportedItems = exportedItems; + this.onExport = this.DefaultExport; + } - public InMemoryExporter(ICollection exportedItems) - { - this.exportedItems = exportedItems; - this.onExport = this.DefaultExport; - } + internal InMemoryExporter(ExportFunc exportFunc) + { + this.onExport = exportFunc; + } + + internal delegate ExportResult ExportFunc(in Batch batch); - internal InMemoryExporter(ExportFunc exportFunc) + public override ExportResult Export(in Batch batch) + { + if (this.disposed) { - this.onExport = exportFunc; + // Note: In-memory exporter is designed for testing purposes so this error is strategic to surface lifecycle management bugs during development. + throw new ObjectDisposedException( + this.GetType().Name, + $"The in-memory exporter is still being invoked after it has been disposed. This could be the result of invalid lifecycle management of the OpenTelemetry .NET SDK. Dispose was called on the following stack trace:{Environment.NewLine}{this.disposedStackTrace}"); } - internal delegate ExportResult ExportFunc(in Batch batch); + return this.onExport(batch); + } - public override ExportResult Export(in Batch batch) + /// + protected override void Dispose(bool disposing) + { + if (!this.disposed) { - if (this.disposed) - { - // Note: In-memory exporter is designed for testing purposes so this error is strategic to surface lifecycle management bugs during development. - throw new ObjectDisposedException( - this.GetType().Name, - $"The in-memory exporter is still being invoked after it has been disposed. This could be the result of invalid lifecycle management of the OpenTelemetry .NET SDK. Dispose was called on the following stack trace:{Environment.NewLine}{this.disposedStackTrace}"); - } - - return this.onExport(batch); + this.disposedStackTrace = Environment.StackTrace; + this.disposed = true; } - /// - protected override void Dispose(bool disposing) - { - if (!this.disposed) - { - this.disposedStackTrace = Environment.StackTrace; - this.disposed = true; - } + base.Dispose(disposing); + } - base.Dispose(disposing); + private ExportResult DefaultExport(in Batch batch) + { + if (this.exportedItems == null) + { + return ExportResult.Failure; } - private ExportResult DefaultExport(in Batch batch) + foreach (var data in batch) { - if (this.exportedItems == null) - { - return ExportResult.Failure; - } - - foreach (var data in batch) - { - this.exportedItems.Add(data); - } - - return ExportResult.Success; + this.exportedItems.Add(data); } + + return ExportResult.Success; } } diff --git a/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterHelperExtensions.cs index 289ac2871a5..3940a846bc3 100644 --- a/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterHelperExtensions.cs @@ -1,39 +1,25 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Exporter; using OpenTelemetry.Internal; -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +public static class InMemoryExporterHelperExtensions { - public static class InMemoryExporterHelperExtensions + /// + /// Adds InMemory exporter to the TracerProvider. + /// + /// builder to use. + /// Collection which will be populated with the exported . + /// The instance of to chain the calls. + public static TracerProviderBuilder AddInMemoryExporter(this TracerProviderBuilder builder, ICollection exportedItems) { - /// - /// Adds InMemory exporter to the TracerProvider. - /// - /// builder to use. - /// Collection which will be populated with the exported . - /// The instance of to chain the calls. - public static TracerProviderBuilder AddInMemoryExporter(this TracerProviderBuilder builder, ICollection exportedItems) - { - Guard.ThrowIfNull(builder); - Guard.ThrowIfNull(exportedItems); + Guard.ThrowIfNull(builder); + Guard.ThrowIfNull(exportedItems); - return builder.AddProcessor(new SimpleActivityExportProcessor(new InMemoryExporter(exportedItems))); - } + return builder.AddProcessor(new SimpleActivityExportProcessor(new InMemoryExporter(exportedItems))); } } diff --git a/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterLoggingExtensions.cs b/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterLoggingExtensions.cs index b4c6ed7900e..982595f8824 100644 --- a/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterLoggingExtensions.cs +++ b/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterLoggingExtensions.cs @@ -1,79 +1,83 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif using OpenTelemetry.Exporter; using OpenTelemetry.Internal; -namespace OpenTelemetry.Logs +namespace OpenTelemetry.Logs; + +public static class InMemoryExporterLoggingExtensions { - public static class InMemoryExporterLoggingExtensions + /// + /// Adds InMemory exporter to the OpenTelemetryLoggerOptions. + /// + /// options to use. + /// Collection which will be populated with the exported . + /// The supplied instance of to chain the calls. + /// todo: [Obsolete("Call LoggerProviderBuilder.AddInMemoryExporter instead this method will be removed in a future version.")] + public static OpenTelemetryLoggerOptions AddInMemoryExporter( + this OpenTelemetryLoggerOptions loggerOptions, + ICollection exportedItems) { - /// - /// Adds InMemory exporter to the OpenTelemetryLoggerOptions. - /// - /// options to use. - /// Collection which will be populated with the exported . - /// The supplied instance of to chain the calls. - /// todo: [Obsolete("Call LoggerProviderBuilder.AddInMemoryExporter instead this method will be removed in a future version.")] - public static OpenTelemetryLoggerOptions AddInMemoryExporter( - this OpenTelemetryLoggerOptions loggerOptions, - ICollection exportedItems) - { - Guard.ThrowIfNull(loggerOptions); - Guard.ThrowIfNull(exportedItems); + Guard.ThrowIfNull(loggerOptions); + Guard.ThrowIfNull(exportedItems); - var logExporter = BuildExporter(exportedItems); + var logExporter = BuildExporter(exportedItems); - return loggerOptions.AddProcessor( - new SimpleLogRecordExportProcessor(logExporter)); - } + return loggerOptions.AddProcessor( + new SimpleLogRecordExportProcessor(logExporter)); + } - /// - /// Adds InMemory exporter to the LoggerProviderBuilder. - /// - /// . - /// Collection which will be populated with the exported . - /// The supplied instance of to chain the calls. - public static LoggerProviderBuilder AddInMemoryExporter( - this LoggerProviderBuilder loggerProviderBuilder, - ICollection exportedItems) - { - Guard.ThrowIfNull(loggerProviderBuilder); - Guard.ThrowIfNull(exportedItems); +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds InMemory exporter to the LoggerProviderBuilder. + /// + /// WARNING: This is an experimental API which might change or be removed in the future. Use at your own risk. + /// . + /// Collection which will be populated with the exported . + /// The supplied instance of to chain the calls. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + /// + /// Adds InMemory exporter to the LoggerProviderBuilder. + /// + /// . + /// Collection which will be populated with the exported . + /// The supplied instance of to chain the calls. + internal +#endif + static LoggerProviderBuilder AddInMemoryExporter( + this LoggerProviderBuilder loggerProviderBuilder, + ICollection exportedItems) + { + Guard.ThrowIfNull(loggerProviderBuilder); + Guard.ThrowIfNull(exportedItems); - var logExporter = BuildExporter(exportedItems); + var logExporter = BuildExporter(exportedItems); - return loggerProviderBuilder.AddProcessor( - new SimpleLogRecordExportProcessor(logExporter)); - } + return loggerProviderBuilder.AddProcessor( + new SimpleLogRecordExportProcessor(logExporter)); + } - private static InMemoryExporter BuildExporter(ICollection exportedItems) - { - return new InMemoryExporter( - exportFunc: (in Batch batch) => ExportLogRecord(in batch, exportedItems)); - } + private static InMemoryExporter BuildExporter(ICollection exportedItems) + { + return new InMemoryExporter( + exportFunc: (in Batch batch) => ExportLogRecord(in batch, exportedItems)); + } - private static ExportResult ExportLogRecord(in Batch batch, ICollection exportedItems) + private static ExportResult ExportLogRecord(in Batch batch, ICollection exportedItems) + { + foreach (var log in batch) { - foreach (var log in batch) - { - exportedItems.Add(log.Copy()); - } - - return ExportResult.Success; + exportedItems.Add(log.Copy()); } + + return ExportResult.Success; } } diff --git a/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterMetricsExtensions.cs b/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterMetricsExtensions.cs index 36c7514a418..e645b67b455 100644 --- a/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterMetricsExtensions.cs +++ b/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterMetricsExtensions.cs @@ -1,205 +1,191 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using OpenTelemetry.Exporter; using OpenTelemetry.Internal; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// Extension methods to simplify registering of the InMemory exporter. +/// +public static class InMemoryExporterMetricsExtensions { + private const int DefaultExportIntervalMilliseconds = Timeout.Infinite; + private const int DefaultExportTimeoutMilliseconds = Timeout.Infinite; + /// - /// Extension methods to simplify registering of the InMemory exporter. + /// Adds InMemory metric exporter to the using default options. /// - public static class InMemoryExporterMetricsExtensions - { - private const int DefaultExportIntervalMilliseconds = Timeout.Infinite; - private const int DefaultExportTimeoutMilliseconds = Timeout.Infinite; - - /// - /// Adds InMemory metric exporter to the using default options. - /// - /// - /// Be aware that may continue to be updated after export. - /// - /// builder to use. - /// Collection which will be populated with the exported . - /// The instance of to chain the calls. - public static MeterProviderBuilder AddInMemoryExporter(this MeterProviderBuilder builder, ICollection exportedItems) - => AddInMemoryExporter(builder, name: null, exportedItems, configureMetricReader: null); - - /// - /// Adds InMemory metric exporter to the . - /// - /// - /// Be aware that may continue to be updated after export. - /// - /// builder to use. - /// Collection which will be populated with the exported . - /// Callback action for configuring . - /// The instance of to chain the calls. - public static MeterProviderBuilder AddInMemoryExporter( - this MeterProviderBuilder builder, - ICollection exportedItems, - Action configureMetricReader) - => AddInMemoryExporter(builder, name: null, exportedItems, configureMetricReader); - - /// - /// Adds InMemory metric exporter to the . - /// - /// - /// Be aware that may continue to be updated after export. - /// - /// builder to use. - /// Name which is used when retrieving options. - /// Collection which will be populated with the exported . - /// Callback action for configuring . - /// The instance of to chain the calls. - public static MeterProviderBuilder AddInMemoryExporter( - this MeterProviderBuilder builder, - string name, - ICollection exportedItems, - Action configureMetricReader) - { - Guard.ThrowIfNull(builder); - Guard.ThrowIfNull(exportedItems); + /// + /// Be aware that may continue to be updated after export. + /// + /// builder to use. + /// Collection which will be populated with the exported . + /// The instance of to chain the calls. + public static MeterProviderBuilder AddInMemoryExporter(this MeterProviderBuilder builder, ICollection exportedItems) + => AddInMemoryExporter(builder, name: null, exportedItems, configureMetricReader: null); - name ??= Options.DefaultName; + /// + /// Adds InMemory metric exporter to the . + /// + /// + /// Be aware that may continue to be updated after export. + /// + /// builder to use. + /// Collection which will be populated with the exported . + /// Callback action for configuring . + /// The instance of to chain the calls. + public static MeterProviderBuilder AddInMemoryExporter( + this MeterProviderBuilder builder, + ICollection exportedItems, + Action configureMetricReader) + => AddInMemoryExporter(builder, name: null, exportedItems, configureMetricReader); - if (configureMetricReader != null) - { - builder.ConfigureServices(services => services.Configure(name, configureMetricReader)); - } + /// + /// Adds InMemory metric exporter to the . + /// + /// + /// Be aware that may continue to be updated after export. + /// + /// builder to use. + /// Name which is used when retrieving options. + /// Collection which will be populated with the exported . + /// Callback action for configuring . + /// The instance of to chain the calls. + public static MeterProviderBuilder AddInMemoryExporter( + this MeterProviderBuilder builder, + string name, + ICollection exportedItems, + Action configureMetricReader) + { + Guard.ThrowIfNull(builder); + Guard.ThrowIfNull(exportedItems); - return builder.AddReader(sp => - { - var options = sp.GetRequiredService>().Get(name); + name ??= Options.DefaultName; - return BuildInMemoryExporterMetricReader(exportedItems, options); - }); + if (configureMetricReader != null) + { + builder.ConfigureServices(services => services.Configure(name, configureMetricReader)); } - /// - /// Adds InMemory metric exporter to the using default options. - /// The exporter will be setup to export . - /// - /// - /// Use this if you need a copy of that will not be updated after export. - /// - /// builder to use. - /// Collection which will be populated with the exported represented as . - /// The instance of to chain the calls. - public static MeterProviderBuilder AddInMemoryExporter( - this MeterProviderBuilder builder, - ICollection exportedItems) - => AddInMemoryExporter(builder, name: null, exportedItems, configureMetricReader: null); - - /// - /// Adds InMemory metric exporter to the . - /// The exporter will be setup to export . - /// - /// - /// Use this if you need a copy of that will not be updated after export. - /// - /// builder to use. - /// Collection which will be populated with the exported represented as . - /// Callback action for configuring . - /// The instance of to chain the calls. - public static MeterProviderBuilder AddInMemoryExporter( - this MeterProviderBuilder builder, - ICollection exportedItems, - Action configureMetricReader) - => AddInMemoryExporter(builder, name: null, exportedItems, configureMetricReader); - - /// - /// Adds InMemory metric exporter to the . - /// The exporter will be setup to export . - /// - /// - /// Use this if you need a copy of that will not be updated after export. - /// - /// builder to use. - /// Name which is used when retrieving options. - /// Collection which will be populated with the exported represented as . - /// Callback action for configuring . - /// The instance of to chain the calls. - public static MeterProviderBuilder AddInMemoryExporter( - this MeterProviderBuilder builder, - string name, - ICollection exportedItems, - Action configureMetricReader) + return builder.AddReader(sp => { - Guard.ThrowIfNull(builder); - Guard.ThrowIfNull(exportedItems); + var options = sp.GetRequiredService>().Get(name); + + return BuildInMemoryExporterMetricReader(exportedItems, options); + }); + } + + /// + /// Adds InMemory metric exporter to the using default options. + /// The exporter will be setup to export . + /// + /// + /// Use this if you need a copy of that will not be updated after export. + /// + /// builder to use. + /// Collection which will be populated with the exported represented as . + /// The instance of to chain the calls. + public static MeterProviderBuilder AddInMemoryExporter( + this MeterProviderBuilder builder, + ICollection exportedItems) + => AddInMemoryExporter(builder, name: null, exportedItems, configureMetricReader: null); - name ??= Options.DefaultName; + /// + /// Adds InMemory metric exporter to the . + /// The exporter will be setup to export . + /// + /// + /// Use this if you need a copy of that will not be updated after export. + /// + /// builder to use. + /// Collection which will be populated with the exported represented as . + /// Callback action for configuring . + /// The instance of to chain the calls. + public static MeterProviderBuilder AddInMemoryExporter( + this MeterProviderBuilder builder, + ICollection exportedItems, + Action configureMetricReader) + => AddInMemoryExporter(builder, name: null, exportedItems, configureMetricReader); - if (configureMetricReader != null) - { - builder.ConfigureServices(services => services.Configure(name, configureMetricReader)); - } + /// + /// Adds InMemory metric exporter to the . + /// The exporter will be setup to export . + /// + /// + /// Use this if you need a copy of that will not be updated after export. + /// + /// builder to use. + /// Name which is used when retrieving options. + /// Collection which will be populated with the exported represented as . + /// Callback action for configuring . + /// The instance of to chain the calls. + public static MeterProviderBuilder AddInMemoryExporter( + this MeterProviderBuilder builder, + string name, + ICollection exportedItems, + Action configureMetricReader) + { + Guard.ThrowIfNull(builder); + Guard.ThrowIfNull(exportedItems); - return builder.AddReader(sp => - { - var options = sp.GetRequiredService>().Get(name); + name ??= Options.DefaultName; - return BuildInMemoryExporterMetricReader(exportedItems, options); - }); + if (configureMetricReader != null) + { + builder.ConfigureServices(services => services.Configure(name, configureMetricReader)); } - private static MetricReader BuildInMemoryExporterMetricReader( - ICollection exportedItems, - MetricReaderOptions metricReaderOptions) + return builder.AddReader(sp => { - var metricExporter = new InMemoryExporter(exportedItems); + var options = sp.GetRequiredService>().Get(name); - return PeriodicExportingMetricReaderHelper.CreatePeriodicExportingMetricReader( - metricExporter, - metricReaderOptions, - DefaultExportIntervalMilliseconds, - DefaultExportTimeoutMilliseconds); - } + return BuildInMemoryExporterMetricReader(exportedItems, options); + }); + } + + private static MetricReader BuildInMemoryExporterMetricReader( + ICollection exportedItems, + MetricReaderOptions metricReaderOptions) + { + var metricExporter = new InMemoryExporter(exportedItems); - private static MetricReader BuildInMemoryExporterMetricReader( - ICollection exportedItems, - MetricReaderOptions metricReaderOptions) + return PeriodicExportingMetricReaderHelper.CreatePeriodicExportingMetricReader( + metricExporter, + metricReaderOptions, + DefaultExportIntervalMilliseconds, + DefaultExportTimeoutMilliseconds); + } + + private static MetricReader BuildInMemoryExporterMetricReader( + ICollection exportedItems, + MetricReaderOptions metricReaderOptions) + { + var metricExporter = new InMemoryExporter( + exportFunc: (in Batch metricBatch) => ExportMetricSnapshot(in metricBatch, exportedItems)); + + return PeriodicExportingMetricReaderHelper.CreatePeriodicExportingMetricReader( + metricExporter, + metricReaderOptions, + DefaultExportIntervalMilliseconds, + DefaultExportTimeoutMilliseconds); + } + + private static ExportResult ExportMetricSnapshot(in Batch batch, ICollection exportedItems) + { + if (exportedItems == null) { - var metricExporter = new InMemoryExporter( - exportFunc: (in Batch metricBatch) => ExportMetricSnapshot(in metricBatch, exportedItems)); - - return PeriodicExportingMetricReaderHelper.CreatePeriodicExportingMetricReader( - metricExporter, - metricReaderOptions, - DefaultExportIntervalMilliseconds, - DefaultExportTimeoutMilliseconds); + return ExportResult.Failure; } - private static ExportResult ExportMetricSnapshot(in Batch batch, ICollection exportedItems) + foreach (var metric in batch) { - if (exportedItems == null) - { - return ExportResult.Failure; - } - - foreach (var metric in batch) - { - exportedItems.Add(new MetricSnapshot(metric)); - } - - return ExportResult.Success; + exportedItems.Add(new MetricSnapshot(metric)); } + + return ExportResult.Success; } } diff --git a/src/OpenTelemetry.Exporter.InMemory/MetricSnapshot.cs b/src/OpenTelemetry.Exporter.InMemory/MetricSnapshot.cs index 73698055410..983cf7bb2b8 100644 --- a/src/OpenTelemetry.Exporter.InMemory/MetricSnapshot.cs +++ b/src/OpenTelemetry.Exporter.InMemory/MetricSnapshot.cs @@ -1,56 +1,42 @@ -// // 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. -// - -namespace OpenTelemetry.Metrics +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Metrics; + +/// +/// This class represents a selective copy of . +/// This contains the minimum fields and properties needed for most +/// unit testing scenarios. +/// +public class MetricSnapshot { - /// - /// This class represents a selective copy of . - /// This contains the minimum fields and properties needed for most - /// unit testing scenarios. - /// - public class MetricSnapshot + private readonly MetricStreamIdentity instrumentIdentity; + + public MetricSnapshot(Metric metric) { - private readonly MetricStreamIdentity instrumentIdentity; + this.instrumentIdentity = metric.InstrumentIdentity; + this.MetricType = metric.MetricType; - public MetricSnapshot(Metric metric) + List metricPoints = new(); + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) { - this.instrumentIdentity = metric.InstrumentIdentity; - this.MetricType = metric.MetricType; - - List metricPoints = new(); - foreach (ref readonly var metricPoint in metric.GetMetricPoints()) - { - metricPoints.Add(metricPoint.Copy()); - } - - this.MetricPoints = metricPoints; + metricPoints.Add(metricPoint.Copy()); } - public string Name => this.instrumentIdentity.InstrumentName; + this.MetricPoints = metricPoints; + } + + public string Name => this.instrumentIdentity.InstrumentName; - public string Description => this.instrumentIdentity.Description; + public string Description => this.instrumentIdentity.Description; - public string Unit => this.instrumentIdentity.Unit; + public string Unit => this.instrumentIdentity.Unit; - public string MeterName => this.instrumentIdentity.MeterName; + public string MeterName => this.instrumentIdentity.MeterName; - public MetricType MetricType { get; } + public MetricType MetricType { get; } - public string MeterVersion => this.instrumentIdentity.MeterVersion; + public string MeterVersion => this.instrumentIdentity.MeterVersion; - public IReadOnlyList MetricPoints { get; } - } + public IReadOnlyList MetricPoints { get; } } diff --git a/src/OpenTelemetry.Exporter.InMemory/OpenTelemetry.Exporter.InMemory.csproj b/src/OpenTelemetry.Exporter.InMemory/OpenTelemetry.Exporter.InMemory.csproj index 20909e3de94..7f7488e8c5a 100644 --- a/src/OpenTelemetry.Exporter.InMemory/OpenTelemetry.Exporter.InMemory.csproj +++ b/src/OpenTelemetry.Exporter.InMemory/OpenTelemetry.Exporter.InMemory.csproj @@ -1,8 +1,7 @@ - - netstandard2.0;net462 + $(TargetFrameworksForLibraries) In-memory exporter for OpenTelemetry .NET $(PackageTags) core- @@ -20,7 +19,12 @@ - + + + + + diff --git a/src/OpenTelemetry.Exporter.Jaeger/.publicApi/net462/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.Jaeger/.publicApi/net462/PublicAPI.Shipped.txt deleted file mode 100644 index 2ec1855b58b..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/.publicApi/net462/PublicAPI.Shipped.txt +++ /dev/null @@ -1,29 +0,0 @@ -OpenTelemetry.Exporter.JaegerExporter -OpenTelemetry.Exporter.JaegerExporter.JaegerExporter(OpenTelemetry.Exporter.JaegerExporterOptions options) -> void -OpenTelemetry.Exporter.JaegerExporterOptions -OpenTelemetry.Exporter.JaegerExporterOptions.AgentHost.get -> string -OpenTelemetry.Exporter.JaegerExporterOptions.AgentHost.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.AgentPort.get -> int -OpenTelemetry.Exporter.JaegerExporterOptions.AgentPort.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.BatchExportProcessorOptions.get -> OpenTelemetry.BatchExportProcessorOptions -OpenTelemetry.Exporter.JaegerExporterOptions.BatchExportProcessorOptions.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.Endpoint.get -> System.Uri -OpenTelemetry.Exporter.JaegerExporterOptions.Endpoint.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.ExportProcessorType.get -> OpenTelemetry.ExportProcessorType -OpenTelemetry.Exporter.JaegerExporterOptions.ExportProcessorType.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.HttpClientFactory.get -> System.Func -OpenTelemetry.Exporter.JaegerExporterOptions.HttpClientFactory.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.JaegerExporterOptions() -> void -OpenTelemetry.Exporter.JaegerExporterOptions.MaxPayloadSizeInBytes.get -> int? -OpenTelemetry.Exporter.JaegerExporterOptions.MaxPayloadSizeInBytes.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.Protocol.get -> OpenTelemetry.Exporter.JaegerExportProtocol -OpenTelemetry.Exporter.JaegerExporterOptions.Protocol.set -> void -OpenTelemetry.Exporter.JaegerExportProtocol -OpenTelemetry.Exporter.JaegerExportProtocol.HttpBinaryThrift = 1 -> OpenTelemetry.Exporter.JaegerExportProtocol -OpenTelemetry.Exporter.JaegerExportProtocol.UdpCompactThrift = 0 -> OpenTelemetry.Exporter.JaegerExportProtocol -OpenTelemetry.Trace.JaegerExporterHelperExtensions -override OpenTelemetry.Exporter.JaegerExporter.Dispose(bool disposing) -> void -override OpenTelemetry.Exporter.JaegerExporter.Export(in OpenTelemetry.Batch activityBatch) -> OpenTelemetry.ExportResult -static OpenTelemetry.Trace.JaegerExporterHelperExtensions.AddJaegerExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.JaegerExporterHelperExtensions.AddJaegerExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.JaegerExporterHelperExtensions.AddJaegerExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Exporter.Jaeger/.publicApi/net6.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.Jaeger/.publicApi/net6.0/PublicAPI.Shipped.txt deleted file mode 100644 index 2ec1855b58b..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/.publicApi/net6.0/PublicAPI.Shipped.txt +++ /dev/null @@ -1,29 +0,0 @@ -OpenTelemetry.Exporter.JaegerExporter -OpenTelemetry.Exporter.JaegerExporter.JaegerExporter(OpenTelemetry.Exporter.JaegerExporterOptions options) -> void -OpenTelemetry.Exporter.JaegerExporterOptions -OpenTelemetry.Exporter.JaegerExporterOptions.AgentHost.get -> string -OpenTelemetry.Exporter.JaegerExporterOptions.AgentHost.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.AgentPort.get -> int -OpenTelemetry.Exporter.JaegerExporterOptions.AgentPort.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.BatchExportProcessorOptions.get -> OpenTelemetry.BatchExportProcessorOptions -OpenTelemetry.Exporter.JaegerExporterOptions.BatchExportProcessorOptions.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.Endpoint.get -> System.Uri -OpenTelemetry.Exporter.JaegerExporterOptions.Endpoint.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.ExportProcessorType.get -> OpenTelemetry.ExportProcessorType -OpenTelemetry.Exporter.JaegerExporterOptions.ExportProcessorType.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.HttpClientFactory.get -> System.Func -OpenTelemetry.Exporter.JaegerExporterOptions.HttpClientFactory.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.JaegerExporterOptions() -> void -OpenTelemetry.Exporter.JaegerExporterOptions.MaxPayloadSizeInBytes.get -> int? -OpenTelemetry.Exporter.JaegerExporterOptions.MaxPayloadSizeInBytes.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.Protocol.get -> OpenTelemetry.Exporter.JaegerExportProtocol -OpenTelemetry.Exporter.JaegerExporterOptions.Protocol.set -> void -OpenTelemetry.Exporter.JaegerExportProtocol -OpenTelemetry.Exporter.JaegerExportProtocol.HttpBinaryThrift = 1 -> OpenTelemetry.Exporter.JaegerExportProtocol -OpenTelemetry.Exporter.JaegerExportProtocol.UdpCompactThrift = 0 -> OpenTelemetry.Exporter.JaegerExportProtocol -OpenTelemetry.Trace.JaegerExporterHelperExtensions -override OpenTelemetry.Exporter.JaegerExporter.Dispose(bool disposing) -> void -override OpenTelemetry.Exporter.JaegerExporter.Export(in OpenTelemetry.Batch activityBatch) -> OpenTelemetry.ExportResult -static OpenTelemetry.Trace.JaegerExporterHelperExtensions.AddJaegerExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.JaegerExporterHelperExtensions.AddJaegerExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.JaegerExporterHelperExtensions.AddJaegerExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Exporter.Jaeger/.publicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.Jaeger/.publicApi/netstandard2.0/PublicAPI.Shipped.txt deleted file mode 100644 index 2ec1855b58b..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/.publicApi/netstandard2.0/PublicAPI.Shipped.txt +++ /dev/null @@ -1,29 +0,0 @@ -OpenTelemetry.Exporter.JaegerExporter -OpenTelemetry.Exporter.JaegerExporter.JaegerExporter(OpenTelemetry.Exporter.JaegerExporterOptions options) -> void -OpenTelemetry.Exporter.JaegerExporterOptions -OpenTelemetry.Exporter.JaegerExporterOptions.AgentHost.get -> string -OpenTelemetry.Exporter.JaegerExporterOptions.AgentHost.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.AgentPort.get -> int -OpenTelemetry.Exporter.JaegerExporterOptions.AgentPort.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.BatchExportProcessorOptions.get -> OpenTelemetry.BatchExportProcessorOptions -OpenTelemetry.Exporter.JaegerExporterOptions.BatchExportProcessorOptions.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.Endpoint.get -> System.Uri -OpenTelemetry.Exporter.JaegerExporterOptions.Endpoint.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.ExportProcessorType.get -> OpenTelemetry.ExportProcessorType -OpenTelemetry.Exporter.JaegerExporterOptions.ExportProcessorType.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.HttpClientFactory.get -> System.Func -OpenTelemetry.Exporter.JaegerExporterOptions.HttpClientFactory.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.JaegerExporterOptions() -> void -OpenTelemetry.Exporter.JaegerExporterOptions.MaxPayloadSizeInBytes.get -> int? -OpenTelemetry.Exporter.JaegerExporterOptions.MaxPayloadSizeInBytes.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.Protocol.get -> OpenTelemetry.Exporter.JaegerExportProtocol -OpenTelemetry.Exporter.JaegerExporterOptions.Protocol.set -> void -OpenTelemetry.Exporter.JaegerExportProtocol -OpenTelemetry.Exporter.JaegerExportProtocol.HttpBinaryThrift = 1 -> OpenTelemetry.Exporter.JaegerExportProtocol -OpenTelemetry.Exporter.JaegerExportProtocol.UdpCompactThrift = 0 -> OpenTelemetry.Exporter.JaegerExportProtocol -OpenTelemetry.Trace.JaegerExporterHelperExtensions -override OpenTelemetry.Exporter.JaegerExporter.Dispose(bool disposing) -> void -override OpenTelemetry.Exporter.JaegerExporter.Export(in OpenTelemetry.Batch activityBatch) -> OpenTelemetry.ExportResult -static OpenTelemetry.Trace.JaegerExporterHelperExtensions.AddJaegerExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.JaegerExporterHelperExtensions.AddJaegerExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.JaegerExporterHelperExtensions.AddJaegerExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Exporter.Jaeger/.publicApi/netstandard2.1/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.Jaeger/.publicApi/netstandard2.1/PublicAPI.Shipped.txt deleted file mode 100644 index 2ec1855b58b..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/.publicApi/netstandard2.1/PublicAPI.Shipped.txt +++ /dev/null @@ -1,29 +0,0 @@ -OpenTelemetry.Exporter.JaegerExporter -OpenTelemetry.Exporter.JaegerExporter.JaegerExporter(OpenTelemetry.Exporter.JaegerExporterOptions options) -> void -OpenTelemetry.Exporter.JaegerExporterOptions -OpenTelemetry.Exporter.JaegerExporterOptions.AgentHost.get -> string -OpenTelemetry.Exporter.JaegerExporterOptions.AgentHost.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.AgentPort.get -> int -OpenTelemetry.Exporter.JaegerExporterOptions.AgentPort.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.BatchExportProcessorOptions.get -> OpenTelemetry.BatchExportProcessorOptions -OpenTelemetry.Exporter.JaegerExporterOptions.BatchExportProcessorOptions.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.Endpoint.get -> System.Uri -OpenTelemetry.Exporter.JaegerExporterOptions.Endpoint.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.ExportProcessorType.get -> OpenTelemetry.ExportProcessorType -OpenTelemetry.Exporter.JaegerExporterOptions.ExportProcessorType.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.HttpClientFactory.get -> System.Func -OpenTelemetry.Exporter.JaegerExporterOptions.HttpClientFactory.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.JaegerExporterOptions() -> void -OpenTelemetry.Exporter.JaegerExporterOptions.MaxPayloadSizeInBytes.get -> int? -OpenTelemetry.Exporter.JaegerExporterOptions.MaxPayloadSizeInBytes.set -> void -OpenTelemetry.Exporter.JaegerExporterOptions.Protocol.get -> OpenTelemetry.Exporter.JaegerExportProtocol -OpenTelemetry.Exporter.JaegerExporterOptions.Protocol.set -> void -OpenTelemetry.Exporter.JaegerExportProtocol -OpenTelemetry.Exporter.JaegerExportProtocol.HttpBinaryThrift = 1 -> OpenTelemetry.Exporter.JaegerExportProtocol -OpenTelemetry.Exporter.JaegerExportProtocol.UdpCompactThrift = 0 -> OpenTelemetry.Exporter.JaegerExportProtocol -OpenTelemetry.Trace.JaegerExporterHelperExtensions -override OpenTelemetry.Exporter.JaegerExporter.Dispose(bool disposing) -> void -override OpenTelemetry.Exporter.JaegerExporter.Export(in OpenTelemetry.Batch activityBatch) -> OpenTelemetry.ExportResult -static OpenTelemetry.Trace.JaegerExporterHelperExtensions.AddJaegerExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.JaegerExporterHelperExtensions.AddJaegerExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.JaegerExporterHelperExtensions.AddJaegerExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/Entities/TField.cs b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/Entities/TField.cs deleted file mode 100644 index e7f50e075fe..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/Entities/TField.cs +++ /dev/null @@ -1,39 +0,0 @@ -// (Turns off StyleCop analysis in this file.) - -// Licensed to the Apache Software Foundation(ASF) under one -// or more contributor license agreements.See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership.The ASF licenses this file -// to you 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. - -namespace Thrift.Protocol.Entities -{ - // ReSharper disable once InconsistentNaming - internal struct TField - { - public TField(string name, TType type, short id) - { - Name = name; - Type = type; - ID = id; - } - - public string Name { get; set; } - - public TType Type { get; set; } - - // ReSharper disable once InconsistentNaming - do not rename - it used for generation - public short ID { get; set; } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/Entities/TList.cs b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/Entities/TList.cs deleted file mode 100644 index fa3d977b97b..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/Entities/TList.cs +++ /dev/null @@ -1,35 +0,0 @@ -// (Turns off StyleCop analysis in this file.) - -// Licensed to the Apache Software Foundation(ASF) under one -// or more contributor license agreements.See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership.The ASF licenses this file -// to you 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. - -namespace Thrift.Protocol.Entities -{ - // ReSharper disable once InconsistentNaming - internal struct TList - { - public TList(TType elementType, int count) - { - ElementType = elementType; - Count = count; - } - - public TType ElementType { get; set; } - - public int Count { get; set; } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/Entities/TMap.cs b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/Entities/TMap.cs deleted file mode 100644 index 1089f88d2b2..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/Entities/TMap.cs +++ /dev/null @@ -1,38 +0,0 @@ -// (Turns off StyleCop analysis in this file.) - -// Licensed to the Apache Software Foundation(ASF) under one -// or more contributor license agreements.See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership.The ASF licenses this file -// to you 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. - -namespace Thrift.Protocol.Entities -{ - // ReSharper disable once InconsistentNaming - internal struct TMap - { - public TMap(TType keyType, TType valueType, int count) - { - KeyType = keyType; - ValueType = valueType; - Count = count; - } - - public TType KeyType { get; set; } - - public TType ValueType { get; set; } - - public int Count { get; set; } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/Entities/TMessage.cs b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/Entities/TMessage.cs deleted file mode 100644 index 87ed863a606..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/Entities/TMessage.cs +++ /dev/null @@ -1,39 +0,0 @@ -// (Turns off StyleCop analysis in this file.) - -// Licensed to the Apache Software Foundation(ASF) under one -// or more contributor license agreements.See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership.The ASF licenses this file -// to you 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. - -namespace Thrift.Protocol.Entities -{ - // ReSharper disable once InconsistentNaming - internal struct TMessage - { - public TMessage(string name, TMessageType type, int seqid) - { - Name = name; - Type = type; - SeqID = seqid; - } - - public string Name { get; set; } - - public TMessageType Type { get; set; } - - // ReSharper disable once InconsistentNaming - do not rename - it used for generation - public int SeqID { get; set; } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/Entities/TMessageType.cs b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/Entities/TMessageType.cs deleted file mode 100644 index d8891eaad62..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/Entities/TMessageType.cs +++ /dev/null @@ -1,30 +0,0 @@ -// (Turns off StyleCop analysis in this file.) - -// Licensed to the Apache Software Foundation(ASF) under one -// or more contributor license agreements.See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership.The ASF licenses this file -// to you 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. - -namespace Thrift.Protocol.Entities -{ - // ReSharper disable once InconsistentNaming - internal enum TMessageType - { - Call = 1, - Reply = 2, - Exception = 3, - Oneway = 4 - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/Entities/TSet.cs b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/Entities/TSet.cs deleted file mode 100644 index 89b68b4c79f..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/Entities/TSet.cs +++ /dev/null @@ -1,40 +0,0 @@ -// (Turns off StyleCop analysis in this file.) - -// Licensed to the Apache Software Foundation(ASF) under one -// or more contributor license agreements.See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership.The ASF licenses this file -// to you 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. - -namespace Thrift.Protocol.Entities -{ - // ReSharper disable once InconsistentNaming - internal struct TSet - { - public TSet(TType elementType, int count) - { - ElementType = elementType; - Count = count; - } - - public TSet(TList list) - : this(list.ElementType, list.Count) - { - } - - public TType ElementType { get; set; } - - public int Count { get; set; } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/Entities/TStruct.cs b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/Entities/TStruct.cs deleted file mode 100644 index bf0e51a16e2..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/Entities/TStruct.cs +++ /dev/null @@ -1,32 +0,0 @@ -// (Turns off StyleCop analysis in this file.) - -// Licensed to the Apache Software Foundation(ASF) under one -// or more contributor license agreements.See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership.The ASF licenses this file -// to you 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. - -namespace Thrift.Protocol.Entities -{ - // ReSharper disable once InconsistentNaming - internal struct TStruct - { - public TStruct(string name) - { - Name = name; - } - - public string Name { get; set; } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/Entities/TType.cs b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/Entities/TType.cs deleted file mode 100644 index 50d531519af..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/Entities/TType.cs +++ /dev/null @@ -1,39 +0,0 @@ -// (Turns off StyleCop analysis in this file.) - -// Licensed to the Apache Software Foundation(ASF) under one -// or more contributor license agreements.See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership.The ASF licenses this file -// to you 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. - -namespace Thrift.Protocol.Entities -{ - // ReSharper disable once InconsistentNaming - internal enum TType : byte - { - Stop = 0, - Void = 1, - Bool = 2, - Byte = 3, - Double = 4, - I16 = 6, - I32 = 8, - I64 = 10, - String = 11, - Struct = 12, - Map = 13, - Set = 14, - List = 15 - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TBinaryProtocol.cs b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TBinaryProtocol.cs deleted file mode 100644 index 4a3454aa40f..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TBinaryProtocol.cs +++ /dev/null @@ -1,231 +0,0 @@ -// (Turns off StyleCop analysis in this file.) - -// Licensed to the Apache Software Foundation(ASF) under one -// or more contributor license agreements.See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership.The ASF licenses this file -// to you 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; -using Thrift.Protocol.Entities; - -namespace Thrift.Protocol -{ - // ReSharper disable once InconsistentNaming - internal sealed class TBinaryProtocol : TProtocol - { - private const uint VersionMask = 0xffff0000; - private const uint Version1 = 0x80010000; - - private readonly bool StrictRead; - private readonly bool StrictWrite; - - // minimize memory allocations by means of an preallocated bytes buffer - // The value of 128 is arbitrarily chosen, the required minimum size must be sizeof(long) - private readonly byte[] PreAllocatedBuffer = new byte[128]; - - private static readonly TStruct AnonymousStruct = new TStruct(string.Empty); - private static readonly TField StopField = new TField() { Type = TType.Stop }; - - public TBinaryProtocol(int initialCapacity = 8192) - : this(false, true, initialCapacity) - { - } - - public TBinaryProtocol(bool strictRead, bool strictWrite, int initialCapacity = 8192) - : base(initialCapacity) - { - StrictRead = strictRead; - StrictWrite = strictWrite; - } - - public override void WriteMessageBegin(TMessage message) - { - if (StrictWrite) - { - var version = Version1 | (uint)message.Type; - WriteI32((int)version); - WriteString(message.Name); - WriteI32(message.SeqID); - } - else - { - WriteString(message.Name); - WriteByte((sbyte)message.Type); - WriteI32(message.SeqID); - } - } - - public override void WriteMessageBegin(TMessage message, out int seqIdPosition) - { - if (StrictWrite) - { - var version = Version1 | (uint)message.Type; - WriteI32((int)version); - WriteString(message.Name); - seqIdPosition = (int)Transport.Position; - WriteI32(message.SeqID); - } - else - { - WriteString(message.Name); - WriteByte((sbyte)message.Type); - seqIdPosition = (int)Transport.Position; - WriteI32(message.SeqID); - } - } - - public override void WriteMessageEnd() - { - } - - public override void WriteStructBegin(TStruct @struct) - { - } - - public override void WriteStructEnd() - { - } - - public override void WriteFieldBegin(TField field) - { - WriteByte((sbyte)field.Type); - WriteI16(field.ID); - } - - public override void WriteFieldEnd() - { - } - - public override void WriteFieldStop() - { - WriteByte((sbyte)TType.Stop); - } - - public override void WriteListBegin(TList list) - { - WriteByte((sbyte)list.ElementType); - WriteI32(list.Count); - } - - public override void WriteListBegin(TList list, out int countPosition) - { - WriteByte((sbyte)list.ElementType); - countPosition = (int)Transport.Position; - WriteI32(list.Count); - } - - public override void WriteListEnd() - { - } - - public override void WriteBool(bool b) - { - WriteByte(b ? (sbyte)1 : (sbyte)0); - } - - public override void WriteByte(sbyte b) - { - PreAllocatedBuffer[0] = (byte)b; - - Transport.Write(PreAllocatedBuffer, 0, 1); - } - - public override void WriteI16(short i16) - { - PreAllocatedBuffer[0] = (byte)(0xff & (i16 >> 8)); - PreAllocatedBuffer[1] = (byte)(0xff & i16); - - Transport.Write(PreAllocatedBuffer, 0, 2); - } - - public override int WriteUI32(uint ui32, Span buffer) - { - if (buffer.Length < 4) - return 0; - - buffer[0] = (byte)(0xff & (ui32 >> 24)); - buffer[1] = (byte)(0xff & (ui32 >> 16)); - buffer[2] = (byte)(0xff & (ui32 >> 8)); - buffer[3] = (byte)(0xff & ui32); - - return 4; - } - - public override void WriteI32(int i32) - { - PreAllocatedBuffer[0] = (byte)(0xff & (i32 >> 24)); - PreAllocatedBuffer[1] = (byte)(0xff & (i32 >> 16)); - PreAllocatedBuffer[2] = (byte)(0xff & (i32 >> 8)); - PreAllocatedBuffer[3] = (byte)(0xff & i32); - - Transport.Write(PreAllocatedBuffer, 0, 4); - } - - public override void WriteI64(long i64) - { - PreAllocatedBuffer[0] = (byte)(0xff & (i64 >> 56)); - PreAllocatedBuffer[1] = (byte)(0xff & (i64 >> 48)); - PreAllocatedBuffer[2] = (byte)(0xff & (i64 >> 40)); - PreAllocatedBuffer[3] = (byte)(0xff & (i64 >> 32)); - PreAllocatedBuffer[4] = (byte)(0xff & (i64 >> 24)); - PreAllocatedBuffer[5] = (byte)(0xff & (i64 >> 16)); - PreAllocatedBuffer[6] = (byte)(0xff & (i64 >> 8)); - PreAllocatedBuffer[7] = (byte)(0xff & i64); - - Transport.Write(PreAllocatedBuffer, 0, 8); - } - - public override void WriteDouble(double d) - { - WriteI64(BitConverter.DoubleToInt64Bits(d)); - } - -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - public override void WriteBinary(ReadOnlySpan bytes) - { - WriteI32(bytes.Length); - Transport.Write(bytes); - } -#endif - - public override void WriteBinary(byte[] bytes, int offset, int count) - { - WriteI32(count); - Transport.Write(bytes, offset, count); - } - - public sealed class Factory : TProtocolFactory - { - private readonly bool StrictRead; - private readonly bool StrictWrite; - - public Factory() - : this(false, true) - { - } - - public Factory(bool strictRead, bool strictWrite) - { - StrictRead = strictRead; - StrictWrite = strictWrite; - } - - public override TProtocol GetProtocol(int initialCapacity = 8192) - { - return new TBinaryProtocol(StrictRead, StrictWrite, initialCapacity); - } - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TCompactProtocol.cs b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TCompactProtocol.cs deleted file mode 100644 index 75f62278d35..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TCompactProtocol.cs +++ /dev/null @@ -1,434 +0,0 @@ -// (Turns off StyleCop analysis in this file.) - -// Licensed to the Apache Software Foundation(ASF) under one -// or more contributor license agreements.See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership.The ASF licenses this file -// to you 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; -using System.Collections.Generic; -using System.Diagnostics; -using Thrift.Protocol.Entities; - -namespace Thrift.Protocol -{ - // ReSharper disable once InconsistentNaming - internal sealed class TCompactProtocol : TProtocol - { - private const byte ProtocolId = 0x82; - private const byte Version = 1; - private const byte VersionMask = 0x1f; // 0001 1111 - private const byte TypeMask = 0xE0; // 1110 0000 - private const byte TypeBits = 0x07; // 0000 0111 - private const int TypeShiftAmount = 5; - private static readonly TStruct AnonymousStruct = new TStruct(string.Empty); - private static readonly TField StopField = new TField(string.Empty, TType.Stop, 0); - - private const byte NoTypeOverride = 0xFF; - - // ReSharper disable once InconsistentNaming - private static readonly byte[] TTypeToCompactType = new byte[16]; - private static readonly TType[] CompactTypeToTType = new TType[13]; - - /// - /// Used to keep track of the last field for the current and previous structs, so we can do the delta stuff. - /// - private readonly Stack _lastField = new Stack(15); - - /// - /// If we encounter a boolean field begin, save the TField here so it can have the value incorporated. - /// - private TField? _booleanField; - - private short _lastFieldId; - - // minimize memory allocations by means of an preallocated bytes buffer - // The value of 128 is arbitrarily chosen, the required minimum size must be sizeof(long) - private readonly byte[] PreAllocatedBuffer = new byte[128]; - - private struct VarInt - { - public byte[] bytes; - public int count; - } - - // minimize memory allocations by means of an preallocated VarInt buffer - private VarInt PreAllocatedVarInt = new VarInt() - { - bytes = new byte[10], // see Int64ToVarInt() - count = 0 - }; - - private readonly byte[] EmptyUInt32Buffer = new byte[5]; - - public TCompactProtocol(int initialCapacity = 8192) - : base(initialCapacity) - { - TTypeToCompactType[(int)TType.Stop] = Types.Stop; - TTypeToCompactType[(int)TType.Bool] = Types.BooleanTrue; - TTypeToCompactType[(int)TType.Byte] = Types.Byte; - TTypeToCompactType[(int)TType.I16] = Types.I16; - TTypeToCompactType[(int)TType.I32] = Types.I32; - TTypeToCompactType[(int)TType.I64] = Types.I64; - TTypeToCompactType[(int)TType.Double] = Types.Double; - TTypeToCompactType[(int)TType.String] = Types.Binary; - TTypeToCompactType[(int)TType.List] = Types.List; - TTypeToCompactType[(int)TType.Set] = Types.Set; - TTypeToCompactType[(int)TType.Map] = Types.Map; - TTypeToCompactType[(int)TType.Struct] = Types.Struct; - - CompactTypeToTType[Types.Stop] = TType.Stop; - CompactTypeToTType[Types.BooleanTrue] = TType.Bool; - CompactTypeToTType[Types.BooleanFalse] = TType.Bool; - CompactTypeToTType[Types.Byte] = TType.Byte; - CompactTypeToTType[Types.I16] = TType.I16; - CompactTypeToTType[Types.I32] = TType.I32; - CompactTypeToTType[Types.I64] = TType.I64; - CompactTypeToTType[Types.Double] = TType.Double; - CompactTypeToTType[Types.Binary] = TType.String; - CompactTypeToTType[Types.List] = TType.List; - CompactTypeToTType[Types.Set] = TType.Set; - CompactTypeToTType[Types.Map] = TType.Map; - CompactTypeToTType[Types.Struct] = TType.Struct; - } - - public override void Reset() - { - _lastField.Clear(); - _lastFieldId = 0; - - base.Reset(); - } - - public override void WriteMessageBegin(TMessage message) - { - PreAllocatedBuffer[0] = ProtocolId; - PreAllocatedBuffer[1] = (byte)((Version & VersionMask) | (((uint)message.Type << TypeShiftAmount) & TypeMask)); - Transport.Write(PreAllocatedBuffer, 0, 2); - - Int32ToVarInt((uint)message.SeqID, ref PreAllocatedVarInt); - Transport.Write(PreAllocatedVarInt.bytes, 0, PreAllocatedVarInt.count); - - WriteString(message.Name); - } - - public override void WriteMessageBegin(TMessage message, out int seqIdPosition) - { - PreAllocatedBuffer[0] = ProtocolId; - PreAllocatedBuffer[1] = (byte)((Version & VersionMask) | (((uint)message.Type << TypeShiftAmount) & TypeMask)); - Transport.Write(PreAllocatedBuffer, 0, 2); - - seqIdPosition = (int)Transport.Position; - // Write empty bytes to reserve the space for the seqId to be written later on. - Transport.Write(EmptyUInt32Buffer, 0, 5); - - WriteString(message.Name); - } - - public override void WriteMessageEnd() - { - } - - /// - /// Write a struct begin. This doesn't actually put anything on the wire. We - /// use it as an opportunity to put special placeholder markers on the field - /// stack so we can get the field id deltas correct. - /// - public override void WriteStructBegin(TStruct @struct) - { - _lastField.Push(_lastFieldId); - _lastFieldId = 0; - } - - public override void WriteStructEnd() - { - _lastFieldId = _lastField.Pop(); - } - - private void WriteFieldBeginInternal(TField field, byte fieldType) - { - // if there's a exType override passed in, use that. Otherwise ask GetCompactType(). - if (fieldType == NoTypeOverride) - fieldType = GetCompactType(field.Type); - - // check if we can use delta encoding for the field id - if (field.ID > _lastFieldId) - { - var delta = field.ID - _lastFieldId; - if (delta <= 15) - { - // Write them together - PreAllocatedBuffer[0] = (byte)((delta << 4) | fieldType); - Transport.Write(PreAllocatedBuffer, 0, 1); - _lastFieldId = field.ID; - return; - } - } - - // Write them separate - PreAllocatedBuffer[0] = fieldType; - Transport.Write(PreAllocatedBuffer, 0, 1); - WriteI16(field.ID); - _lastFieldId = field.ID; - } - - public override void WriteFieldBegin(TField field) - { - if (field.Type == TType.Bool) - { - _booleanField = field; - } - else - { - WriteFieldBeginInternal(field, NoTypeOverride); - } - } - - public override void WriteFieldEnd() - { - } - - public override void WriteFieldStop() - { - PreAllocatedBuffer[0] = Types.Stop; - Transport.Write(PreAllocatedBuffer, 0, 1); - } - - public override void WriteListBegin(TList list) - { - if (list.Count <= 14) - { - PreAllocatedBuffer[0] = (byte)((list.Count << 4) | GetCompactType(list.ElementType)); - Transport.Write(PreAllocatedBuffer, 0, 1); - } - else - { - PreAllocatedBuffer[0] = (byte)(0xf0 | GetCompactType(list.ElementType)); - Transport.Write(PreAllocatedBuffer, 0, 1); - - Int32ToVarInt((uint)list.Count, ref PreAllocatedVarInt); - Transport.Write(PreAllocatedVarInt.bytes, 0, PreAllocatedVarInt.count); - } - } - - public override void WriteListBegin(TList list, out int countPosition) - { - /* - Note: Compact sizing disabled because size might not be known initially. - if (list.Count <= 14) - { - PreAllocatedBuffer[0] = (byte)((list.Count << 4) | GetCompactType(list.ElementType)); - Transport.Write(PreAllocatedBuffer, 0, 1); - } - else*/ - { - PreAllocatedBuffer[0] = (byte)(0xf0 | GetCompactType(list.ElementType)); - Transport.Write(PreAllocatedBuffer, 0, 1); - - countPosition = (int)Transport.Position; - // Write empty bytes to reserve the space for the count to be written later on. - Transport.Write(EmptyUInt32Buffer, 0, 5); - } - } - - public override void WriteListEnd() - { - } - - public override void WriteBool(bool b) - { - /* - Write a boolean value. Potentially, this could be a boolean field, in - which case the field header info isn't written yet. If so, decide what the - right exType header is for the value and then Write the field header. - Otherwise, Write a single byte. - */ - - if (_booleanField != null) - { - // we haven't written the field header yet - var type = b ? Types.BooleanTrue : Types.BooleanFalse; - WriteFieldBeginInternal(_booleanField.Value, type); - _booleanField = null; - } - else - { - // we're not part of a field, so just write the value. - PreAllocatedBuffer[0] = b ? Types.BooleanTrue : Types.BooleanFalse; - Transport.Write(PreAllocatedBuffer, 0, 1); - } - } - - public override void WriteByte(sbyte b) - { - PreAllocatedBuffer[0] = (byte)b; - Transport.Write(PreAllocatedBuffer, 0, 1); - } - - public override void WriteI16(short i16) - { - Int32ToVarInt(IntToZigzag(i16), ref PreAllocatedVarInt); - Transport.Write(PreAllocatedVarInt.bytes, 0, PreAllocatedVarInt.count); - } - - private static void Int32ToVarInt(uint n, ref VarInt varint) - { - // Write an i32 as a varint. Results in 1 - 5 bytes on the wire. - varint.count = 0; - Debug.Assert(varint.bytes.Length >= 5); - - while (true) - { - if ((n & ~0x7F) == 0) - { - varint.bytes[varint.count++] = (byte)n; - break; - } - - varint.bytes[varint.count++] = (byte)((n & 0x7F) | 0x80); - n >>= 7; - } - } - - public override void WriteI32(int i32) - { - Int32ToVarInt(IntToZigzag(i32), ref PreAllocatedVarInt); - Transport.Write(PreAllocatedVarInt.bytes, 0, PreAllocatedVarInt.count); - } - - public override int WriteUI32(uint ui32, Span buffer) - { - if (buffer.Length < 5) - return 0; - - buffer[0] = (byte)(0x80 | (ui32 & 0x7F)); - ui32 >>= 7; - buffer[1] = (byte)(0x80 | (ui32 & 0x7F)); - ui32 >>= 7; - buffer[2] = (byte)(0x80 | (ui32 & 0x7F)); - ui32 >>= 7; - buffer[3] = (byte)(0x80 | (ui32 & 0x7F)); - ui32 >>= 7; - buffer[4] = (byte)ui32; - - return 5; - } - - static private void Int64ToVarInt(ulong n, ref VarInt varint) - { - // Write an i64 as a varint. Results in 1-10 bytes on the wire. - varint.count = 0; - Debug.Assert(varint.bytes.Length >= 10); - - while (true) - { - if ((n & ~(ulong)0x7FL) == 0) - { - varint.bytes[varint.count++] = (byte)n; - break; - } - varint.bytes[varint.count++] = (byte)((n & 0x7F) | 0x80); - n >>= 7; - } - } - - public override void WriteI64(long i64) - { - Int64ToVarInt(LongToZigzag(i64), ref PreAllocatedVarInt); - Transport.Write(PreAllocatedVarInt.bytes, 0, PreAllocatedVarInt.count); - } - - public override void WriteDouble(double d) - { - FixedLongToBytes(BitConverter.DoubleToInt64Bits(d), PreAllocatedBuffer, 0); - Transport.Write(PreAllocatedBuffer, 0, 8); - } - -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - public override void WriteBinary(ReadOnlySpan bytes) - { - Int32ToVarInt((uint)bytes.Length, ref PreAllocatedVarInt); - Transport.Write(PreAllocatedVarInt.bytes, 0, PreAllocatedVarInt.count); - Transport.Write(bytes); - } -#endif - - public override void WriteBinary(byte[] bytes, int offset, int count) - { - Int32ToVarInt((uint)count, ref PreAllocatedVarInt); - Transport.Write(PreAllocatedVarInt.bytes, 0, PreAllocatedVarInt.count); - Transport.Write(bytes, offset, count); - } - - private static byte GetCompactType(TType ttype) - { - // Given a TType value, find the appropriate TCompactProtocol.Types constant. - return TTypeToCompactType[(int)ttype]; - } - - private static ulong LongToZigzag(long n) - { - // Convert l into a zigzag long. This allows negative numbers to be represented compactly as a varint - return (ulong)(n << 1) ^ (ulong)(n >> 63); - } - - private static uint IntToZigzag(int n) - { - // Convert n into a zigzag int. This allows negative numbers to be represented compactly as a varint - return (uint)(n << 1) ^ (uint)(n >> 31); - } - - private static void FixedLongToBytes(long n, byte[] buf, int off) - { - // Convert a long into little-endian bytes in buf starting at off and going until off+7. - buf[off + 0] = (byte)(n & 0xff); - buf[off + 1] = (byte)((n >> 8) & 0xff); - buf[off + 2] = (byte)((n >> 16) & 0xff); - buf[off + 3] = (byte)((n >> 24) & 0xff); - buf[off + 4] = (byte)((n >> 32) & 0xff); - buf[off + 5] = (byte)((n >> 40) & 0xff); - buf[off + 6] = (byte)((n >> 48) & 0xff); - buf[off + 7] = (byte)((n >> 56) & 0xff); - } - - public sealed class Factory : TProtocolFactory - { - public override TProtocol GetProtocol(int initialCapacity = 8192) - { - return new TCompactProtocol(initialCapacity); - } - } - - /// - /// All of the on-wire exType codes. - /// - private static class Types - { - public const byte Stop = 0x00; - public const byte BooleanTrue = 0x01; - public const byte BooleanFalse = 0x02; - public const byte Byte = 0x03; - public const byte I16 = 0x04; - public const byte I32 = 0x05; - public const byte I64 = 0x06; - public const byte Double = 0x07; - public const byte Binary = 0x08; - public const byte List = 0x09; - public const byte Set = 0x0A; - public const byte Map = 0x0B; - public const byte Struct = 0x0C; - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TProtocol.cs b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TProtocol.cs deleted file mode 100644 index ae88af98a4c..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TProtocol.cs +++ /dev/null @@ -1,189 +0,0 @@ -// (Turns off StyleCop analysis in this file.) - -// Licensed to the Apache Software Foundation(ASF) under one -// or more contributor license agreements.See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership.The ASF licenses this file -// to you 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; -using System.Buffers; -using System.IO; -using System.Text; -using Thrift.Protocol.Entities; - -namespace Thrift.Protocol -{ - // ReSharper disable once InconsistentNaming - internal abstract class TProtocol : IDisposable - { - public const int DefaultRecursionDepth = 64; - - private bool _isDisposed; - - protected TProtocol(int initialCapacity = 8192) - { - Transport = new MemoryStream(initialCapacity); - RecursionLimit = DefaultRecursionDepth; - RecursionDepth = 0; - } - - protected MemoryStream Transport { get; } - - protected int RecursionDepth { get; set; } - - protected int RecursionLimit { get; set; } - - public ArraySegment WrittenData - { - get => new ArraySegment(Transport.GetBuffer(), 0, (int)Transport.Length); - } - - public int Position - { - get => (int)Transport.Position; - set => Transport.Position = value; - } - - public int Length => (int)Transport.Length; - - public void Clear(int offset = 0) - { - if (offset > Transport.Length) - throw new ArgumentOutOfRangeException(nameof(offset)); - - Transport.Position = offset; - Transport.SetLength(offset); - } - - public virtual void Reset() - { - RecursionDepth = 0; - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - public void IncrementRecursionDepth() - { - if (RecursionDepth >= RecursionLimit) - { - throw new TProtocolException(TProtocolException.DEPTH_LIMIT, $"Depth of recursion exceeded the limit: {RecursionLimit}"); - } - - ++RecursionDepth; - } - - public void DecrementRecursionDepth() - { - --RecursionDepth; - } - - protected virtual void Dispose(bool disposing) - { - if (!_isDisposed) - { - if (disposing) - { - Transport.Dispose(); - } - } - _isDisposed = true; - } - - public abstract void WriteMessageBegin(TMessage message); - - public abstract void WriteMessageBegin(TMessage message, out int seqIdPosition); - - public abstract void WriteMessageEnd(); - - public abstract void WriteStructBegin(TStruct @struct); - - public abstract void WriteStructEnd(); - - public abstract void WriteFieldBegin(TField field); - - public abstract void WriteFieldEnd(); - - public abstract void WriteFieldStop(); - - public abstract void WriteListBegin(TList list); - - public abstract void WriteListBegin(TList list, out int countPosition); - - public abstract void WriteListEnd(); - - public abstract void WriteBool(bool b); - - public abstract void WriteByte(sbyte b); - - public abstract void WriteI16(short i16); - - public abstract void WriteI32(int i32); - - public abstract int WriteUI32(uint ui32, Span buffer); - - public abstract void WriteI64(long i64); - - public abstract void WriteDouble(double d); - - public virtual void WriteString(string s) - { -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - if (s.Length <= 128) - { - Span buffer = stackalloc byte[256]; - int numberOfBytes = Encoding.UTF8.GetBytes(s, buffer); - WriteBinary(buffer.Slice(0, numberOfBytes)); - return; - } -#endif - var buf = ArrayPool.Shared.Rent(Encoding.UTF8.GetByteCount(s)); - try - { - var numberOfBytes = Encoding.UTF8.GetBytes(s, 0, s.Length, buf, 0); - - WriteBinary(buf, 0, numberOfBytes); - } - finally - { - ArrayPool.Shared.Return(buf); - } - } - -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - public abstract void WriteBinary(ReadOnlySpan bytes); -#endif - - public abstract void WriteBinary(byte[] bytes, int offset, int count); - - public void WriteRaw(byte[] bytes) - { - this.Transport.Write(bytes, 0, bytes.Length); - } - - public void WriteRaw(byte[] bytes, int offset, int count) - { - this.Transport.Write(bytes, offset, count); - } - - public void WriteRaw(ArraySegment bytes) - { - this.Transport.Write(bytes.Array, bytes.Offset, bytes.Count); - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TProtocolException.cs b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TProtocolException.cs deleted file mode 100644 index 8e6d3a8121f..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TProtocolException.cs +++ /dev/null @@ -1,64 +0,0 @@ -// (Turns off StyleCop analysis in this file.) - -// Licensed to the Apache Software Foundation(ASF) under one -// or more contributor license agreements.See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership.The ASF licenses this file -// to you 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. - -// ReSharper disable InconsistentNaming -using System; - -namespace Thrift.Protocol -{ - internal class TProtocolException : TException - { - // do not rename public constants - they used in generated files - public const int UNKNOWN = 0; - public const int INVALID_DATA = 1; - public const int NEGATIVE_SIZE = 2; - public const int SIZE_LIMIT = 3; - public const int BAD_VERSION = 4; - public const int NOT_IMPLEMENTED = 5; - public const int DEPTH_LIMIT = 6; - - protected int Type = UNKNOWN; - - public TProtocolException() - { - } - - public TProtocolException(int type, Exception inner = null) - : base(string.Empty, inner) - { - Type = type; - } - - public TProtocolException(int type, string message, Exception inner = null) - : base(message, inner) - { - Type = type; - } - - public TProtocolException(string message, Exception inner = null) - : base(message, inner) - { - } - - public int GetExceptionType() - { - return Type; - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TProtocolFactory.cs b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TProtocolFactory.cs deleted file mode 100644 index 8f6d2a4b50d..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TProtocolFactory.cs +++ /dev/null @@ -1,27 +0,0 @@ -// (Turns off StyleCop analysis in this file.) - -// Licensed to the Apache Software Foundation(ASF) under one -// or more contributor license agreements.See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership.The ASF licenses this file -// to you 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. - -namespace Thrift.Protocol -{ - // ReSharper disable once InconsistentNaming - internal abstract class TProtocolFactory - { - public abstract TProtocol GetProtocol(int initialCapacity = 8192); - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TUnionBase.cs b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TUnionBase.cs deleted file mode 100644 index 6694f5b05eb..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TUnionBase.cs +++ /dev/null @@ -1,26 +0,0 @@ -// (Turns off StyleCop analysis in this file.) - -// Licensed to the Apache Software Foundation(ASF) under one -// or more contributor license agreements.See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership.The ASF licenses this file -// to you 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. - -namespace Thrift.Protocol -{ - internal interface TUnionBase - { - void Write(TProtocol tProtocol); - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/README.md b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/README.md deleted file mode 100644 index cc82e705b51..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# OpenTelemetry - Jaeger Exporter - Apache Thrift - -This folder contains a stripped-down and customized fork of the [ApacheThrift -0.13.0.1](https://www.nuget.org/packages/ApacheThrift/0.13.0.1) library from the -[apache/thrift](https://github.com/apache/thrift/tree/0.13.0) repo. Only the -client bits we need to transmit spans to Jaeger using the compact Thrift -protocol over UDP and binary Thrift over HTTP are included. Further -customizations have been made to improve the performance of our specific use -cases. diff --git a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/TException.cs b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/TException.cs deleted file mode 100644 index 14d46fe6ad4..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/TException.cs +++ /dev/null @@ -1,36 +0,0 @@ -// (Turns off StyleCop analysis in this file.) - -// Licensed to the Apache Software Foundation(ASF) under one -// or more contributor license agreements.See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership.The ASF licenses this file -// to you 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; - -namespace Thrift -{ - // ReSharper disable once InconsistentNaming - internal class TException : Exception - { - public TException() - { - } - - public TException(string message, Exception inner) - : base(message, inner) - { - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/AssemblyInfo.cs b/src/OpenTelemetry.Exporter.Jaeger/AssemblyInfo.cs deleted file mode 100644 index 48003e39f91..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/AssemblyInfo.cs +++ /dev/null @@ -1,28 +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.Runtime.CompilerServices; - -#if SIGNED -[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Jaeger.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] -[assembly: InternalsVisibleTo("Benchmarks, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] - -// Used by Moq. -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] -#else -[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Jaeger.Tests")] -[assembly: InternalsVisibleTo("Benchmarks")] -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Used by Moq. -#endif diff --git a/src/OpenTelemetry.Exporter.Jaeger/CHANGELOG.md b/src/OpenTelemetry.Exporter.Jaeger/CHANGELOG.md deleted file mode 100644 index bdd4d000b78..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/CHANGELOG.md +++ /dev/null @@ -1,370 +0,0 @@ -# Changelog - -## Unreleased - -## 1.5.1 - -Released 2023-Jun-26 - -## 1.5.0 - -Released 2023-Jun-05 - -## 1.5.0-rc.1 - -Released 2023-May-25 - -* Added direct reference to `System.Text.Encodings.Web` with minimum version of -`4.7.2` in response to [CVE-2021-26701](https://github.com/dotnet/runtime/issues/49377). - -## 1.5.0-alpha.2 - -Released 2023-Mar-31 - -* Enabled performance optimizations for .NET 6.0+ runtimes. - ([#4349](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4349)) - -## 1.5.0-alpha.1 - -Released 2023-Mar-07 - -## 1.4.0 - -Released 2023-Feb-24 - -* Updated OTel SDK dependency to 1.4.0 - -## 1.4.0-rc.4 - -Released 2023-Feb-10 - -## 1.4.0-rc.3 - -Released 2023-Feb-01 - -## 1.4.0-rc.2 - -Released 2023-Jan-09 - -## 1.4.0-rc.1 - -Released 2022-Dec-12 - -## 1.4.0-beta.3 - -Released 2022-Nov-07 - -* Bumped the minimum required version of `System.Text.Json` to 4.7.2 in response -to [CVE-2021-26701](https://github.com/dotnet/runtime/issues/49377). -([#3789](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3789)) - -## 1.4.0-beta.2 - -Released 2022-Oct-17 - -* Added support for loading environment variables from `IConfiguration` when - using the `AddJaegerExporter` extension - ([#3720](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3720)) - -## 1.4.0-beta.1 - -Released 2022-Sep-29 - -* Added overloads which accept a name to the `TracerProviderBuilder` - `AddJaegerExporter` extension to allow for more fine-grained options - management - ([#3656](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3656)) - -## 1.4.0-alpha.2 - -Released 2022-Aug-18 - -## 1.4.0-alpha.1 - -Released 2022-Aug-02 - -## 1.3.0 - -Released 2022-Jun-03 - -## 1.3.0-rc.2 - -Released 2022-June-1 - -* Improve the conversion and formatting of attribute values. - The list of data types that must be supported per the - [OpenTelemetry specification](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/common#attribute) - is more narrow than what the .NET OpenTelemetry SDK supports. Numeric - [built-in value types](https://docs.microsoft.com/dotnet/csharp/language-reference/builtin-types/built-in-types) - are supported by converting to a `long` or `double` as appropriate except for - numeric types that could cause overflow (`ulong`) or rounding (`decimal`) - which are converted to strings. Non-numeric built-in types - `string`, - `char`, `bool` are supported. All other types are converted to a `string`. - Array values are also supported. - ([#3281](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3281)) -* Fix conversion of array-valued resource attributes. They were previously - converted to a string like "System.String[]". - ([#3281](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3281)) -* Fix exporting of array-valued attributes on an `Activity`. Previously, each - item in the array would result in a new tag on an exported `Activity`. Now, - array-valued attributes are serialzed to a JSON-array representation. - ([#3281](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3281)) - -## 1.3.0-beta.2 - -Released 2022-May-16 - -* Removes net5.0 target and replaced with net6.0 - as .NET 5.0 is going out of support. - The package keeps netstandard2.1 target, so it - can still be used with .NET5.0 apps. - ([#3147](https://github.com/open-telemetry/opentelemetry-dotnet/issues/3147)) - -## 1.3.0-beta.1 - -Released 2022-Apr-15 - -* Removes .NET Framework 4.6.1. The minimum .NET Framework - version supported is .NET 4.6.2. ([#3190](https://github.com/open-telemetry/opentelemetry-dotnet/issues/3190)) - -## 1.2.0 - -Released 2022-Apr-15 - -## 1.2.0-rc5 - -Released 2022-Apr-12 - -## 1.2.0-rc4 - -Released 2022-Mar-30 - -* Added support for Activity Status and StatusDescription which were - added to Activity from `System.Diagnostics.DiagnosticSource` version 6.0. - Prior to version 6.0, setting the status of an Activity was provided by the - .NET OpenTelemetry API via the `Activity.SetStatus` extension method in the - `OpenTelemetry.Trace` namespace. Internally, this extension method added the - status as tags on the Activity: `otel.status_code` and `otel.status_description`. - Therefore, to maintain backward compatibility, the exporter falls back to using - these tags to infer status. - ([#3073](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3073)) - -## 1.2.0-rc3 - -Released 2022-Mar-04 - -* Change supported values for `OTEL_EXPORTER_JAEGER_PROTOCOL` - Supported values: `udp/thrift.compact` and `http/thrift.binary` defined - in the [specification](https://github.com/open-telemetry/opentelemetry-specification/blob/9a0a3300c6269c2837a1d7c9c5232ec816f63222/specification/sdk-environment-variables.md?plain=1#L129). - ([#2914](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2914)) -* Change handling of `OTEL_EXPORTER_JAEGER_ENDPOINT` to require the path to - post. Previous versions of this library would append `/api/traces` to the - value specified in this variable, but now the application author must do so. - This change must also be made if you manually configure the - `JaegerExporterOptions` class - the `Endpoint` must now include the path. - For most environments, this will be `/api/traces`. The effective default - is still `http://localhost:14268/api/traces`. This was done to match - the clarified [specification](https://github.com/open-telemetry/opentelemetry-specification/pull/2333)) - ([#2847](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2847)) - -## 1.2.0-rc2 - -Released 2022-Feb-02 - -* Improved span duration's precision from millisecond to microsecond - ([#2814](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2814)) - -## 1.2.0-rc1 - -Released 2021-Nov-29 - -## 1.2.0-beta2 - -Released 2021-Nov-19 - -* Changed `JaegerExporterOptions` constructor to throw - `FormatException` if it fails to parse any of the supported environment - variables. - -* Added support for sending spans directly to a Jaeger Collector over HTTP - ([#2574](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2574)) - -## 1.2.0-beta1 - -Released 2021-Oct-08 - -## 1.2.0-alpha4 - -Released 2021-Sep-23 - -## 1.2.0-alpha3 - -Released 2021-Sep-13 - -* `JaegerExporterOptions.BatchExportProcessorOptions` is initialized with - `BatchExportActivityProcessorOptions` which supports field value overriding - using `OTEL_BSP_SCHEDULE_DELAY`, `OTEL_BSP_EXPORT_TIMEOUT`, - `OTEL_BSP_MAX_QUEUE_SIZE`, `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` - environmental variables as defined in the - [specification](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.5.0/specification/sdk-environment-variables.md#batch-span-processor). - ([#2219](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2219)) - -## 1.2.0-alpha2 - -Released 2021-Aug-24 - -## 1.2.0-alpha1 - -Released 2021-Jul-23 - -* Removes .NET Framework 4.6 support. The minimum .NET Framework - version supported is .NET 4.6.1. ([#2138](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2138)) - -* The `JaegerExporterOptions` defaults can be overridden using - `OTEL_EXPORTER_JAEGER_AGENT_HOST` and `OTEL_EXPORTER_JAEGER_AGENT_PORT` - environmental variables as defined in the - [specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#jaeger-exporter). - ([#2123](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2123)) - -## 1.1.0 - -Released 2021-Jul-12 - -## 1.1.0-rc1 - -Released 2021-Jun-25 - -## 1.1.0-beta4 - -Released 2021-Jun-09 - -## 1.1.0-beta3 - -Released 2021-May-11 - -## 1.1.0-beta2 - -Released 2021-Apr-23 - -* When using OpenTelemetry.Extensions.Hosting you can now bind - `JaegerExporterOptions` to `IConfiguration` using the `Configure` extension - (ex: - `services.Configure(this.Configuration.GetSection("Jaeger"));`). - ([#1889](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1889)) -* Fixed data corruption when creating Jaeger Batch messages - ([#1372](https://github.com/open-telemetry/opentelemetry-dotnet/issues/1372)) - -## 1.1.0-beta1 - -Released 2021-Mar-19 - -## 1.0.1 - -Released 2021-Feb-10 - -## 1.0.0-rc4 - -Released 2021-Feb-09 - -## 1.0.0-rc3 - -Released 2021-Feb-04 - -* Moved `JaegerExporter` and `JaegerExporterOptions` classes to - `OpenTelemetry.Exporter` namespace. - ([#1770](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1770)) -* Default ServiceName, if not found in Resource is obtained from SDK - using GetDefaultResource(). - [#1768](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1768) -* Removed ProcessTags from JaegerExporterOptions. The alternate option is - to use Resource attributes. - -## 1.0.0-rc2 - -Released 2021-Jan-29 - -* Changed `JaegerExporter` class and constructor from internal to public. - ([#1612](https://github.com/open-telemetry/opentelemetry-dotnet/issues/1612)) - -* In `JaegerExporterOptions`: Exporter options now include a switch for Batch vs - Simple exporter, and settings for batch exporting properties. - -* Jaeger will now set the `error` tag when `otel.status_code` is set to `ERROR`. - ([#1579](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1579) - [#1620](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1620)) - -* Jaeger will no longer send the `otel.status_code` tag if the value is `UNSET`. - ([#1609](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1609) - [#1620](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1620)) - -* Span Event.Name will now be populated as the `event` field on Jaeger Logs - instead of `message`. - ([#1609](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1609)) - -* `JaegerExporter` batch format has changed to be compliant with the spec. This - may impact the way spans are displayed in Jaeger UI. - ([#1732](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1732)) - -## 1.0.0-rc1.1 - -Released 2020-Nov-17 - -* Jaeger tags used for InstrumentationLibrary changed from library.name, - library.version to otel.library.name, otel.library.version respectively. - ([#1513](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1513)) -* The `JaegerExporter` class has been made internal. - ([#1540](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1540)) -* Removed `ServiceName` from options available on the `AddJaegerExporter` - extension. It is not required by the - [specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk_exporters/jaeger.md). - ([#1572](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1572)) - -## 0.8.0-beta.1 - -Released 2020-Nov-5 - -* Moving Jaeger Process from public to internal. - ([#1421](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1421)) - -## 0.7.0-beta.1 - -Released 2020-Oct-16 - -* Renamed `MaxPacketSize` -> `MaxPayloadSizeInBytes` on `JaegerExporterOptions`. - Lowered the default value from 65,000 to 4096. - ([#1247](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1274)) - -## 0.6.0-beta.1 - -Released 2020-Sep-15 - -* Removed `MaxFlushInterval` from `JaegerExporterOptions`. Batching is now - handled by `BatchExportActivityProcessor` exclusively. - ([#1254](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1254)) - -## 0.5.0-beta.2 - -Released 2020-08-28 - -* Changed `JaegerExporter` to use `BatchExportActivityProcessor` by default. - ([#1125](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1125)) -* Span links will now be sent as `FOLLOWS_FROM` reference type. Previously they - were sent as `CHILD_OF`. - ([#970](https://github.com/open-telemetry/opentelemetry-dotnet/pull/970)) -* Fixed issue when span has both the `net.peer.name` and `net.peer.port` - attributes but did not include `net.peer.port` in the `peer.service` field. - ([#1195](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1195)) - -* Renamed extension method from `UseJaegerExporter` to `AddJaegerExporter`. - -## 0.4.0-beta.2 - -Released 2020-07-24 - -* First beta release - -## 0.3.0-beta - -Released 2020-07-23 - -* Initial release diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/Batch.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/Batch.cs deleted file mode 100644 index d8f4f23058e..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/Batch.cs +++ /dev/null @@ -1,83 +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 Thrift.Protocol; -using Thrift.Protocol.Entities; - -namespace OpenTelemetry.Exporter.Jaeger.Implementation -{ - internal sealed class Batch - { - public Batch(Process process, TProtocol protocol) - { - this.BatchBeginMessage = GenerateBeginMessage(process, protocol, out int spanCountPosition); - this.SpanCountPosition = spanCountPosition; - this.BatchEndMessage = GenerateEndMessage(protocol); - } - - public byte[] BatchBeginMessage { get; } - - public int SpanCountPosition { get; set; } - - public byte[] BatchEndMessage { get; } - - public int MinimumMessageSize => this.BatchBeginMessage.Length - + this.BatchEndMessage.Length; - - private static byte[] GenerateBeginMessage(Process process, TProtocol oprot, out int spanCountPosition) - { - var struc = new TStruct("Batch"); - - oprot.WriteStructBegin(struc); - - var field = new TField - { - Name = "process", - Type = TType.Struct, - ID = 1, - }; - - oprot.WriteFieldBegin(field); - process.Write(oprot); - oprot.WriteFieldEnd(); - - field.Name = "spans"; - field.Type = TType.List; - field.ID = 2; - - oprot.WriteFieldBegin(field); - - oprot.WriteListBegin(new TList(TType.Struct, 0), out spanCountPosition); - - byte[] beginMessage = oprot.WrittenData.ToArray(); - oprot.Clear(); - return beginMessage; - } - - private static byte[] GenerateEndMessage(TProtocol oprot) - { - oprot.WriteListEnd(); - - oprot.WriteFieldEnd(); - oprot.WriteFieldStop(); - oprot.WriteStructEnd(); - - byte[] endMessage = oprot.WrittenData.ToArray(); - oprot.Clear(); - return endMessage; - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/EmitBatchArgs.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/EmitBatchArgs.cs deleted file mode 100644 index f8ac2bbd899..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/EmitBatchArgs.cs +++ /dev/null @@ -1,74 +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 Thrift.Protocol; -using Thrift.Protocol.Entities; - -namespace OpenTelemetry.Exporter.Jaeger.Implementation -{ - internal sealed class EmitBatchArgs - { - public EmitBatchArgs(TProtocol protocol) - { - this.EmitBatchArgsBeginMessage = GenerateBeginMessage(protocol, out int seqIdPosition); - this.SeqIdPosition = seqIdPosition; - this.EmitBatchArgsEndMessage = GenerateEndMessage(protocol); - } - - public byte[] EmitBatchArgsBeginMessage { get; } - - public int SeqIdPosition { get; } - - public byte[] EmitBatchArgsEndMessage { get; } - - public int MinimumMessageSize => this.EmitBatchArgsBeginMessage.Length - + this.EmitBatchArgsEndMessage.Length; - - private static byte[] GenerateBeginMessage(TProtocol oprot, out int seqIdPosition) - { - oprot.WriteMessageBegin(new TMessage("emitBatch", TMessageType.Oneway, 0), out seqIdPosition); - - var struc = new TStruct("emitBatch_args"); - oprot.WriteStructBegin(struc); - - var field = new TField - { - Name = "batch", - Type = TType.Struct, - ID = 1, - }; - - oprot.WriteFieldBegin(field); - - byte[] beginMessage = oprot.WrittenData.ToArray(); - oprot.Clear(); - return beginMessage; - } - - private static byte[] GenerateEndMessage(TProtocol oprot) - { - oprot.WriteFieldEnd(); - oprot.WriteFieldStop(); - oprot.WriteStructEnd(); - - oprot.WriteMessageEnd(); - - byte[] endMessage = oprot.WrittenData.ToArray(); - oprot.Clear(); - return endMessage; - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/IJaegerClient.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/IJaegerClient.cs deleted file mode 100644 index ed4171666e2..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/IJaegerClient.cs +++ /dev/null @@ -1,29 +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. -// - -namespace OpenTelemetry.Exporter.Jaeger.Implementation -{ - internal interface IJaegerClient : IDisposable - { - bool Connected { get; } - - void Connect(); - - void Close(); - - int Send(byte[] buffer, int offset, int count); - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/Int128.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/Int128.cs deleted file mode 100644 index d4a1232e861..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/Int128.cs +++ /dev/null @@ -1,63 +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 System.Runtime.InteropServices; - -namespace OpenTelemetry.Exporter.Jaeger.Implementation -{ - internal readonly struct Int128 - { - public static Int128 Empty; - - private const int SpanIdBytes = 8; - private const int TraceIdBytes = 16; - - public Int128(ActivitySpanId spanId) - { - Span bytes = stackalloc byte[SpanIdBytes]; - spanId.CopyTo(bytes); - - if (BitConverter.IsLittleEndian) - { - bytes.Reverse(); - } - - var longs = MemoryMarshal.Cast(bytes); - this.High = 0; - this.Low = longs[0]; - } - - public Int128(ActivityTraceId traceId) - { - Span bytes = stackalloc byte[TraceIdBytes]; - traceId.CopyTo(bytes); - - if (BitConverter.IsLittleEndian) - { - bytes.Reverse(); - } - - var longs = MemoryMarshal.Cast(bytes); - this.High = BitConverter.IsLittleEndian ? longs[1] : longs[0]; - this.Low = BitConverter.IsLittleEndian ? longs[0] : longs[1]; - } - - public long High { get; } - - public long Low { get; } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerActivityExtensions.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerActivityExtensions.cs deleted file mode 100644 index a2f33e1560e..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerActivityExtensions.cs +++ /dev/null @@ -1,378 +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 System.Runtime.CompilerServices; -using OpenTelemetry.Internal; -using OpenTelemetry.Trace; - -namespace OpenTelemetry.Exporter.Jaeger.Implementation -{ - internal static class JaegerActivityExtensions - { - internal const string JaegerErrorFlagTagName = "error"; - - private const int DaysPerYear = 365; - - // Number of days in 4 years - private const int DaysPer4Years = (DaysPerYear * 4) + 1; // 1461 - - // Number of days in 100 years - private const int DaysPer100Years = (DaysPer4Years * 25) - 1; // 36524 - - // Number of days in 400 years - private const int DaysPer400Years = (DaysPer100Years * 4) + 1; // 146097 - - // Number of days from 1/1/0001 to 12/31/1969 - private const int DaysTo1970 = (DaysPer400Years * 4) + (DaysPer100Years * 3) + (DaysPer4Years * 17) + DaysPerYear; // 719,162 - - private const long UnixEpochTicks = DaysTo1970 * TimeSpan.TicksPerDay; - private const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; - private const long UnixEpochMicroseconds = UnixEpochTicks / TicksPerMicrosecond; // 62,135,596,800,000,000 - - public static JaegerSpan ToJaegerSpan(this Activity activity) - { - var jaegerTags = new TagEnumerationState - { - Tags = PooledList.Create(), - }; - - jaegerTags.EnumerateTags(activity); - - if (activity.Status != ActivityStatusCode.Unset) - { - if (activity.Status == ActivityStatusCode.Ok) - { - PooledList.Add( - ref jaegerTags.Tags, - new JaegerTag(SpanAttributeConstants.StatusCodeKey, JaegerTagType.STRING, vStr: "OK")); - } - else - { - PooledList.Add( - ref jaegerTags.Tags, - new JaegerTag(SpanAttributeConstants.StatusCodeKey, JaegerTagType.STRING, vStr: "ERROR")); - - PooledList.Add( - ref jaegerTags.Tags, - new JaegerTag(JaegerErrorFlagTagName, JaegerTagType.BOOL, vBool: true)); - - PooledList.Add( - ref jaegerTags.Tags, - new JaegerTag(SpanAttributeConstants.StatusDescriptionKey, JaegerTagType.STRING, vStr: activity.StatusDescription ?? string.Empty)); - } - } - else if (jaegerTags.StatusCode.HasValue && jaegerTags.StatusCode != StatusCode.Unset) - { - PooledList.Add( - ref jaegerTags.Tags, - new JaegerTag( - SpanAttributeConstants.StatusCodeKey, - JaegerTagType.STRING, - vStr: StatusHelper.GetTagValueForStatusCode(jaegerTags.StatusCode.Value))); - - if (jaegerTags.StatusCode == StatusCode.Error) - { - PooledList.Add( - ref jaegerTags.Tags, - new JaegerTag(JaegerErrorFlagTagName, JaegerTagType.BOOL, vBool: true)); - - PooledList.Add( - ref jaegerTags.Tags, - new JaegerTag(SpanAttributeConstants.StatusDescriptionKey, JaegerTagType.STRING, vStr: jaegerTags.StatusDescription ?? string.Empty)); - } - } - - string peerServiceName = null; - if (activity.Kind == ActivityKind.Client || activity.Kind == ActivityKind.Producer) - { - PeerServiceResolver.Resolve(ref jaegerTags, out peerServiceName, out bool addAsTag); - - if (peerServiceName != null && addAsTag) - { - PooledList.Add(ref jaegerTags.Tags, new JaegerTag(SemanticConventions.AttributePeerService, JaegerTagType.STRING, vStr: peerServiceName)); - } - } - - // The Span.Kind must translate into a tag. - // See https://opentracing.io/specification/conventions/ - if (activity.Kind != ActivityKind.Internal) - { - string spanKind = null; - - if (activity.Kind == ActivityKind.Server) - { - spanKind = "server"; - } - else if (activity.Kind == ActivityKind.Client) - { - spanKind = "client"; - } - else if (activity.Kind == ActivityKind.Consumer) - { - spanKind = "consumer"; - } - else if (activity.Kind == ActivityKind.Producer) - { - spanKind = "producer"; - } - - if (spanKind != null) - { - PooledList.Add(ref jaegerTags.Tags, new JaegerTag("span.kind", JaegerTagType.STRING, vStr: spanKind)); - } - } - - var activitySource = activity.Source; - if (!string.IsNullOrEmpty(activitySource.Name)) - { - PooledList.Add(ref jaegerTags.Tags, new JaegerTag("otel.library.name", JaegerTagType.STRING, vStr: activitySource.Name)); - if (!string.IsNullOrEmpty(activitySource.Version)) - { - PooledList.Add(ref jaegerTags.Tags, new JaegerTag("otel.library.version", JaegerTagType.STRING, vStr: activitySource.Version)); - } - } - - var traceId = Int128.Empty; - var spanId = Int128.Empty; - var parentSpanId = Int128.Empty; - - if (activity.IdFormat == ActivityIdFormat.W3C) - { - // TODO: The check above should be enforced by the usage of the exporter. Perhaps enforce at higher-level. - traceId = new Int128(activity.TraceId); - spanId = new Int128(activity.SpanId); - if (activity.ParentSpanId != default) - { - parentSpanId = new Int128(activity.ParentSpanId); - } - } - - return new JaegerSpan( - peerServiceName: peerServiceName, - traceIdLow: traceId.Low, - traceIdHigh: traceId.High, - spanId: spanId.Low, - parentSpanId: parentSpanId.Low, - operationName: activity.DisplayName, - flags: (activity.Context.TraceFlags & ActivityTraceFlags.Recorded) > 0 ? 0x1 : 0, - startTime: ToEpochMicroseconds(activity.StartTimeUtc), - duration: activity.Duration.Ticks / TicksPerMicrosecond, - references: activity.ToJaegerSpanRefs(), - tags: jaegerTags.Tags, - logs: activity.ToJaegerLogs()); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static PooledList ToJaegerSpanRefs(this Activity activity) - { - LinkEnumerationState references = default; - - references.EnumerateLinks(activity); - - return references.SpanRefs; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static PooledList ToJaegerLogs(this Activity activity) - { - EventEnumerationState logs = default; - - logs.EnumerateEvents(activity); - - return logs.Logs; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static JaegerLog ToJaegerLog(this in ActivityEvent timedEvent) - { - var jaegerTags = new EventTagsEnumerationState - { - Tags = PooledList.Create(), - }; - - jaegerTags.EnumerateTags(in timedEvent); - - if (!jaegerTags.HasEvent) - { - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk_exporters/jaeger.md#events - PooledList.Add(ref jaegerTags.Tags, new JaegerTag("event", JaegerTagType.STRING, vStr: timedEvent.Name)); - } - - // TODO: Use the same function as JaegerConversionExtensions or check that the perf here is acceptable. - return new JaegerLog(timedEvent.Timestamp.ToEpochMicroseconds(), jaegerTags.Tags); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static JaegerSpanRef ToJaegerSpanRef(this in ActivityLink link) - { - var traceId = new Int128(link.Context.TraceId); - var spanId = new Int128(link.Context.SpanId); - - // Assume FOLLOWS_FROM for links, mirrored from Java: https://github.com/open-telemetry/opentelemetry-java/pull/481#discussion_r312577862 - var refType = JaegerSpanRefType.FOLLOWS_FROM; - - return new JaegerSpanRef(refType, traceId.Low, traceId.High, spanId.Low); - } - - public static long ToEpochMicroseconds(this DateTime utcDateTime) - { - // Truncate sub-microsecond precision before offsetting by the Unix Epoch to avoid - // the last digit being off by one for dates that result in negative Unix times - long microseconds = utcDateTime.Ticks / TicksPerMicrosecond; - return microseconds - UnixEpochMicroseconds; - } - - public static long ToEpochMicroseconds(this DateTimeOffset timestamp) - { - // Truncate sub-microsecond precision before offsetting by the Unix Epoch to avoid - // the last digit being off by one for dates that result in negative Unix times - long microseconds = timestamp.UtcDateTime.Ticks / TicksPerMicrosecond; - return microseconds - UnixEpochMicroseconds; - } - - private struct TagEnumerationState : PeerServiceResolver.IPeerServiceState - { - public PooledList Tags; - - public string PeerService { get; set; } - - public int? PeerServicePriority { get; set; } - - public string HostName { get; set; } - - public string IpAddress { get; set; } - - public long Port { get; set; } - - public StatusCode? StatusCode { get; set; } - - public string StatusDescription { get; set; } - - public void EnumerateTags(Activity activity) - { - foreach (ref readonly var tag in activity.EnumerateTagObjects()) - { - if (tag.Value != null) - { - var key = tag.Key; - - if (!JaegerTagTransformer.Instance.TryTransformTag(tag, out var jaegerTag)) - { - continue; - } - - if (jaegerTag.VStr != null) - { - PeerServiceResolver.InspectTag(ref this, key, jaegerTag.VStr); - - if (key == SpanAttributeConstants.StatusCodeKey) - { - StatusCode? statusCode = StatusHelper.GetStatusCodeForTagValue(jaegerTag.VStr); - this.StatusCode = statusCode; - continue; - } - else if (key == SpanAttributeConstants.StatusDescriptionKey) - { - this.StatusDescription = jaegerTag.VStr; - continue; - } - } - else if (jaegerTag.VLong.HasValue) - { - PeerServiceResolver.InspectTag(ref this, key, jaegerTag.VLong.Value); - } - - PooledList.Add(ref this.Tags, jaegerTag); - } - } - } - } - - private struct LinkEnumerationState - { - public bool Created; - - public PooledList SpanRefs; - - public void EnumerateLinks(Activity activity) - { - var enumerator = activity.EnumerateLinks(); - - if (enumerator.MoveNext()) - { - this.SpanRefs = PooledList.Create(); - this.Created = true; - - do - { - ref readonly var link = ref enumerator.Current; - PooledList.Add(ref this.SpanRefs, link.ToJaegerSpanRef()); - } - while (enumerator.MoveNext()); - } - } - } - - private struct EventEnumerationState - { - public bool Created; - - public PooledList Logs; - - public void EnumerateEvents(Activity activity) - { - var enumerator = activity.EnumerateEvents(); - - if (enumerator.MoveNext()) - { - this.Logs = PooledList.Create(); - this.Created = true; - - do - { - ref readonly var @event = ref enumerator.Current; - PooledList.Add(ref this.Logs, @event.ToJaegerLog()); - } - while (enumerator.MoveNext()); - } - } - } - - private struct EventTagsEnumerationState - { - public PooledList Tags; - - public bool HasEvent; - - public void EnumerateTags(in ActivityEvent @event) - { - foreach (ref readonly var tag in @event.EnumerateTagObjects()) - { - if (JaegerTagTransformer.Instance.TryTransformTag(tag, out var result)) - { - PooledList.Add(ref this.Tags, result); - - if (tag.Key == "event") - { - this.HasEvent = true; - } - } - } - } - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerExporterEventSource.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerExporterEventSource.cs deleted file mode 100644 index 722061969da..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerExporterEventSource.cs +++ /dev/null @@ -1,57 +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.Tracing; -using OpenTelemetry.Internal; - -namespace OpenTelemetry.Exporter.Jaeger.Implementation -{ - /// - /// EventSource events emitted from the project. - /// - [EventSource(Name = "OpenTelemetry-Exporter-Jaeger")] - internal sealed class JaegerExporterEventSource : EventSource - { - public static JaegerExporterEventSource Log = new(); - - [NonEvent] - public void FailedExport(Exception ex) - { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.FailedExport(ex.ToInvariantString()); - } - } - - [Event(1, Message = "Failed to send spans: '{0}'", Level = EventLevel.Error)] - public void FailedExport(string exception) - { - this.WriteEvent(1, exception); - } - - [Event(2, Message = "Unsupported attribute type '{0}' for '{1}'. Attribute will not be exported.", Level = EventLevel.Warning)] - public void UnsupportedAttributeType(string type, string key) - { - this.WriteEvent(2, type.ToString(), key); - } - - [Event(3, Message = "{0} environment variable has an invalid value: '{1}'", Level = EventLevel.Warning)] - public void InvalidEnvironmentVariable(string key, string value) - { - this.WriteEvent(3, key, value); - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerExporterException.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerExporterException.cs deleted file mode 100644 index 9603af88d72..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerExporterException.cs +++ /dev/null @@ -1,30 +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. -// - -namespace OpenTelemetry.Exporter.Jaeger.Implementation -{ -#pragma warning disable CA1032 // Implement standard exception constructors -#pragma warning disable CA1064 // Exceptions should be public - internal sealed class JaegerExporterException : Exception -#pragma warning restore CA1064 // Exceptions should be public -#pragma warning restore CA1032 // Implement standard exception constructors - { - public JaegerExporterException(string message, Exception originalException) - : base(message, originalException) - { - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerHttpClient.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerHttpClient.cs deleted file mode 100644 index 478c35035d3..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerHttpClient.cs +++ /dev/null @@ -1,86 +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; -#if NETFRAMEWORK -using System.Net.Http; -#endif -using System.Net.Http.Headers; - -namespace OpenTelemetry.Exporter.Jaeger.Implementation -{ - internal sealed class JaegerHttpClient : IJaegerClient - { - private static readonly MediaTypeHeaderValue ContentTypeHeader = new("application/vnd.apache.thrift.binary"); - - private readonly Uri endpoint; - private readonly HttpClient httpClient; - private bool disposed; - - public JaegerHttpClient(Uri endpoint, HttpClient httpClient) - { - Debug.Assert(endpoint != null, "endpoint is null"); - Debug.Assert(httpClient != null, "httpClient is null"); - - this.endpoint = endpoint; - this.httpClient = httpClient; - } - - public bool Connected => true; - - public void Close() - { - } - - public void Connect() - { - } - - public void Dispose() - { - if (this.disposed) - { - return; - } - - this.httpClient.Dispose(); - - this.disposed = true; - } - - public int Send(byte[] buffer, int offset, int count) - { - // Prevent Jaeger's HTTP operations from being instrumented. - using var scope = SuppressInstrumentationScope.Begin(); - - using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, this.endpoint); - - request.Content = new ByteArrayContent(buffer, offset, count) - { - Headers = { ContentType = ContentTypeHeader }, - }; - -#if NET6_0_OR_GREATER - using HttpResponseMessage response = this.httpClient.Send(request); -#else - using HttpResponseMessage response = this.httpClient.SendAsync(request).GetAwaiter().GetResult(); -#endif - response.EnsureSuccessStatusCode(); - - return count; - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerLog.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerLog.cs deleted file mode 100644 index 29dc1ad1845..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerLog.cs +++ /dev/null @@ -1,92 +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.Text; -using OpenTelemetry.Internal; -using Thrift.Protocol; -using Thrift.Protocol.Entities; - -namespace OpenTelemetry.Exporter.Jaeger.Implementation -{ - internal readonly struct JaegerLog : TUnionBase - { - public JaegerLog(long timestamp, in PooledList fields) - { - this.Timestamp = timestamp; - this.Fields = fields; - } - - public long Timestamp { get; } - - public PooledList Fields { get; } - - public void Write(TProtocol oprot) - { - oprot.IncrementRecursionDepth(); - try - { - var struc = new TStruct("Log"); - oprot.WriteStructBegin(struc); - - var field = new TField - { - Name = "timestamp", - Type = TType.I64, - ID = 1, - }; - - oprot.WriteFieldBegin(field); - oprot.WriteI64(this.Timestamp); - oprot.WriteFieldEnd(); - - field.Name = "fields"; - field.Type = TType.List; - field.ID = 2; - - oprot.WriteFieldBegin(field); - { - oprot.WriteListBegin(new TList(TType.Struct, this.Fields.Count)); - - for (int i = 0; i < this.Fields.Count; i++) - { - this.Fields[i].Write(oprot); - } - - oprot.WriteListEnd(); - } - - oprot.WriteFieldEnd(); - oprot.WriteFieldStop(); - oprot.WriteStructEnd(); - } - finally - { - oprot.DecrementRecursionDepth(); - } - } - - public override string ToString() - { - var sb = new StringBuilder("Log("); - sb.Append(", Timestamp: "); - sb.Append(this.Timestamp); - sb.Append(", Fields: "); - sb.Append(this.Fields); - sb.Append(')'); - return sb.ToString(); - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpan.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpan.cs deleted file mode 100644 index 1090d3a531a..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpan.cs +++ /dev/null @@ -1,279 +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.Text; -using OpenTelemetry.Internal; -using Thrift.Protocol; -using Thrift.Protocol.Entities; - -namespace OpenTelemetry.Exporter.Jaeger.Implementation -{ - internal readonly struct JaegerSpan : TUnionBase - { - public JaegerSpan( - string peerServiceName, - long traceIdLow, - long traceIdHigh, - long spanId, - long parentSpanId, - string operationName, - int flags, - long startTime, - long duration, - in PooledList references, - in PooledList tags, - in PooledList logs) - { - this.PeerServiceName = peerServiceName; - this.TraceIdLow = traceIdLow; - this.TraceIdHigh = traceIdHigh; - this.SpanId = spanId; - this.ParentSpanId = parentSpanId; - this.OperationName = operationName; - this.Flags = flags; - this.StartTime = startTime; - this.Duration = duration; - this.References = references; - this.Tags = tags; - this.Logs = logs; - } - - public string PeerServiceName { get; } - - public long TraceIdLow { get; } - - public long TraceIdHigh { get; } - - public long SpanId { get; } - - public long ParentSpanId { get; } - - public string OperationName { get; } - - public PooledList References { get; } - - public int Flags { get; } - - public long StartTime { get; } - - public long Duration { get; } - - public PooledList Tags { get; } - - public PooledList Logs { get; } - - public void Write(TProtocol oprot) - { - oprot.IncrementRecursionDepth(); - try - { - var struc = new TStruct("Span"); - oprot.WriteStructBegin(struc); - - var field = new TField - { - Name = "traceIdLow", - Type = TType.I64, - ID = 1, - }; - - oprot.WriteFieldBegin(field); - oprot.WriteI64(this.TraceIdLow); - oprot.WriteFieldEnd(); - - field.Name = "traceIdHigh"; - field.Type = TType.I64; - field.ID = 2; - - oprot.WriteFieldBegin(field); - oprot.WriteI64(this.TraceIdHigh); - oprot.WriteFieldEnd(); - - field.Name = "spanId"; - field.Type = TType.I64; - field.ID = 3; - - oprot.WriteFieldBegin(field); - oprot.WriteI64(this.SpanId); - oprot.WriteFieldEnd(); - - field.Name = "parentSpanId"; - field.Type = TType.I64; - field.ID = 4; - - oprot.WriteFieldBegin(field); - oprot.WriteI64(this.ParentSpanId); - oprot.WriteFieldEnd(); - - field.Name = "operationName"; - field.Type = TType.String; - field.ID = 5; - - oprot.WriteFieldBegin(field); - oprot.WriteString(this.OperationName); - oprot.WriteFieldEnd(); - - if (!this.References.IsEmpty) - { - field.Name = "references"; - field.Type = TType.List; - field.ID = 6; - oprot.WriteFieldBegin(field); - { - oprot.WriteListBegin(new TList(TType.Struct, this.References.Count)); - - for (int i = 0; i < this.References.Count; i++) - { - this.References[i].Write(oprot); - } - - oprot.WriteListEnd(); - } - - oprot.WriteFieldEnd(); - } - - field.Name = "flags"; - field.Type = TType.I32; - field.ID = 7; - - oprot.WriteFieldBegin(field); - oprot.WriteI32(this.Flags); - oprot.WriteFieldEnd(); - - field.Name = "startTime"; - field.Type = TType.I64; - field.ID = 8; - - oprot.WriteFieldBegin(field); - oprot.WriteI64(this.StartTime); - oprot.WriteFieldEnd(); - - field.Name = "duration"; - field.Type = TType.I64; - field.ID = 9; - - oprot.WriteFieldBegin(field); - oprot.WriteI64(this.Duration); - oprot.WriteFieldEnd(); - - if (!this.Tags.IsEmpty) - { - field.Name = "JaegerTags"; - field.Type = TType.List; - field.ID = 10; - - oprot.WriteFieldBegin(field); - { - oprot.WriteListBegin(new TList(TType.Struct, this.Tags.Count)); - - for (int i = 0; i < this.Tags.Count; i++) - { - this.Tags[i].Write(oprot); - } - - oprot.WriteListEnd(); - } - - oprot.WriteFieldEnd(); - } - - if (!this.Logs.IsEmpty) - { - field.Name = "logs"; - field.Type = TType.List; - field.ID = 11; - oprot.WriteFieldBegin(field); - { - oprot.WriteListBegin(new TList(TType.Struct, this.Logs.Count)); - - for (int i = 0; i < this.Logs.Count; i++) - { - this.Logs[i].Write(oprot); - } - - oprot.WriteListEnd(); - } - - oprot.WriteFieldEnd(); - } - - oprot.WriteFieldStop(); - oprot.WriteStructEnd(); - } - finally - { - oprot.DecrementRecursionDepth(); - } - } - - public void Return() - { - this.References.Return(); - this.Tags.Return(); - if (!this.Logs.IsEmpty) - { - for (int i = 0; i < this.Logs.Count; i++) - { - this.Logs[i].Fields.Return(); - } - - this.Logs.Return(); - } - } - - public override string ToString() - { - var sb = new StringBuilder("Span("); - sb.Append(", TraceIdLow: "); - sb.Append(this.TraceIdLow); - sb.Append(", TraceIdHigh: "); - sb.Append(this.TraceIdHigh); - sb.Append(", SpanId: "); - sb.Append(this.SpanId); - sb.Append(", ParentSpanId: "); - sb.Append(this.ParentSpanId); - sb.Append(", OperationName: "); - sb.Append(this.OperationName); - if (!this.References.IsEmpty) - { - sb.Append(", References: "); - sb.Append(this.References); - } - - sb.Append(", Flags: "); - sb.Append(this.Flags); - sb.Append(", StartTime: "); - sb.Append(this.StartTime); - sb.Append(", Duration: "); - sb.Append(this.Duration); - if (!this.Tags.IsEmpty) - { - sb.Append(", JaegerTags: "); - sb.Append(this.Tags); - } - - if (!this.Logs.IsEmpty) - { - sb.Append(", Logs: "); - sb.Append(this.Logs); - } - - sb.Append(')'); - return sb.ToString(); - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpanRef.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpanRef.cs deleted file mode 100644 index 5856cf2a7f0..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpanRef.cs +++ /dev/null @@ -1,111 +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.Text; -using Thrift.Protocol; -using Thrift.Protocol.Entities; - -namespace OpenTelemetry.Exporter.Jaeger.Implementation -{ - internal readonly struct JaegerSpanRef : TUnionBase - { - public JaegerSpanRef(JaegerSpanRefType refType, long traceIdLow, long traceIdHigh, long spanId) - { - this.RefType = refType; - this.TraceIdLow = traceIdLow; - this.TraceIdHigh = traceIdHigh; - this.SpanId = spanId; - } - - public JaegerSpanRefType RefType { get; } - - public long TraceIdLow { get; } - - public long TraceIdHigh { get; } - - public long SpanId { get; } - - public void Write(TProtocol oprot) - { - oprot.IncrementRecursionDepth(); - try - { - var struc = new TStruct("SpanRef"); - oprot.WriteStructBegin(struc); - - var field = new TField - { - Name = "refType", - Type = TType.I32, - ID = 1, - }; - - oprot.WriteFieldBegin(field); - oprot.WriteI32((int)this.RefType); - oprot.WriteFieldEnd(); - - field.Name = "traceIdLow"; - field.Type = TType.I64; - field.ID = 2; - - oprot.WriteFieldBegin(field); - oprot.WriteI64(this.TraceIdLow); - oprot.WriteFieldEnd(); - - field.Name = "traceIdHigh"; - field.Type = TType.I64; - field.ID = 3; - - oprot.WriteFieldBegin(field); - oprot.WriteI64(this.TraceIdHigh); - oprot.WriteFieldEnd(); - - field.Name = "spanId"; - field.Type = TType.I64; - field.ID = 4; - - oprot.WriteFieldBegin(field); - oprot.WriteI64(this.SpanId); - oprot.WriteFieldEnd(); - oprot.WriteFieldStop(); - oprot.WriteStructEnd(); - } - finally - { - oprot.DecrementRecursionDepth(); - } - } - - /// - /// - /// - /// A string representation of the object. - public override string ToString() - { - var sb = new StringBuilder("SpanRef("); - sb.Append(", RefType: "); - sb.Append(this.RefType); - sb.Append(", TraceIdLow: "); - sb.Append(this.TraceIdLow); - sb.Append(", TraceIdHigh: "); - sb.Append(this.TraceIdHigh); - sb.Append(", SpanId: "); - sb.Append(this.SpanId); - sb.Append(')'); - return sb.ToString(); - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpanRefType.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpanRefType.cs deleted file mode 100644 index 031a6d151f7..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpanRefType.cs +++ /dev/null @@ -1,34 +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. -// - -namespace OpenTelemetry.Exporter.Jaeger.Implementation -{ - /// - /// Represents the different types of Jaeger Spans. - /// - internal enum JaegerSpanRefType - { - /// - /// A child span. - /// - CHILD_OF = 0, - - /// - /// A sibling span. - /// - FOLLOWS_FROM = 1, - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTag.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTag.cs deleted file mode 100644 index 5ea83000d04..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTag.cs +++ /dev/null @@ -1,181 +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.Text; -using Thrift.Protocol; -using Thrift.Protocol.Entities; - -namespace OpenTelemetry.Exporter.Jaeger.Implementation -{ - internal readonly struct JaegerTag : TUnionBase - { - public JaegerTag( - string key, - JaegerTagType vType, - string vStr = null, - double? vDouble = null, - bool? vBool = null, - long? vLong = null, - byte[] vBinary = null) - { - this.Key = key; - this.VType = vType; - - this.VStr = vStr; - this.VDouble = vDouble; - this.VBool = vBool; - this.VLong = vLong; - this.VBinary = vBinary; - } - - public string Key { get; } - - public JaegerTagType VType { get; } - - public string VStr { get; } - - public double? VDouble { get; } - - public bool? VBool { get; } - - public long? VLong { get; } - - public byte[] VBinary { get; } - - public void Write(TProtocol oprot) - { - oprot.IncrementRecursionDepth(); - try - { - var struc = new TStruct("Tag"); - oprot.WriteStructBegin(struc); - - var field = new TField - { - Name = "key", - Type = TType.String, - ID = 1, - }; - - oprot.WriteFieldBegin(field); - oprot.WriteString(this.Key); - oprot.WriteFieldEnd(); - - field.Name = "vType"; - field.Type = TType.I32; - field.ID = 2; - - oprot.WriteFieldBegin(field); - oprot.WriteI32((int)this.VType); - oprot.WriteFieldEnd(); - - if (this.VStr != null) - { - field.Name = "vStr"; - field.Type = TType.String; - field.ID = 3; - oprot.WriteFieldBegin(field); - oprot.WriteString(this.VStr); - oprot.WriteFieldEnd(); - } - else if (this.VDouble.HasValue) - { - field.Name = "vDouble"; - field.Type = TType.Double; - field.ID = 4; - oprot.WriteFieldBegin(field); - oprot.WriteDouble(this.VDouble.Value); - oprot.WriteFieldEnd(); - } - else if (this.VBool.HasValue) - { - field.Name = "vBool"; - field.Type = TType.Bool; - field.ID = 5; - oprot.WriteFieldBegin(field); - oprot.WriteBool(this.VBool.Value); - oprot.WriteFieldEnd(); - } - else if (this.VLong.HasValue) - { - field.Name = "vLong"; - field.Type = TType.I64; - field.ID = 6; - oprot.WriteFieldBegin(field); - oprot.WriteI64(this.VLong.Value); - oprot.WriteFieldEnd(); - } - else if (this.VBinary != null) - { - field.Name = "vBinary"; - field.Type = TType.String; - field.ID = 7; - oprot.WriteFieldBegin(field); - oprot.WriteBinary(this.VBinary, 0, this.VBinary.Length); - oprot.WriteFieldEnd(); - } - - oprot.WriteFieldStop(); - oprot.WriteStructEnd(); - } - finally - { - oprot.DecrementRecursionDepth(); - } - } - - public override string ToString() - { - var sb = new StringBuilder("Tag("); - sb.Append(", Key: "); - sb.Append(this.Key); - sb.Append(", VType: "); - sb.Append(this.VType); - if (this.VStr != null) - { - sb.Append(", VStr: "); - sb.Append(this.VStr); - } - - if (this.VDouble.HasValue) - { - sb.Append(", VDouble: "); - sb.Append(this.VDouble); - } - - if (this.VBool.HasValue) - { - sb.Append(", VBool: "); - sb.Append(this.VBool); - } - - if (this.VLong.HasValue) - { - sb.Append(", VLong: "); - sb.Append(this.VLong); - } - - if (this.VBinary != null) - { - sb.Append(", VBinary: "); - sb.Append(this.VBinary); - } - - sb.Append(')'); - return sb.ToString(); - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTagTransformer.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTagTransformer.cs deleted file mode 100644 index bd4af789e4c..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTagTransformer.cs +++ /dev/null @@ -1,51 +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 OpenTelemetry.Internal; - -namespace OpenTelemetry.Exporter.Jaeger.Implementation; - -internal sealed class JaegerTagTransformer : TagTransformer -{ - private JaegerTagTransformer() - { - } - - public static JaegerTagTransformer Instance { get; } = new(); - - protected override JaegerTag TransformIntegralTag(string key, long value) - { - return new JaegerTag(key, JaegerTagType.LONG, vLong: value); - } - - protected override JaegerTag TransformFloatingPointTag(string key, double value) - { - return new JaegerTag(key, JaegerTagType.DOUBLE, vDouble: value); - } - - protected override JaegerTag TransformBooleanTag(string key, bool value) - { - return new JaegerTag(key, JaegerTagType.BOOL, vBool: value); - } - - protected override JaegerTag TransformStringTag(string key, string value) - { - return new JaegerTag(key, JaegerTagType.STRING, vStr: value); - } - - protected override JaegerTag TransformArrayTag(string key, Array array) - => this.TransformStringTag(key, System.Text.Json.JsonSerializer.Serialize(array)); -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTagType.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTagType.cs deleted file mode 100644 index 3aeb586747c..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTagType.cs +++ /dev/null @@ -1,49 +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. -// - -namespace OpenTelemetry.Exporter.Jaeger.Implementation -{ - /// - /// Indicates the data type of a Jaeger tag. - /// - internal enum JaegerTagType - { - /// - /// Tag contains a string. - /// - STRING = 0, - - /// - /// Tag contains a double. - /// - DOUBLE = 1, - - /// - /// Tag contains a boolean. - /// - BOOL = 2, - - /// - /// Tag contains a long. - /// - LONG = 3, - - /// - /// Tag contains binary data. - /// - BINARY = 4, - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerUdpClient.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerUdpClient.cs deleted file mode 100644 index 71b473a3739..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerUdpClient.cs +++ /dev/null @@ -1,59 +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.Net.Sockets; - -namespace OpenTelemetry.Exporter.Jaeger.Implementation -{ - internal sealed class JaegerUdpClient : IJaegerClient - { - private readonly string host; - private readonly int port; - private readonly UdpClient client; - private bool disposed; - - public JaegerUdpClient(string host, int port) - { - this.host = host; - this.port = port; - this.client = new UdpClient(); - } - - public bool Connected => this.client.Client.Connected; - - public void Close() => this.client.Close(); - - public void Connect() => this.client.Connect(this.host, this.port); - - public int Send(byte[] buffer, int offset, int count) - { - return this.client.Client.Send(buffer, offset, count, SocketFlags.None); - } - - /// - public void Dispose() - { - if (this.disposed) - { - return; - } - - this.client.Dispose(); - - this.disposed = true; - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/Process.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/Process.cs deleted file mode 100644 index 858071131cf..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/Process.cs +++ /dev/null @@ -1,95 +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.Text; -using Thrift.Protocol; -using Thrift.Protocol.Entities; - -namespace OpenTelemetry.Exporter.Jaeger.Implementation -{ - internal sealed class Process - { - public string ServiceName { get; set; } - - public Dictionary Tags { get; set; } - - public override string ToString() - { - var sb = new StringBuilder("Process("); - sb.Append(", ServiceName: "); - sb.Append(this.ServiceName); - - if (this.Tags != null) - { - sb.Append(", Tags: "); - sb.Append(this.Tags); - } - - sb.Append(')'); - return sb.ToString(); - } - - public void Write(TProtocol oprot) - { - oprot.IncrementRecursionDepth(); - - try - { - var struc = new TStruct("Process"); - oprot.WriteStructBegin(struc); - - var field = new TField - { - Name = "serviceName", - Type = TType.String, - ID = 1, - }; - - oprot.WriteFieldBegin(field); - oprot.WriteString(this.ServiceName); - oprot.WriteFieldEnd(); - - if (this.Tags != null) - { - field.Name = "tags"; - field.Type = TType.List; - field.ID = 2; - - oprot.WriteFieldBegin(field); - { - oprot.WriteListBegin(new TList(TType.Struct, this.Tags.Count)); - - foreach (var jt in this.Tags) - { - jt.Value.Write(oprot); - } - - oprot.WriteListEnd(); - } - - oprot.WriteFieldEnd(); - } - - oprot.WriteFieldStop(); - oprot.WriteStructEnd(); - } - finally - { - oprot.DecrementRecursionDepth(); - } - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/ShimExtensions.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/ShimExtensions.cs deleted file mode 100644 index 9ad9a263ce7..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/ShimExtensions.cs +++ /dev/null @@ -1,36 +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. -// - -#if NETSTANDARD2_0 || NETFRAMEWORK -namespace System -{ - internal static class ShimExtensions - { - public static byte[] ToArray(this ArraySegment arraySegment) - { - int count = arraySegment.Count; - if (count == 0) - { - return Array.Empty(); - } - - var array = new byte[count]; - Array.Copy(arraySegment.Array, arraySegment.Offset, array, 0, count); - return array; - } - } -} -#endif diff --git a/src/OpenTelemetry.Exporter.Jaeger/JaegerExportProtocol.cs b/src/OpenTelemetry.Exporter.Jaeger/JaegerExportProtocol.cs deleted file mode 100644 index 8898ac96130..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/JaegerExportProtocol.cs +++ /dev/null @@ -1,40 +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. -// - -namespace OpenTelemetry.Exporter -{ - /// - /// Defines the exporter protocols supported by the . - /// - public enum JaegerExportProtocol : byte - { - /// - /// Compact thrift protocol over UDP. - /// - /// - /// Note: Supported by Jaeger Agents only. - /// - UdpCompactThrift = 0, - - /// - /// Binary thrift protocol over HTTP. - /// - /// - /// Note: Supported by Jaeger Collectors only. - /// - HttpBinaryThrift = 1, - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/JaegerExporter.cs b/src/OpenTelemetry.Exporter.Jaeger/JaegerExporter.cs deleted file mode 100644 index 3180e3d2986..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/JaegerExporter.cs +++ /dev/null @@ -1,288 +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 System.Runtime.CompilerServices; -using OpenTelemetry.Exporter.Jaeger.Implementation; -using OpenTelemetry.Internal; -using OpenTelemetry.Resources; -using Thrift.Protocol; -using Process = OpenTelemetry.Exporter.Jaeger.Implementation.Process; - -namespace OpenTelemetry.Exporter -{ - public class JaegerExporter : BaseExporter - { - internal uint NumberOfSpansInCurrentBatch; - - private readonly byte[] uInt32Storage = new byte[8]; - private readonly int maxPayloadSizeInBytes; - private readonly IJaegerClient client; - private readonly TProtocol batchWriter; - private readonly TProtocol spanWriter; - private readonly bool sendUsingEmitBatchArgs; - private int minimumBatchSizeInBytes; - private int currentBatchSizeInBytes; - private int spanStartPosition; - private uint sequenceId; - private bool disposed; - - public JaegerExporter(JaegerExporterOptions options) - : this(options, null) - { - } - - internal JaegerExporter(JaegerExporterOptions options, TProtocolFactory protocolFactory = null, IJaegerClient client = null) - { - Guard.ThrowIfNull(options); - - this.maxPayloadSizeInBytes = (!options.MaxPayloadSizeInBytes.HasValue || options.MaxPayloadSizeInBytes <= 0) - ? JaegerExporterOptions.DefaultMaxPayloadSizeInBytes - : options.MaxPayloadSizeInBytes.Value; - - if (options.Protocol == JaegerExportProtocol.UdpCompactThrift) - { - protocolFactory ??= new TCompactProtocol.Factory(); - client ??= new JaegerUdpClient(options.AgentHost, options.AgentPort); - this.sendUsingEmitBatchArgs = true; - } - else if (options.Protocol == JaegerExportProtocol.HttpBinaryThrift) - { - protocolFactory ??= new TBinaryProtocol.Factory(); - client ??= new JaegerHttpClient( - options.Endpoint, - options.HttpClientFactory?.Invoke() ?? throw new InvalidOperationException("JaegerExporterOptions was missing HttpClientFactory or it returned null.")); - } - else - { - throw new NotSupportedException(); - } - - JaegerTagTransformer.LogUnsupportedAttributeType = (string tagValueType, string tagKey) => - { - JaegerExporterEventSource.Log.UnsupportedAttributeType(tagValueType, tagKey); - }; - - ConfigurationExtensions.LogInvalidEnvironmentVariable = (string key, string value) => - { - JaegerExporterEventSource.Log.InvalidEnvironmentVariable(key, value); - }; - - this.client = client; - this.batchWriter = protocolFactory.GetProtocol(this.maxPayloadSizeInBytes * 2); - this.spanWriter = protocolFactory.GetProtocol(this.maxPayloadSizeInBytes); - - this.Process = new(); - - client.Connect(); - } - - internal Process Process { get; } - - internal EmitBatchArgs EmitBatchArgs { get; private set; } - - internal Batch Batch { get; private set; } - - /// - public override ExportResult Export(in Batch activityBatch) - { - try - { - if (this.Batch == null) - { - this.SetResourceAndInitializeBatch(this.ParentProvider.GetResource()); - } - - foreach (var activity in activityBatch) - { - var jaegerSpan = activity.ToJaegerSpan(); - this.AppendSpan(jaegerSpan); - jaegerSpan.Return(); - } - - this.SendCurrentBatch(); - - return ExportResult.Success; - } - catch (Exception ex) - { - JaegerExporterEventSource.Log.FailedExport(ex); - - return ExportResult.Failure; - } - } - - internal void SetResourceAndInitializeBatch(Resource resource) - { - Guard.ThrowIfNull(resource); - - var process = this.Process; - - string serviceName = null; - string serviceNamespace = null; - foreach (var label in resource.Attributes) - { - string key = label.Key; - - if (label.Value is string strVal) - { - switch (key) - { - case ResourceSemanticConventions.AttributeServiceName: - serviceName = strVal; - continue; - case ResourceSemanticConventions.AttributeServiceNamespace: - serviceNamespace = strVal; - continue; - } - } - - if (JaegerTagTransformer.Instance.TryTransformTag(label, out var result)) - { - if (process.Tags == null) - { - process.Tags = new Dictionary(); - } - - process.Tags[key] = result; - } - } - - if (!string.IsNullOrWhiteSpace(serviceName)) - { - serviceName = string.IsNullOrEmpty(serviceNamespace) - ? serviceName - : serviceNamespace + "." + serviceName; - } - else - { - serviceName = (string)this.ParentProvider.GetDefaultResource().Attributes.FirstOrDefault( - pair => pair.Key == ResourceSemanticConventions.AttributeServiceName).Value; - } - - process.ServiceName = serviceName; - - this.Batch = new Batch(process, this.batchWriter); - if (this.sendUsingEmitBatchArgs) - { - this.EmitBatchArgs = new EmitBatchArgs(this.batchWriter); - this.Batch.SpanCountPosition += this.EmitBatchArgs.EmitBatchArgsBeginMessage.Length; - this.batchWriter.WriteRaw(this.EmitBatchArgs.EmitBatchArgsBeginMessage); - } - - this.batchWriter.WriteRaw(this.Batch.BatchBeginMessage); - this.spanStartPosition = this.batchWriter.Position; - - this.minimumBatchSizeInBytes = this.EmitBatchArgs?.MinimumMessageSize ?? 0 - + this.Batch.MinimumMessageSize; - - this.ResetBatch(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void AppendSpan(JaegerSpan jaegerSpan) - { - jaegerSpan.Write(this.spanWriter); - try - { - var spanTotalBytesNeeded = this.spanWriter.Length; - - if (this.NumberOfSpansInCurrentBatch > 0 - && this.currentBatchSizeInBytes + spanTotalBytesNeeded >= this.maxPayloadSizeInBytes) - { - this.SendCurrentBatch(); - } - - var spanData = this.spanWriter.WrittenData; - this.batchWriter.WriteRaw(spanData); - - this.NumberOfSpansInCurrentBatch++; - this.currentBatchSizeInBytes += spanTotalBytesNeeded; - } - finally - { - this.spanWriter.Clear(); - } - } - - internal void SendCurrentBatch() - { - try - { - this.batchWriter.WriteRaw(this.Batch.BatchEndMessage); - - if (this.sendUsingEmitBatchArgs) - { - this.batchWriter.WriteRaw(this.EmitBatchArgs.EmitBatchArgsEndMessage); - - this.WriteUInt32AtPosition(this.EmitBatchArgs.SeqIdPosition, ++this.sequenceId); - } - - this.WriteUInt32AtPosition(this.Batch.SpanCountPosition, this.NumberOfSpansInCurrentBatch); - - var writtenData = this.batchWriter.WrittenData; - - this.client.Send(writtenData.Array, writtenData.Offset, writtenData.Count); - } - finally - { - this.ResetBatch(); - } - } - - /// - protected override void Dispose(bool disposing) - { - if (!this.disposed) - { - if (disposing) - { - try - { - this.client.Close(); - } - catch - { - } - - this.client.Dispose(); - this.batchWriter.Dispose(); - this.spanWriter.Dispose(); - } - - this.disposed = true; - } - - base.Dispose(disposing); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void WriteUInt32AtPosition(int position, uint value) - { - this.batchWriter.Position = position; - int numberOfBytes = this.batchWriter.WriteUI32(value, this.uInt32Storage); - this.batchWriter.WriteRaw(this.uInt32Storage, 0, numberOfBytes); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ResetBatch() - { - this.currentBatchSizeInBytes = this.minimumBatchSizeInBytes; - this.NumberOfSpansInCurrentBatch = 0; - this.batchWriter.Clear(this.spanStartPosition); - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterHelperExtensions.cs deleted file mode 100644 index ce1bf441f29..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterHelperExtensions.cs +++ /dev/null @@ -1,137 +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; -#if NETFRAMEWORK -using System.Net.Http; -#endif -using System.Reflection; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using OpenTelemetry.Exporter; -using OpenTelemetry.Internal; - -namespace OpenTelemetry.Trace -{ - /// - /// Extension methods to simplify registering a Jaeger exporter. - /// - public static class JaegerExporterHelperExtensions - { - /// - /// Adds Jaeger exporter to the TracerProvider. - /// - /// builder to use. - /// The instance of to chain the calls. - public static TracerProviderBuilder AddJaegerExporter(this TracerProviderBuilder builder) - => AddJaegerExporter(builder, name: null, configure: null); - - /// - /// Adds Jaeger exporter to the TracerProvider. - /// - /// builder to use. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static TracerProviderBuilder AddJaegerExporter(this TracerProviderBuilder builder, Action configure) - => AddJaegerExporter(builder, name: null, configure); - - /// - /// Adds Jaeger exporter to the TracerProvider. - /// - /// builder to use. - /// Name which is used when retrieving options. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static TracerProviderBuilder AddJaegerExporter( - this TracerProviderBuilder builder, - string name, - Action configure) - { - Guard.ThrowIfNull(builder); - - name ??= Options.DefaultName; - - builder.ConfigureServices(services => - { - if (configure != null) - { - services.Configure(name, configure); - } - - services.RegisterOptionsFactory( - (sp, configuration, name) => new JaegerExporterOptions( - configuration, - sp.GetRequiredService>().Get(name))); - }); - - return builder.AddProcessor(sp => - { - var options = sp.GetRequiredService>().Get(name); - - return BuildJaegerExporterProcessor(options, sp); - }); - } - - private static BaseProcessor BuildJaegerExporterProcessor( - JaegerExporterOptions options, - IServiceProvider serviceProvider) - { - if (options.Protocol == JaegerExportProtocol.HttpBinaryThrift - && options.HttpClientFactory == JaegerExporterOptions.DefaultHttpClientFactory) - { - options.HttpClientFactory = () => - { - Type httpClientFactoryType = Type.GetType("System.Net.Http.IHttpClientFactory, Microsoft.Extensions.Http", throwOnError: false); - if (httpClientFactoryType != null) - { - object httpClientFactory = serviceProvider.GetService(httpClientFactoryType); - if (httpClientFactory != null) - { - MethodInfo createClientMethod = httpClientFactoryType.GetMethod( - "CreateClient", - BindingFlags.Public | BindingFlags.Instance, - binder: null, - new Type[] { typeof(string) }, - modifiers: null); - if (createClientMethod != null) - { - return (HttpClient)createClientMethod.Invoke(httpClientFactory, new object[] { "JaegerExporter" }); - } - } - } - - return new HttpClient(); - }; - } - - var jaegerExporter = new JaegerExporter(options); - - if (options.ExportProcessorType == ExportProcessorType.Simple) - { - return new SimpleActivityExportProcessor(jaegerExporter); - } - else - { - return new BatchActivityExportProcessor( - jaegerExporter, - options.BatchExportProcessorOptions.MaxQueueSize, - options.BatchExportProcessorOptions.ScheduledDelayMilliseconds, - options.BatchExportProcessorOptions.ExporterTimeoutMilliseconds, - options.BatchExportProcessorOptions.MaxExportBatchSize); - } - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterOptions.cs b/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterOptions.cs deleted file mode 100644 index 33de53ab745..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterOptions.cs +++ /dev/null @@ -1,146 +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; -#if NETFRAMEWORK -using System.Net.Http; -#endif -using Microsoft.Extensions.Configuration; -using OpenTelemetry.Internal; -using OpenTelemetry.Trace; - -namespace OpenTelemetry.Exporter -{ - /// - /// Jaeger exporter options. - /// OTEL_EXPORTER_JAEGER_AGENT_HOST, OTEL_EXPORTER_JAEGER_AGENT_PORT - /// environment variables are parsed during object construction. - /// - public class JaegerExporterOptions - { - internal const int DefaultMaxPayloadSizeInBytes = 4096; - - internal const string OTelProtocolEnvVarKey = "OTEL_EXPORTER_JAEGER_PROTOCOL"; - internal const string OTelAgentHostEnvVarKey = "OTEL_EXPORTER_JAEGER_AGENT_HOST"; - internal const string OTelAgentPortEnvVarKey = "OTEL_EXPORTER_JAEGER_AGENT_PORT"; - internal const string OTelEndpointEnvVarKey = "OTEL_EXPORTER_JAEGER_ENDPOINT"; - internal const string DefaultJaegerEndpoint = "http://localhost:14268/api/traces"; - - internal static readonly Func DefaultHttpClientFactory = () => new HttpClient(); - - /// - /// Initializes a new instance of the class. - /// - public JaegerExporterOptions() - : this(new ConfigurationBuilder().AddEnvironmentVariables().Build(), new()) - { - } - - internal JaegerExporterOptions( - IConfiguration configuration, - BatchExportActivityProcessorOptions defaultBatchOptions) - { - Debug.Assert(configuration != null, "configuration was null"); - Debug.Assert(defaultBatchOptions != null, "defaultBatchOptions was null"); - - if (configuration.TryGetValue( - OTelProtocolEnvVarKey, - JaegerExporterProtocolParser.TryParse, - out var protocol)) - { - this.Protocol = protocol; - } - - if (configuration.TryGetStringValue(OTelAgentHostEnvVarKey, out var agentHost)) - { - this.AgentHost = agentHost; - } - - if (configuration.TryGetIntValue(OTelAgentPortEnvVarKey, out var agentPort)) - { - this.AgentPort = agentPort; - } - - if (configuration.TryGetUriValue(OTelEndpointEnvVarKey, out var endpoint)) - { - this.Endpoint = endpoint; - } - - this.BatchExportProcessorOptions = defaultBatchOptions; - } - - /// - /// Gets or sets the to use when - /// communicating to Jaeger. Default value: . - /// - public JaegerExportProtocol Protocol { get; set; } = JaegerExportProtocol.UdpCompactThrift; - - /// - /// Gets or sets the Jaeger agent host. Default value: localhost. - /// - public string AgentHost { get; set; } = "localhost"; - - /// - /// Gets or sets the Jaeger agent port. Default value: 6831. - /// - public int AgentPort { get; set; } = 6831; - - /// - /// Gets or sets the Jaeger HTTP endpoint. Default value: "http://localhost:14268/api/traces". - /// Typically https://jaeger-server-name:14268/api/traces. - /// - public Uri Endpoint { get; set; } = new Uri(DefaultJaegerEndpoint); - - /// - /// Gets or sets the maximum payload size in bytes. Default value: 4096. - /// - public int? MaxPayloadSizeInBytes { get; set; } = DefaultMaxPayloadSizeInBytes; - - /// - /// Gets or sets the export processor type to be used with Jaeger Exporter. The default value is . - /// - public ExportProcessorType ExportProcessorType { get; set; } = ExportProcessorType.Batch; - - /// - /// Gets or sets the BatchExportProcessor options. Ignored unless ExportProcessorType is BatchExporter. - /// - public BatchExportProcessorOptions BatchExportProcessorOptions { get; set; } - - /// - /// Gets or sets the factory function called to create the instance that will be used at runtime to - /// transmit spans over HTTP. The returned instance will be reused for - /// all export invocations. - /// - /// - /// Notes: - /// - /// This is only invoked for the protocol. - /// The default behavior when using the extension is if an IHttpClientFactory - /// instance can be resolved through the application then an will be - /// created through the factory with the name "JaegerExporter" otherwise - /// an will be instantiated directly. - /// - /// - public Func HttpClientFactory { get; set; } = DefaultHttpClientFactory; - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterProtocolParser.cs b/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterProtocolParser.cs deleted file mode 100644 index 2a770135541..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterProtocolParser.cs +++ /dev/null @@ -1,36 +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. -// - -namespace OpenTelemetry.Exporter; - -internal static class JaegerExporterProtocolParser -{ - public static bool TryParse(string value, out JaegerExportProtocol result) - { - switch (value.Trim().ToLower()) - { - case "udp/thrift.compact": - result = JaegerExportProtocol.UdpCompactThrift; - return true; - case "http/thrift.binary": - result = JaegerExportProtocol.HttpBinaryThrift; - return true; - default: - result = default; - return false; - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/OpenTelemetry.Exporter.Jaeger.csproj b/src/OpenTelemetry.Exporter.Jaeger/OpenTelemetry.Exporter.Jaeger.csproj deleted file mode 100644 index ae8cf5f832f..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/OpenTelemetry.Exporter.Jaeger.csproj +++ /dev/null @@ -1,44 +0,0 @@ - - - - - net6.0;netstandard2.1;netstandard2.0;net462 - Jaeger exporter for OpenTelemetry .NET - $(PackageTags);Jaeger;distributed-tracing - core- - - - disable - - - - $(NoWarn),1591 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/OpenTelemetry.Exporter.Jaeger/README.md b/src/OpenTelemetry.Exporter.Jaeger/README.md deleted file mode 100644 index 3202341a9ea..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/README.md +++ /dev/null @@ -1,132 +0,0 @@ -# Jaeger Exporter for OpenTelemetry .NET - -[![NuGet](https://img.shields.io/nuget/v/OpenTelemetry.Exporter.Jaeger.svg)](https://www.nuget.org/packages/OpenTelemetry.Exporter.Jaeger) -[![NuGet](https://img.shields.io/nuget/dt/OpenTelemetry.Exporter.Jaeger.svg)](https://www.nuget.org/packages/OpenTelemetry.Exporter.Jaeger) - -The Jaeger exporter converts OpenTelemetry traces into the Jaeger model -following the [OpenTelemetry specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk_exporters/jaeger.md). - -The exporter communicates to a Jaeger Agent through the Thrift protocol on -the Compact Thrift API port, and as such only supports Thrift over UDP. - -> **Note** This component is scheduled to be -> [deprecated](https://github.com/open-telemetry/opentelemetry-specification/pull/2858) -and users are advised to move to [OTLP -Exporter](../OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md). The -[getting started with Jaeger](../../docs/trace/getting-started-jaeger/README.md) -tutorial shows how to use OTLP Exporter to export traces to Jaeger. - -## Installation - -```shell -dotnet add package OpenTelemetry.Exporter.Jaeger -``` - -## Configuration - -You can configure the `JaegerExporter` through `JaegerExporterOptions` -and environment variables. The `JaegerExporterOptions` setters -take precedence over the environment variables. - -## Options Properties - -The `JaegerExporter` can be configured using the `JaegerExporterOptions` -properties: - -* `AgentHost`: The Jaeger Agent host (default `localhost`). Used for - `UdpCompactThrift` protocol. - -* `AgentPort`: The Jaeger Agent port (default `6831`). Used for - `UdpCompactThrift` protocol. - -* `BatchExportProcessorOptions`: Configuration options for the batch exporter. - Only used if `ExportProcessorType` is set to `Batch`. - -* `Endpoint`: The Jaeger Collector HTTP endpoint (default - `http://localhost:14268`). Used for `HttpBinaryThrift` protocol. - -* `ExportProcessorType`: Whether the exporter should use [Batch or Simple - exporting - processor](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#built-in-span-processors) - (default `ExportProcessorType.Batch`). - -* `HttpClientFactory`: A factory function called to create the `HttpClient` - instance that will be used at runtime to transmit spans over HTTP when the - `HttpBinaryThrift` protocol is configured. See [Configure - HttpClient](#configure-httpclient) for more details. - -* `MaxPayloadSizeInBytes`: The maximum size of each batch that gets sent to the - agent or collector (default `4096`). - -* `Protocol`: The protocol to use. The default value is `UdpCompactThrift`. - - | Protocol | Description | - |------------------|-------------------------------------------------------| - |`UdpCompactThrift`| Apache Thrift compact over UDP to a Jaeger Agent. | - |`HttpBinaryThrift`| Apache Thrift binary over HTTP to a Jaeger Collector. | - -See the [`TestJaegerExporter.cs`](../../examples/Console/TestJaegerExporter.cs) -for an example of how to use the exporter. - -## Environment Variables - -The following environment variables can be used to override the default -values of the `JaegerExporterOptions` -(following the [OpenTelemetry specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#jaeger-exporter)). - -| Environment variable | `JaegerExporterOptions` property | -|-----------------------------------|-----------------------------------------------------------| -| `OTEL_EXPORTER_JAEGER_AGENT_HOST` | `AgentHost` | -| `OTEL_EXPORTER_JAEGER_AGENT_PORT` | `AgentPort` | -| `OTEL_EXPORTER_JAEGER_ENDPOINT` | `Endpoint` | -| `OTEL_EXPORTER_JAEGER_PROTOCOL` | `Protocol` (`udp/thrift.compact` or `http/thrift.binary`) | - -## Configure HttpClient - -The `HttpClientFactory` option is provided on `JaegerExporterOptions` for users -who want to configure the `HttpClient` used by the `JaegerExporter` when -`HttpBinaryThrift` protocol is used. Simply replace the function with your own -implementation if you want to customize the generated `HttpClient`: - -```csharp -services.AddOpenTelemetry() - .WithTracing(builder => builder - .AddJaegerExporter(o => - { - o.Protocol = JaegerExportProtocol.HttpBinaryThrift; - o.HttpClientFactory = () => - { - HttpClient client = new HttpClient(); - client.DefaultRequestHeaders.Add("X-MyCustomHeader", "value"); - return client; - }; - })); -``` - -For users using -[IHttpClientFactory](https://docs.microsoft.com/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests) -you may also customize the named "JaegerExporter" `HttpClient` using the -built-in `AddHttpClient` extension: - -```csharp -services.AddHttpClient( - "JaegerExporter", - configureClient: (client) => - client.DefaultRequestHeaders.Add("X-MyCustomHeader", "value")); -``` - -Note: The single instance returned by `HttpClientFactory` is reused by all -export requests. - -## Troubleshooting - -This component uses an -[EventSource](https://docs.microsoft.com/dotnet/api/system.diagnostics.tracing.eventsource) -with the name "OpenTelemetry-Exporter-Jaeger" for its internal logging. Please -refer to [SDK troubleshooting](../OpenTelemetry/README.md#troubleshooting) for -instructions on seeing these internal logs. - -## References - -* [Jaeger](https://www.jaegertracing.io) -* [OpenTelemetry Project](https://opentelemetry.io/) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Grpc/OpenTelemetry.Exporter.OpenTelemetryProtocol.Grpc.csproj b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Grpc/OpenTelemetry.Exporter.OpenTelemetryProtocol.Grpc.csproj deleted file mode 100644 index a757059df0f..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Grpc/OpenTelemetry.Exporter.OpenTelemetryProtocol.Grpc.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - netstandard2.0 - OpenTelemetry protocol exporter over gRPC for OpenTelemetry .NET - $(PackageTags);OTLP;gRPC - core- - - - - - false - - - - - - - - - - - - - diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Grpc/README.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Grpc/README.md deleted file mode 100644 index f26f22a5dae..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Grpc/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# gRPC-based implementation of OTLP Exporter for OpenTelemetry .NET - -[![NuGet](https://img.shields.io/nuget/v/OpenTelemetry.Exporter.OpenTelemetryProtocol.Grpc.svg)](https://www.nuget.org/packages/OpenTelemetry.Exporter.OpenTelemetryProtocol.Grpc) -[![NuGet](https://img.shields.io/nuget/dt/OpenTelemetry.Exporter.OpenTelemetryProtocol.Grpc.svg)](https://www.nuget.org/packages/OpenTelemetry.Exporter.OpenTelemetryProtocol.Grpc) - -[gRPC-based implementation of OTLP Exporter](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md) -implementation. - -## Prerequisite - -* [Get OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) - -## Installation - -```shell -dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol.Grpc -``` - -## Configuration - -You can configure the `OtlpExporter` through `Options` types properties -and environment variables. -The `Options` type setters take precedence over the environment variables. - -## Options Properties - -* `BatchExportProcessorOptions`: Configuration options for the batch exporter. - Only used if ExportProcessorType is set to Batch. - -* `Endpoint`: Target to which the exporter is going to send traces or metrics. - The endpoint must be a valid Uri with scheme (http or https) and host, and MAY - contain a port and path. - -* `ExportProcessorType`: Whether the exporter should use [Batch or Simple - exporting - processor](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#built-in-span-processors). - -* `Headers`: Optional headers for the connection. - -* `TimeoutMilliseconds` : Max waiting time for the backend to process a batch. - -See the [`TestOtlpExporter.cs`](../../examples/Console/TestOtlpExporter.cs) for -an example of how to use the exporter. - -## Environment Variables - -The following environment variables can be used to override the default -values of the `OtlpExporterOptions` -(following the [OpenTelemetry specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md)). - -| Environment variable | `OtlpExporterOptions` property | -| ------------------------------| --------------------------------------| -| `OTEL_EXPORTER_OTLP_ENDPOINT` | `Endpoint` | -| `OTEL_EXPORTER_OTLP_HEADERS` | `Headers` | -| `OTEL_EXPORTER_OTLP_TIMEOUT` | `TimeoutMilliseconds` | - -The following environment variables can be used to override the default -values of the `PeriodicExportingMetricReaderOptions` -(following the [OpenTelemetry specification](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.12.0/specification/sdk-environment-variables.md#periodic-exporting-metricreader). - -| Environment variable | `PeriodicExportingMetricReaderOptions` property | -| ------------------------------| ------------------------------------------------| -| `OTEL_METRIC_EXPORT_INTERVAL` | `ExportIntervalMilliseconds` | -| `OTEL_METRIC_EXPORT_TIMEOUT` | `ExportTimeoutMilliseconds` | - -## References - -* [OpenTelemetry - Collector](https://github.com/open-telemetry/opentelemetry-collector) -* [OpenTelemetry Project](https://opentelemetry.io/) -* [OTLP proto files](https://github.com/open-telemetry/opentelemetry-proto) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Experimental/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Experimental/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..e6bd747c9de --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Experimental/PublicAPI.Unshipped.txt @@ -0,0 +1,5 @@ +static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.LoggerProviderBuilder! builder) -> OpenTelemetry.Logs.LoggerProviderBuilder! +static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.LoggerProviderBuilder! builder, string? name, System.Action? configureExporterAndProcessor) -> OpenTelemetry.Logs.LoggerProviderBuilder! +static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.LoggerProviderBuilder! builder, string? name, System.Action? configureExporter) -> OpenTelemetry.Logs.LoggerProviderBuilder! +static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.LoggerProviderBuilder! builder, System.Action! configureExporterAndProcessor) -> OpenTelemetry.Logs.LoggerProviderBuilder! +static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.LoggerProviderBuilder! builder, System.Action! configureExporter) -> OpenTelemetry.Logs.LoggerProviderBuilder! diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Shipped.txt new file mode 100644 index 00000000000..4b7af046438 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Shipped.txt @@ -0,0 +1,47 @@ +#nullable enable +~OpenTelemetry.Exporter.OtlpExporterOptions.BatchExportProcessorOptions.get -> OpenTelemetry.BatchExportProcessorOptions +~OpenTelemetry.Exporter.OtlpExporterOptions.BatchExportProcessorOptions.set -> void +~OpenTelemetry.Exporter.OtlpExporterOptions.Endpoint.get -> System.Uri +~OpenTelemetry.Exporter.OtlpExporterOptions.Endpoint.set -> void +~OpenTelemetry.Exporter.OtlpExporterOptions.Headers.get -> string +~OpenTelemetry.Exporter.OtlpExporterOptions.Headers.set -> void +~OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.get -> System.Func +~OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.set -> void +~OpenTelemetry.Exporter.OtlpMetricExporter.OtlpMetricExporter(OpenTelemetry.Exporter.OtlpExporterOptions options) -> void +~OpenTelemetry.Exporter.OtlpTraceExporter.OtlpTraceExporter(OpenTelemetry.Exporter.OtlpExporterOptions options) -> void +~override OpenTelemetry.Exporter.OtlpMetricExporter.Export(in OpenTelemetry.Batch metrics) -> OpenTelemetry.ExportResult +~override OpenTelemetry.Exporter.OtlpTraceExporter.Export(in OpenTelemetry.Batch activityBatch) -> OpenTelemetry.ExportResult +OpenTelemetry.Exporter.OtlpExporterOptions +OpenTelemetry.Exporter.OtlpExporterOptions.ExportProcessorType.get -> OpenTelemetry.ExportProcessorType +OpenTelemetry.Exporter.OtlpExporterOptions.ExportProcessorType.set -> void +OpenTelemetry.Exporter.OtlpExporterOptions.OtlpExporterOptions() -> void +OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.get -> OpenTelemetry.Exporter.OtlpExportProtocol +OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.set -> void +OpenTelemetry.Exporter.OtlpExporterOptions.TimeoutMilliseconds.get -> int +OpenTelemetry.Exporter.OtlpExporterOptions.TimeoutMilliseconds.set -> void +OpenTelemetry.Exporter.OtlpExportProtocol +OpenTelemetry.Exporter.OtlpExportProtocol.Grpc = 0 -> OpenTelemetry.Exporter.OtlpExportProtocol +OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf = 1 -> OpenTelemetry.Exporter.OtlpExportProtocol +OpenTelemetry.Exporter.OtlpLogExporter +OpenTelemetry.Exporter.OtlpLogExporter.OtlpLogExporter(OpenTelemetry.Exporter.OtlpExporterOptions! options) -> void +OpenTelemetry.Exporter.OtlpMetricExporter +OpenTelemetry.Exporter.OtlpTraceExporter +OpenTelemetry.Logs.OtlpLogExporterHelperExtensions +OpenTelemetry.Metrics.OtlpMetricExporterExtensions +OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions +override OpenTelemetry.Exporter.OtlpLogExporter.Export(in OpenTelemetry.Batch logRecordBatch) -> OpenTelemetry.ExportResult +override OpenTelemetry.Exporter.OtlpMetricExporter.OnShutdown(int timeoutMilliseconds) -> bool +override OpenTelemetry.Exporter.OtlpTraceExporter.OnShutdown(int timeoutMilliseconds) -> bool +static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions! loggerOptions) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! +static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions! loggerOptions, string? name, System.Action? configureExporterAndProcessor) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! +static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions! loggerOptions, string? name, System.Action? configure) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! +static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions! loggerOptions, System.Action! configureExporterAndProcessor) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! +static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions! loggerOptions, System.Action! configure) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! +static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder! builder) -> OpenTelemetry.Metrics.MeterProviderBuilder! +static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder! builder, string? name, System.Action? configureExporterAndMetricReader) -> OpenTelemetry.Metrics.MeterProviderBuilder! +static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder! builder, string? name, System.Action? configure) -> OpenTelemetry.Metrics.MeterProviderBuilder! +static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder! builder, System.Action! configureExporterAndMetricReader) -> OpenTelemetry.Metrics.MeterProviderBuilder! +static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder! builder, System.Action! configure) -> OpenTelemetry.Metrics.MeterProviderBuilder! +static OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Trace.TracerProviderBuilder! builder) -> OpenTelemetry.Trace.TracerProviderBuilder! +static OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Trace.TracerProviderBuilder! builder, string? name, System.Action? configure) -> OpenTelemetry.Trace.TracerProviderBuilder! +static OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Trace.TracerProviderBuilder! builder, System.Action! configure) -> OpenTelemetry.Trace.TracerProviderBuilder! diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Grpc/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt similarity index 100% rename from src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Grpc/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt rename to src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net462/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net462/PublicAPI.Shipped.txt deleted file mode 100644 index 9b4a2a730a3..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net462/PublicAPI.Shipped.txt +++ /dev/null @@ -1,37 +0,0 @@ -OpenTelemetry.Exporter.OtlpExporterOptions -OpenTelemetry.Exporter.OtlpExporterOptions.BatchExportProcessorOptions.get -> OpenTelemetry.BatchExportProcessorOptions -OpenTelemetry.Exporter.OtlpExporterOptions.BatchExportProcessorOptions.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.Endpoint.get -> System.Uri -OpenTelemetry.Exporter.OtlpExporterOptions.Endpoint.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.ExportProcessorType.get -> OpenTelemetry.ExportProcessorType -OpenTelemetry.Exporter.OtlpExporterOptions.ExportProcessorType.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.Headers.get -> string -OpenTelemetry.Exporter.OtlpExporterOptions.Headers.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.get -> System.Func -OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.OtlpExporterOptions() -> void -OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.get -> OpenTelemetry.Exporter.OtlpExportProtocol -OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.TimeoutMilliseconds.get -> int -OpenTelemetry.Exporter.OtlpExporterOptions.TimeoutMilliseconds.set -> void -OpenTelemetry.Exporter.OtlpExportProtocol -OpenTelemetry.Exporter.OtlpExportProtocol.Grpc = 0 -> OpenTelemetry.Exporter.OtlpExportProtocol -OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf = 1 -> OpenTelemetry.Exporter.OtlpExportProtocol -OpenTelemetry.Exporter.OtlpMetricExporter -OpenTelemetry.Exporter.OtlpMetricExporter.OtlpMetricExporter(OpenTelemetry.Exporter.OtlpExporterOptions options) -> void -OpenTelemetry.Exporter.OtlpTraceExporter -OpenTelemetry.Exporter.OtlpTraceExporter.OtlpTraceExporter(OpenTelemetry.Exporter.OtlpExporterOptions options) -> void -OpenTelemetry.Metrics.OtlpMetricExporterExtensions -OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions -override OpenTelemetry.Exporter.OtlpMetricExporter.Export(in OpenTelemetry.Batch metrics) -> OpenTelemetry.ExportResult -override OpenTelemetry.Exporter.OtlpMetricExporter.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.Exporter.OtlpTraceExporter.Export(in OpenTelemetry.Batch activityBatch) -> OpenTelemetry.ExportResult -override OpenTelemetry.Exporter.OtlpTraceExporter.OnShutdown(int timeoutMilliseconds) -> bool -static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, string name, System.Action configureExporterAndMetricReader) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, string name, System.Action configureExporter) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action configureExporterAndMetricReader) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action configureExporter) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net462/PublicAPI.Unshipped.txt deleted file mode 100644 index 07a63e57297..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net462/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,3 +0,0 @@ -OpenTelemetry.Logs.OtlpLogExporterHelperExtensions -static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions loggerOptions) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions -static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions loggerOptions, System.Action configure) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net6.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net6.0/PublicAPI.Shipped.txt deleted file mode 100644 index 9b4a2a730a3..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net6.0/PublicAPI.Shipped.txt +++ /dev/null @@ -1,37 +0,0 @@ -OpenTelemetry.Exporter.OtlpExporterOptions -OpenTelemetry.Exporter.OtlpExporterOptions.BatchExportProcessorOptions.get -> OpenTelemetry.BatchExportProcessorOptions -OpenTelemetry.Exporter.OtlpExporterOptions.BatchExportProcessorOptions.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.Endpoint.get -> System.Uri -OpenTelemetry.Exporter.OtlpExporterOptions.Endpoint.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.ExportProcessorType.get -> OpenTelemetry.ExportProcessorType -OpenTelemetry.Exporter.OtlpExporterOptions.ExportProcessorType.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.Headers.get -> string -OpenTelemetry.Exporter.OtlpExporterOptions.Headers.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.get -> System.Func -OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.OtlpExporterOptions() -> void -OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.get -> OpenTelemetry.Exporter.OtlpExportProtocol -OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.TimeoutMilliseconds.get -> int -OpenTelemetry.Exporter.OtlpExporterOptions.TimeoutMilliseconds.set -> void -OpenTelemetry.Exporter.OtlpExportProtocol -OpenTelemetry.Exporter.OtlpExportProtocol.Grpc = 0 -> OpenTelemetry.Exporter.OtlpExportProtocol -OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf = 1 -> OpenTelemetry.Exporter.OtlpExportProtocol -OpenTelemetry.Exporter.OtlpMetricExporter -OpenTelemetry.Exporter.OtlpMetricExporter.OtlpMetricExporter(OpenTelemetry.Exporter.OtlpExporterOptions options) -> void -OpenTelemetry.Exporter.OtlpTraceExporter -OpenTelemetry.Exporter.OtlpTraceExporter.OtlpTraceExporter(OpenTelemetry.Exporter.OtlpExporterOptions options) -> void -OpenTelemetry.Metrics.OtlpMetricExporterExtensions -OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions -override OpenTelemetry.Exporter.OtlpMetricExporter.Export(in OpenTelemetry.Batch metrics) -> OpenTelemetry.ExportResult -override OpenTelemetry.Exporter.OtlpMetricExporter.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.Exporter.OtlpTraceExporter.Export(in OpenTelemetry.Batch activityBatch) -> OpenTelemetry.ExportResult -override OpenTelemetry.Exporter.OtlpTraceExporter.OnShutdown(int timeoutMilliseconds) -> bool -static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, string name, System.Action configureExporterAndMetricReader) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, string name, System.Action configureExporter) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action configureExporterAndMetricReader) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action configureExporter) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net6.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net6.0/PublicAPI.Unshipped.txt deleted file mode 100644 index 07a63e57297..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net6.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,3 +0,0 @@ -OpenTelemetry.Logs.OtlpLogExporterHelperExtensions -static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions loggerOptions) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions -static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions loggerOptions, System.Action configure) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/netstandard2.0/PublicAPI.Shipped.txt deleted file mode 100644 index 9b4a2a730a3..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/netstandard2.0/PublicAPI.Shipped.txt +++ /dev/null @@ -1,37 +0,0 @@ -OpenTelemetry.Exporter.OtlpExporterOptions -OpenTelemetry.Exporter.OtlpExporterOptions.BatchExportProcessorOptions.get -> OpenTelemetry.BatchExportProcessorOptions -OpenTelemetry.Exporter.OtlpExporterOptions.BatchExportProcessorOptions.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.Endpoint.get -> System.Uri -OpenTelemetry.Exporter.OtlpExporterOptions.Endpoint.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.ExportProcessorType.get -> OpenTelemetry.ExportProcessorType -OpenTelemetry.Exporter.OtlpExporterOptions.ExportProcessorType.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.Headers.get -> string -OpenTelemetry.Exporter.OtlpExporterOptions.Headers.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.get -> System.Func -OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.OtlpExporterOptions() -> void -OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.get -> OpenTelemetry.Exporter.OtlpExportProtocol -OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.TimeoutMilliseconds.get -> int -OpenTelemetry.Exporter.OtlpExporterOptions.TimeoutMilliseconds.set -> void -OpenTelemetry.Exporter.OtlpExportProtocol -OpenTelemetry.Exporter.OtlpExportProtocol.Grpc = 0 -> OpenTelemetry.Exporter.OtlpExportProtocol -OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf = 1 -> OpenTelemetry.Exporter.OtlpExportProtocol -OpenTelemetry.Exporter.OtlpMetricExporter -OpenTelemetry.Exporter.OtlpMetricExporter.OtlpMetricExporter(OpenTelemetry.Exporter.OtlpExporterOptions options) -> void -OpenTelemetry.Exporter.OtlpTraceExporter -OpenTelemetry.Exporter.OtlpTraceExporter.OtlpTraceExporter(OpenTelemetry.Exporter.OtlpExporterOptions options) -> void -OpenTelemetry.Metrics.OtlpMetricExporterExtensions -OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions -override OpenTelemetry.Exporter.OtlpMetricExporter.Export(in OpenTelemetry.Batch metrics) -> OpenTelemetry.ExportResult -override OpenTelemetry.Exporter.OtlpMetricExporter.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.Exporter.OtlpTraceExporter.Export(in OpenTelemetry.Batch activityBatch) -> OpenTelemetry.ExportResult -override OpenTelemetry.Exporter.OtlpTraceExporter.OnShutdown(int timeoutMilliseconds) -> bool -static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, string name, System.Action configureExporterAndMetricReader) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, string name, System.Action configureExporter) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action configureExporterAndMetricReader) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action configureExporter) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt deleted file mode 100644 index 07a63e57297..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,3 +0,0 @@ -OpenTelemetry.Logs.OtlpLogExporterHelperExtensions -static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions loggerOptions) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions -static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions loggerOptions, System.Action configure) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/netstandard2.1/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/netstandard2.1/PublicAPI.Shipped.txt deleted file mode 100644 index 9b4a2a730a3..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/netstandard2.1/PublicAPI.Shipped.txt +++ /dev/null @@ -1,37 +0,0 @@ -OpenTelemetry.Exporter.OtlpExporterOptions -OpenTelemetry.Exporter.OtlpExporterOptions.BatchExportProcessorOptions.get -> OpenTelemetry.BatchExportProcessorOptions -OpenTelemetry.Exporter.OtlpExporterOptions.BatchExportProcessorOptions.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.Endpoint.get -> System.Uri -OpenTelemetry.Exporter.OtlpExporterOptions.Endpoint.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.ExportProcessorType.get -> OpenTelemetry.ExportProcessorType -OpenTelemetry.Exporter.OtlpExporterOptions.ExportProcessorType.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.Headers.get -> string -OpenTelemetry.Exporter.OtlpExporterOptions.Headers.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.get -> System.Func -OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.OtlpExporterOptions() -> void -OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.get -> OpenTelemetry.Exporter.OtlpExportProtocol -OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.TimeoutMilliseconds.get -> int -OpenTelemetry.Exporter.OtlpExporterOptions.TimeoutMilliseconds.set -> void -OpenTelemetry.Exporter.OtlpExportProtocol -OpenTelemetry.Exporter.OtlpExportProtocol.Grpc = 0 -> OpenTelemetry.Exporter.OtlpExportProtocol -OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf = 1 -> OpenTelemetry.Exporter.OtlpExportProtocol -OpenTelemetry.Exporter.OtlpMetricExporter -OpenTelemetry.Exporter.OtlpMetricExporter.OtlpMetricExporter(OpenTelemetry.Exporter.OtlpExporterOptions options) -> void -OpenTelemetry.Exporter.OtlpTraceExporter -OpenTelemetry.Exporter.OtlpTraceExporter.OtlpTraceExporter(OpenTelemetry.Exporter.OtlpExporterOptions options) -> void -OpenTelemetry.Metrics.OtlpMetricExporterExtensions -OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions -override OpenTelemetry.Exporter.OtlpMetricExporter.Export(in OpenTelemetry.Batch metrics) -> OpenTelemetry.ExportResult -override OpenTelemetry.Exporter.OtlpMetricExporter.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.Exporter.OtlpTraceExporter.Export(in OpenTelemetry.Batch activityBatch) -> OpenTelemetry.ExportResult -override OpenTelemetry.Exporter.OtlpTraceExporter.OnShutdown(int timeoutMilliseconds) -> bool -static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, string name, System.Action configureExporterAndMetricReader) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, string name, System.Action configureExporter) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action configureExporterAndMetricReader) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action configureExporter) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt deleted file mode 100644 index 07a63e57297..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,3 +0,0 @@ -OpenTelemetry.Logs.OtlpLogExporterHelperExtensions -static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions loggerOptions) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions -static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions loggerOptions, System.Action configure) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/AssemblyInfo.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/AssemblyInfo.cs index 2bc348cad9a..3f125304deb 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/AssemblyInfo.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/AssemblyInfo.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Runtime.CompilerServices; @@ -20,14 +7,8 @@ [assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] [assembly: InternalsVisibleTo("Benchmarks, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] [assembly: InternalsVisibleTo("MockOpenTelemetryCollector, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] - -// Used by Moq. -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] #else [assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests")] [assembly: InternalsVisibleTo("Benchmarks")] [assembly: InternalsVisibleTo("MockOpenTelemetryCollector")] - -// Used by Moq. -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] #endif diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md index d3168653c1c..d5d71920bd0 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md @@ -2,6 +2,152 @@ ## Unreleased +* **Experimental (pre-release builds only):** Added + `LoggerProviderBuilder.AddOtlpExporter` registration extensions. + [#5103](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5103) + +* Removed the `OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES` + environment variable, following the stabilization of the exception attributes + `exception.type`, `exception.message`, and `exception.stacktrace` in the + [OpenTelemetry Semantic + Conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/exceptions/exceptions-logs.md#semantic-conventions-for-exceptions-in-logs). + These attributes, corresponding to `LogRecord.Exception`, are now stable and + will be automatically included in exports. + ([#5258](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5258)) + +* Updated `OtlpLogExporter` to set `body` on the data model from + `LogRecord.Body` if `{OriginalFormat}` attribute is NOT found and + `FormattedMessage` is `null`. This is typically the case when using the + experimental Logs Bridge API. + ([#5268](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5268)) + +* Updated `OtlpLogExporter` to set instrumentation scope name on the data model + from `LogRecord.Logger.Name` if `LogRecord.CategoryName` is `null`. This is + typically the case when using the experimental Logs Bridge API. + ([#5300](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5300)) + +* URL encoded values in `OTEL_EXPORTER_OTLP_HEADERS` are now correctly decoded + as it is mandated by the specification. + ([#5316](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5268)) + +* **Experimental (pre-release builds only):** Add support in + `OtlpMetricExporter` for emitting exemplars supplied on Counters, Gauges, and + ExponentialHistograms. + ([#5397](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5397)) + +## 1.7.0 + +Released 2023-Dec-08 + +## 1.7.0-rc.1 + +Released 2023-Nov-29 + +* Made `OpenTelemetry.Exporter.OtlpLogExporter` public. + ([#4979](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4979)) + +* Updated the `OpenTelemetryLoggerOptions.AddOtlpExporter` extension to retrieve + `OtlpExporterOptions` and `LogRecordExportProcessorOptions` using the + `IServiceProvider` / Options API so that they can be controlled via + `IConfiguration` (similar to metrics and traces). + ([#4916](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4916)) + +* Added an `OpenTelemetryLoggerOptions.AddOtlpExporter` extension overload which + accepts a `name` parameter to support named options. + ([#4916](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4916)) + +* Add support for Instrumentation Scope Attributes (i.e [Meter + Tags](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.meter.tags)), + fixing issue + [#4563](https://github.com/open-telemetry/opentelemetry-dotnet/issues/4563). + ([#5089](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5089)) + +## 1.7.0-alpha.1 + +Released 2023-Oct-16 + +* Bumped the version of `Google.Protobuf` used by the project to `3.22.5` so + that consuming applications can be published as NativeAOT successfully. Also, + a new performance feature can be used instead of reflection emit, which is + not AOT-compatible. Removed the dependency on `System.Reflection.Emit.Lightweight`. + ([#4859](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4859)) + +* Added support for `OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT` + and `OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT`. + ([#4887](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4887)) + +* Added ability to export attributes corresponding to `LogRecord.Exception` i.e. +`exception.type`, `exception.message` and `exception.stacktrace`. These +attributes will be exported when +`OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES` environment +variable will be set to `true`. + + **NOTE**: These attributes were removed in [1.6.0-rc.1](#160-rc1) release in + order to support stable release of OTLP Log Exporter. The attributes will now be + available via environment variable mentioned above. + ([#4892](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4892)) + +* Added ability to export attributes corresponding to `LogRecord.EventId.Id` as +`logrecord.event.id` and `LogRecord.EventId.Name` as `logrecord.event.name`. The +attributes will be exported when +`OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES` will be set to `true`. + + **NOTE**: These attributes were removed in [1.6.0-rc.1](#160-rc1) release in + order to support stable release of OTLP Log Exporter. The attributes will now + be available via environment variable mentioned above. + ([#4925](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4925)) + +* `LogRecord.CategoryName` will now be exported as +[InstrumentationScope](https://github.com/open-telemetry/opentelemetry-dotnet/blob/3c2bb7c93dd2e697636479a1882f49bb0c4a362e/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/common/v1/common.proto#L71-L81) +`name` field under +[ScopeLogs](https://github.com/open-telemetry/opentelemetry-dotnet/blob/3c2bb7c93dd2e697636479a1882f49bb0c4a362e/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/logs/v1/logs.proto#L64-L75). +([#4941](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4941)) + +## 1.6.0 + +Released 2023-Sep-05 + +## 1.6.0-rc.1 + +Released 2023-Aug-21 + +* **Breaking change**: Excluded attributes corresponding to +`LogRecord.Exception`, `LogRecord.EventId` and `LogRecord.CategoryName` from the +exported data. See following details for reasoning behind removing each +individual property: + * `LogRecord.Exception`: The semantic conventions for attributes corresponding + to exception data are not yet stable. Track issue + [#4831](https://github.com/open-telemetry/opentelemetry-dotnet/issues/4831) + for details. + * `LogRecord.EventId`: The attributes corresponding to this property are + specific to .NET logging data model and there is no established convention + defined for them yet. Track issue + [#4776](https://github.com/open-telemetry/opentelemetry-dotnet/issues/4776) + for details. + * `LogRecord.CategoryName`: The attribute corresponding to this property is + specific to .NET logging data model and there is no established convention + defined for it yet. Track issue + [#3491](https://github.com/open-telemetry/opentelemetry-dotnet/issues/3491) + for details. + + This change is temporarily done in order to release **stable** version of OTLP + Log Exporter. + ([#4781](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4781)) + +* Added extension method for configuring export processor options for otlp log +exporter. +([#4733](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4733)) + +* Added support for configuring the metric exporter's temporality using the + environment variable `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` as + defined in the + [specification](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.23.0/specification/metrics/sdk_exporters/otlp.md#additional-configuration). + ([#4667](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4667)) + +## 1.6.0-alpha.1 + +Released 2023-Jul-12 + * Merged `OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs` package into `OpenTelemetry.Exporter.OpenTelemetryProtocol`. Going Forward, `OpenTelemetry.Exporter.OpenTelemetryProtocol` will be the only package needed @@ -10,16 +156,30 @@ are now included in this package. ([#4556](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4556)) -* Add back support for Exemplars. See [exemplars](../../docs/metrics/customizing-the-sdk/README.md#exemplars) - for instructions to enable exemplars. - ([#4553](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4553)) - -* Updated Grpc.Net.Client to v2.45 to fix unobserved exception +* Updated Grpc.Net.Client to `2.45.0` to fix unobserved exception from failed calls. ([#4573](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4573)) -* Updated to support `Severity` and `SeverityText` when exporting `LogRecord`s. - ([#4568](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4568)) +* Updated Grpc.Net.Client to `2.52.0` to address the vulnerability reported by + CVE-2023-32731. Refer to + [https://github.com/grpc/grpc/pull/32309](https://github.com/grpc/grpc/pull/32309) + for more details. + ([#4647](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4647)) + +* **Experimental (pre-release builds only):** + + * Note: See + [#4735](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4735) + for the introduction of experimental api support. + + * Add back support for Exemplars. See + [exemplars](../../docs/metrics/customizing-the-sdk/README.md#exemplars) for + instructions to enable exemplars. + ([#4553](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4553)) + + * Updated to support `Severity` and `SeverityText` when exporting + `LogRecord`s. + ([#4568](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4568)) ## 1.5.1 @@ -133,7 +293,7 @@ Released 2022-Oct-17 * Adds support for limiting the length and count of attributes exported from the OTLP log exporter. These - [Attribute Limits](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#attribute-limits) + [Attribute Limits](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md#attribute-limits) are configured via the environment variables defined in the specification. ([#3684](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3684)) @@ -158,7 +318,7 @@ Released 2022-Sep-29 Released 2022-Aug-18 * When using [Attribute - Limits](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#attribute-limits) + Limits](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md#attribute-limits) the OTLP exporter will now send "dropped" counts where applicable (ex: [dropped_attributes_count](https://github.com/open-telemetry/opentelemetry-proto/blob/001e5eabf3ea0193ef9343c1b9a057d23d583d7c/opentelemetry/proto/trace/v1/trace.proto#L191)). ([#3580](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3580)) @@ -169,7 +329,7 @@ Released 2022-Aug-02 * Adds support for limiting the length and count of attributes exported from the OTLP exporter. These - [Attribute Limits](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#attribute-limits) + [Attribute Limits](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md#attribute-limits) are configured via the environment variables defined in the specification. ([#3376](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3376)) @@ -418,7 +578,7 @@ Released 2021-Apr-23 ([#1873](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1873)) * Null values in string arrays are preserved according to - [spec](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/common.md). + [spec](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/README.md). ([#1919](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1919) [#1945](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1945)) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ActivityExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ActivityExtensions.cs index 10896a2d50d..c99f0c2abde 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ActivityExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ActivityExtensions.cs @@ -1,26 +1,10 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Collections.Concurrent; using System.Diagnostics; -using System.Reflection; -using System.Reflection.Emit; using System.Runtime.CompilerServices; using Google.Protobuf; -using Google.Protobuf.Collections; using OpenTelemetry.Internal; using OpenTelemetry.Proto.Collector.Trace.V1; using OpenTelemetry.Proto.Common.V1; @@ -29,402 +13,379 @@ using OpenTelemetry.Trace; using OtlpTrace = OpenTelemetry.Proto.Trace.V1; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; + +internal static class ActivityExtensions { - internal static class ActivityExtensions + private static readonly ConcurrentBag SpanListPool = new(); + + internal static void AddBatch( + this ExportTraceServiceRequest request, + SdkLimitOptions sdkLimitOptions, + Resource processResource, + in Batch activityBatch) { - private static readonly ConcurrentBag SpanListPool = new(); - private static readonly Action, int> RepeatedFieldOfSpanSetCountAction = CreateRepeatedFieldOfSpanSetCountAction(); - - internal static void AddBatch( - this ExportTraceServiceRequest request, - SdkLimitOptions sdkLimitOptions, - Resource processResource, - in Batch activityBatch) + Dictionary spansByLibrary = new Dictionary(); + ResourceSpans resourceSpans = new ResourceSpans { - Dictionary spansByLibrary = new Dictionary(); - ResourceSpans resourceSpans = new ResourceSpans - { - Resource = processResource, - }; - request.ResourceSpans.Add(resourceSpans); + Resource = processResource, + }; + request.ResourceSpans.Add(resourceSpans); - foreach (var activity in activityBatch) + foreach (var activity in activityBatch) + { + Span span = activity.ToOtlpSpan(sdkLimitOptions); + if (span == null) { - Span span = activity.ToOtlpSpan(sdkLimitOptions); - if (span == null) - { - OpenTelemetryProtocolExporterEventSource.Log.CouldNotTranslateActivity( - nameof(ActivityExtensions), - nameof(AddBatch)); - continue; - } - - var activitySourceName = activity.Source.Name; - if (!spansByLibrary.TryGetValue(activitySourceName, out var spans)) - { - spans = GetSpanListFromPool(activitySourceName, activity.Source.Version); + OpenTelemetryProtocolExporterEventSource.Log.CouldNotTranslateActivity( + nameof(ActivityExtensions), + nameof(AddBatch)); + continue; + } - spansByLibrary.Add(activitySourceName, spans); - resourceSpans.ScopeSpans.Add(spans); - } + var activitySourceName = activity.Source.Name; + if (!spansByLibrary.TryGetValue(activitySourceName, out var spans)) + { + spans = GetSpanListFromPool(activitySourceName, activity.Source.Version); - spans.Spans.Add(span); + spansByLibrary.Add(activitySourceName, spans); + resourceSpans.ScopeSpans.Add(spans); } + + spans.Spans.Add(span); } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void Return(this ExportTraceServiceRequest request) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Return(this ExportTraceServiceRequest request) + { + var resourceSpans = request.ResourceSpans.FirstOrDefault(); + if (resourceSpans == null) { - var resourceSpans = request.ResourceSpans.FirstOrDefault(); - if (resourceSpans == null) - { - return; - } + return; + } - foreach (var scope in resourceSpans.ScopeSpans) - { - RepeatedFieldOfSpanSetCountAction(scope.Spans, 0); - SpanListPool.Add(scope); - } + foreach (var scope in resourceSpans.ScopeSpans) + { + scope.Spans.Clear(); + SpanListPool.Add(scope); } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static ScopeSpans GetSpanListFromPool(string name, string version) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ScopeSpans GetSpanListFromPool(string name, string version) + { + if (!SpanListPool.TryTake(out var spans)) { - if (!SpanListPool.TryTake(out var spans)) + spans = new ScopeSpans { - spans = new ScopeSpans + Scope = new InstrumentationScope { - Scope = new InstrumentationScope - { - Name = name, // Name is enforced to not be null, but it can be empty. - Version = version ?? string.Empty, // NRE throw by proto - }, - }; - } - else - { - spans.Scope.Name = name; - spans.Scope.Version = version ?? string.Empty; - } - - return spans; + Name = name, // Name is enforced to not be null, but it can be empty. + Version = version ?? string.Empty, // NRE throw by proto + }, + }; + } + else + { + spans.Scope.Name = name; + spans.Scope.Version = version ?? string.Empty; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static Span ToOtlpSpan(this Activity activity, SdkLimitOptions sdkLimitOptions) + return spans; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Span ToOtlpSpan(this Activity activity, SdkLimitOptions sdkLimitOptions) + { + if (activity.IdFormat != ActivityIdFormat.W3C) { - if (activity.IdFormat != ActivityIdFormat.W3C) - { - // Only ActivityIdFormat.W3C is supported, in principle this should never be - // hit under the OpenTelemetry SDK. - return null; - } + // Only ActivityIdFormat.W3C is supported, in principle this should never be + // hit under the OpenTelemetry SDK. + return null; + } - byte[] traceIdBytes = new byte[16]; - byte[] spanIdBytes = new byte[8]; + byte[] traceIdBytes = new byte[16]; + byte[] spanIdBytes = new byte[8]; - activity.TraceId.CopyTo(traceIdBytes); - activity.SpanId.CopyTo(spanIdBytes); + activity.TraceId.CopyTo(traceIdBytes); + activity.SpanId.CopyTo(spanIdBytes); - var parentSpanIdString = ByteString.Empty; - if (activity.ParentSpanId != default) - { - byte[] parentSpanIdBytes = new byte[8]; - activity.ParentSpanId.CopyTo(parentSpanIdBytes); - parentSpanIdString = UnsafeByteOperations.UnsafeWrap(parentSpanIdBytes); - } + var parentSpanIdString = ByteString.Empty; + if (activity.ParentSpanId != default) + { + byte[] parentSpanIdBytes = new byte[8]; + activity.ParentSpanId.CopyTo(parentSpanIdBytes); + parentSpanIdString = UnsafeByteOperations.UnsafeWrap(parentSpanIdBytes); + } - var startTimeUnixNano = activity.StartTimeUtc.ToUnixTimeNanoseconds(); - var otlpSpan = new Span - { - Name = activity.DisplayName, + var startTimeUnixNano = activity.StartTimeUtc.ToUnixTimeNanoseconds(); + var otlpSpan = new Span + { + Name = activity.DisplayName, - // There is an offset of 1 on the OTLP enum. - Kind = (Span.Types.SpanKind)(activity.Kind + 1), + // There is an offset of 1 on the OTLP enum. + Kind = (Span.Types.SpanKind)(activity.Kind + 1), - TraceId = UnsafeByteOperations.UnsafeWrap(traceIdBytes), - SpanId = UnsafeByteOperations.UnsafeWrap(spanIdBytes), - ParentSpanId = parentSpanIdString, - TraceState = activity.TraceStateString ?? string.Empty, + TraceId = UnsafeByteOperations.UnsafeWrap(traceIdBytes), + SpanId = UnsafeByteOperations.UnsafeWrap(spanIdBytes), + ParentSpanId = parentSpanIdString, + TraceState = activity.TraceStateString ?? string.Empty, - StartTimeUnixNano = (ulong)startTimeUnixNano, - EndTimeUnixNano = (ulong)(startTimeUnixNano + activity.Duration.ToNanoseconds()), - }; + StartTimeUnixNano = (ulong)startTimeUnixNano, + EndTimeUnixNano = (ulong)(startTimeUnixNano + activity.Duration.ToNanoseconds()), + }; - TagEnumerationState otlpTags = new() - { - SdkLimitOptions = sdkLimitOptions, - Span = otlpSpan, - }; - otlpTags.EnumerateTags(activity, sdkLimitOptions.SpanAttributeCountLimit ?? int.MaxValue); + TagEnumerationState otlpTags = new() + { + SdkLimitOptions = sdkLimitOptions, + Span = otlpSpan, + }; + otlpTags.EnumerateTags(activity, sdkLimitOptions.SpanAttributeCountLimit ?? int.MaxValue); - if (activity.Kind == ActivityKind.Client || activity.Kind == ActivityKind.Producer) - { - PeerServiceResolver.Resolve(ref otlpTags, out string peerServiceName, out bool addAsTag); + if (activity.Kind == ActivityKind.Client || activity.Kind == ActivityKind.Producer) + { + PeerServiceResolver.Resolve(ref otlpTags, out string peerServiceName, out bool addAsTag); - if (peerServiceName != null && addAsTag) - { - otlpSpan.Attributes.Add( - new KeyValue - { - Key = SemanticConventions.AttributePeerService, - Value = new AnyValue { StringValue = peerServiceName }, - }); - } + if (peerServiceName != null && addAsTag) + { + otlpSpan.Attributes.Add( + new KeyValue + { + Key = SemanticConventions.AttributePeerService, + Value = new AnyValue { StringValue = peerServiceName }, + }); } + } - otlpSpan.Status = activity.ToOtlpStatus(ref otlpTags); + otlpSpan.Status = activity.ToOtlpStatus(ref otlpTags); - EventEnumerationState otlpEvents = new() - { - SdkLimitOptions = sdkLimitOptions, - Span = otlpSpan, - }; - otlpEvents.EnumerateEvents(activity, sdkLimitOptions.SpanEventCountLimit ?? int.MaxValue); + EventEnumerationState otlpEvents = new() + { + SdkLimitOptions = sdkLimitOptions, + Span = otlpSpan, + }; + otlpEvents.EnumerateEvents(activity, sdkLimitOptions.SpanEventCountLimit ?? int.MaxValue); - LinkEnumerationState otlpLinks = new() - { - SdkLimitOptions = sdkLimitOptions, - Span = otlpSpan, - }; - otlpLinks.EnumerateLinks(activity, sdkLimitOptions.SpanLinkCountLimit ?? int.MaxValue); + LinkEnumerationState otlpLinks = new() + { + SdkLimitOptions = sdkLimitOptions, + Span = otlpSpan, + }; + otlpLinks.EnumerateLinks(activity, sdkLimitOptions.SpanLinkCountLimit ?? int.MaxValue); + + return otlpSpan; + } - return otlpSpan; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static OtlpTrace.Status ToOtlpStatus(this Activity activity, ref TagEnumerationState otlpTags) + { + var statusCodeForTagValue = StatusHelper.GetStatusCodeForTagValue(otlpTags.StatusCode); + if (activity.Status == ActivityStatusCode.Unset && statusCodeForTagValue == null) + { + return null; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static OtlpTrace.Status ToOtlpStatus(this Activity activity, ref TagEnumerationState otlpTags) + OtlpTrace.Status.Types.StatusCode otlpActivityStatusCode = OtlpTrace.Status.Types.StatusCode.Unset; + string otlpStatusDescription = null; + if (activity.Status != ActivityStatusCode.Unset) { - var statusCodeForTagValue = StatusHelper.GetStatusCodeForTagValue(otlpTags.StatusCode); - if (activity.Status == ActivityStatusCode.Unset && statusCodeForTagValue == null) + // The numerical values of the two enumerations match, a simple cast is enough. + otlpActivityStatusCode = (OtlpTrace.Status.Types.StatusCode)(int)activity.Status; + if (activity.Status == ActivityStatusCode.Error && !string.IsNullOrEmpty(activity.StatusDescription)) { - return null; + otlpStatusDescription = activity.StatusDescription; } - - OtlpTrace.Status.Types.StatusCode otlpActivityStatusCode = OtlpTrace.Status.Types.StatusCode.Unset; - string otlpStatusDescription = null; - if (activity.Status != ActivityStatusCode.Unset) + } + else + { + if (statusCodeForTagValue != StatusCode.Unset) { // The numerical values of the two enumerations match, a simple cast is enough. - otlpActivityStatusCode = (OtlpTrace.Status.Types.StatusCode)(int)activity.Status; - if (activity.Status == ActivityStatusCode.Error && !string.IsNullOrEmpty(activity.StatusDescription)) + otlpActivityStatusCode = (OtlpTrace.Status.Types.StatusCode)(int)statusCodeForTagValue; + if (statusCodeForTagValue == StatusCode.Error && !string.IsNullOrEmpty(otlpTags.StatusDescription)) { - otlpStatusDescription = activity.StatusDescription; + otlpStatusDescription = otlpTags.StatusDescription; } } - else - { - if (statusCodeForTagValue != StatusCode.Unset) - { - // The numerical values of the two enumerations match, a simple cast is enough. - otlpActivityStatusCode = (OtlpTrace.Status.Types.StatusCode)(int)statusCodeForTagValue; - if (statusCodeForTagValue == StatusCode.Error && !string.IsNullOrEmpty(otlpTags.StatusDescription)) - { - otlpStatusDescription = otlpTags.StatusDescription; - } - } - } - - var otlpStatus = new OtlpTrace.Status { Code = otlpActivityStatusCode }; - if (!string.IsNullOrEmpty(otlpStatusDescription)) - { - otlpStatus.Message = otlpStatusDescription; - } - - return otlpStatus; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Span.Types.Link ToOtlpLink(in ActivityLink activityLink, SdkLimitOptions sdkLimitOptions) + var otlpStatus = new OtlpTrace.Status { Code = otlpActivityStatusCode }; + if (!string.IsNullOrEmpty(otlpStatusDescription)) { - byte[] traceIdBytes = new byte[16]; - byte[] spanIdBytes = new byte[8]; + otlpStatus.Message = otlpStatusDescription; + } - activityLink.Context.TraceId.CopyTo(traceIdBytes); - activityLink.Context.SpanId.CopyTo(spanIdBytes); + return otlpStatus; + } - var otlpLink = new Span.Types.Link - { - TraceId = UnsafeByteOperations.UnsafeWrap(traceIdBytes), - SpanId = UnsafeByteOperations.UnsafeWrap(spanIdBytes), - }; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Span.Types.Link ToOtlpLink(in ActivityLink activityLink, SdkLimitOptions sdkLimitOptions) + { + byte[] traceIdBytes = new byte[16]; + byte[] spanIdBytes = new byte[8]; + + activityLink.Context.TraceId.CopyTo(traceIdBytes); + activityLink.Context.SpanId.CopyTo(spanIdBytes); - int maxTags = sdkLimitOptions.SpanLinkAttributeCountLimit ?? int.MaxValue; - foreach (ref readonly var tag in activityLink.EnumerateTagObjects()) + var otlpLink = new Span.Types.Link + { + TraceId = UnsafeByteOperations.UnsafeWrap(traceIdBytes), + SpanId = UnsafeByteOperations.UnsafeWrap(spanIdBytes), + }; + + int maxTags = sdkLimitOptions.SpanLinkAttributeCountLimit ?? int.MaxValue; + foreach (ref readonly var tag in activityLink.EnumerateTagObjects()) + { + if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var attribute, sdkLimitOptions.AttributeValueLengthLimit)) { - if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var attribute, sdkLimitOptions.AttributeValueLengthLimit)) + if (otlpLink.Attributes.Count < maxTags) { - if (otlpLink.Attributes.Count < maxTags) - { - otlpLink.Attributes.Add(attribute); - } - else - { - otlpLink.DroppedAttributesCount++; - } + otlpLink.Attributes.Add(attribute); + } + else + { + otlpLink.DroppedAttributesCount++; } } - - return otlpLink; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Span.Types.Event ToOtlpEvent(in ActivityEvent activityEvent, SdkLimitOptions sdkLimitOptions) + return otlpLink; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Span.Types.Event ToOtlpEvent(in ActivityEvent activityEvent, SdkLimitOptions sdkLimitOptions) + { + var otlpEvent = new Span.Types.Event { - var otlpEvent = new Span.Types.Event - { - Name = activityEvent.Name, - TimeUnixNano = (ulong)activityEvent.Timestamp.ToUnixTimeNanoseconds(), - }; + Name = activityEvent.Name, + TimeUnixNano = (ulong)activityEvent.Timestamp.ToUnixTimeNanoseconds(), + }; - int maxTags = sdkLimitOptions.SpanEventAttributeCountLimit ?? int.MaxValue; - foreach (ref readonly var tag in activityEvent.EnumerateTagObjects()) + int maxTags = sdkLimitOptions.SpanEventAttributeCountLimit ?? int.MaxValue; + foreach (ref readonly var tag in activityEvent.EnumerateTagObjects()) + { + if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var attribute, sdkLimitOptions.AttributeValueLengthLimit)) { - if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var attribute, sdkLimitOptions.AttributeValueLengthLimit)) + if (otlpEvent.Attributes.Count < maxTags) { - if (otlpEvent.Attributes.Count < maxTags) - { - otlpEvent.Attributes.Add(attribute); - } - else - { - otlpEvent.DroppedAttributesCount++; - } + otlpEvent.Attributes.Add(attribute); + } + else + { + otlpEvent.DroppedAttributesCount++; } } - - return otlpEvent; } - private static Action, int> CreateRepeatedFieldOfSpanSetCountAction() - { - FieldInfo repeatedFieldOfSpanCountField = typeof(RepeatedField).GetField("count", BindingFlags.NonPublic | BindingFlags.Instance); - - DynamicMethod dynamicMethod = new DynamicMethod( - "CreateSetCountAction", - null, - new[] { typeof(RepeatedField), typeof(int) }, - typeof(ActivityExtensions).Module, - skipVisibility: true); + return otlpEvent; + } - var generator = dynamicMethod.GetILGenerator(); + private struct TagEnumerationState : PeerServiceResolver.IPeerServiceState + { + public SdkLimitOptions SdkLimitOptions; - generator.Emit(OpCodes.Ldarg_0); - generator.Emit(OpCodes.Ldarg_1); - generator.Emit(OpCodes.Stfld, repeatedFieldOfSpanCountField); - generator.Emit(OpCodes.Ret); + public Span Span; - return (Action, int>)dynamicMethod.CreateDelegate(typeof(Action, int>)); - } + public string StatusCode; - private struct TagEnumerationState : PeerServiceResolver.IPeerServiceState - { - public SdkLimitOptions SdkLimitOptions; + public string StatusDescription; - public Span Span; + public string PeerService { get; set; } - public string StatusCode; + public int? PeerServicePriority { get; set; } - public string StatusDescription; + public string HostName { get; set; } - public string PeerService { get; set; } + public string IpAddress { get; set; } - public int? PeerServicePriority { get; set; } + public long Port { get; set; } - public string HostName { get; set; } + public void EnumerateTags(Activity activity, int maxTags) + { + foreach (ref readonly var tag in activity.EnumerateTagObjects()) + { + if (tag.Value == null) + { + continue; + } - public string IpAddress { get; set; } + var key = tag.Key; - public long Port { get; set; } + switch (key) + { + case SpanAttributeConstants.StatusCodeKey: + this.StatusCode = tag.Value as string; + continue; + case SpanAttributeConstants.StatusDescriptionKey: + this.StatusDescription = tag.Value as string; + continue; + } - public void EnumerateTags(Activity activity, int maxTags) - { - foreach (ref readonly var tag in activity.EnumerateTagObjects()) + if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var attribute, this.SdkLimitOptions.AttributeValueLengthLimit)) { - if (tag.Value == null) + if (this.Span.Attributes.Count < maxTags) { - continue; + this.Span.Attributes.Add(attribute); } - - var key = tag.Key; - - switch (key) + else { - case SpanAttributeConstants.StatusCodeKey: - this.StatusCode = tag.Value as string; - continue; - case SpanAttributeConstants.StatusDescriptionKey: - this.StatusDescription = tag.Value as string; - continue; + this.Span.DroppedAttributesCount++; } - if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var attribute, this.SdkLimitOptions.AttributeValueLengthLimit)) + if (attribute.Value.ValueCase == AnyValue.ValueOneofCase.StringValue) { - if (this.Span.Attributes.Count < maxTags) - { - this.Span.Attributes.Add(attribute); - } - else - { - this.Span.DroppedAttributesCount++; - } - - if (attribute.Value.ValueCase == AnyValue.ValueOneofCase.StringValue) - { - // Note: tag.Value is used and not attribute.Value here because attribute.Value may be truncated - PeerServiceResolver.InspectTag(ref this, key, tag.Value as string); - } - else if (attribute.Value.ValueCase == AnyValue.ValueOneofCase.IntValue) - { - PeerServiceResolver.InspectTag(ref this, key, attribute.Value.IntValue); - } + // Note: tag.Value is used and not attribute.Value here because attribute.Value may be truncated + PeerServiceResolver.InspectTag(ref this, key, tag.Value as string); + } + else if (attribute.Value.ValueCase == AnyValue.ValueOneofCase.IntValue) + { + PeerServiceResolver.InspectTag(ref this, key, attribute.Value.IntValue); } } } } + } - private struct EventEnumerationState - { - public SdkLimitOptions SdkLimitOptions; + private struct EventEnumerationState + { + public SdkLimitOptions SdkLimitOptions; - public Span Span; + public Span Span; - public void EnumerateEvents(Activity activity, int maxEvents) + public void EnumerateEvents(Activity activity, int maxEvents) + { + foreach (ref readonly var @event in activity.EnumerateEvents()) { - foreach (ref readonly var @event in activity.EnumerateEvents()) + if (this.Span.Events.Count < maxEvents) { - if (this.Span.Events.Count < maxEvents) - { - this.Span.Events.Add(ToOtlpEvent(in @event, this.SdkLimitOptions)); - } - else - { - this.Span.DroppedEventsCount++; - } + this.Span.Events.Add(ToOtlpEvent(in @event, this.SdkLimitOptions)); + } + else + { + this.Span.DroppedEventsCount++; } } } + } - private struct LinkEnumerationState - { - public SdkLimitOptions SdkLimitOptions; + private struct LinkEnumerationState + { + public SdkLimitOptions SdkLimitOptions; - public Span Span; + public Span Span; - public void EnumerateLinks(Activity activity, int maxLinks) + public void EnumerateLinks(Activity activity, int maxLinks) + { + foreach (ref readonly var link in activity.EnumerateLinks()) { - foreach (ref readonly var link in activity.EnumerateLinks()) + if (this.Span.Links.Count < maxLinks) { - if (this.Span.Links.Count < maxLinks) - { - this.Span.Links.Add(ToOtlpLink(in link, this.SdkLimitOptions)); - } - else - { - this.Span.DroppedLinksCount++; - } + this.Span.Links.Add(ToOtlpLink(in link, this.SdkLimitOptions)); + } + else + { + this.Span.DroppedLinksCount++; } } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExperimentalOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExperimentalOptions.cs new file mode 100644 index 00000000000..61f126b1229 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExperimentalOptions.cs @@ -0,0 +1,36 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using Microsoft.Extensions.Configuration; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; + +internal sealed class ExperimentalOptions +{ + public const string LogRecordEventIdAttribute = "logrecord.event.id"; + + public const string LogRecordEventNameAttribute = "logrecord.event.name"; + + public const string EmitLogEventEnvVar = "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES"; + + public ExperimentalOptions() + : this(new ConfigurationBuilder().AddEnvironmentVariables().Build()) + { + } + + public ExperimentalOptions(IConfiguration configuration) + { + if (configuration.TryGetBoolValue(EmitLogEventEnvVar, out var emitLogEventAttributes)) + { + this.EmitLogEventAttributes = emitLogEventAttributes; + } + } + + /// + /// Gets or sets a value indicating whether log event attributes should be exported. + /// + public bool EmitLogEventAttributes { get; set; } = false; +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpGrpcExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpGrpcExportClient.cs index bc833bea2d0..4bb48cb8fe6 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpGrpcExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpGrpcExportClient.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Grpc.Core; using OpenTelemetry.Internal; @@ -20,56 +7,57 @@ using Grpc.Net.Client; #endif -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; + +/// Base class for sending OTLP export request over gRPC. +/// Type of export request. +internal abstract class BaseOtlpGrpcExportClient : IExportClient { - /// Base class for sending OTLP export request over gRPC. - /// Type of export request. - internal abstract class BaseOtlpGrpcExportClient : IExportClient + protected static readonly ExportClientGrpcResponse SuccessExportResponse = new ExportClientGrpcResponse(success: true, deadlineUtc: null, exception: null); + + protected BaseOtlpGrpcExportClient(OtlpExporterOptions options) { - protected BaseOtlpGrpcExportClient(OtlpExporterOptions options) - { - Guard.ThrowIfNull(options); - Guard.ThrowIfInvalidTimeout(options.TimeoutMilliseconds); + Guard.ThrowIfNull(options); + Guard.ThrowIfInvalidTimeout(options.TimeoutMilliseconds); - ExporterClientValidation.EnsureUnencryptedSupportIsEnabled(options); + ExporterClientValidation.EnsureUnencryptedSupportIsEnabled(options); - this.Endpoint = new UriBuilder(options.Endpoint).Uri; - this.Headers = options.GetMetadataFromHeaders(); - this.TimeoutMilliseconds = options.TimeoutMilliseconds; - } + this.Endpoint = new UriBuilder(options.Endpoint).Uri; + this.Headers = options.GetMetadataFromHeaders(); + this.TimeoutMilliseconds = options.TimeoutMilliseconds; + } #if NETSTANDARD2_1 || NET6_0_OR_GREATER - internal GrpcChannel Channel { get; set; } + internal GrpcChannel Channel { get; set; } #else - internal Channel Channel { get; set; } + internal Channel Channel { get; set; } #endif - internal Uri Endpoint { get; } + internal Uri Endpoint { get; } - internal Metadata Headers { get; } + internal Metadata Headers { get; } - internal int TimeoutMilliseconds { get; } + internal int TimeoutMilliseconds { get; } - /// - public abstract bool SendExportRequest(TRequest request, CancellationToken cancellationToken = default); + /// + public abstract ExportClientResponse SendExportRequest(TRequest request, CancellationToken cancellationToken = default); - /// - public virtual bool Shutdown(int timeoutMilliseconds) + /// + public virtual bool Shutdown(int timeoutMilliseconds) + { + if (this.Channel == null) { - if (this.Channel == null) - { - return true; - } + return true; + } - if (timeoutMilliseconds == -1) - { - this.Channel.ShutdownAsync().Wait(); - return true; - } - else - { - return Task.WaitAny(new Task[] { this.Channel.ShutdownAsync(), Task.Delay(timeoutMilliseconds) }) == 0; - } + if (timeoutMilliseconds == -1) + { + this.Channel.ShutdownAsync().Wait(); + return true; + } + else + { + return Task.WaitAny(new Task[] { this.Channel.ShutdownAsync(), Task.Delay(timeoutMilliseconds) }) == 0; } } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpHttpExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpHttpExportClient.cs index 0f1f6b4b6f1..4aad820b1e2 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpHttpExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpHttpExportClient.cs @@ -1,101 +1,101 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #if NETFRAMEWORK using System.Net.Http; #endif using OpenTelemetry.Internal; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; + +/// Base class for sending OTLP export request over HTTP. +/// Type of export request. +internal abstract class BaseOtlpHttpExportClient : IExportClient { - /// Base class for sending OTLP export request over HTTP. - /// Type of export request. - internal abstract class BaseOtlpHttpExportClient : IExportClient + private static readonly ExportClientHttpResponse SuccessExportResponse = new ExportClientHttpResponse(success: true, deadlineUtc: null, response: null, exception: null); + + protected BaseOtlpHttpExportClient(OtlpExporterOptions options, HttpClient httpClient, string signalPath) { - protected BaseOtlpHttpExportClient(OtlpExporterOptions options, HttpClient httpClient, string signalPath) - { - Guard.ThrowIfNull(options); - Guard.ThrowIfNull(httpClient); - Guard.ThrowIfNull(signalPath); - Guard.ThrowIfInvalidTimeout(options.TimeoutMilliseconds); - - Uri exporterEndpoint = !options.ProgrammaticallyModifiedEndpoint - ? options.Endpoint.AppendPathIfNotPresent(signalPath) - : options.Endpoint; - this.Endpoint = new UriBuilder(exporterEndpoint).Uri; - this.Headers = options.GetHeaders>((d, k, v) => d.Add(k, v)); - this.HttpClient = httpClient; - } + Guard.ThrowIfNull(options); + Guard.ThrowIfNull(httpClient); + Guard.ThrowIfNull(signalPath); + Guard.ThrowIfInvalidTimeout(options.TimeoutMilliseconds); + + Uri exporterEndpoint = !options.ProgrammaticallyModifiedEndpoint + ? options.Endpoint.AppendPathIfNotPresent(signalPath) + : options.Endpoint; + this.Endpoint = new UriBuilder(exporterEndpoint).Uri; + this.Headers = options.GetHeaders>((d, k, v) => d.Add(k, v)); + this.HttpClient = httpClient; + } - internal HttpClient HttpClient { get; } + internal HttpClient HttpClient { get; } - internal Uri Endpoint { get; set; } + internal Uri Endpoint { get; set; } - internal IReadOnlyDictionary Headers { get; } + internal IReadOnlyDictionary Headers { get; } - /// - public bool SendExportRequest(TRequest request, CancellationToken cancellationToken = default) + /// + public ExportClientResponse SendExportRequest(TRequest request, CancellationToken cancellationToken = default) + { + // `HttpClient.Timeout.TotalMilliseconds` would be populated with the correct timeout value for both the exporter configuration cases: + // 1. User provides their own HttpClient. This case is straightforward as the user wants to use their `HttpClient` and thereby the same client's timeout value. + // 2. If the user configures timeout via the exporter options, then the timeout set for the `HttpClient` initialized by the exporter will be set to user provided value. + DateTime deadline = DateTime.UtcNow.AddMilliseconds(this.HttpClient.Timeout.TotalMilliseconds); + try { - try - { - using var httpRequest = this.CreateHttpRequest(request); + using var httpRequest = this.CreateHttpRequest(request); - using var httpResponse = this.SendHttpRequest(httpRequest, cancellationToken); + using var httpResponse = this.SendHttpRequest(httpRequest, cancellationToken); - httpResponse?.EnsureSuccessStatusCode(); + try + { + httpResponse.EnsureSuccessStatusCode(); } catch (HttpRequestException ex) { - OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(this.Endpoint, ex); - - return false; + return new ExportClientHttpResponse(success: false, deadlineUtc: deadline, response: httpResponse, ex); } - return true; + // We do not need to return back response and deadline for successful response so using cached value. + return SuccessExportResponse; } - - /// - public bool Shutdown(int timeoutMilliseconds) + catch (HttpRequestException ex) { - this.HttpClient.CancelPendingRequests(); - return true; + OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(this.Endpoint, ex); + + return new ExportClientHttpResponse(success: false, deadlineUtc: deadline, response: null, exception: ex); } + } + + /// + public bool Shutdown(int timeoutMilliseconds) + { + this.HttpClient.CancelPendingRequests(); + return true; + } - protected abstract HttpContent CreateHttpContent(TRequest exportRequest); + protected abstract HttpContent CreateHttpContent(TRequest exportRequest); - protected HttpRequestMessage CreateHttpRequest(TRequest exportRequest) + protected HttpRequestMessage CreateHttpRequest(TRequest exportRequest) + { + var request = new HttpRequestMessage(HttpMethod.Post, this.Endpoint); + foreach (var header in this.Headers) { - var request = new HttpRequestMessage(HttpMethod.Post, this.Endpoint); - foreach (var header in this.Headers) - { - request.Headers.Add(header.Key, header.Value); - } + request.Headers.Add(header.Key, header.Value); + } - request.Content = this.CreateHttpContent(exportRequest); + request.Content = this.CreateHttpContent(exportRequest); - return request; - } + return request; + } - protected HttpResponseMessage SendHttpRequest(HttpRequestMessage request, CancellationToken cancellationToken) - { + protected HttpResponseMessage SendHttpRequest(HttpRequestMessage request, CancellationToken cancellationToken) + { #if NET6_0_OR_GREATER - return this.HttpClient.Send(request, cancellationToken); + return this.HttpClient.Send(request, cancellationToken); #else - return this.HttpClient.SendAsync(request, cancellationToken).GetAwaiter().GetResult(); + return this.HttpClient.SendAsync(request, cancellationToken).GetAwaiter().GetResult(); #endif - } } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientGrpcResponse.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientGrpcResponse.cs new file mode 100644 index 00000000000..cadecc5b3c8 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientGrpcResponse.cs @@ -0,0 +1,17 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; + +internal sealed class ExportClientGrpcResponse : ExportClientResponse +{ + public ExportClientGrpcResponse( + bool success, + DateTime? deadlineUtc, + Exception? exception) + : base(success, deadlineUtc, exception) + { + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientHttpResponse.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientHttpResponse.cs new file mode 100644 index 00000000000..a3c6e581b93 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientHttpResponse.cs @@ -0,0 +1,30 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Net; +#if NETFRAMEWORK +using System.Net.Http; +#endif +using System.Net.Http.Headers; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; + +internal sealed class ExportClientHttpResponse : ExportClientResponse +{ + public ExportClientHttpResponse( + bool success, + DateTime? deadlineUtc, + HttpResponseMessage? response, + Exception? exception) + : base(success, deadlineUtc, exception) + { + this.Headers = response?.Headers; + this.StatusCode = response?.StatusCode; + } + + public HttpResponseHeaders? Headers { get; } + + public HttpStatusCode? StatusCode { get; } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientResponse.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientResponse.cs new file mode 100644 index 00000000000..2113e96d870 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientResponse.cs @@ -0,0 +1,22 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; + +internal abstract class ExportClientResponse +{ + protected ExportClientResponse(bool success, DateTime? deadlineUtc, Exception? exception) + { + this.Success = success; + this.Exception = exception; + this.DeadlineUtc = deadlineUtc; + } + + public bool Success { get; } + + public Exception? Exception { get; } + + public DateTime? DeadlineUtc { get; } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExporterClientValidation.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExporterClientValidation.cs index 14238daab6a..87b817e05e5 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExporterClientValidation.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExporterClientValidation.cs @@ -1,42 +1,28 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; + +internal static class ExporterClientValidation { - internal static class ExporterClientValidation + internal static void EnsureUnencryptedSupportIsEnabled(OtlpExporterOptions options) { - internal static void EnsureUnencryptedSupportIsEnabled(OtlpExporterOptions options) - { - var version = Environment.Version; + var version = Environment.Version; - // This verification is only required for .NET Core 3.x - if (version.Major != 3) - { - return; - } + // This verification is only required for .NET Core 3.x + if (version.Major != 3) + { + return; + } - if (options.Endpoint.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase)) + if (options.Endpoint.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase)) + { + if (AppContext.TryGetSwitch( + "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", out var unencryptedIsSupported) == false + || unencryptedIsSupported == false) { - if (AppContext.TryGetSwitch( - "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", out var unencryptedIsSupported) == false - || unencryptedIsSupported == false) - { - throw new InvalidOperationException( - "Calling insecure gRPC services on .NET Core 3.x requires enabling the 'System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport' switch. See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client"); - } + throw new InvalidOperationException( + "Calling insecure gRPC services on .NET Core 3.x requires enabling the 'System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport' switch. See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client"); } } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/IExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/IExportClient.cs index c33a11ee8b3..a13a63e743c 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/IExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/IExportClient.cs @@ -1,43 +1,31 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient +#nullable enable + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; + +/// Export client interface. +/// Type of export request. +internal interface IExportClient { - /// Export client interface. - /// Type of export request. - internal interface IExportClient - { - /// - /// Method for sending export request to the server. - /// - /// The request to send to the server. - /// An optional token for canceling the call. - /// True if the request has been sent successfully, otherwise false. - bool SendExportRequest(TRequest request, CancellationToken cancellationToken = default); + /// + /// Method for sending export request to the server. + /// + /// The request to send to the server. + /// An optional token for canceling the call. + /// . + ExportClientResponse SendExportRequest(TRequest request, CancellationToken cancellationToken = default); - /// - /// Method for shutting down the export client. - /// - /// - /// The number of milliseconds to wait, or Timeout.Infinite to - /// wait indefinitely. - /// - /// - /// Returns true if shutdown succeeded; otherwise, false. - /// - bool Shutdown(int timeoutMilliseconds); - } + /// + /// Method for shutting down the export client. + /// + /// + /// The number of milliseconds to wait, or Timeout.Infinite to + /// wait indefinitely. + /// + /// + /// Returns true if shutdown succeeded; otherwise, false. + /// + bool Shutdown(int timeoutMilliseconds); } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcLogExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcLogExportClient.cs index 0b56f302198..4caf8a7d6ae 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcLogExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcLogExportClient.cs @@ -1,60 +1,47 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Grpc.Core; using OtlpCollector = OpenTelemetry.Proto.Collector.Logs.V1; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; + +/// Class for sending OTLP Logs export request over gRPC. +internal sealed class OtlpGrpcLogExportClient : BaseOtlpGrpcExportClient { - /// Class for sending OTLP Logs export request over gRPC. - internal sealed class OtlpGrpcLogExportClient : BaseOtlpGrpcExportClient - { - private readonly OtlpCollector.LogsService.LogsServiceClient logsClient; + private readonly OtlpCollector.LogsService.LogsServiceClient logsClient; - public OtlpGrpcLogExportClient(OtlpExporterOptions options, OtlpCollector.LogsService.LogsServiceClient logsServiceClient = null) - : base(options) + public OtlpGrpcLogExportClient(OtlpExporterOptions options, OtlpCollector.LogsService.LogsServiceClient logsServiceClient = null) + : base(options) + { + if (logsServiceClient != null) { - if (logsServiceClient != null) - { - this.logsClient = logsServiceClient; - } - else - { - this.Channel = options.CreateChannel(); - this.logsClient = new OtlpCollector.LogsService.LogsServiceClient(this.Channel); - } + this.logsClient = logsServiceClient; } - - /// - public override bool SendExportRequest(OtlpCollector.ExportLogsServiceRequest request, CancellationToken cancellationToken = default) + else { - var deadline = DateTime.UtcNow.AddMilliseconds(this.TimeoutMilliseconds); + this.Channel = options.CreateChannel(); + this.logsClient = new OtlpCollector.LogsService.LogsServiceClient(this.Channel); + } + } - try - { - this.logsClient.Export(request, headers: this.Headers, deadline: deadline, cancellationToken: cancellationToken); - } - catch (RpcException ex) - { - OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(this.Endpoint, ex); + /// + public override ExportClientResponse SendExportRequest(OtlpCollector.ExportLogsServiceRequest request, CancellationToken cancellationToken = default) + { + var deadline = DateTime.UtcNow.AddMilliseconds(this.TimeoutMilliseconds); + + try + { + this.logsClient.Export(request, headers: this.Headers, deadline: deadline, cancellationToken: cancellationToken); - return false; - } + // We do not need to return back response and deadline for successful response so using cached value. + return SuccessExportResponse; + } + catch (RpcException ex) + { + OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(this.Endpoint, ex); - return true; + return new ExportClientGrpcResponse(success: false, deadlineUtc: deadline, exception: ex); } } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcMetricsExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcMetricsExportClient.cs index c4b51f04643..666413c874f 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcMetricsExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcMetricsExportClient.cs @@ -1,60 +1,47 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Grpc.Core; using OtlpCollector = OpenTelemetry.Proto.Collector.Metrics.V1; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; + +/// Class for sending OTLP metrics export request over gRPC. +internal sealed class OtlpGrpcMetricsExportClient : BaseOtlpGrpcExportClient { - /// Class for sending OTLP metrics export request over gRPC. - internal sealed class OtlpGrpcMetricsExportClient : BaseOtlpGrpcExportClient - { - private readonly OtlpCollector.MetricsService.MetricsServiceClient metricsClient; + private readonly OtlpCollector.MetricsService.MetricsServiceClient metricsClient; - public OtlpGrpcMetricsExportClient(OtlpExporterOptions options, OtlpCollector.MetricsService.MetricsServiceClient metricsServiceClient = null) - : base(options) + public OtlpGrpcMetricsExportClient(OtlpExporterOptions options, OtlpCollector.MetricsService.MetricsServiceClient metricsServiceClient = null) + : base(options) + { + if (metricsServiceClient != null) { - if (metricsServiceClient != null) - { - this.metricsClient = metricsServiceClient; - } - else - { - this.Channel = options.CreateChannel(); - this.metricsClient = new OtlpCollector.MetricsService.MetricsServiceClient(this.Channel); - } + this.metricsClient = metricsServiceClient; } - - /// - public override bool SendExportRequest(OtlpCollector.ExportMetricsServiceRequest request, CancellationToken cancellationToken = default) + else { - var deadline = DateTime.UtcNow.AddMilliseconds(this.TimeoutMilliseconds); + this.Channel = options.CreateChannel(); + this.metricsClient = new OtlpCollector.MetricsService.MetricsServiceClient(this.Channel); + } + } - try - { - this.metricsClient.Export(request, headers: this.Headers, deadline: deadline, cancellationToken: cancellationToken); - } - catch (RpcException ex) - { - OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(this.Endpoint, ex); + /// + public override ExportClientResponse SendExportRequest(OtlpCollector.ExportMetricsServiceRequest request, CancellationToken cancellationToken = default) + { + var deadline = DateTime.UtcNow.AddMilliseconds(this.TimeoutMilliseconds); + + try + { + this.metricsClient.Export(request, headers: this.Headers, deadline: deadline, cancellationToken: cancellationToken); - return false; - } + // We do not need to return back response and deadline for successful response so using cached value. + return SuccessExportResponse; + } + catch (RpcException ex) + { + OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(this.Endpoint, ex); - return true; + return new ExportClientGrpcResponse(success: false, deadlineUtc: deadline, exception: ex); } } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcTraceExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcTraceExportClient.cs index b4c9084cd2c..6918189f5c7 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcTraceExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcTraceExportClient.cs @@ -1,60 +1,47 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Grpc.Core; using OtlpCollector = OpenTelemetry.Proto.Collector.Trace.V1; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; + +/// Class for sending OTLP trace export request over gRPC. +internal sealed class OtlpGrpcTraceExportClient : BaseOtlpGrpcExportClient { - /// Class for sending OTLP trace export request over gRPC. - internal sealed class OtlpGrpcTraceExportClient : BaseOtlpGrpcExportClient - { - private readonly OtlpCollector.TraceService.TraceServiceClient traceClient; + private readonly OtlpCollector.TraceService.TraceServiceClient traceClient; - public OtlpGrpcTraceExportClient(OtlpExporterOptions options, OtlpCollector.TraceService.TraceServiceClient traceServiceClient = null) - : base(options) + public OtlpGrpcTraceExportClient(OtlpExporterOptions options, OtlpCollector.TraceService.TraceServiceClient traceServiceClient = null) + : base(options) + { + if (traceServiceClient != null) { - if (traceServiceClient != null) - { - this.traceClient = traceServiceClient; - } - else - { - this.Channel = options.CreateChannel(); - this.traceClient = new OtlpCollector.TraceService.TraceServiceClient(this.Channel); - } + this.traceClient = traceServiceClient; } - - /// - public override bool SendExportRequest(OtlpCollector.ExportTraceServiceRequest request, CancellationToken cancellationToken = default) + else { - var deadline = DateTime.UtcNow.AddMilliseconds(this.TimeoutMilliseconds); + this.Channel = options.CreateChannel(); + this.traceClient = new OtlpCollector.TraceService.TraceServiceClient(this.Channel); + } + } - try - { - this.traceClient.Export(request, headers: this.Headers, deadline: deadline, cancellationToken: cancellationToken); - } - catch (RpcException ex) - { - OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(this.Endpoint, ex); + /// + public override ExportClientResponse SendExportRequest(OtlpCollector.ExportTraceServiceRequest request, CancellationToken cancellationToken = default) + { + var deadline = DateTime.UtcNow.AddMilliseconds(this.TimeoutMilliseconds); + + try + { + this.traceClient.Export(request, headers: this.Headers, deadline: deadline, cancellationToken: cancellationToken); - return false; - } + // We do not need to return back response and deadline for successful response so using cached value. + return SuccessExportResponse; + } + catch (RpcException ex) + { + OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(this.Endpoint, ex); - return true; + return new ExportClientGrpcResponse(success: false, deadlineUtc: deadline, exception: ex); } } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpLogExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpLogExportClient.cs index 69bc9f9e6d8..ff872c1073a 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpLogExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpLogExportClient.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Net; #if NETFRAMEWORK @@ -23,61 +10,60 @@ using Google.Protobuf; using OtlpCollector = OpenTelemetry.Proto.Collector.Logs.V1; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; + +/// Class for sending OTLP log export request over HTTP. +internal sealed class OtlpHttpLogExportClient : BaseOtlpHttpExportClient { - /// Class for sending OTLP log export request over HTTP. - internal sealed class OtlpHttpLogExportClient : BaseOtlpHttpExportClient - { - internal const string MediaContentType = "application/x-protobuf"; - private const string LogsExportPath = "v1/logs"; + internal const string MediaContentType = "application/x-protobuf"; + private const string LogsExportPath = "v1/logs"; - public OtlpHttpLogExportClient(OtlpExporterOptions options, HttpClient httpClient) - : base(options, httpClient, LogsExportPath) - { - } + public OtlpHttpLogExportClient(OtlpExporterOptions options, HttpClient httpClient) + : base(options, httpClient, LogsExportPath) + { + } - protected override HttpContent CreateHttpContent(OtlpCollector.ExportLogsServiceRequest exportRequest) - { - return new ExportRequestContent(exportRequest); - } + protected override HttpContent CreateHttpContent(OtlpCollector.ExportLogsServiceRequest exportRequest) + { + return new ExportRequestContent(exportRequest); + } - internal sealed class ExportRequestContent : HttpContent - { - private static readonly MediaTypeHeaderValue ProtobufMediaTypeHeader = new(MediaContentType); + internal sealed class ExportRequestContent : HttpContent + { + private static readonly MediaTypeHeaderValue ProtobufMediaTypeHeader = new(MediaContentType); - private readonly OtlpCollector.ExportLogsServiceRequest exportRequest; + private readonly OtlpCollector.ExportLogsServiceRequest exportRequest; - public ExportRequestContent(OtlpCollector.ExportLogsServiceRequest exportRequest) - { - this.exportRequest = exportRequest; - this.Headers.ContentType = ProtobufMediaTypeHeader; - } + public ExportRequestContent(OtlpCollector.ExportLogsServiceRequest exportRequest) + { + this.exportRequest = exportRequest; + this.Headers.ContentType = ProtobufMediaTypeHeader; + } #if NET6_0_OR_GREATER - protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken) - { - this.SerializeToStreamInternal(stream); - } + protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken) + { + this.SerializeToStreamInternal(stream); + } #endif - protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) - { - this.SerializeToStreamInternal(stream); - return Task.CompletedTask; - } + protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + this.SerializeToStreamInternal(stream); + return Task.CompletedTask; + } - protected override bool TryComputeLength(out long length) - { - // We can't know the length of the content being pushed to the output stream. - length = -1; - return false; - } + protected override bool TryComputeLength(out long length) + { + // We can't know the length of the content being pushed to the output stream. + length = -1; + return false; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SerializeToStreamInternal(Stream stream) - { - this.exportRequest.WriteTo(stream); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SerializeToStreamInternal(Stream stream) + { + this.exportRequest.WriteTo(stream); } } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpMetricsExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpMetricsExportClient.cs index 7d96818f928..1a893ddea2f 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpMetricsExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpMetricsExportClient.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Net; #if NETFRAMEWORK @@ -23,61 +10,60 @@ using Google.Protobuf; using OtlpCollector = OpenTelemetry.Proto.Collector.Metrics.V1; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; + +/// Class for sending OTLP metrics export request over HTTP. +internal sealed class OtlpHttpMetricsExportClient : BaseOtlpHttpExportClient { - /// Class for sending OTLP metrics export request over HTTP. - internal sealed class OtlpHttpMetricsExportClient : BaseOtlpHttpExportClient - { - internal const string MediaContentType = "application/x-protobuf"; - private const string MetricsExportPath = "v1/metrics"; + internal const string MediaContentType = "application/x-protobuf"; + private const string MetricsExportPath = "v1/metrics"; - public OtlpHttpMetricsExportClient(OtlpExporterOptions options, HttpClient httpClient) - : base(options, httpClient, MetricsExportPath) - { - } + public OtlpHttpMetricsExportClient(OtlpExporterOptions options, HttpClient httpClient) + : base(options, httpClient, MetricsExportPath) + { + } - protected override HttpContent CreateHttpContent(OtlpCollector.ExportMetricsServiceRequest exportRequest) - { - return new ExportRequestContent(exportRequest); - } + protected override HttpContent CreateHttpContent(OtlpCollector.ExportMetricsServiceRequest exportRequest) + { + return new ExportRequestContent(exportRequest); + } - internal sealed class ExportRequestContent : HttpContent - { - private static readonly MediaTypeHeaderValue ProtobufMediaTypeHeader = new(MediaContentType); + internal sealed class ExportRequestContent : HttpContent + { + private static readonly MediaTypeHeaderValue ProtobufMediaTypeHeader = new(MediaContentType); - private readonly OtlpCollector.ExportMetricsServiceRequest exportRequest; + private readonly OtlpCollector.ExportMetricsServiceRequest exportRequest; - public ExportRequestContent(OtlpCollector.ExportMetricsServiceRequest exportRequest) - { - this.exportRequest = exportRequest; - this.Headers.ContentType = ProtobufMediaTypeHeader; - } + public ExportRequestContent(OtlpCollector.ExportMetricsServiceRequest exportRequest) + { + this.exportRequest = exportRequest; + this.Headers.ContentType = ProtobufMediaTypeHeader; + } #if NET6_0_OR_GREATER - protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken) - { - this.SerializeToStreamInternal(stream); - } + protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken) + { + this.SerializeToStreamInternal(stream); + } #endif - protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) - { - this.SerializeToStreamInternal(stream); - return Task.CompletedTask; - } + protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + this.SerializeToStreamInternal(stream); + return Task.CompletedTask; + } - protected override bool TryComputeLength(out long length) - { - // We can't know the length of the content being pushed to the output stream. - length = -1; - return false; - } + protected override bool TryComputeLength(out long length) + { + // We can't know the length of the content being pushed to the output stream. + length = -1; + return false; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SerializeToStreamInternal(Stream stream) - { - this.exportRequest.WriteTo(stream); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SerializeToStreamInternal(Stream stream) + { + this.exportRequest.WriteTo(stream); } } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpTraceExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpTraceExportClient.cs index b8b110a3b3c..7e49fac8cf6 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpTraceExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpTraceExportClient.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Net; #if NETFRAMEWORK @@ -23,61 +10,60 @@ using Google.Protobuf; using OtlpCollector = OpenTelemetry.Proto.Collector.Trace.V1; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; + +/// Class for sending OTLP trace export request over HTTP. +internal sealed class OtlpHttpTraceExportClient : BaseOtlpHttpExportClient { - /// Class for sending OTLP trace export request over HTTP. - internal sealed class OtlpHttpTraceExportClient : BaseOtlpHttpExportClient - { - internal const string MediaContentType = "application/x-protobuf"; - private const string TracesExportPath = "v1/traces"; + internal const string MediaContentType = "application/x-protobuf"; + private const string TracesExportPath = "v1/traces"; - public OtlpHttpTraceExportClient(OtlpExporterOptions options, HttpClient httpClient) - : base(options, httpClient, TracesExportPath) - { - } + public OtlpHttpTraceExportClient(OtlpExporterOptions options, HttpClient httpClient) + : base(options, httpClient, TracesExportPath) + { + } - protected override HttpContent CreateHttpContent(OtlpCollector.ExportTraceServiceRequest exportRequest) - { - return new ExportRequestContent(exportRequest); - } + protected override HttpContent CreateHttpContent(OtlpCollector.ExportTraceServiceRequest exportRequest) + { + return new ExportRequestContent(exportRequest); + } - internal sealed class ExportRequestContent : HttpContent - { - private static readonly MediaTypeHeaderValue ProtobufMediaTypeHeader = new(MediaContentType); + internal sealed class ExportRequestContent : HttpContent + { + private static readonly MediaTypeHeaderValue ProtobufMediaTypeHeader = new(MediaContentType); - private readonly OtlpCollector.ExportTraceServiceRequest exportRequest; + private readonly OtlpCollector.ExportTraceServiceRequest exportRequest; - public ExportRequestContent(OtlpCollector.ExportTraceServiceRequest exportRequest) - { - this.exportRequest = exportRequest; - this.Headers.ContentType = ProtobufMediaTypeHeader; - } + public ExportRequestContent(OtlpCollector.ExportTraceServiceRequest exportRequest) + { + this.exportRequest = exportRequest; + this.Headers.ContentType = ProtobufMediaTypeHeader; + } #if NET6_0_OR_GREATER - protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken) - { - this.SerializeToStreamInternal(stream); - } + protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken) + { + this.SerializeToStreamInternal(stream); + } #endif - protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) - { - this.SerializeToStreamInternal(stream); - return Task.CompletedTask; - } + protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + this.SerializeToStreamInternal(stream); + return Task.CompletedTask; + } - protected override bool TryComputeLength(out long length) - { - // We can't know the length of the content being pushed to the output stream. - length = -1; - return false; - } + protected override bool TryComputeLength(out long length) + { + // We can't know the length of the content being pushed to the output stream. + length = -1; + return false; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SerializeToStreamInternal(Stream stream) - { - this.exportRequest.WriteTo(stream); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SerializeToStreamInternal(Stream stream) + { + this.exportRequest.WriteTo(stream); } } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpRetry.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpRetry.cs new file mode 100644 index 00000000000..e1db5e5007b --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpRetry.cs @@ -0,0 +1,238 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using System.Net; +using System.Net.Http.Headers; +using Google.Rpc; +using Grpc.Core; +using Status = Google.Rpc.Status; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; + +/// +/// Implementation of the OTLP retry policy used by both OTLP/gRPC and OTLP/HTTP. +/// +/// OTLP/gRPC +/// https://github.com/open-telemetry/opentelemetry-proto/blob/main/docs/specification.md#failures +/// +/// OTLP/HTTP +/// https://github.com/open-telemetry/opentelemetry-proto/blob/main/docs/specification.md#failures-1 +/// +/// The specification requires retries use an exponential backoff strategy, +/// but does not provide specifics for the implementation. As such, this +/// implementation is inspired by the retry strategy provided by +/// Grpc.Net.Client which implements the gRPC retry specification. +/// +/// Grpc.Net.Client retry implementation +/// https://github.com/grpc/grpc-dotnet/blob/83d12ea1cb628156c990243bc98699829b88738b/src/Grpc.Net.Client/Internal/Retry/RetryCall.cs#L94 +/// +/// gRPC retry specification +/// https://github.com/grpc/proposal/blob/master/A6-client-retries.md +/// +/// The gRPC retry specification outlines configurable parameters used in its +/// exponential backoff strategy: initial backoff, max backoff, backoff +/// multiplier, and max retry attempts. The OTLP specification does not declare +/// a similar set of parameters, so this implementation uses fixed settings. +/// Furthermore, since the OTLP spec does not specify a max number of attempts, +/// this implementation will retry until the deadline is reached. +/// +/// The throttling mechanism for OTLP differs from the throttling mechanism +/// described in the gRPC retry specification. See: +/// https://github.com/open-telemetry/opentelemetry-proto/blob/main/docs/specification.md#otlpgrpc-throttling. +/// +internal static class OtlpRetry +{ + public const string GrpcStatusDetailsHeader = "grpc-status-details-bin"; + public const int InitialBackoffMilliseconds = 1000; + private const int MaxBackoffMilliseconds = 5000; + private const double BackoffMultiplier = 1.5; + +#if !NET6_0_OR_GREATER + private static readonly Random Random = new Random(); +#endif + + public static bool TryGetHttpRetryResult(HttpStatusCode statusCode, DateTime? deadline, HttpResponseHeaders responseHeaders, int retryDelayMilliseconds, out RetryResult retryResult) + { + return TryGetRetryResult(statusCode, IsHttpStatusCodeRetryable, deadline, responseHeaders, TryGetHttpRetryDelay, retryDelayMilliseconds, out retryResult); + } + + public static bool TryGetGrpcRetryResult(StatusCode statusCode, DateTime? deadline, Metadata trailers, int retryDelayMilliseconds, out RetryResult retryResult) + { + return TryGetRetryResult(statusCode, IsGrpcStatusCodeRetryable, deadline, trailers, TryGetGrpcRetryDelay, retryDelayMilliseconds, out retryResult); + } + + private static bool TryGetRetryResult(TStatusCode statusCode, Func isRetryable, DateTime? deadline, TCarrier carrier, Func throttleGetter, int nextRetryDelayMilliseconds, out RetryResult retryResult) + { + retryResult = default; + + // TODO: Consider introducing a fixed max number of retries (e.g. max 5 retries). + // The spec does not specify a max number of retries, but it may be bad to not cap the number of attempts. + // Without a max number of retry attempts, retries would continue until the deadline. + // Maybe this is ok? However, it may lead to an unexpected behavioral change. For example: + // 1) When using a batch processor, a longer delay due to repeated + // retries up to the deadline may lead to a higher chance that the queue will be exhausted. + // 2) When using the simple processor, a longer delay due to repeated + // retries up to the deadline will lead to a prolonged blocking call. + // if (attemptCount >= MaxAttempts) + // { + // return false + // } + + if (IsDeadlineExceeded(deadline)) + { + return false; + } + + var throttleDelay = throttleGetter(statusCode, carrier); + var retryable = isRetryable(statusCode, throttleDelay.HasValue); + if (!retryable) + { + return false; + } + + var delayDuration = throttleDelay.HasValue + ? throttleDelay.Value + : TimeSpan.FromMilliseconds(GetRandomNumber(0, nextRetryDelayMilliseconds)); + + if (deadline.HasValue && IsDeadlineExceeded(deadline + delayDuration)) + { + return false; + } + + if (throttleDelay.HasValue) + { + try + { + // TODO: Consider making nextRetryDelayMilliseconds a double to avoid the need for convert/overflow handling + nextRetryDelayMilliseconds = Convert.ToInt32(throttleDelay.Value.TotalMilliseconds); + } + catch (OverflowException) + { + nextRetryDelayMilliseconds = MaxBackoffMilliseconds; + } + } + + nextRetryDelayMilliseconds = CalculateNextRetryDelay(nextRetryDelayMilliseconds); + retryResult = new RetryResult(throttleDelay.HasValue, delayDuration, nextRetryDelayMilliseconds); + return true; + } + + private static bool IsDeadlineExceeded(DateTime? deadline) + { + // This implementation is internal, and it is guaranteed that deadline is UTC. + return deadline.HasValue && deadline <= DateTime.UtcNow; + } + + private static int CalculateNextRetryDelay(int nextRetryDelayMilliseconds) + { + var nextMilliseconds = nextRetryDelayMilliseconds * BackoffMultiplier; + nextMilliseconds = Math.Min(nextMilliseconds, MaxBackoffMilliseconds); + return Convert.ToInt32(nextMilliseconds); + } + + private static TimeSpan? TryGetGrpcRetryDelay(StatusCode statusCode, Metadata trailers) + { + Debug.Assert(trailers != null, "trailers was null"); + + if (statusCode != StatusCode.ResourceExhausted && statusCode != StatusCode.Unavailable) + { + return null; + } + + var statusDetails = trailers.Get(GrpcStatusDetailsHeader); + if (statusDetails != null && statusDetails.IsBinary) + { + var status = Status.Parser.ParseFrom(statusDetails.ValueBytes); + foreach (var item in status.Details) + { + var success = item.TryUnpack(out var retryInfo); + if (success) + { + return retryInfo.RetryDelay.ToTimeSpan(); + } + } + } + + return null; + } + + private static TimeSpan? TryGetHttpRetryDelay(HttpStatusCode statusCode, HttpResponseHeaders headers) + { + Debug.Assert(headers != null, "headers was null"); + +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + return statusCode == HttpStatusCode.TooManyRequests || statusCode == HttpStatusCode.ServiceUnavailable +#else + return statusCode == (HttpStatusCode)429 || statusCode == HttpStatusCode.ServiceUnavailable +#endif + ? headers.RetryAfter?.Delta + : null; + } + + private static bool IsGrpcStatusCodeRetryable(StatusCode statusCode, bool hasRetryDelay) + { + switch (statusCode) + { + case StatusCode.Cancelled: + case StatusCode.DeadlineExceeded: + case StatusCode.Aborted: + case StatusCode.OutOfRange: + case StatusCode.Unavailable: + case StatusCode.DataLoss: + return true; + case StatusCode.ResourceExhausted: + return hasRetryDelay; + default: + return false; + } + } + +#pragma warning disable SA1313 // Parameter should begin with lower-case letter + private static bool IsHttpStatusCodeRetryable(HttpStatusCode statusCode, bool _) +#pragma warning restore SA1313 // Parameter should begin with lower-case letter + { + switch (statusCode) + { +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + case HttpStatusCode.TooManyRequests: +#else + case (HttpStatusCode)429: +#endif + case HttpStatusCode.BadGateway: + case HttpStatusCode.ServiceUnavailable: + case HttpStatusCode.GatewayTimeout: + return true; + default: + return false; + } + } + + private static int GetRandomNumber(int min, int max) + { +#if NET6_0_OR_GREATER + return Random.Shared.Next(min, max); +#else + // TODO: Implement this better to minimize lock contention. + // Consider pulling in Random.Shared implementation. + lock (Random) + { + return Random.Next(min, max); + } +#endif + } + + public readonly struct RetryResult + { + public readonly bool Throttled; + public readonly TimeSpan RetryDelay; + public readonly int NextRetryDelayMilliseconds; + + public RetryResult(bool throttled, TimeSpan retryDelay, int nextRetryDelayMilliseconds) + { + this.Throttled = throttled; + this.RetryDelay = retryDelay; + this.NextRetryDelayMilliseconds = nextRetryDelayMilliseconds; + } + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/LogRecordExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/LogRecordExtensions.cs deleted file mode 100644 index c827a2cd0e9..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/LogRecordExtensions.cs +++ /dev/null @@ -1,241 +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.Runtime.CompilerServices; -using Google.Protobuf; -using OpenTelemetry.Internal; -using OpenTelemetry.Logs; -using OpenTelemetry.Trace; -using OtlpCollector = OpenTelemetry.Proto.Collector.Logs.V1; -using OtlpCommon = OpenTelemetry.Proto.Common.V1; -using OtlpLogs = OpenTelemetry.Proto.Logs.V1; -using OtlpResource = OpenTelemetry.Proto.Resource.V1; - -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation -{ - internal static class LogRecordExtensions - { - internal static void AddBatch( - this OtlpCollector.ExportLogsServiceRequest request, - SdkLimitOptions sdkLimitOptions, - OtlpResource.Resource processResource, - in Batch logRecordBatch) - { - var resourceLogs = new OtlpLogs.ResourceLogs - { - Resource = processResource, - }; - request.ResourceLogs.Add(resourceLogs); - - var scopeLogs = new OtlpLogs.ScopeLogs(); - resourceLogs.ScopeLogs.Add(scopeLogs); - - foreach (var logRecord in logRecordBatch) - { - var otlpLogRecord = logRecord.ToOtlpLog(sdkLimitOptions); - if (otlpLogRecord != null) - { - scopeLogs.LogRecords.Add(otlpLogRecord); - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static OtlpLogs.LogRecord ToOtlpLog(this LogRecord logRecord, SdkLimitOptions sdkLimitOptions) - { - OtlpLogs.LogRecord otlpLogRecord = null; - - try - { - var timestamp = (ulong)logRecord.Timestamp.ToUnixTimeNanoseconds(); - otlpLogRecord = new OtlpLogs.LogRecord - { - TimeUnixNano = timestamp, - ObservedTimeUnixNano = timestamp, - SeverityNumber = GetSeverityNumber(logRecord.Severity), - }; - - if (!string.IsNullOrWhiteSpace(logRecord.SeverityText)) - { - otlpLogRecord.SeverityText = logRecord.SeverityText; - } - else if (logRecord.Severity.HasValue) - { - otlpLogRecord.SeverityText = logRecord.Severity.Value.ToShortName(); - } - - var attributeValueLengthLimit = sdkLimitOptions.AttributeValueLengthLimit; - var attributeCountLimit = sdkLimitOptions.AttributeCountLimit ?? int.MaxValue; - - // First add the generic attributes like Category, EventId and Exception, - // so they are less likely being dropped because of AttributeCountLimit. - - if (!string.IsNullOrEmpty(logRecord.CategoryName)) - { - // TODO: - // 1. Track the following issue, and map CategoryName to Name - // if it makes it to log data model. - // https://github.com/open-telemetry/opentelemetry-specification/issues/2398 - // 2. Confirm if this name for attribute is good. - otlpLogRecord.AddStringAttribute("dotnet.ilogger.category", logRecord.CategoryName, attributeValueLengthLimit, attributeCountLimit); - } - - if (logRecord.EventId.Id != default) - { - otlpLogRecord.AddIntAttribute(nameof(logRecord.EventId.Id), logRecord.EventId.Id, attributeCountLimit); - } - - if (!string.IsNullOrEmpty(logRecord.EventId.Name)) - { - otlpLogRecord.AddStringAttribute(nameof(logRecord.EventId.Name), logRecord.EventId.Name, attributeValueLengthLimit, attributeCountLimit); - } - - if (logRecord.Exception != null) - { - otlpLogRecord.AddStringAttribute(SemanticConventions.AttributeExceptionType, logRecord.Exception.GetType().Name, attributeValueLengthLimit, attributeCountLimit); - otlpLogRecord.AddStringAttribute(SemanticConventions.AttributeExceptionMessage, logRecord.Exception.Message, attributeValueLengthLimit, attributeCountLimit); - otlpLogRecord.AddStringAttribute(SemanticConventions.AttributeExceptionStacktrace, logRecord.Exception.ToInvariantString(), attributeValueLengthLimit, attributeCountLimit); - } - - bool bodyPopulatedFromFormattedMessage = false; - if (logRecord.FormattedMessage != null) - { - otlpLogRecord.Body = new OtlpCommon.AnyValue { StringValue = logRecord.FormattedMessage }; - bodyPopulatedFromFormattedMessage = true; - } - - if (logRecord.Attributes != null) - { - foreach (var attribute in logRecord.Attributes) - { - // Special casing {OriginalFormat} - // See https://github.com/open-telemetry/opentelemetry-dotnet/pull/3182 - // for explanation. - if (attribute.Key.Equals("{OriginalFormat}") && !bodyPopulatedFromFormattedMessage) - { - otlpLogRecord.Body = new OtlpCommon.AnyValue { StringValue = attribute.Value as string }; - } - else if (OtlpKeyValueTransformer.Instance.TryTransformTag(attribute, out var result, attributeValueLengthLimit)) - { - otlpLogRecord.AddAttribute(result, attributeCountLimit); - } - } - } - - if (logRecord.TraceId != default && logRecord.SpanId != default) - { - byte[] traceIdBytes = new byte[16]; - byte[] spanIdBytes = new byte[8]; - - logRecord.TraceId.CopyTo(traceIdBytes); - logRecord.SpanId.CopyTo(spanIdBytes); - - otlpLogRecord.TraceId = UnsafeByteOperations.UnsafeWrap(traceIdBytes); - otlpLogRecord.SpanId = UnsafeByteOperations.UnsafeWrap(spanIdBytes); - otlpLogRecord.Flags = (uint)logRecord.TraceFlags; - } - - logRecord.ForEachScope(ProcessScope, otlpLogRecord); - - void ProcessScope(LogRecordScope scope, OtlpLogs.LogRecord otlpLog) - { - foreach (var scopeItem in scope) - { - if (scopeItem.Key.Equals("{OriginalFormat}") || string.IsNullOrEmpty(scopeItem.Key)) - { - // Ignore if the scope key is empty. - // Ignore if the scope key is {OriginalFormat} - // Attributes should not contain duplicates, - // and it is expensive to de-dup, so this - // exporter is going to pass the scope items as is. - // {OriginalFormat} is going to be the key - // if one uses formatted string for scopes - // and if there are nested scopes, this is - // guaranteed to create duplicate keys. - // Similar for empty keys, which is what the - // key is going to be if user simply - // passes a string as scope. - // To summarize this exporter only allows - // IReadOnlyList> - // or IEnumerable>. - // and expect users to provide unique keys. - // Note: It is possible that we allow users - // to override this exporter feature. So not blocking - // empty/{OriginalFormat} in the SDK itself. - } - else - { - if (OtlpKeyValueTransformer.Instance.TryTransformTag(scopeItem, out var result, attributeValueLengthLimit)) - { - otlpLog.AddAttribute(result, attributeCountLimit); - } - } - } - } - } - catch (Exception ex) - { - OpenTelemetryProtocolExporterEventSource.Log.CouldNotTranslateLogRecord(ex.Message); - } - - return otlpLogRecord; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void AddAttribute(this OtlpLogs.LogRecord logRecord, OtlpCommon.KeyValue attribute, int maxAttributeCount) - { - if (logRecord.Attributes.Count < maxAttributeCount) - { - logRecord.Attributes.Add(attribute); - } - else - { - logRecord.DroppedAttributesCount++; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void AddStringAttribute(this OtlpLogs.LogRecord logRecord, string key, string value, int? maxValueLength, int maxAttributeCount) - { - var attributeItem = new KeyValuePair(key, value); - if (OtlpKeyValueTransformer.Instance.TryTransformTag(attributeItem, out var result, maxValueLength)) - { - logRecord.AddAttribute(result, maxAttributeCount); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void AddIntAttribute(this OtlpLogs.LogRecord logRecord, string key, int value, int maxAttributeCount) - { - var attributeItem = new KeyValuePair(key, value); - if (OtlpKeyValueTransformer.Instance.TryTransformTag(attributeItem, out var result)) - { - logRecord.AddAttribute(result, maxAttributeCount); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static OtlpLogs.SeverityNumber GetSeverityNumber(LogRecordSeverity? severity) - { - if (!severity.HasValue) - { - return OtlpLogs.SeverityNumber.Unspecified; - } - - return (OtlpLogs.SeverityNumber)(int)severity.Value; - } - } -} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs index 5ac61d2e068..fb9266cb613 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs @@ -1,443 +1,449 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Collections.Concurrent; -using System.Reflection; -using System.Reflection.Emit; +using System.Diagnostics; using System.Runtime.CompilerServices; using Google.Protobuf; using Google.Protobuf.Collections; using OpenTelemetry.Metrics; +using OpenTelemetry.Proto.Metrics.V1; +using AggregationTemporality = OpenTelemetry.Metrics.AggregationTemporality; +using Metric = OpenTelemetry.Metrics.Metric; using OtlpCollector = OpenTelemetry.Proto.Collector.Metrics.V1; using OtlpCommon = OpenTelemetry.Proto.Common.V1; using OtlpMetrics = OpenTelemetry.Proto.Metrics.V1; using OtlpResource = OpenTelemetry.Proto.Resource.V1; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; + +internal static class MetricItemExtensions { - internal static class MetricItemExtensions + private static readonly ConcurrentBag MetricListPool = new(); + + internal static void AddMetrics( + this OtlpCollector.ExportMetricsServiceRequest request, + OtlpResource.Resource processResource, + in Batch metrics) { - private static readonly ConcurrentBag MetricListPool = new(); - private static readonly Action, int> RepeatedFieldOfMetricSetCountAction = CreateRepeatedFieldOfMetricSetCountAction(); + var metricsByLibrary = new Dictionary(); + var resourceMetrics = new ResourceMetrics + { + Resource = processResource, + }; + request.ResourceMetrics.Add(resourceMetrics); - internal static void AddMetrics( - this OtlpCollector.ExportMetricsServiceRequest request, - OtlpResource.Resource processResource, - in Batch metrics) + foreach (var metric in metrics) { - var metricsByLibrary = new Dictionary(); - var resourceMetrics = new OtlpMetrics.ResourceMetrics + var otlpMetric = metric.ToOtlpMetric(); + + // TODO: Replace null check with exception handling. + if (otlpMetric == null) { - Resource = processResource, - }; - request.ResourceMetrics.Add(resourceMetrics); + OpenTelemetryProtocolExporterEventSource.Log.CouldNotTranslateMetric( + nameof(MetricItemExtensions), + nameof(AddMetrics)); + continue; + } - foreach (var metric in metrics) + var meterName = metric.MeterName; + if (!metricsByLibrary.TryGetValue(meterName, out var scopeMetrics)) { - var otlpMetric = metric.ToOtlpMetric(); + scopeMetrics = GetMetricListFromPool(meterName, metric.MeterVersion, metric.MeterTags); - // TODO: Replace null check with exception handling. - if (otlpMetric == null) - { - OpenTelemetryProtocolExporterEventSource.Log.CouldNotTranslateMetric( - nameof(MetricItemExtensions), - nameof(AddMetrics)); - continue; - } + metricsByLibrary.Add(meterName, scopeMetrics); + resourceMetrics.ScopeMetrics.Add(scopeMetrics); + } - var meterName = metric.MeterName; - if (!metricsByLibrary.TryGetValue(meterName, out var scopeMetrics)) - { - scopeMetrics = GetMetricListFromPool(meterName, metric.MeterVersion); + scopeMetrics.Metrics.Add(otlpMetric); + } + } - metricsByLibrary.Add(meterName, scopeMetrics); - resourceMetrics.ScopeMetrics.Add(scopeMetrics); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Return(this OtlpCollector.ExportMetricsServiceRequest request) + { + var resourceMetrics = request.ResourceMetrics.FirstOrDefault(); + if (resourceMetrics == null) + { + return; + } - scopeMetrics.Metrics.Add(otlpMetric); - } + foreach (var scopeMetrics in resourceMetrics.ScopeMetrics) + { + scopeMetrics.Metrics.Clear(); + scopeMetrics.Scope.Attributes.Clear(); + MetricListPool.Add(scopeMetrics); } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void Return(this OtlpCollector.ExportMetricsServiceRequest request) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ScopeMetrics GetMetricListFromPool(string name, string version, IEnumerable> meterTags) + { + if (!MetricListPool.TryTake(out var scopeMetrics)) { - var resourceMetrics = request.ResourceMetrics.FirstOrDefault(); - if (resourceMetrics == null) + scopeMetrics = new ScopeMetrics { - return; - } + Scope = new OtlpCommon.InstrumentationScope + { + Name = name, // Name is enforced to not be null, but it can be empty. + Version = version ?? string.Empty, // NRE throw by proto + }, + }; - foreach (var scope in resourceMetrics.ScopeMetrics) + if (meterTags != null) { - RepeatedFieldOfMetricSetCountAction(scope.Metrics, 0); - MetricListPool.Add(scope); + AddScopeAttributes(meterTags, scopeMetrics.Scope.Attributes); } } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static OtlpMetrics.ScopeMetrics GetMetricListFromPool(string name, string version) + else { - if (!MetricListPool.TryTake(out var metrics)) - { - metrics = new OtlpMetrics.ScopeMetrics - { - Scope = new OtlpCommon.InstrumentationScope - { - Name = name, // Name is enforced to not be null, but it can be empty. - Version = version ?? string.Empty, // NRE throw by proto - }, - }; - } - else + scopeMetrics.Scope.Name = name; + scopeMetrics.Scope.Version = version ?? string.Empty; + if (meterTags != null) { - metrics.Scope.Name = name; - metrics.Scope.Version = version ?? string.Empty; + AddScopeAttributes(meterTags, scopeMetrics.Scope.Attributes); } - - return metrics; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static OtlpMetrics.Metric ToOtlpMetric(this Metric metric) + return scopeMetrics; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static OtlpMetrics.Metric ToOtlpMetric(this Metric metric) + { + var otlpMetric = new OtlpMetrics.Metric { - var otlpMetric = new OtlpMetrics.Metric - { - Name = metric.Name, - }; + Name = metric.Name, + }; - if (metric.Description != null) - { - otlpMetric.Description = metric.Description; - } + if (metric.Description != null) + { + otlpMetric.Description = metric.Description; + } - if (metric.Unit != null) - { - otlpMetric.Unit = metric.Unit; - } + if (metric.Unit != null) + { + otlpMetric.Unit = metric.Unit; + } - OtlpMetrics.AggregationTemporality temporality; - if (metric.Temporality == AggregationTemporality.Delta) - { - temporality = OtlpMetrics.AggregationTemporality.Delta; - } - else - { - temporality = OtlpMetrics.AggregationTemporality.Cumulative; - } + OtlpMetrics.AggregationTemporality temporality; + if (metric.Temporality == AggregationTemporality.Delta) + { + temporality = OtlpMetrics.AggregationTemporality.Delta; + } + else + { + temporality = OtlpMetrics.AggregationTemporality.Cumulative; + } - switch (metric.MetricType) - { - case MetricType.LongSum: - case MetricType.LongSumNonMonotonic: + switch (metric.MetricType) + { + case MetricType.LongSum: + case MetricType.LongSumNonMonotonic: + { + var sum = new Sum + { + IsMonotonic = metric.MetricType == MetricType.LongSum, + AggregationTemporality = temporality, + }; + + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) { - var sum = new OtlpMetrics.Sum + var dataPoint = new NumberDataPoint { - IsMonotonic = metric.MetricType == MetricType.LongSum, - AggregationTemporality = temporality, + StartTimeUnixNano = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(), + TimeUnixNano = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(), }; - foreach (ref readonly var metricPoint in metric.GetMetricPoints()) - { - var dataPoint = new OtlpMetrics.NumberDataPoint - { - StartTimeUnixNano = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(), - TimeUnixNano = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(), - }; + AddAttributes(metricPoint.Tags, dataPoint.Attributes); - AddAttributes(metricPoint.Tags, dataPoint.Attributes); + dataPoint.AsInt = metricPoint.GetSumLong(); - dataPoint.AsInt = metricPoint.GetSumLong(); - sum.DataPoints.Add(dataPoint); + if (metricPoint.TryGetExemplars(out var exemplars)) + { + foreach (ref readonly var exemplar in exemplars) + { + dataPoint.Exemplars.Add( + ToOtlpExemplar(exemplar.LongValue, in exemplar)); + } } - otlpMetric.Sum = sum; - break; + sum.DataPoints.Add(dataPoint); } - case MetricType.DoubleSum: - case MetricType.DoubleSumNonMonotonic: + otlpMetric.Sum = sum; + break; + } + + case MetricType.DoubleSum: + case MetricType.DoubleSumNonMonotonic: + { + var sum = new Sum + { + IsMonotonic = metric.MetricType == MetricType.DoubleSum, + AggregationTemporality = temporality, + }; + + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) { - var sum = new OtlpMetrics.Sum + var dataPoint = new NumberDataPoint { - IsMonotonic = metric.MetricType == MetricType.DoubleSum, - AggregationTemporality = temporality, + StartTimeUnixNano = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(), + TimeUnixNano = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(), }; - foreach (ref readonly var metricPoint in metric.GetMetricPoints()) - { - var dataPoint = new OtlpMetrics.NumberDataPoint - { - StartTimeUnixNano = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(), - TimeUnixNano = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(), - }; + AddAttributes(metricPoint.Tags, dataPoint.Attributes); - AddAttributes(metricPoint.Tags, dataPoint.Attributes); + dataPoint.AsDouble = metricPoint.GetSumDouble(); - dataPoint.AsDouble = metricPoint.GetSumDouble(); - sum.DataPoints.Add(dataPoint); + if (metricPoint.TryGetExemplars(out var exemplars)) + { + foreach (ref readonly var exemplar in exemplars) + { + dataPoint.Exemplars.Add( + ToOtlpExemplar(exemplar.DoubleValue, in exemplar)); + } } - otlpMetric.Sum = sum; - break; + sum.DataPoints.Add(dataPoint); } - case MetricType.LongGauge: + otlpMetric.Sum = sum; + break; + } + + case MetricType.LongGauge: + { + var gauge = new Gauge(); + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) { - var gauge = new OtlpMetrics.Gauge(); - foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + var dataPoint = new NumberDataPoint { - var dataPoint = new OtlpMetrics.NumberDataPoint - { - StartTimeUnixNano = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(), - TimeUnixNano = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(), - }; + StartTimeUnixNano = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(), + TimeUnixNano = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(), + }; - AddAttributes(metricPoint.Tags, dataPoint.Attributes); + AddAttributes(metricPoint.Tags, dataPoint.Attributes); - dataPoint.AsInt = metricPoint.GetGaugeLastValueLong(); - gauge.DataPoints.Add(dataPoint); + dataPoint.AsInt = metricPoint.GetGaugeLastValueLong(); + + if (metricPoint.TryGetExemplars(out var exemplars)) + { + foreach (ref readonly var exemplar in exemplars) + { + dataPoint.Exemplars.Add( + ToOtlpExemplar(exemplar.LongValue, in exemplar)); + } } - otlpMetric.Gauge = gauge; - break; + gauge.DataPoints.Add(dataPoint); } - case MetricType.DoubleGauge: + otlpMetric.Gauge = gauge; + break; + } + + case MetricType.DoubleGauge: + { + var gauge = new Gauge(); + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) { - var gauge = new OtlpMetrics.Gauge(); - foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + var dataPoint = new NumberDataPoint { - var dataPoint = new OtlpMetrics.NumberDataPoint - { - StartTimeUnixNano = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(), - TimeUnixNano = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(), - }; + StartTimeUnixNano = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(), + TimeUnixNano = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(), + }; + + AddAttributes(metricPoint.Tags, dataPoint.Attributes); - AddAttributes(metricPoint.Tags, dataPoint.Attributes); + dataPoint.AsDouble = metricPoint.GetGaugeLastValueDouble(); - dataPoint.AsDouble = metricPoint.GetGaugeLastValueDouble(); - gauge.DataPoints.Add(dataPoint); + if (metricPoint.TryGetExemplars(out var exemplars)) + { + foreach (ref readonly var exemplar in exemplars) + { + dataPoint.Exemplars.Add( + ToOtlpExemplar(exemplar.DoubleValue, in exemplar)); + } } - otlpMetric.Gauge = gauge; - break; + gauge.DataPoints.Add(dataPoint); } - case MetricType.Histogram: + otlpMetric.Gauge = gauge; + break; + } + + case MetricType.Histogram: + { + var histogram = new Histogram { - var histogram = new OtlpMetrics.Histogram - { - AggregationTemporality = temporality, - }; + AggregationTemporality = temporality, + }; - foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + { + var dataPoint = new HistogramDataPoint { - var dataPoint = new OtlpMetrics.HistogramDataPoint - { - StartTimeUnixNano = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(), - TimeUnixNano = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(), - }; + StartTimeUnixNano = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(), + TimeUnixNano = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(), + }; - AddAttributes(metricPoint.Tags, dataPoint.Attributes); - dataPoint.Count = (ulong)metricPoint.GetHistogramCount(); - dataPoint.Sum = metricPoint.GetHistogramSum(); + AddAttributes(metricPoint.Tags, dataPoint.Attributes); + dataPoint.Count = (ulong)metricPoint.GetHistogramCount(); + dataPoint.Sum = metricPoint.GetHistogramSum(); - if (metricPoint.TryGetHistogramMinMaxValues(out double min, out double max)) - { - dataPoint.Min = min; - dataPoint.Max = max; - } + if (metricPoint.TryGetHistogramMinMaxValues(out double min, out double max)) + { + dataPoint.Min = min; + dataPoint.Max = max; + } - foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets()) + foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets()) + { + dataPoint.BucketCounts.Add((ulong)histogramMeasurement.BucketCount); + if (histogramMeasurement.ExplicitBound != double.PositiveInfinity) { - dataPoint.BucketCounts.Add((ulong)histogramMeasurement.BucketCount); - if (histogramMeasurement.ExplicitBound != double.PositiveInfinity) - { - dataPoint.ExplicitBounds.Add(histogramMeasurement.ExplicitBound); - } + dataPoint.ExplicitBounds.Add(histogramMeasurement.ExplicitBound); } + } - var exemplars = metricPoint.GetExemplars(); - foreach (var examplar in exemplars) + if (metricPoint.TryGetExemplars(out var exemplars)) + { + foreach (ref readonly var exemplar in exemplars) { - if (examplar.Timestamp != default) - { - byte[] traceIdBytes = new byte[16]; - examplar.TraceId?.CopyTo(traceIdBytes); - - byte[] spanIdBytes = new byte[8]; - examplar.SpanId?.CopyTo(spanIdBytes); - - var otlpExemplar = new OtlpMetrics.Exemplar - { - TimeUnixNano = (ulong)examplar.Timestamp.ToUnixTimeNanoseconds(), - TraceId = UnsafeByteOperations.UnsafeWrap(traceIdBytes), - SpanId = UnsafeByteOperations.UnsafeWrap(spanIdBytes), - AsDouble = examplar.DoubleValue, - }; - - if (examplar.FilteredTags != null) - { - foreach (var tag in examplar.FilteredTags) - { - if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var result)) - { - otlpExemplar.FilteredAttributes.Add(result); - } - } - } - - dataPoint.Exemplars.Add(otlpExemplar); - } + dataPoint.Exemplars.Add( + ToOtlpExemplar(exemplar.DoubleValue, in exemplar)); } - - histogram.DataPoints.Add(dataPoint); } - otlpMetric.Histogram = histogram; - break; + histogram.DataPoints.Add(dataPoint); } - case MetricType.ExponentialHistogram: + otlpMetric.Histogram = histogram; + break; + } + + case MetricType.ExponentialHistogram: + { + var histogram = new ExponentialHistogram { - var histogram = new OtlpMetrics.ExponentialHistogram + AggregationTemporality = temporality, + }; + + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + { + var dataPoint = new ExponentialHistogramDataPoint { - AggregationTemporality = temporality, + StartTimeUnixNano = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(), + TimeUnixNano = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(), }; - foreach (ref readonly var metricPoint in metric.GetMetricPoints()) - { - var dataPoint = new OtlpMetrics.ExponentialHistogramDataPoint - { - StartTimeUnixNano = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(), - TimeUnixNano = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(), - }; + AddAttributes(metricPoint.Tags, dataPoint.Attributes); + dataPoint.Count = (ulong)metricPoint.GetHistogramCount(); + dataPoint.Sum = metricPoint.GetHistogramSum(); - AddAttributes(metricPoint.Tags, dataPoint.Attributes); - dataPoint.Count = (ulong)metricPoint.GetHistogramCount(); - dataPoint.Sum = metricPoint.GetHistogramSum(); + if (metricPoint.TryGetHistogramMinMaxValues(out double min, out double max)) + { + dataPoint.Min = min; + dataPoint.Max = max; + } - if (metricPoint.TryGetHistogramMinMaxValues(out double min, out double max)) - { - dataPoint.Min = min; - dataPoint.Max = max; - } + var exponentialHistogramData = metricPoint.GetExponentialHistogramData(); + dataPoint.Scale = exponentialHistogramData.Scale; + dataPoint.ZeroCount = (ulong)exponentialHistogramData.ZeroCount; - var exponentialHistogramData = metricPoint.GetExponentialHistogramData(); - dataPoint.Scale = exponentialHistogramData.Scale; - dataPoint.ZeroCount = (ulong)exponentialHistogramData.ZeroCount; + dataPoint.Positive = new ExponentialHistogramDataPoint.Types.Buckets(); + dataPoint.Positive.Offset = exponentialHistogramData.PositiveBuckets.Offset; + foreach (var bucketCount in exponentialHistogramData.PositiveBuckets) + { + dataPoint.Positive.BucketCounts.Add((ulong)bucketCount); + } - dataPoint.Positive = new OtlpMetrics.ExponentialHistogramDataPoint.Types.Buckets(); - dataPoint.Positive.Offset = exponentialHistogramData.PositiveBuckets.Offset; - foreach (var bucketCount in exponentialHistogramData.PositiveBuckets) + if (metricPoint.TryGetExemplars(out var exemplars)) + { + foreach (ref readonly var exemplar in exemplars) { - dataPoint.Positive.BucketCounts.Add((ulong)bucketCount); + dataPoint.Exemplars.Add( + ToOtlpExemplar(exemplar.DoubleValue, in exemplar)); } - - // TODO: exemplars. - - histogram.DataPoints.Add(dataPoint); } - otlpMetric.ExponentialHistogram = histogram; - break; + histogram.DataPoints.Add(dataPoint); } - } - return otlpMetric; + otlpMetric.ExponentialHistogram = histogram; + break; + } } - private static void AddAttributes(ReadOnlyTagCollection tags, RepeatedField attributes) + return otlpMetric; + } + + internal static OtlpMetrics.Exemplar ToOtlpExemplar(T value, in Metrics.Exemplar exemplar) + where T : struct + { + var otlpExemplar = new OtlpMetrics.Exemplar { - foreach (var tag in tags) - { - if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var result)) - { - attributes.Add(result); - } - } - } + TimeUnixNano = (ulong)exemplar.Timestamp.ToUnixTimeNanoseconds(), + }; - /* - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static OtlpMetrics.Exemplar ToOtlpExemplar(this IExemplar exemplar) + if (exemplar.TraceId != default) { - var otlpExemplar = new OtlpMetrics.Exemplar(); + byte[] traceIdBytes = new byte[16]; + exemplar.TraceId.CopyTo(traceIdBytes); - if (exemplar.Value is double doubleValue) - { - otlpExemplar.AsDouble = doubleValue; - } - else if (exemplar.Value is long longValue) - { - otlpExemplar.AsInt = longValue; - } - else - { - // TODO: Determine how we want to handle exceptions here. - // Do we want to just skip this exemplar and move on? - // Should we skip recording the whole metric? - throw new ArgumentException(); - } + byte[] spanIdBytes = new byte[8]; + exemplar.SpanId.CopyTo(spanIdBytes); - otlpExemplar.TimeUnixNano = (ulong)exemplar.Timestamp.ToUnixTimeNanoseconds(); + otlpExemplar.TraceId = UnsafeByteOperations.UnsafeWrap(traceIdBytes); + otlpExemplar.SpanId = UnsafeByteOperations.UnsafeWrap(spanIdBytes); + } - // TODO: Do the TagEnumerationState thing. - foreach (var tag in exemplar.FilteredTags) - { - otlpExemplar.FilteredAttributes.Add(tag.ToOtlpAttribute()); - } + if (typeof(T) == typeof(long)) + { + otlpExemplar.AsInt = (long)(object)value; + } + else if (typeof(T) == typeof(double)) + { + otlpExemplar.AsDouble = (double)(object)value; + } + else + { + Debug.Fail("Unexpected type"); + otlpExemplar.AsDouble = Convert.ToDouble(value); + } - if (exemplar.TraceId != default) + foreach (var tag in exemplar.FilteredTags) + { + if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var result)) { - byte[] traceIdBytes = new byte[16]; - exemplar.TraceId.CopyTo(traceIdBytes); - otlpExemplar.TraceId = UnsafeByteOperations.UnsafeWrap(traceIdBytes); + otlpExemplar.FilteredAttributes.Add(result); } + } - if (exemplar.SpanId != default) + return otlpExemplar; + } + + private static void AddAttributes(ReadOnlyTagCollection tags, RepeatedField attributes) + { + foreach (var tag in tags) + { + if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var result)) { - byte[] spanIdBytes = new byte[8]; - exemplar.SpanId.CopyTo(spanIdBytes); - otlpExemplar.SpanId = UnsafeByteOperations.UnsafeWrap(spanIdBytes); + attributes.Add(result); } - - return otlpExemplar; } - */ + } - private static Action, int> CreateRepeatedFieldOfMetricSetCountAction() + private static void AddScopeAttributes(IEnumerable> meterTags, RepeatedField attributes) + { + foreach (var tag in meterTags) { - FieldInfo repeatedFieldOfMetricCountField = typeof(RepeatedField).GetField("count", BindingFlags.NonPublic | BindingFlags.Instance); - - DynamicMethod dynamicMethod = new DynamicMethod( - "CreateSetCountAction", - null, - new[] { typeof(RepeatedField), typeof(int) }, - typeof(MetricItemExtensions).Module, - skipVisibility: true); - - var generator = dynamicMethod.GetILGenerator(); - - generator.Emit(OpCodes.Ldarg_0); - generator.Emit(OpCodes.Ldarg_1); - generator.Emit(OpCodes.Stfld, repeatedFieldOfMetricCountField); - generator.Emit(OpCodes.Ret); - - return (Action, int>)dynamicMethod.CreateDelegate(typeof(Action, int>)); + if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var result)) + { + attributes.Add(result); + } } } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs index 8bab415b3e8..97fe4dcb80b 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs @@ -1,94 +1,95 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Tracing; using OpenTelemetry.Internal; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; + +[EventSource(Name = "OpenTelemetry-Exporter-OpenTelemetryProtocol")] +internal sealed class OpenTelemetryProtocolExporterEventSource : EventSource { - [EventSource(Name = "OpenTelemetry-Exporter-OpenTelemetryProtocol")] - internal sealed class OpenTelemetryProtocolExporterEventSource : EventSource - { - public static readonly OpenTelemetryProtocolExporterEventSource Log = new(); + public static readonly OpenTelemetryProtocolExporterEventSource Log = new(); - [NonEvent] - public void FailedToReachCollector(Uri collectorUri, Exception ex) + [NonEvent] + public void FailedToReachCollector(Uri collectorUri, Exception ex) + { + if (Log.IsEnabled(EventLevel.Error, EventKeywords.All)) { - if (Log.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - var rawCollectorUri = collectorUri.ToString(); - this.FailedToReachCollector(rawCollectorUri, ex.ToInvariantString()); - } + var rawCollectorUri = collectorUri.ToString(); + this.FailedToReachCollector(rawCollectorUri, ex.ToInvariantString()); } + } - [NonEvent] - public void ExportMethodException(Exception ex) + [NonEvent] + public void ExportMethodException(Exception ex, bool isRetry = false) + { + if (Log.IsEnabled(EventLevel.Error, EventKeywords.All)) { - if (Log.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.ExportMethodException(ex.ToInvariantString()); - } + this.ExportMethodException(ex.ToInvariantString(), isRetry); } + } - [Event(2, Message = "Exporter failed send data to collector to {0} endpoint. Data will not be sent. Exception: {1}", Level = EventLevel.Error)] - public void FailedToReachCollector(string rawCollectorUri, string ex) + [NonEvent] + public void TrySubmitRequestException(Exception ex) + { + if (Log.IsEnabled(EventLevel.Error, EventKeywords.All)) { - this.WriteEvent(2, rawCollectorUri, ex); + this.TrySubmitRequestException(ex.ToInvariantString()); } + } - [Event(3, Message = "Could not translate activity from class '{0}' and method '{1}', span will not be recorded.", Level = EventLevel.Informational)] - public void CouldNotTranslateActivity(string className, string methodName) - { - this.WriteEvent(3, className, methodName); - } + [Event(2, Message = "Exporter failed send data to collector to {0} endpoint. Data will not be sent. Exception: {1}", Level = EventLevel.Error)] + public void FailedToReachCollector(string rawCollectorUri, string ex) + { + this.WriteEvent(2, rawCollectorUri, ex); + } - [Event(4, Message = "Unknown error in export method: {0}", Level = EventLevel.Error)] - public void ExportMethodException(string ex) - { - this.WriteEvent(4, ex); - } + [Event(3, Message = "Could not translate activity from class '{0}' and method '{1}', span will not be recorded.", Level = EventLevel.Informational)] + public void CouldNotTranslateActivity(string className, string methodName) + { + this.WriteEvent(3, className, methodName); + } - [Event(5, Message = "Could not translate metric from class '{0}' and method '{1}', metric will not be recorded.", Level = EventLevel.Informational)] - public void CouldNotTranslateMetric(string className, string methodName) - { - this.WriteEvent(5, className, methodName); - } + [Event(4, Message = "Unknown error in export method. Message: '{0}'. IsRetry: {1}", Level = EventLevel.Error)] + public void ExportMethodException(string ex, bool isRetry) + { + this.WriteEvent(4, ex, isRetry); + } - [Event(8, Message = "Unsupported value for protocol '{0}' is configured, default protocol 'grpc' will be used.", Level = EventLevel.Warning)] - public void UnsupportedProtocol(string protocol) - { - this.WriteEvent(8, protocol); - } + [Event(5, Message = "Could not translate metric from class '{0}' and method '{1}', metric will not be recorded.", Level = EventLevel.Informational)] + public void CouldNotTranslateMetric(string className, string methodName) + { + this.WriteEvent(5, className, methodName); + } - [Event(9, Message = "Could not translate LogRecord due to Exception: '{0}'. Log will not be exported.", Level = EventLevel.Warning)] - public void CouldNotTranslateLogRecord(string exceptionMessage) - { - this.WriteEvent(9, exceptionMessage); - } + [Event(8, Message = "Unsupported value for protocol '{0}' is configured, default protocol 'grpc' will be used.", Level = EventLevel.Warning)] + public void UnsupportedProtocol(string protocol) + { + this.WriteEvent(8, protocol); + } - [Event(10, Message = "Unsupported attribute type '{0}' for '{1}'. Attribute will not be exported.", Level = EventLevel.Warning)] - public void UnsupportedAttributeType(string type, string key) - { - this.WriteEvent(10, type.ToString(), key); - } + [Event(9, Message = "Could not translate LogRecord due to Exception: '{0}'. Log will not be exported.", Level = EventLevel.Warning)] + public void CouldNotTranslateLogRecord(string exceptionMessage) + { + this.WriteEvent(9, exceptionMessage); + } - [Event(11, Message = "{0} environment variable has an invalid value: '{1}'", Level = EventLevel.Warning)] - public void InvalidEnvironmentVariable(string key, string value) - { - this.WriteEvent(11, key, value); - } + [Event(10, Message = "Unsupported attribute type '{0}' for '{1}'. Attribute will not be exported.", Level = EventLevel.Warning)] + public void UnsupportedAttributeType(string type, string key) + { + this.WriteEvent(10, type.ToString(), key); + } + + [Event(11, Message = "{0} environment variable has an invalid value: '{1}'", Level = EventLevel.Warning)] + public void InvalidEnvironmentVariable(string key, string value) + { + this.WriteEvent(11, key, value); + } + + [Event(12, Message = "Unknown error in TrySubmitRequest method. Message: '{0}'", Level = EventLevel.Error)] + public void TrySubmitRequestException(string ex) + { + this.WriteEvent(12, ex); } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpKeyValueTransformer.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpKeyValueTransformer.cs index 038d76cf92b..3f82c37573e 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpKeyValueTransformer.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpKeyValueTransformer.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Internal; using OtlpCommon = OpenTelemetry.Proto.Common.V1; diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpLogRecordTransformer.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpLogRecordTransformer.cs new file mode 100644 index 00000000000..97be1c2d9f6 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpLogRecordTransformer.cs @@ -0,0 +1,286 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; +using Google.Protobuf; +using OpenTelemetry.Internal; +using OpenTelemetry.Logs; +using OpenTelemetry.Trace; +using OtlpCollector = OpenTelemetry.Proto.Collector.Logs.V1; +using OtlpCommon = OpenTelemetry.Proto.Common.V1; +using OtlpLogs = OpenTelemetry.Proto.Logs.V1; +using OtlpResource = OpenTelemetry.Proto.Resource.V1; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; + +internal sealed class OtlpLogRecordTransformer +{ + internal static readonly ConcurrentBag LogListPool = new(); + + private readonly SdkLimitOptions sdkLimitOptions; + private readonly ExperimentalOptions experimentalOptions; + + public OtlpLogRecordTransformer(SdkLimitOptions sdkLimitOptions, ExperimentalOptions experimentalOptions) + { + this.sdkLimitOptions = sdkLimitOptions; + this.experimentalOptions = experimentalOptions; + } + + internal OtlpCollector.ExportLogsServiceRequest BuildExportRequest( + OtlpResource.Resource processResource, + in Batch logRecordBatch) + { + // TODO: https://github.com/open-telemetry/opentelemetry-dotnet/issues/4943 + Dictionary logsByCategory = new Dictionary(); + + var request = new OtlpCollector.ExportLogsServiceRequest(); + + var resourceLogs = new OtlpLogs.ResourceLogs + { + Resource = processResource, + }; + request.ResourceLogs.Add(resourceLogs); + + foreach (var logRecord in logRecordBatch) + { + var otlpLogRecord = this.ToOtlpLog(logRecord); + if (otlpLogRecord != null) + { + var scopeName = logRecord.Logger.Name; + if (!logsByCategory.TryGetValue(scopeName, out var scopeLogs)) + { + scopeLogs = this.GetLogListFromPool(scopeName); + logsByCategory.Add(scopeName, scopeLogs); + resourceLogs.ScopeLogs.Add(scopeLogs); + } + + scopeLogs.LogRecords.Add(otlpLogRecord); + } + } + + return request; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void Return(OtlpCollector.ExportLogsServiceRequest request) + { + var resourceLogs = request.ResourceLogs.FirstOrDefault(); + if (resourceLogs == null) + { + return; + } + + foreach (var scope in resourceLogs.ScopeLogs) + { + scope.LogRecords.Clear(); + LogListPool.Add(scope); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal OtlpLogs.ScopeLogs GetLogListFromPool(string name) + { + if (!LogListPool.TryTake(out var logs)) + { + logs = new OtlpLogs.ScopeLogs + { + Scope = new OtlpCommon.InstrumentationScope + { + Name = name, // Name is enforced to not be null, but it can be empty. + Version = string.Empty, // proto requires this to be non-null. + }, + }; + } + else + { + logs.Scope.Name = name; + logs.Scope.Version = string.Empty; + } + + return logs; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal OtlpLogs.LogRecord ToOtlpLog(LogRecord logRecord) + { + OtlpLogs.LogRecord otlpLogRecord = null; + + try + { + var timestamp = (ulong)logRecord.Timestamp.ToUnixTimeNanoseconds(); + otlpLogRecord = new OtlpLogs.LogRecord + { + TimeUnixNano = timestamp, + ObservedTimeUnixNano = timestamp, + SeverityNumber = GetSeverityNumber(logRecord.Severity), + }; + + if (!string.IsNullOrWhiteSpace(logRecord.SeverityText)) + { + otlpLogRecord.SeverityText = logRecord.SeverityText; + } + else if (logRecord.Severity.HasValue) + { + otlpLogRecord.SeverityText = logRecord.Severity.Value.ToShortName(); + } + + var attributeValueLengthLimit = this.sdkLimitOptions.LogRecordAttributeValueLengthLimit; + var attributeCountLimit = this.sdkLimitOptions.LogRecordAttributeCountLimit ?? int.MaxValue; + + if (this.experimentalOptions.EmitLogEventAttributes) + { + if (logRecord.EventId.Id != default) + { + AddIntAttribute(otlpLogRecord, ExperimentalOptions.LogRecordEventIdAttribute, logRecord.EventId.Id, attributeCountLimit); + } + + if (!string.IsNullOrEmpty(logRecord.EventId.Name)) + { + AddStringAttribute(otlpLogRecord, ExperimentalOptions.LogRecordEventNameAttribute, logRecord.EventId.Name, attributeValueLengthLimit, attributeCountLimit); + } + } + + if (logRecord.Exception != null) + { + AddStringAttribute(otlpLogRecord, SemanticConventions.AttributeExceptionType, logRecord.Exception.GetType().Name, attributeValueLengthLimit, attributeCountLimit); + AddStringAttribute(otlpLogRecord, SemanticConventions.AttributeExceptionMessage, logRecord.Exception.Message, attributeValueLengthLimit, attributeCountLimit); + AddStringAttribute(otlpLogRecord, SemanticConventions.AttributeExceptionStacktrace, logRecord.Exception.ToInvariantString(), attributeValueLengthLimit, attributeCountLimit); + } + + bool bodyPopulatedFromFormattedMessage = false; + if (logRecord.FormattedMessage != null) + { + otlpLogRecord.Body = new OtlpCommon.AnyValue { StringValue = logRecord.FormattedMessage }; + bodyPopulatedFromFormattedMessage = true; + } + + if (logRecord.Attributes != null) + { + foreach (var attribute in logRecord.Attributes) + { + // Special casing {OriginalFormat} + // See https://github.com/open-telemetry/opentelemetry-dotnet/pull/3182 + // for explanation. + if (attribute.Key.Equals("{OriginalFormat}") && !bodyPopulatedFromFormattedMessage) + { + otlpLogRecord.Body = new OtlpCommon.AnyValue { StringValue = attribute.Value as string }; + } + else if (OtlpKeyValueTransformer.Instance.TryTransformTag(attribute, out var result, attributeValueLengthLimit)) + { + AddAttribute(otlpLogRecord, result, attributeCountLimit); + } + } + + // Supports setting Body directly on LogRecord for the Logs Bridge API. + if (otlpLogRecord.Body == null && logRecord.Body != null) + { + // If {OriginalFormat} is not present in the attributes, + // use logRecord.Body if it is set. + otlpLogRecord.Body = new OtlpCommon.AnyValue { StringValue = logRecord.Body }; + } + } + + if (logRecord.TraceId != default && logRecord.SpanId != default) + { + byte[] traceIdBytes = new byte[16]; + byte[] spanIdBytes = new byte[8]; + + logRecord.TraceId.CopyTo(traceIdBytes); + logRecord.SpanId.CopyTo(spanIdBytes); + + otlpLogRecord.TraceId = UnsafeByteOperations.UnsafeWrap(traceIdBytes); + otlpLogRecord.SpanId = UnsafeByteOperations.UnsafeWrap(spanIdBytes); + otlpLogRecord.Flags = (uint)logRecord.TraceFlags; + } + + logRecord.ForEachScope(ProcessScope, otlpLogRecord); + + void ProcessScope(LogRecordScope scope, OtlpLogs.LogRecord otlpLog) + { + foreach (var scopeItem in scope) + { + if (scopeItem.Key.Equals("{OriginalFormat}") || string.IsNullOrEmpty(scopeItem.Key)) + { + // Ignore if the scope key is empty. + // Ignore if the scope key is {OriginalFormat} + // Attributes should not contain duplicates, + // and it is expensive to de-dup, so this + // exporter is going to pass the scope items as is. + // {OriginalFormat} is going to be the key + // if one uses formatted string for scopes + // and if there are nested scopes, this is + // guaranteed to create duplicate keys. + // Similar for empty keys, which is what the + // key is going to be if user simply + // passes a string as scope. + // To summarize this exporter only allows + // IReadOnlyList> + // or IEnumerable>. + // and expect users to provide unique keys. + // Note: It is possible that we allow users + // to override this exporter feature. So not blocking + // empty/{OriginalFormat} in the SDK itself. + } + else + { + if (OtlpKeyValueTransformer.Instance.TryTransformTag(scopeItem, out var result, attributeValueLengthLimit)) + { + AddAttribute(otlpLog, result, attributeCountLimit); + } + } + } + } + } + catch (Exception ex) + { + OpenTelemetryProtocolExporterEventSource.Log.CouldNotTranslateLogRecord(ex.Message); + } + + return otlpLogRecord; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AddAttribute(OtlpLogs.LogRecord logRecord, OtlpCommon.KeyValue attribute, int maxAttributeCount) + { + if (logRecord.Attributes.Count < maxAttributeCount) + { + logRecord.Attributes.Add(attribute); + } + else + { + logRecord.DroppedAttributesCount++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AddStringAttribute(OtlpLogs.LogRecord logRecord, string key, string value, int? maxValueLength, int maxAttributeCount) + { + var attributeItem = new KeyValuePair(key, value); + if (OtlpKeyValueTransformer.Instance.TryTransformTag(attributeItem, out var result, maxValueLength)) + { + AddAttribute(logRecord, result, maxAttributeCount); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AddIntAttribute(OtlpLogs.LogRecord logRecord, string key, int value, int maxAttributeCount) + { + var attributeItem = new KeyValuePair(key, value); + if (OtlpKeyValueTransformer.Instance.TryTransformTag(attributeItem, out var result)) + { + AddAttribute(logRecord, result, maxAttributeCount); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static OtlpLogs.SeverityNumber GetSeverityNumber(LogRecordSeverity? severity) + { + if (!severity.HasValue) + { + return OtlpLogs.SeverityNumber.Unspecified; + } + + return (OtlpLogs.SeverityNumber)(int)severity.Value; + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ResourceExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ResourceExtensions.cs index 9417ee3bbcd..77c01c368ae 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ResourceExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ResourceExtensions.cs @@ -1,51 +1,37 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Resources; using OtlpCommon = OpenTelemetry.Proto.Common.V1; using OtlpResource = OpenTelemetry.Proto.Resource.V1; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; + +internal static class ResourceExtensions { - internal static class ResourceExtensions + public static OtlpResource.Resource ToOtlpResource(this Resource resource) { - public static OtlpResource.Resource ToOtlpResource(this Resource resource) - { - var processResource = new OtlpResource.Resource(); + var processResource = new OtlpResource.Resource(); - foreach (KeyValuePair attribute in resource.Attributes) + foreach (KeyValuePair attribute in resource.Attributes) + { + if (OtlpKeyValueTransformer.Instance.TryTransformTag(attribute, out var result)) { - if (OtlpKeyValueTransformer.Instance.TryTransformTag(attribute, out var result)) - { - processResource.Attributes.Add(result); - } + processResource.Attributes.Add(result); } + } - if (!processResource.Attributes.Any(kvp => kvp.Key == ResourceSemanticConventions.AttributeServiceName)) + if (!processResource.Attributes.Any(kvp => kvp.Key == ResourceSemanticConventions.AttributeServiceName)) + { + var serviceName = (string)ResourceBuilder.CreateDefault().Build().Attributes.FirstOrDefault( + kvp => kvp.Key == ResourceSemanticConventions.AttributeServiceName).Value; + processResource.Attributes.Add(new OtlpCommon.KeyValue { - var serviceName = (string)ResourceBuilder.CreateDefault().Build().Attributes.FirstOrDefault( - kvp => kvp.Key == ResourceSemanticConventions.AttributeServiceName).Value; - processResource.Attributes.Add(new OtlpCommon.KeyValue - { - Key = ResourceSemanticConventions.AttributeServiceName, - Value = new OtlpCommon.AnyValue { StringValue = serviceName }, - }); - } - - return processResource; + Key = ResourceSemanticConventions.AttributeServiceName, + Value = new OtlpCommon.AnyValue { StringValue = serviceName }, + }); } + + return processResource; } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/SdkLimitOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/SdkLimitOptions.cs index 28503c1bdad..190335e5456 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/SdkLimitOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/SdkLimitOptions.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.Configuration; using OpenTelemetry.Internal; @@ -30,6 +17,10 @@ internal sealed class SdkLimitOptions private bool spanEventAttributeCountLimitSet; private int? spanLinkAttributeCountLimit; private bool spanLinkAttributeCountLimitSet; + private int? logRecordAttributeValueLengthLimit; + private bool logRecordAttributeValueLengthLimitSet; + private int? logRecordAttributeCountLimit; + private bool logRecordAttributeCountLimitSet; public SdkLimitOptions() : this(new ConfigurationBuilder().AddEnvironmentVariables().Build()) @@ -38,17 +29,21 @@ public SdkLimitOptions() internal SdkLimitOptions(IConfiguration configuration) { - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#attribute-limits + // https://github.com/open-telemetry/opentelemetry-specification/blob/v1.25.0/specification/configuration/sdk-environment-variables.md#attribute-limits SetIntConfigValue(configuration, "OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT", value => this.AttributeValueLengthLimit = value, null); SetIntConfigValue(configuration, "OTEL_ATTRIBUTE_COUNT_LIMIT", value => this.AttributeCountLimit = value, DefaultSdkLimit); - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#span-limits + // https://github.com/open-telemetry/opentelemetry-specification/blob/v1.25.0/specification/configuration/sdk-environment-variables.md#span-limits SetIntConfigValue(configuration, "OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT", value => this.SpanAttributeValueLengthLimit = value, null); SetIntConfigValue(configuration, "OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT", value => this.SpanAttributeCountLimit = value, null); SetIntConfigValue(configuration, "OTEL_SPAN_EVENT_COUNT_LIMIT", value => this.SpanEventCountLimit = value, DefaultSdkLimit); SetIntConfigValue(configuration, "OTEL_SPAN_LINK_COUNT_LIMIT", value => this.SpanLinkCountLimit = value, DefaultSdkLimit); SetIntConfigValue(configuration, "OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT", value => this.SpanEventAttributeCountLimit = value, null); SetIntConfigValue(configuration, "OTEL_LINK_ATTRIBUTE_COUNT_LIMIT", value => this.SpanLinkAttributeCountLimit = value, null); + + // https://github.com/open-telemetry/opentelemetry-specification/blob/v1.25.0/specification/configuration/sdk-environment-variables.md#logrecord-limits + SetIntConfigValue(configuration, "OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT", value => this.LogRecordAttributeValueLengthLimit = value, null); + SetIntConfigValue(configuration, "OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT", value => this.LogRecordAttributeCountLimit = value, null); } /// @@ -135,6 +130,38 @@ public int? SpanLinkAttributeCountLimit } } + /// + /// Gets or sets the maximum allowed log record attribute value size. + /// + /// + /// Note: Overrides the setting for log records if specified. + /// + public int? LogRecordAttributeValueLengthLimit + { + get => this.logRecordAttributeValueLengthLimitSet ? this.logRecordAttributeValueLengthLimit : this.AttributeValueLengthLimit; + set + { + this.logRecordAttributeValueLengthLimitSet = true; + this.logRecordAttributeValueLengthLimit = value; + } + } + + /// + /// Gets or sets the maximum allowed log record attribute count. + /// + /// + /// Note: Overrides the setting for log records if specified. + /// + public int? LogRecordAttributeCountLimit + { + get => this.logRecordAttributeCountLimitSet ? this.logRecordAttributeCountLimit : this.AttributeCountLimit; + set + { + this.logRecordAttributeCountLimitSet = true; + this.logRecordAttributeCountLimit = value; + } + } + private static void SetIntConfigValue(IConfiguration configuration, string key, Action setter, int? defaultValue) { if (configuration.TryGetIntValue(key, out var result)) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/TimestampHelpers.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/TimestampHelpers.cs index ab33dba63fb..515c1e6ba39 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/TimestampHelpers.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/TimestampHelpers.cs @@ -1,42 +1,28 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; + +/// +/// Helpers to convert .NET time related types to the timestamp used in OTLP. +/// +internal static class TimestampHelpers { - /// - /// Helpers to convert .NET time related types to the timestamp used in OTLP. - /// - internal static class TimestampHelpers - { - private const long NanosecondsPerTicks = 100; - private const long UnixEpochTicks = 621355968000000000; // = DateTimeOffset.FromUnixTimeMilliseconds(0).Ticks + private const long NanosecondsPerTicks = 100; + private const long UnixEpochTicks = 621355968000000000; // = DateTimeOffset.FromUnixTimeMilliseconds(0).Ticks - internal static long ToUnixTimeNanoseconds(this DateTime dt) - { - return (dt.Ticks - UnixEpochTicks) * NanosecondsPerTicks; - } + internal static long ToUnixTimeNanoseconds(this DateTime dt) + { + return (dt.Ticks - UnixEpochTicks) * NanosecondsPerTicks; + } - internal static long ToUnixTimeNanoseconds(this DateTimeOffset dto) - { - return (dto.Ticks - UnixEpochTicks) * NanosecondsPerTicks; - } + internal static long ToUnixTimeNanoseconds(this DateTimeOffset dto) + { + return (dto.Ticks - UnixEpochTicks) * NanosecondsPerTicks; + } - internal static long ToNanoseconds(this TimeSpan duration) - { - return duration.Ticks * NanosecondsPerTicks; - } + internal static long ToNanoseconds(this TimeSpan duration) + { + return duration.Ticks * NanosecondsPerTicks; } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterTransmissionHandler.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterTransmissionHandler.cs new file mode 100644 index 00000000000..71f4d5ebac1 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterTransmissionHandler.cs @@ -0,0 +1,120 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Diagnostics; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; + +internal class OtlpExporterTransmissionHandler +{ + public OtlpExporterTransmissionHandler(IExportClient exportClient) + { + Guard.ThrowIfNull(exportClient); + + this.ExportClient = exportClient; + } + + protected IExportClient ExportClient { get; } + + /// + /// Attempts to send an export request to the server. + /// + /// The request to send to the server. + /// if the request is sent successfully; otherwise, . + /// + public bool TrySubmitRequest(TRequest request) + { + try + { + var response = this.ExportClient.SendExportRequest(request); + if (response.Success) + { + return true; + } + + return this.OnSubmitRequestFailure(request, response); + } + catch (Exception ex) + { + OpenTelemetryProtocolExporterEventSource.Log.TrySubmitRequestException(ex); + return false; + } + } + + /// + /// Attempts to shutdown the transmission handler, blocks the current thread + /// until shutdown completed or timed out. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns if shutdown succeeded; otherwise, . + /// + public bool Shutdown(int timeoutMilliseconds) + { + Guard.ThrowIfInvalidTimeout(timeoutMilliseconds); + + var sw = timeoutMilliseconds == Timeout.Infinite ? null : Stopwatch.StartNew(); + + this.OnShutdown(timeoutMilliseconds); + + if (sw != null) + { + var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; + + return this.ExportClient.Shutdown((int)Math.Max(timeout, 0)); + } + + return this.ExportClient.Shutdown(timeoutMilliseconds); + } + + /// + /// Fired when the transmission handler is shutdown. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + protected virtual void OnShutdown(int timeoutMilliseconds) + { + } + + /// + /// Fired when a request could not be submitted. + /// + /// The request that was attempted to send to the server. + /// . + /// If the request is resubmitted and succeeds; otherwise, . + protected virtual bool OnSubmitRequestFailure(TRequest request, ExportClientResponse response) + { + return false; + } + + /// + /// Fired when resending a request to the server. + /// + /// The request to be resent to the server. + /// . + /// If the retry succeeds; otherwise, . + protected bool TryRetryRequest(TRequest request, out ExportClientResponse response) + { + response = this.ExportClient.SendExportRequest(request); + if (!response.Success) + { + OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(response.Exception, isRetry: true); + return false; + } + + return true; + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/google/rpc/error_details.proto b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/google/rpc/error_details.proto new file mode 100644 index 00000000000..0d5461a7287 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/google/rpc/error_details.proto @@ -0,0 +1,37 @@ +// Copyright 2022 Google LLC +// +// 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. + +syntax = "proto3"; + +package google.rpc; + +import "google/protobuf/duration.proto"; + +// Describes when the clients can retry a failed request. Clients could ignore +// the recommendation here or retry when this information is missing from error +// responses. +// +// It's always recommended that clients should use exponential backoff when +// retrying. +// +// Clients should wait until `retry_delay` amount of time has passed since +// receiving the error response before retrying. If retrying requests also +// fail, clients should use an exponential backoff scheme to gradually increase +// the delay between retries based on `retry_delay`, until either a maximum +// number of retries have been reached or a maximum retry delay cap has been +// reached. +message RetryInfo { + // Clients should wait at least this long between retrying the same request. + google.protobuf.Duration retry_delay = 1; +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/google/rpc/status.proto b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/google/rpc/status.proto new file mode 100644 index 00000000000..40c5bf318c2 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/google/rpc/status.proto @@ -0,0 +1,42 @@ +// Copyright 2022 Google LLC +// +// 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. + +syntax = "proto3"; + +package google.rpc; + +import "google/protobuf/any.proto"; + +// The `Status` type defines a logical error model that is suitable for +// different programming environments, including REST APIs and RPC APIs. It is +// used by [gRPC](https://github.com/grpc). Each `Status` message contains +// three pieces of data: error code, error message, and error details. +// +// You can find out more about this error model and how to work with it in the +// [API Design Guide](https://cloud.google.com/apis/design/errors). +message Status { + // The status code, which should be an enum value of + // [google.rpc.Code][google.rpc.Code]. + int32 code = 1; + + // A developer-facing error message, which should be in English. Any + // user-facing error message should be localized and sent in the + // [google.rpc.Status.details][google.rpc.Status.details] field, or localized + // by the client. + string message = 2; + + // A list of messages that carry the error details. There is a common set of + // message types for APIs to use. + repeated google.protobuf.Any details = 3; +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/logs/v1/logs.proto b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/logs/v1/logs.proto index 0b4b649729c..f9b97dd7451 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/logs/v1/logs.proto +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/logs/v1/logs.proto @@ -55,6 +55,9 @@ message ResourceLogs { // A list of ScopeLogs that originate from a resource. repeated ScopeLogs scope_logs = 2; + // The Schema URL, if known. This is the identifier of the Schema that the resource data + // is recorded in. To learn more about Schema URL see + // https://opentelemetry.io/docs/specs/otel/schemas/#schema-url // This schema_url applies to the data in the "resource" field. It does not apply // to the data in the "scope_logs" field which have their own schema_url field. string schema_url = 3; @@ -70,6 +73,9 @@ message ScopeLogs { // A list of log records. repeated LogRecord log_records = 2; + // The Schema URL, if known. This is the identifier of the Schema that the log data + // is recorded in. To learn more about Schema URL see + // https://opentelemetry.io/docs/specs/otel/schemas/#schema-url // This schema_url applies to all logs in the "logs" field. string schema_url = 3; } @@ -104,9 +110,11 @@ enum SeverityNumber { SEVERITY_NUMBER_FATAL4 = 24; } -// LogRecordFlags is defined as a protobuf 'uint32' type and is to be used as -// bit-fields. Each non-zero value defined in this enum is a bit-mask. -// To extract the bit-field, for example, use an expression like: +// LogRecordFlags represents constants used to interpret the +// LogRecord.flags field, which is protobuf 'fixed32' type and is to +// be used as bit-fields. Each non-zero value defined in this enum is +// a bit-mask. To extract the bit-field, for example, use an +// expression like: // // (logRecord.flags & LOG_RECORD_FLAGS_TRACE_FLAGS_MASK) // diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/metrics/v1/metrics.proto b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/metrics/v1/metrics.proto index da986dda185..3394aee9338 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/metrics/v1/metrics.proto +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/metrics/v1/metrics.proto @@ -55,6 +55,9 @@ message ResourceMetrics { // A list of metrics that originate from a resource. repeated ScopeMetrics scope_metrics = 2; + // The Schema URL, if known. This is the identifier of the Schema that the resource data + // is recorded in. To learn more about Schema URL see + // https://opentelemetry.io/docs/specs/otel/schemas/#schema-url // This schema_url applies to the data in the "resource" field. It does not apply // to the data in the "scope_metrics" field which have their own schema_url field. string schema_url = 3; @@ -70,6 +73,9 @@ message ScopeMetrics { // A list of metrics that originate from an instrumentation library. repeated Metric metrics = 2; + // The Schema URL, if known. This is the identifier of the Schema that the metric data + // is recorded in. To learn more about Schema URL see + // https://opentelemetry.io/docs/specs/otel/schemas/#schema-url // This schema_url applies to all metrics in the "metrics" field. string schema_url = 3; } @@ -162,7 +168,7 @@ message ScopeMetrics { message Metric { reserved 4, 6, 8; - // name of the metric, including its DNS name prefix. It must be unique. + // name of the metric. string name = 1; // description of the metric, which can be used in documentation. diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/trace/v1/trace.proto b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/trace/v1/trace.proto index b2869edc421..a1fdfa3ac97 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/trace/v1/trace.proto +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/trace/v1/trace.proto @@ -55,6 +55,9 @@ message ResourceSpans { // A list of ScopeSpans that originate from a resource. repeated ScopeSpans scope_spans = 2; + // The Schema URL, if known. This is the identifier of the Schema that the resource data + // is recorded in. To learn more about Schema URL see + // https://opentelemetry.io/docs/specs/otel/schemas/#schema-url // This schema_url applies to the data in the "resource" field. It does not apply // to the data in the "scope_spans" field which have their own schema_url field. string schema_url = 3; @@ -70,6 +73,9 @@ message ScopeSpans { // A list of Spans that originate from an instrumentation scope. repeated Span spans = 2; + // The Schema URL, if known. This is the identifier of the Schema that the span data + // is recorded in. To learn more about Schema URL see + // https://opentelemetry.io/docs/specs/otel/schemas/#schema-url // This schema_url applies to all spans and span events in the "spans" field. string schema_url = 3; } @@ -103,6 +109,22 @@ message Span { // field must be empty. The ID is an 8-byte array. bytes parent_span_id = 4; + // Flags, a bit field. 8 least significant bits are the trace + // flags as defined in W3C Trace Context specification. Readers + // MUST not assume that 24 most significant bits will be zero. + // To read the 8-bit W3C trace flag, use `flags & SPAN_FLAGS_TRACE_FLAGS_MASK`. + // + // When creating span messages, if the message is logically forwarded from another source + // with an equivalent flags fields (i.e., usually another OTLP span message), the field SHOULD + // be copied as-is. If creating from a source that does not have an equivalent flags field + // (such as a runtime representation of an OpenTelemetry span), the high 24 bits MUST + // be set to zero. + // + // [Optional]. + // + // See https://www.w3.org/TR/trace-context-2/#trace-flags for the flag definitions. + fixed32 flags = 16; + // A description of the span's operation. // // For example, the name can be a qualified method name or a file name @@ -236,6 +258,16 @@ message Span { // dropped_attributes_count is the number of dropped attributes. If the value is 0, // then no attributes were dropped. uint32 dropped_attributes_count = 5; + + // Flags, a bit field. 8 least significant bits are the trace + // flags as defined in W3C Trace Context specification. Readers + // MUST not assume that 24 most significant bits will be zero. + // When creating new spans, the most-significant 24-bits MUST be + // zero. To read the 8-bit W3C trace flag (use flags & + // SPAN_FLAGS_TRACE_FLAGS_MASK). [Optional]. + // + // See https://www.w3.org/TR/trace-context-2/#trace-flags for the flag definitions. + fixed32 flags = 6; } // links is a collection of Links, which are references from this span to a span @@ -274,3 +306,28 @@ message Status { // The status code. StatusCode code = 3; } + +// SpanFlags represents constants used to interpret the +// Span.flags field, which is protobuf 'fixed32' type and is to +// be used as bit-fields. Each non-zero value defined in this enum is +// a bit-mask. To extract the bit-field, for example, use an +// expression like: +// +// (span.flags & SPAN_FLAGS_TRACE_FLAGS_MASK) +// +// See https://www.w3.org/TR/trace-context-2/#trace-flags for the flag definitions. +// +// Note that Span flags were introduced in version 1.1 of the +// OpenTelemetry protocol. Older Span producers do not set this +// field, consequently consumers should not rely on the absence of a +// particular flag bit to indicate the presence of a particular feature. +enum SpanFlags { + // The zero value for the enum. Should not be used for comparisons. + // Instead use bitwise "and" with the appropriate mask as shown above. + SPAN_FLAGS_DO_NOT_USE = 0; + + // Bits 0-7 are used for trace flags. + SPAN_FLAGS_TRACE_FLAGS_MASK = 0x000000FF; + + // Bits 8-31 are reserved for future use. +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj index e2fd4d2703c..bed60120451 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj @@ -1,13 +1,13 @@ - - net6.0;netstandard2.1;netstandard2.0;net462 + $(TargetFrameworksForLibrariesExtended) OpenTelemetry protocol exporter for OpenTelemetry .NET $(PackageTags);OTLP core- disable + BUILDING_INTERNAL_PERSISTENT_STORAGE;$(DefineConstants) @@ -15,27 +15,34 @@ - - + + - - + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportProtocol.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportProtocol.cs index 5a830082a5b..10375e1dfc6 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportProtocol.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportProtocol.cs @@ -1,34 +1,20 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Exporter +namespace OpenTelemetry.Exporter; + +/// +/// Supported by OTLP exporter protocol types according to the specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md. +/// +public enum OtlpExportProtocol : byte { /// - /// Supported by OTLP exporter protocol types according to the specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md. + /// OTLP over gRPC (corresponds to 'grpc' Protocol configuration option). Used as default. /// - public enum OtlpExportProtocol : byte - { - /// - /// OTLP over gRPC (corresponds to 'grpc' Protocol configuration option). Used as default. - /// - Grpc = 0, + Grpc = 0, - /// - /// OTLP over HTTP with protobuf payloads (corresponds to 'http/protobuf' Protocol configuration option). - /// - HttpProtobuf = 1, - } + /// + /// OTLP over HTTP with protobuf payloads (corresponds to 'http/protobuf' Protocol configuration option). + /// + HttpProtobuf = 1, } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportProtocolParser.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportProtocolParser.cs index 23062ce458b..e5e788c5617 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportProtocolParser.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportProtocolParser.cs @@ -1,37 +1,23 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Exporter +namespace OpenTelemetry.Exporter; + +internal static class OtlpExportProtocolParser { - internal static class OtlpExportProtocolParser + public static bool TryParse(string value, out OtlpExportProtocol result) { - public static bool TryParse(string value, out OtlpExportProtocol result) + switch (value?.Trim()) { - switch (value?.Trim()) - { - case "grpc": - result = OtlpExportProtocol.Grpc; - return true; - case "http/protobuf": - result = OtlpExportProtocol.HttpProtobuf; - return true; - default: - result = default; - return false; - } + case "grpc": + result = OtlpExportProtocol.Grpc; + return true; + case "http/protobuf": + result = OtlpExportProtocol.HttpProtobuf; + return true; + default: + result = default; + return false; } } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index 1aed1cd6339..e4c820cf58a 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Reflection; @@ -26,195 +13,201 @@ using OpenTelemetry.Metrics; using OpenTelemetry.Trace; -namespace OpenTelemetry.Exporter +namespace OpenTelemetry.Exporter; + +/// +/// OpenTelemetry Protocol (OTLP) exporter options. +/// OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TIMEOUT, OTEL_EXPORTER_OTLP_PROTOCOL +/// environment variables are parsed during object construction. +/// +public class OtlpExporterOptions { - /// - /// OpenTelemetry Protocol (OTLP) exporter options. - /// OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TIMEOUT, OTEL_EXPORTER_OTLP_PROTOCOL - /// environment variables are parsed during object construction. - /// - public class OtlpExporterOptions + internal const string EndpointEnvVarName = "OTEL_EXPORTER_OTLP_ENDPOINT"; + internal const string HeadersEnvVarName = "OTEL_EXPORTER_OTLP_HEADERS"; + internal const string TimeoutEnvVarName = "OTEL_EXPORTER_OTLP_TIMEOUT"; + internal const string ProtocolEnvVarName = "OTEL_EXPORTER_OTLP_PROTOCOL"; + + internal static readonly KeyValuePair[] StandardHeaders = new KeyValuePair[] { - internal const string EndpointEnvVarName = "OTEL_EXPORTER_OTLP_ENDPOINT"; - internal const string HeadersEnvVarName = "OTEL_EXPORTER_OTLP_HEADERS"; - internal const string TimeoutEnvVarName = "OTEL_EXPORTER_OTLP_TIMEOUT"; - internal const string ProtocolEnvVarName = "OTEL_EXPORTER_OTLP_PROTOCOL"; + new KeyValuePair("User-Agent", GetUserAgentString()), + }; - internal static readonly KeyValuePair[] StandardHeaders = new KeyValuePair[] - { - new KeyValuePair("User-Agent", GetUserAgentString()), - }; + internal readonly Func DefaultHttpClientFactory; + + private const string DefaultGrpcEndpoint = "http://localhost:4317"; + private const string DefaultHttpEndpoint = "http://localhost:4318"; + private const OtlpExportProtocol DefaultOtlpExportProtocol = OtlpExportProtocol.Grpc; + private const string UserAgentProduct = "OTel-OTLP-Exporter-Dotnet"; - internal readonly Func DefaultHttpClientFactory; + private Uri endpoint; - private const string DefaultGrpcEndpoint = "http://localhost:4317"; - private const string DefaultHttpEndpoint = "http://localhost:4318"; - private const OtlpExportProtocol DefaultOtlpExportProtocol = OtlpExportProtocol.Grpc; - private const string UserAgentProduct = "OTel-OTLP-Exporter-Dotnet"; + /// + /// Initializes a new instance of the class. + /// + public OtlpExporterOptions() + : this(new ConfigurationBuilder().AddEnvironmentVariables().Build(), new()) + { + } - private Uri endpoint; + internal OtlpExporterOptions( + IConfiguration configuration, + BatchExportActivityProcessorOptions defaultBatchOptions) + { + Debug.Assert(configuration != null, "configuration was null"); + Debug.Assert(defaultBatchOptions != null, "defaultBatchOptions was null"); - /// - /// Initializes a new instance of the class. - /// - public OtlpExporterOptions() - : this(new ConfigurationBuilder().AddEnvironmentVariables().Build(), new()) + if (configuration.TryGetUriValue(EndpointEnvVarName, out var endpoint)) { + this.endpoint = endpoint; } - internal OtlpExporterOptions( - IConfiguration configuration, - BatchExportActivityProcessorOptions defaultBatchOptions) + if (configuration.TryGetStringValue(HeadersEnvVarName, out var headers)) { - Debug.Assert(configuration != null, "configuration was null"); - Debug.Assert(defaultBatchOptions != null, "defaultBatchOptions was null"); - - if (configuration.TryGetUriValue(EndpointEnvVarName, out var endpoint)) - { - this.endpoint = endpoint; - } - - if (configuration.TryGetStringValue(HeadersEnvVarName, out var headers)) - { - this.Headers = headers; - } + this.Headers = headers; + } - if (configuration.TryGetIntValue(TimeoutEnvVarName, out var timeout)) - { - this.TimeoutMilliseconds = timeout; - } + if (configuration.TryGetIntValue(TimeoutEnvVarName, out var timeout)) + { + this.TimeoutMilliseconds = timeout; + } - if (configuration.TryGetValue( - ProtocolEnvVarName, - OtlpExportProtocolParser.TryParse, - out var protocol)) - { - this.Protocol = protocol; - } + if (configuration.TryGetValue( + ProtocolEnvVarName, + OtlpExportProtocolParser.TryParse, + out var protocol)) + { + this.Protocol = protocol; + } - this.HttpClientFactory = this.DefaultHttpClientFactory = () => + this.HttpClientFactory = this.DefaultHttpClientFactory = () => + { + return new HttpClient { - return new HttpClient - { - Timeout = TimeSpan.FromMilliseconds(this.TimeoutMilliseconds), - }; + Timeout = TimeSpan.FromMilliseconds(this.TimeoutMilliseconds), }; + }; - this.BatchExportProcessorOptions = defaultBatchOptions; - } + this.BatchExportProcessorOptions = defaultBatchOptions; + } - /// - /// Gets or sets the target to which the exporter is going to send telemetry. - /// Must be a valid Uri with scheme (http or https) and host, and - /// may contain a port and path. The default value is - /// * http://localhost:4317 for - /// * http://localhost:4318 for . - /// - public Uri Endpoint + /// + /// Gets or sets the target to which the exporter is going to send telemetry. + /// Must be a valid Uri with scheme (http or https) and host, and + /// may contain a port and path. The default value is + /// * http://localhost:4317 for + /// * http://localhost:4318 for . + /// + public Uri Endpoint + { + get { - get + if (this.endpoint == null) { - if (this.endpoint == null) - { - this.endpoint = this.Protocol == OtlpExportProtocol.Grpc - ? new Uri(DefaultGrpcEndpoint) - : new Uri(DefaultHttpEndpoint); - } - - return this.endpoint; + this.endpoint = this.Protocol == OtlpExportProtocol.Grpc + ? new Uri(DefaultGrpcEndpoint) + : new Uri(DefaultHttpEndpoint); } - set - { - this.endpoint = value; - this.ProgrammaticallyModifiedEndpoint = true; - } + return this.endpoint; } - /// - /// Gets or sets optional headers for the connection. Refer to the - /// specification for information on the expected format for Headers. - /// - public string Headers { get; set; } - - /// - /// Gets or sets the max waiting time (in milliseconds) for the backend to process each batch. The default value is 10000. - /// - public int TimeoutMilliseconds { get; set; } = 10000; - - /// - /// Gets or sets the the OTLP transport protocol. Supported values: Grpc and HttpProtobuf. - /// - public OtlpExportProtocol Protocol { get; set; } = DefaultOtlpExportProtocol; - - /// - /// Gets or sets the export processor type to be used with the OpenTelemetry Protocol Exporter. The default value is . - /// - public ExportProcessorType ExportProcessorType { get; set; } = ExportProcessorType.Batch; - - /// - /// Gets or sets the BatchExportProcessor options. Ignored unless ExportProcessorType is Batch. - /// - public BatchExportProcessorOptions BatchExportProcessorOptions { get; set; } - - /// - /// Gets or sets the factory function called to create the instance that will be used at runtime to - /// transmit telemetry over HTTP. The returned instance will be reused - /// for all export invocations. - /// - /// - /// Notes: - /// - /// This is only invoked for the protocol. - /// The default behavior when using the extension is if an IHttpClientFactory - /// instance can be resolved through the application then an will be - /// created through the factory with the name "OtlpTraceExporter" - /// otherwise an will be instantiated - /// directly. - /// The default behavior when using the extension is if an IHttpClientFactory - /// instance can be resolved through the application then an will be - /// created through the factory with the name "OtlpMetricExporter" - /// otherwise an will be instantiated - /// directly. - /// - /// - public Func HttpClientFactory { get; set; } - - /// - /// Gets a value indicating whether was modified via its setter. - /// - internal bool ProgrammaticallyModifiedEndpoint { get; private set; } - - internal static void RegisterOtlpExporterOptionsFactory(IServiceCollection services) + set { - services.RegisterOptionsFactory( - (sp, configuration, name) => new OtlpExporterOptions( - configuration, - sp.GetRequiredService>().Get(name))); + this.endpoint = value; + this.ProgrammaticallyModifiedEndpoint = true; } + } + + /// + /// Gets or sets optional headers for the connection. Refer to the + /// specification for information on the expected format for Headers. + /// + public string Headers { get; set; } + + /// + /// Gets or sets the max waiting time (in milliseconds) for the backend to process each batch. The default value is 10000. + /// + public int TimeoutMilliseconds { get; set; } = 10000; + + /// + /// Gets or sets the the OTLP transport protocol. Supported values: Grpc and HttpProtobuf. + /// + public OtlpExportProtocol Protocol { get; set; } = DefaultOtlpExportProtocol; + + /// + /// Gets or sets the export processor type to be used with the OpenTelemetry Protocol Exporter. The default value is . + /// + /// Note: This only applies when exporting traces. + public ExportProcessorType ExportProcessorType { get; set; } = ExportProcessorType.Batch; + + /// + /// Gets or sets the BatchExportProcessor options. Ignored unless ExportProcessorType is Batch. + /// + /// Note: This only applies when exporting traces. + public BatchExportProcessorOptions BatchExportProcessorOptions { get; set; } - private static string GetUserAgentString() + /// + /// Gets or sets the factory function called to create the instance that will be used at runtime to + /// transmit telemetry over HTTP. The returned instance will be reused + /// for all export invocations. + /// + /// + /// Notes: + /// + /// This is only invoked for the protocol. + /// The default behavior when using the extension is if an IHttpClientFactory + /// instance can be resolved through the application then an will be + /// created through the factory with the name "OtlpTraceExporter" + /// otherwise an will be instantiated + /// directly. + /// The default behavior when using the extension is if an IHttpClientFactory + /// instance can be resolved through the application then an will be + /// created through the factory with the name "OtlpMetricExporter" + /// otherwise an will be instantiated + /// directly. + /// + /// + public Func HttpClientFactory { get; set; } + + /// + /// Gets a value indicating whether was modified via its setter. + /// + internal bool ProgrammaticallyModifiedEndpoint { get; private set; } + + internal static void RegisterOtlpExporterOptionsFactory(IServiceCollection services) + { + services.RegisterOptionsFactory(CreateOtlpExporterOptions); + } + + internal static OtlpExporterOptions CreateOtlpExporterOptions( + IServiceProvider serviceProvider, + IConfiguration configuration, + string name) + => new( + configuration, + serviceProvider.GetRequiredService>().Get(name)); + + private static string GetUserAgentString() + { + try { - try - { - var assemblyVersion = typeof(OtlpExporterOptions).Assembly.GetCustomAttribute(); - var informationalVersion = assemblyVersion.InformationalVersion; - return string.IsNullOrEmpty(informationalVersion) ? UserAgentProduct : $"{UserAgentProduct}/{informationalVersion}"; - } - catch (Exception) - { - return UserAgentProduct; - } + var assemblyVersion = typeof(OtlpExporterOptions).Assembly.GetCustomAttribute(); + var informationalVersion = assemblyVersion.InformationalVersion; + return string.IsNullOrEmpty(informationalVersion) ? UserAgentProduct : $"{UserAgentProduct}/{informationalVersion}"; + } + catch (Exception) + { + return UserAgentProduct; } } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs index c4aa08f23d5..44133af1f84 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #if NETFRAMEWORK using System.Net.Http; @@ -23,171 +10,183 @@ #if NETSTANDARD2_1 || NET6_0_OR_GREATER using Grpc.Net.Client; #endif +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; using LogOtlpCollector = OpenTelemetry.Proto.Collector.Logs.V1; using MetricsOtlpCollector = OpenTelemetry.Proto.Collector.Metrics.V1; using TraceOtlpCollector = OpenTelemetry.Proto.Collector.Trace.V1; -namespace OpenTelemetry.Exporter +namespace OpenTelemetry.Exporter; + +internal static class OtlpExporterOptionsExtensions { - internal static class OtlpExporterOptionsExtensions - { #if NETSTANDARD2_1 || NET6_0_OR_GREATER - public static GrpcChannel CreateChannel(this OtlpExporterOptions options) + public static GrpcChannel CreateChannel(this OtlpExporterOptions options) #else - public static Channel CreateChannel(this OtlpExporterOptions options) + public static Channel CreateChannel(this OtlpExporterOptions options) #endif + { + if (options.Endpoint.Scheme != Uri.UriSchemeHttp && options.Endpoint.Scheme != Uri.UriSchemeHttps) { - if (options.Endpoint.Scheme != Uri.UriSchemeHttp && options.Endpoint.Scheme != Uri.UriSchemeHttps) - { - throw new NotSupportedException($"Endpoint URI scheme ({options.Endpoint.Scheme}) is not supported. Currently only \"http\" and \"https\" are supported."); - } + throw new NotSupportedException($"Endpoint URI scheme ({options.Endpoint.Scheme}) is not supported. Currently only \"http\" and \"https\" are supported."); + } #if NETSTANDARD2_1 || NET6_0_OR_GREATER - return GrpcChannel.ForAddress(options.Endpoint); + return GrpcChannel.ForAddress(options.Endpoint); #else - ChannelCredentials channelCredentials; - if (options.Endpoint.Scheme == Uri.UriSchemeHttps) - { - channelCredentials = new SslCredentials(); - } - else - { - channelCredentials = ChannelCredentials.Insecure; - } - - return new Channel(options.Endpoint.Authority, channelCredentials); -#endif + ChannelCredentials channelCredentials; + if (options.Endpoint.Scheme == Uri.UriSchemeHttps) + { + channelCredentials = new SslCredentials(); } - - public static Metadata GetMetadataFromHeaders(this OtlpExporterOptions options) + else { - return options.GetHeaders((m, k, v) => m.Add(k, v)); + channelCredentials = ChannelCredentials.Insecure; } - public static THeaders GetHeaders(this OtlpExporterOptions options, Action addHeader) - where THeaders : new() + return new Channel(options.Endpoint.Authority, channelCredentials); +#endif + } + + public static Metadata GetMetadataFromHeaders(this OtlpExporterOptions options) + { + return options.GetHeaders((m, k, v) => m.Add(k, v)); + } + + public static THeaders GetHeaders(this OtlpExporterOptions options, Action addHeader) + where THeaders : new() + { + var optionHeaders = options.Headers; + var headers = new THeaders(); + if (!string.IsNullOrEmpty(optionHeaders)) { - var optionHeaders = options.Headers; - var headers = new THeaders(); - if (!string.IsNullOrEmpty(optionHeaders)) - { - Array.ForEach( - optionHeaders.Split(','), - (pair) => - { - // Specify the maximum number of substrings to return to 2 - // This treats everything that follows the first `=` in the string as the value to be added for the metadata key - var keyValueData = pair.Split(new char[] { '=' }, 2); - if (keyValueData.Length != 2) - { - throw new ArgumentException("Headers provided in an invalid format."); - } + // According to the specification, URL-encoded headers must be supported. + optionHeaders = Uri.UnescapeDataString(optionHeaders); - var key = keyValueData[0].Trim(); - var value = keyValueData[1].Trim(); - addHeader(headers, key, value); - }); - } + Array.ForEach( + optionHeaders.Split(','), + (pair) => + { + // Specify the maximum number of substrings to return to 2 + // This treats everything that follows the first `=` in the string as the value to be added for the metadata key + var keyValueData = pair.Split(new char[] { '=' }, 2); + if (keyValueData.Length != 2) + { + throw new ArgumentException("Headers provided in an invalid format."); + } - foreach (var header in OtlpExporterOptions.StandardHeaders) - { - addHeader(headers, header.Key, header.Value); - } + var key = keyValueData[0].Trim(); + var value = keyValueData[1].Trim(); + addHeader(headers, key, value); + }); + } - return headers; + foreach (var header in OtlpExporterOptions.StandardHeaders) + { + addHeader(headers, header.Key, header.Value); } - public static IExportClient GetTraceExportClient(this OtlpExporterOptions options) => - options.Protocol switch - { - OtlpExportProtocol.Grpc => new OtlpGrpcTraceExportClient(options), - OtlpExportProtocol.HttpProtobuf => new OtlpHttpTraceExportClient( - options, - options.HttpClientFactory?.Invoke() ?? throw new InvalidOperationException("OtlpExporterOptions was missing HttpClientFactory or it returned null.")), - _ => throw new NotSupportedException($"Protocol {options.Protocol} is not supported."), - }; + return headers; + } - public static IExportClient GetMetricsExportClient(this OtlpExporterOptions options) => - options.Protocol switch - { - OtlpExportProtocol.Grpc => new OtlpGrpcMetricsExportClient(options), - OtlpExportProtocol.HttpProtobuf => new OtlpHttpMetricsExportClient( - options, - options.HttpClientFactory?.Invoke() ?? throw new InvalidOperationException("OtlpExporterOptions was missing HttpClientFactory or it returned null.")), - _ => throw new NotSupportedException($"Protocol {options.Protocol} is not supported."), - }; + public static OtlpExporterTransmissionHandler GetTraceExportTransmissionHandler(this OtlpExporterOptions options) + => new(GetTraceExportClient(options)); - public static IExportClient GetLogExportClient(this OtlpExporterOptions options) => - options.Protocol switch - { - OtlpExportProtocol.Grpc => new OtlpGrpcLogExportClient(options), - OtlpExportProtocol.HttpProtobuf => new OtlpHttpLogExportClient( - options, - options.HttpClientFactory?.Invoke() ?? throw new InvalidOperationException("OtlpExporterOptions was missing HttpClientFactory or it returned null.")), - _ => throw new NotSupportedException($"Protocol {options.Protocol} is not supported."), - }; + public static OtlpExporterTransmissionHandler GetMetricsExportTransmissionHandler(this OtlpExporterOptions options) + => new(GetMetricsExportClient(options)); + + public static OtlpExporterTransmissionHandler GetLogsExportTransmissionHandler(this OtlpExporterOptions options) + => new(GetLogExportClient(options)); - public static void TryEnableIHttpClientFactoryIntegration(this OtlpExporterOptions options, IServiceProvider serviceProvider, string httpClientName) + public static IExportClient GetTraceExportClient(this OtlpExporterOptions options) => + options.Protocol switch + { + OtlpExportProtocol.Grpc => new OtlpGrpcTraceExportClient(options), + OtlpExportProtocol.HttpProtobuf => new OtlpHttpTraceExportClient( + options, + options.HttpClientFactory?.Invoke() ?? throw new InvalidOperationException("OtlpExporterOptions was missing HttpClientFactory or it returned null.")), + _ => throw new NotSupportedException($"Protocol {options.Protocol} is not supported."), + }; + + public static IExportClient GetMetricsExportClient(this OtlpExporterOptions options) => + options.Protocol switch + { + OtlpExportProtocol.Grpc => new OtlpGrpcMetricsExportClient(options), + OtlpExportProtocol.HttpProtobuf => new OtlpHttpMetricsExportClient( + options, + options.HttpClientFactory?.Invoke() ?? throw new InvalidOperationException("OtlpExporterOptions was missing HttpClientFactory or it returned null.")), + _ => throw new NotSupportedException($"Protocol {options.Protocol} is not supported."), + }; + + public static IExportClient GetLogExportClient(this OtlpExporterOptions options) => + options.Protocol switch { - if (serviceProvider != null - && options.Protocol == OtlpExportProtocol.HttpProtobuf - && options.HttpClientFactory == options.DefaultHttpClientFactory) + OtlpExportProtocol.Grpc => new OtlpGrpcLogExportClient(options), + OtlpExportProtocol.HttpProtobuf => new OtlpHttpLogExportClient( + options, + options.HttpClientFactory?.Invoke() ?? throw new InvalidOperationException("OtlpExporterOptions was missing HttpClientFactory or it returned null.")), + _ => throw new NotSupportedException($"Protocol {options.Protocol} is not supported."), + }; + + public static void TryEnableIHttpClientFactoryIntegration(this OtlpExporterOptions options, IServiceProvider serviceProvider, string httpClientName) + { + if (serviceProvider != null + && options.Protocol == OtlpExportProtocol.HttpProtobuf + && options.HttpClientFactory == options.DefaultHttpClientFactory) + { + options.HttpClientFactory = () => { - options.HttpClientFactory = () => + Type httpClientFactoryType = Type.GetType("System.Net.Http.IHttpClientFactory, Microsoft.Extensions.Http", throwOnError: false); + if (httpClientFactoryType != null) { - Type httpClientFactoryType = Type.GetType("System.Net.Http.IHttpClientFactory, Microsoft.Extensions.Http", throwOnError: false); - if (httpClientFactoryType != null) + object httpClientFactory = serviceProvider.GetService(httpClientFactoryType); + if (httpClientFactory != null) { - object httpClientFactory = serviceProvider.GetService(httpClientFactoryType); - if (httpClientFactory != null) + MethodInfo createClientMethod = httpClientFactoryType.GetMethod( + "CreateClient", + BindingFlags.Public | BindingFlags.Instance, + binder: null, + new Type[] { typeof(string) }, + modifiers: null); + if (createClientMethod != null) { - MethodInfo createClientMethod = httpClientFactoryType.GetMethod( - "CreateClient", - BindingFlags.Public | BindingFlags.Instance, - binder: null, - new Type[] { typeof(string) }, - modifiers: null); - if (createClientMethod != null) - { - HttpClient client = (HttpClient)createClientMethod.Invoke(httpClientFactory, new object[] { httpClientName }); - - client.Timeout = TimeSpan.FromMilliseconds(options.TimeoutMilliseconds); - - return client; - } + HttpClient client = (HttpClient)createClientMethod.Invoke(httpClientFactory, new object[] { httpClientName }); + + client.Timeout = TimeSpan.FromMilliseconds(options.TimeoutMilliseconds); + + return client; } } + } - return options.DefaultHttpClientFactory(); - }; - } + return options.DefaultHttpClientFactory(); + }; } + } - internal static Uri AppendPathIfNotPresent(this Uri uri, string path) - { - var absoluteUri = uri.AbsoluteUri; - var separator = string.Empty; + internal static Uri AppendPathIfNotPresent(this Uri uri, string path) + { + var absoluteUri = uri.AbsoluteUri; + var separator = string.Empty; - if (absoluteUri.EndsWith("/")) + if (absoluteUri.EndsWith("/")) + { + // Endpoint already ends with 'path/' + if (absoluteUri.EndsWith(string.Concat(path, "/"), StringComparison.OrdinalIgnoreCase)) { - // Endpoint already ends with 'path/' - if (absoluteUri.EndsWith(string.Concat(path, "/"), StringComparison.OrdinalIgnoreCase)) - { - return uri; - } + return uri; } - else + } + else + { + // Endpoint already ends with 'path' + if (absoluteUri.EndsWith(path, StringComparison.OrdinalIgnoreCase)) { - // Endpoint already ends with 'path' - if (absoluteUri.EndsWith(path, StringComparison.OrdinalIgnoreCase)) - { - return uri; - } - - separator = "/"; + return uri; } - return new Uri(string.Concat(uri.AbsoluteUri, separator, path)); + separator = "/"; } + + return new Uri(string.Concat(uri.AbsoluteUri, separator, path)); } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs index 8eaed5aa464..8e5c626d917 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs @@ -1,119 +1,111 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable using System.Diagnostics; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; -using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; using OpenTelemetry.Internal; using OpenTelemetry.Logs; using OtlpCollector = OpenTelemetry.Proto.Collector.Logs.V1; using OtlpResource = OpenTelemetry.Proto.Resource.V1; -namespace OpenTelemetry.Exporter +namespace OpenTelemetry.Exporter; + +/// +/// Exporter consuming and exporting the data using +/// the OpenTelemetry protocol (OTLP). +/// +public sealed class OtlpLogExporter : BaseExporter { + private readonly OtlpExporterTransmissionHandler transmissionHandler; + private readonly OtlpLogRecordTransformer otlpLogRecordTransformer; + + private OtlpResource.Resource? processResource; + /// - /// Exporter consuming and exporting the data using - /// the OpenTelemetry protocol (OTLP). + /// Initializes a new instance of the class. /// - internal sealed class OtlpLogExporter : BaseExporter + /// Configuration options for the exporter. + public OtlpLogExporter(OtlpExporterOptions options) + : this(options, sdkLimitOptions: new(), experimentalOptions: new(), transmissionHandler: null) { - private readonly SdkLimitOptions sdkLimitOptions; - private readonly IExportClient exportClient; + } - private OtlpResource.Resource processResource; + /// + /// Initializes a new instance of the class. + /// + /// Configuration options for the exporter. + /// . + /// . + /// . + internal OtlpLogExporter( + OtlpExporterOptions exporterOptions, + SdkLimitOptions sdkLimitOptions, + ExperimentalOptions experimentalOptions, + OtlpExporterTransmissionHandler? transmissionHandler = null) + { + Debug.Assert(exporterOptions != null, "exporterOptions was null"); + Debug.Assert(sdkLimitOptions != null, "sdkLimitOptions was null"); + Debug.Assert(experimentalOptions != null, "experimentalOptions was null"); - /// - /// Initializes a new instance of the class. - /// - /// Configuration options for the exporter. - public OtlpLogExporter(OtlpExporterOptions options) - : this(options, new(), null) + // Each of the Otlp exporters: Traces, Metrics, and Logs set the same value for `OtlpKeyValueTransformer.LogUnsupportedAttributeType` + // and `ConfigurationExtensions.LogInvalidEnvironmentVariable` so it should be fine even if these exporters are used together. + OtlpKeyValueTransformer.LogUnsupportedAttributeType = (string tagValueType, string tagKey) => { - } + OpenTelemetryProtocolExporterEventSource.Log.UnsupportedAttributeType(tagValueType, tagKey); + }; - /// - /// Initializes a new instance of the class. - /// - /// Configuration options for the exporter. - /// . - /// Client used for sending export request. - internal OtlpLogExporter( - OtlpExporterOptions exporterOptions, - SdkLimitOptions sdkLimitOptions, - IExportClient exportClient = null) + ConfigurationExtensions.LogInvalidEnvironmentVariable = (string key, string value) => { - Debug.Assert(exporterOptions != null, "exporterOptions was null"); - Debug.Assert(sdkLimitOptions != null, "sdkLimitOptions was null"); + OpenTelemetryProtocolExporterEventSource.Log.InvalidEnvironmentVariable(key, value); + }; - this.sdkLimitOptions = sdkLimitOptions; + this.transmissionHandler = transmissionHandler ?? exporterOptions.GetLogsExportTransmissionHandler(); - // Each of the Otlp exporters: Traces, Metrics, and Logs set the same value for `OtlpKeyValueTransformer.LogUnsupportedAttributeType` - // and `ConfigurationExtensions.LogInvalidEnvironmentVariable` so it should be fine even if these exporters are used together. - OtlpKeyValueTransformer.LogUnsupportedAttributeType = (string tagValueType, string tagKey) => - { - OpenTelemetryProtocolExporterEventSource.Log.UnsupportedAttributeType(tagValueType, tagKey); - }; + this.otlpLogRecordTransformer = new OtlpLogRecordTransformer(sdkLimitOptions!, experimentalOptions!); + } - ConfigurationExtensions.LogInvalidEnvironmentVariable = (string key, string value) => - { - OpenTelemetryProtocolExporterEventSource.Log.InvalidEnvironmentVariable(key, value); - }; + internal OtlpResource.Resource ProcessResource + => this.processResource ??= this.ParentProvider.GetResource().ToOtlpResource(); - if (exportClient != null) - { - this.exportClient = exportClient; - } - else - { - this.exportClient = exporterOptions.GetLogExportClient(); - } - } + /// + public override ExportResult Export(in Batch logRecordBatch) + { + // Prevents the exporter's gRPC and HTTP operations from being instrumented. + using var scope = SuppressInstrumentationScope.Begin(); - internal OtlpResource.Resource ProcessResource => this.processResource ??= this.ParentProvider.GetResource().ToOtlpResource(); + OtlpCollector.ExportLogsServiceRequest? request = null; - /// - public override ExportResult Export(in Batch logRecordBatch) + try { - // Prevents the exporter's gRPC and HTTP operations from being instrumented. - using var scope = SuppressInstrumentationScope.Begin(); - - var request = new OtlpCollector.ExportLogsServiceRequest(); - - try - { - request.AddBatch(this.sdkLimitOptions, this.ProcessResource, logRecordBatch); + request = this.otlpLogRecordTransformer.BuildExportRequest(this.ProcessResource, logRecordBatch); - if (!this.exportClient.SendExportRequest(request)) - { - return ExportResult.Failure; - } - } - catch (Exception ex) + if (!this.transmissionHandler.TrySubmitRequest(request)) { - OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(ex); return ExportResult.Failure; } - - return ExportResult.Success; } - - /// - protected override bool OnShutdown(int timeoutMilliseconds) + catch (Exception ex) { - return this.exportClient?.Shutdown(timeoutMilliseconds) ?? true; + OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(ex); + return ExportResult.Failure; } + finally + { + if (request != null) + { + this.otlpLogRecordTransformer.Return(request); + } + } + + return ExportResult.Success; + } + + /// + protected override bool OnShutdown(int timeoutMilliseconds) + { + return this.transmissionHandler?.Shutdown(timeoutMilliseconds) ?? true; } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporterHelperExtensions.cs index d8b7b98e259..85298517f95 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporterHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporterHelperExtensions.cs @@ -1,21 +1,19 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable using System.Diagnostics; +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using OpenTelemetry.Exporter; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using OpenTelemetry.Internal; namespace OpenTelemetry.Logs; @@ -25,16 +23,16 @@ namespace OpenTelemetry.Logs; public static class OtlpLogExporterHelperExtensions { /// - /// Adds OTLP Exporter as a configuration to the OpenTelemetry ILoggingBuilder. + /// Adds an OTLP Exporter to the OpenTelemetry . /// /// /// options to use. /// The instance of to chain the calls. public static OpenTelemetryLoggerOptions AddOtlpExporter(this OpenTelemetryLoggerOptions loggerOptions) - => AddOtlpExporterInternal(loggerOptions, configure: null); + => AddOtlpExporter(loggerOptions, name: null, configure: null); /// - /// Adds OTLP Exporter as a configuration to the OpenTelemetry ILoggingBuilder. + /// Adds an OTLP Exporter to the OpenTelemetry . /// /// options to use. /// Callback action for configuring . @@ -42,40 +40,410 @@ public static OpenTelemetryLoggerOptions AddOtlpExporter(this OpenTelemetryLogge public static OpenTelemetryLoggerOptions AddOtlpExporter( this OpenTelemetryLoggerOptions loggerOptions, Action configure) - => AddOtlpExporterInternal(loggerOptions, configure); + => AddOtlpExporter(loggerOptions, name: null, configure); - private static OpenTelemetryLoggerOptions AddOtlpExporterInternal( - OpenTelemetryLoggerOptions loggerOptions, - Action configure) + /// + /// Adds an OTLP Exporter to the OpenTelemetry . + /// + /// options to use. + /// Optional name which is used when retrieving options. + /// Optional callback action for configuring . + /// The instance of to chain the calls. + public static OpenTelemetryLoggerOptions AddOtlpExporter( + this OpenTelemetryLoggerOptions loggerOptions, + string? name, + Action? configure) { - var exporterOptions = new OtlpExporterOptions(); + Guard.ThrowIfNull(loggerOptions); + + var finalOptionsName = name ?? Options.DefaultName; - // TODO: We are using span/activity batch environment variable keys - // here when we should be using the ones for logs. - var defaultBatchOptions = exporterOptions.BatchExportProcessorOptions; + return loggerOptions.AddProcessor(sp => + { + var exporterOptions = GetOptions(sp, name, finalOptionsName, OtlpExporterOptions.CreateOtlpExporterOptions); - Debug.Assert(defaultBatchOptions != null, "defaultBatchOptions was null"); + var processorOptions = sp.GetRequiredService>().Get(finalOptionsName); - configure?.Invoke(exporterOptions); + configure?.Invoke(exporterOptions); - var otlpExporter = new OtlpLogExporter(exporterOptions); + return BuildOtlpLogExporter( + sp, + exporterOptions, + processorOptions, + GetOptions(sp, Options.DefaultName, Options.DefaultName, (sp, c, n) => new SdkLimitOptions(c)), + GetOptions(sp, name, finalOptionsName, (sp, c, n) => new ExperimentalOptions(c))); + }); + } - if (exporterOptions.ExportProcessorType == ExportProcessorType.Simple) + /// + /// Adds an OTLP Exporter to the OpenTelemetry . + /// + /// options to use. + /// Callback action for configuring and . + /// The instance of to chain the calls. + public static OpenTelemetryLoggerOptions AddOtlpExporter( + this OpenTelemetryLoggerOptions loggerOptions, + Action configureExporterAndProcessor) + => AddOtlpExporter(loggerOptions, name: null, configureExporterAndProcessor); + + /// + /// Adds an OTLP Exporter to the OpenTelemetry . + /// + /// options to use. + /// Optional name which is used when retrieving options. + /// Optional callback action for configuring and . + /// The instance of to chain the calls. + public static OpenTelemetryLoggerOptions AddOtlpExporter( + this OpenTelemetryLoggerOptions loggerOptions, + string? name, + Action? configureExporterAndProcessor) + { + Guard.ThrowIfNull(loggerOptions); + + var finalOptionsName = name ?? Options.DefaultName; + + return loggerOptions.AddProcessor(sp => { - loggerOptions.AddProcessor(new SimpleLogRecordExportProcessor(otlpExporter)); + var exporterOptions = GetOptions(sp, name, finalOptionsName, OtlpExporterOptions.CreateOtlpExporterOptions); + + var processorOptions = sp.GetRequiredService>().Get(finalOptionsName); + + configureExporterAndProcessor?.Invoke(exporterOptions, processorOptions); + + return BuildOtlpLogExporter( + sp, + exporterOptions, + processorOptions, + GetOptions(sp, Options.DefaultName, Options.DefaultName, (sp, c, n) => new SdkLimitOptions(c)), + GetOptions(sp, name, finalOptionsName, (sp, c, n) => new ExperimentalOptions(c))); + }); + } + +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds an OTLP exporter to the LoggerProvider. + /// + /// WARNING: This is an experimental API which might change or be removed in the future. Use at your own risk. + /// builder to use. + /// The instance of to chain the calls. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + /// + /// Adds an OTLP exporter to the LoggerProvider. + /// + /// builder to use. + /// The instance of to chain the calls. + internal +#endif + static LoggerProviderBuilder AddOtlpExporter(this LoggerProviderBuilder builder) + => AddOtlpExporter(builder, name: null, configureExporter: null); + +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds an OTLP exporter to the LoggerProvider. + /// + /// WARNING: This is an experimental API which might change or be removed in the future. Use at your own risk. + /// builder to use. + /// Callback action for configuring . + /// The instance of to chain the calls. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + /// + /// Adds an OTLP exporter to the LoggerProvider. + /// + /// builder to use. + /// Callback action for configuring . + /// The instance of to chain the calls. + internal +#endif + static LoggerProviderBuilder AddOtlpExporter(this LoggerProviderBuilder builder, Action configureExporter) + => AddOtlpExporter(builder, name: null, configureExporter); + +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds an OTLP exporter to the LoggerProvider. + /// + /// WARNING: This is an experimental API which might change or be removed in the future. Use at your own risk. + /// builder to use. + /// Callback action for + /// configuring and . + /// The instance of to chain the calls. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + internal +#endif + static LoggerProviderBuilder AddOtlpExporter(this LoggerProviderBuilder builder, Action configureExporterAndProcessor) + => AddOtlpExporter(builder, name: null, configureExporterAndProcessor); + +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds OpenTelemetry Protocol (OTLP) exporter to the LoggerProvider. + /// + /// WARNING: This is an experimental API which might change or be removed in the future. Use at your own risk. + /// builder to use. + /// Optional name which is used when retrieving options. + /// Optional callback action for configuring . + /// The instance of to chain the calls. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + /// + /// Adds OpenTelemetry Protocol (OTLP) exporter to the LoggerProvider. + /// + /// builder to use. + /// Optional name which is used when retrieving options. + /// Optional callback action for configuring . + /// The instance of to chain the calls. + internal +#endif + static LoggerProviderBuilder AddOtlpExporter( + this LoggerProviderBuilder builder, + string? name, + Action? configureExporter) + { + var finalOptionsName = name ?? Options.DefaultName; + + builder.ConfigureServices(services => + { + if (name != null && configureExporter != null) + { + // If we are using named options we register the + // configuration delegate into options pipeline. + services.Configure(finalOptionsName, configureExporter); + } + + RegisterOptions(services); + }); + + return builder.AddProcessor(sp => + { + OtlpExporterOptions exporterOptions; + + if (name == null) + { + // If we are NOT using named options we create a new + // instance always. The reason for this is + // OtlpExporterOptions is shared by all signals. Without a + // name, delegates for all signals will mix together. See: + // https://github.com/open-telemetry/opentelemetry-dotnet/issues/4043 + exporterOptions = sp.GetRequiredService>().Create(finalOptionsName); + + // Configuration delegate is executed inline on the fresh instance. + configureExporter?.Invoke(exporterOptions); + } + else + { + // When using named options we can properly utilize Options + // API to create or reuse an instance. + exporterOptions = sp.GetRequiredService>().Get(finalOptionsName); + } + + // Note: Not using finalOptionsName here for SdkLimitOptions. + // There should only be one provider for a given service + // collection so SdkLimitOptions is treated as a single default + // instance. + var sdkLimitOptions = sp.GetRequiredService>().CurrentValue; + + return BuildOtlpLogExporter( + sp, + exporterOptions, + sp.GetRequiredService>().Get(finalOptionsName), + sdkLimitOptions, + sp.GetRequiredService>().Get(finalOptionsName)); + }); + } + +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds an OTLP exporter to the LoggerProvider. + /// + /// WARNING: This is an experimental API which might change or be removed in the future. Use at your own risk. + /// builder to use. + /// Optional name which is used when retrieving options. + /// Optional callback action for + /// configuring and . + /// The instance of to chain the calls. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + /// + /// Adds an OTLP exporter to the LoggerProvider. + /// + /// builder to use. + /// Optional name which is used when retrieving options. + /// Optional callback action for + /// configuring and . + /// The instance of to chain the calls. + internal +#endif + static LoggerProviderBuilder AddOtlpExporter( + this LoggerProviderBuilder builder, + string? name, + Action? configureExporterAndProcessor) + { + var finalOptionsName = name ?? Options.DefaultName; + + builder.ConfigureServices(RegisterOptions); + + return builder.AddProcessor(sp => + { + OtlpExporterOptions exporterOptions; + + if (name == null) + { + // If we are NOT using named options we create a new + // instance always. The reason for this is + // OtlpExporterOptions is shared by all signals. Without a + // name, delegates for all signals will mix together. See: + // https://github.com/open-telemetry/opentelemetry-dotnet/issues/4043 + exporterOptions = sp.GetRequiredService>().Create(finalOptionsName); + } + else + { + // When using named options we can properly utilize Options + // API to create or reuse an instance. + exporterOptions = sp.GetRequiredService>().Get(finalOptionsName); + } + + var processorOptions = sp.GetRequiredService>().Get(finalOptionsName); + + // Configuration delegate is executed inline. + configureExporterAndProcessor?.Invoke(exporterOptions, processorOptions); + + // Note: Not using finalOptionsName here for SdkLimitOptions. + // There should only be one provider for a given service + // collection so SdkLimitOptions is treated as a single default + // instance. + var sdkLimitOptions = sp.GetRequiredService>().CurrentValue; + + return BuildOtlpLogExporter( + sp, + exporterOptions, + processorOptions, + sdkLimitOptions, + sp.GetRequiredService>().Get(finalOptionsName)); + }); + } + + internal static BaseProcessor BuildOtlpLogExporter( + IServiceProvider sp, + OtlpExporterOptions exporterOptions, + LogRecordExportProcessorOptions processorOptions, + SdkLimitOptions sdkLimitOptions, + ExperimentalOptions experimentalOptions, + Func, BaseExporter>? configureExporterInstance = null) + { + // Note: sp is not currently used by this method but it should be used + // at some point for IHttpClientFactory integration. + Debug.Assert(sp != null, "sp was null"); + Debug.Assert(exporterOptions != null, "exporterOptions was null"); + Debug.Assert(processorOptions != null, "processorOptions was null"); + Debug.Assert(sdkLimitOptions != null, "sdkLimitOptions was null"); + Debug.Assert(experimentalOptions != null, "experimentalOptions was null"); + + /* + * Note: + * + * We don't currently enable IHttpClientFactory for OtlpLogExporter. + * + * The DefaultHttpClientFactory requires the ILoggerFactory in its ctor: + * https://github.com/dotnet/runtime/blob/fa40ecf7d36bf4e31d7ae968807c1c529bac66d6/src/libraries/Microsoft.Extensions.Http/src/DefaultHttpClientFactory.cs#L64 + * + * This creates a circular reference: ILoggerFactory -> + * OpenTelemetryLoggerProvider -> OtlpLogExporter -> IHttpClientFactory + * -> ILoggerFactory + * + * exporterOptions.TryEnableIHttpClientFactoryIntegration(sp, + * "OtlpLogExporter"); + */ + + BaseExporter otlpExporter = new OtlpLogExporter( + exporterOptions!, + sdkLimitOptions!, + experimentalOptions!); + + if (configureExporterInstance != null) + { + otlpExporter = configureExporterInstance(otlpExporter); + } + + if (processorOptions!.ExportProcessorType == ExportProcessorType.Simple) + { + return new SimpleLogRecordExportProcessor(otlpExporter); } else { - var batchOptions = exporterOptions.BatchExportProcessorOptions ?? defaultBatchOptions; + var batchOptions = processorOptions.BatchExportProcessorOptions; - loggerOptions.AddProcessor(new BatchLogRecordExportProcessor( + return new BatchLogRecordExportProcessor( otlpExporter, batchOptions.MaxQueueSize, batchOptions.ScheduledDelayMilliseconds, batchOptions.ExporterTimeoutMilliseconds, - batchOptions.MaxExportBatchSize)); + batchOptions.MaxExportBatchSize); + } + } + + private static void RegisterOptions(IServiceCollection services) + { + OtlpExporterOptions.RegisterOtlpExporterOptionsFactory(services); + services.RegisterOptionsFactory(configuration => new SdkLimitOptions(configuration)); + services.RegisterOptionsFactory(configuration => new ExperimentalOptions(configuration)); + } + + private static T GetOptions( + IServiceProvider sp, + string? name, + string finalName, + Func createOptionsFunc) + where T : class, new() + { + // Note: If OtlpExporter has been registered for tracing and/or metrics + // then IOptionsFactory will be set by a call to + // OtlpExporterOptions.RegisterOtlpExporterOptionsFactory. However, if we + // are only using logging, we don't have an opportunity to do that + // registration so we manually create a factory. + + var optionsFactory = sp.GetRequiredService>(); + if (optionsFactory is not DelegatingOptionsFactory) + { + optionsFactory = new DelegatingOptionsFactory( + (c, n) => createOptionsFunc(sp, c, n), + sp.GetRequiredService(), + sp.GetServices>(), + sp.GetServices>(), + sp.GetServices>()); + + return optionsFactory.Create(finalName); + } + + if (name == null) + { + // If we are NOT using named options we create a new + // instance always. The reason for this is + // OtlpExporterOptions is shared by all signals. Without a + // name, delegates for all signals will mix together. + return optionsFactory.Create(finalName); } - return loggerOptions; + // If we have a valid factory AND we are using named options, we can + // safely use the Options API fully. + return sp.GetRequiredService>().Get(finalName); } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporter.cs index c67ffc30a8a..a0026d1e9f4 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporter.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporter.cs @@ -1,112 +1,93 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; -using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; using OpenTelemetry.Internal; using OpenTelemetry.Metrics; using OtlpCollector = OpenTelemetry.Proto.Collector.Metrics.V1; using OtlpResource = OpenTelemetry.Proto.Resource.V1; -namespace OpenTelemetry.Exporter +namespace OpenTelemetry.Exporter; + +/// +/// Exporter consuming and exporting the data using +/// the OpenTelemetry protocol (OTLP). +/// +public class OtlpMetricExporter : BaseExporter { + private readonly OtlpExporterTransmissionHandler transmissionHandler; + + private OtlpResource.Resource processResource; + /// - /// Exporter consuming and exporting the data using - /// the OpenTelemetry protocol (OTLP). + /// Initializes a new instance of the class. /// - public class OtlpMetricExporter : BaseExporter + /// Configuration options for the exporter. + public OtlpMetricExporter(OtlpExporterOptions options) + : this(options, transmissionHandler: null) { - private readonly IExportClient exportClient; - - private OtlpResource.Resource processResource; + } - /// - /// Initializes a new instance of the class. - /// - /// Configuration options for the exporter. - public OtlpMetricExporter(OtlpExporterOptions options) - : this(options, null) + /// + /// Initializes a new instance of the class. + /// + /// Configuration options for the export. + /// . + internal OtlpMetricExporter( + OtlpExporterOptions options, + OtlpExporterTransmissionHandler transmissionHandler = null) + { + // Each of the Otlp exporters: Traces, Metrics, and Logs set the same value for `OtlpKeyValueTransformer.LogUnsupportedAttributeType` + // and `ConfigurationExtensions.LogInvalidEnvironmentVariable` so it should be fine even if these exporters are used together. + OtlpKeyValueTransformer.LogUnsupportedAttributeType = (string tagValueType, string tagKey) => { - } + OpenTelemetryProtocolExporterEventSource.Log.UnsupportedAttributeType(tagValueType, tagKey); + }; - /// - /// Initializes a new instance of the class. - /// - /// Configuration options for the export. - /// Client used for sending export request. - internal OtlpMetricExporter(OtlpExporterOptions options, IExportClient exportClient = null) + ConfigurationExtensions.LogInvalidEnvironmentVariable = (string key, string value) => { - // Each of the Otlp exporters: Traces, Metrics, and Logs set the same value for `OtlpKeyValueTransformer.LogUnsupportedAttributeType` - // and `ConfigurationExtensions.LogInvalidEnvironmentVariable` so it should be fine even if these exporters are used together. - OtlpKeyValueTransformer.LogUnsupportedAttributeType = (string tagValueType, string tagKey) => - { - OpenTelemetryProtocolExporterEventSource.Log.UnsupportedAttributeType(tagValueType, tagKey); - }; - - ConfigurationExtensions.LogInvalidEnvironmentVariable = (string key, string value) => - { - OpenTelemetryProtocolExporterEventSource.Log.InvalidEnvironmentVariable(key, value); - }; + OpenTelemetryProtocolExporterEventSource.Log.InvalidEnvironmentVariable(key, value); + }; - if (exportClient != null) - { - this.exportClient = exportClient; - } - else - { - this.exportClient = options.GetMetricsExportClient(); - } - } + this.transmissionHandler = transmissionHandler ?? options.GetMetricsExportTransmissionHandler(); + } - internal OtlpResource.Resource ProcessResource => this.processResource ??= this.ParentProvider.GetResource().ToOtlpResource(); + internal OtlpResource.Resource ProcessResource => this.processResource ??= this.ParentProvider.GetResource().ToOtlpResource(); - /// - public override ExportResult Export(in Batch metrics) - { - // Prevents the exporter's gRPC and HTTP operations from being instrumented. - using var scope = SuppressInstrumentationScope.Begin(); + /// + public override ExportResult Export(in Batch metrics) + { + // Prevents the exporter's gRPC and HTTP operations from being instrumented. + using var scope = SuppressInstrumentationScope.Begin(); - var request = new OtlpCollector.ExportMetricsServiceRequest(); + var request = new OtlpCollector.ExportMetricsServiceRequest(); - try - { - request.AddMetrics(this.ProcessResource, metrics); + try + { + request.AddMetrics(this.ProcessResource, metrics); - if (!this.exportClient.SendExportRequest(request)) - { - return ExportResult.Failure; - } - } - catch (Exception ex) + if (!this.transmissionHandler.TrySubmitRequest(request)) { - OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(ex); return ExportResult.Failure; } - finally - { - request.Return(); - } - - return ExportResult.Success; } - - /// - protected override bool OnShutdown(int timeoutMilliseconds) + catch (Exception ex) + { + OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(ex); + return ExportResult.Failure; + } + finally { - return this.exportClient?.Shutdown(timeoutMilliseconds) ?? true; + request.Return(); } + + return ExportResult.Success; + } + + /// + protected override bool OnShutdown(int timeoutMilliseconds) + { + return this.transmissionHandler.Shutdown(timeoutMilliseconds); } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterExtensions.cs index df4f9e5be80..f6e3be715d7 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterExtensions.cs @@ -1,171 +1,197 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +#nullable enable + +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using OpenTelemetry.Exporter; using OpenTelemetry.Internal; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// Extension methods to simplify registering of the OpenTelemetry Protocol (OTLP) exporter. +/// +public static class OtlpMetricExporterExtensions { + internal const string OtlpMetricExporterTemporalityPreferenceEnvVarKey = "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"; + /// - /// Extension methods to simplify registering of the OpenTelemetry Protocol (OTLP) exporter. + /// Adds to the using default options. /// - public static class OtlpMetricExporterExtensions - { - /// - /// Adds to the using default options. - /// - /// builder to use. - /// The instance of to chain the calls. - public static MeterProviderBuilder AddOtlpExporter(this MeterProviderBuilder builder) - => AddOtlpExporter(builder, name: null, configureExporter: null); - - /// - /// Adds to the . - /// - /// builder to use. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static MeterProviderBuilder AddOtlpExporter(this MeterProviderBuilder builder, Action configureExporter) - => AddOtlpExporter(builder, name: null, configureExporter); - - /// - /// Adds to the . - /// - /// builder to use. - /// Name which is used when retrieving options. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static MeterProviderBuilder AddOtlpExporter( - this MeterProviderBuilder builder, - string name, - Action configureExporter) - { - Guard.ThrowIfNull(builder); + /// builder to use. + /// The instance of to chain the calls. + public static MeterProviderBuilder AddOtlpExporter(this MeterProviderBuilder builder) + => AddOtlpExporter(builder, name: null, configure: null); - var finalOptionsName = name ?? Options.DefaultName; + /// + /// Adds to the . + /// + /// builder to use. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static MeterProviderBuilder AddOtlpExporter(this MeterProviderBuilder builder, Action configure) + => AddOtlpExporter(builder, name: null, configure); - builder.ConfigureServices(services => - { - if (name != null && configureExporter != null) - { - // If we are using named options we register the - // configuration delegate into options pipeline. - services.Configure(finalOptionsName, configureExporter); - } + /// + /// Adds to the . + /// + /// builder to use. + /// Optional name which is used when retrieving options. + /// Optional callback action for configuring . + /// The instance of to chain the calls. + public static MeterProviderBuilder AddOtlpExporter( + this MeterProviderBuilder builder, + string? name, + Action? configure) + { + Guard.ThrowIfNull(builder); - OtlpExporterOptions.RegisterOtlpExporterOptionsFactory(services); - }); + var finalOptionsName = name ?? Options.DefaultName; - return builder.AddReader(sp => + builder.ConfigureServices(services => + { + if (name != null && configure != null) { - OtlpExporterOptions exporterOptions; + // If we are using named options we register the + // configuration delegate into options pipeline. + services.Configure(finalOptionsName, configure); + } - if (name == null) - { - // If we are NOT using named options we create a new - // instance always. The reason for this is - // OtlpExporterOptions is shared by all signals. Without a - // name, delegates for all signals will mix together. See: - // https://github.com/open-telemetry/opentelemetry-dotnet/issues/4043 - exporterOptions = sp.GetRequiredService>().Create(finalOptionsName); - - // Configuration delegate is executed inline on the fresh instance. - configureExporter?.Invoke(exporterOptions); - } - else - { - // When using named options we can properly utilize Options - // API to create or reuse an instance. - exporterOptions = sp.GetRequiredService>().Get(finalOptionsName); - } - - return BuildOtlpExporterMetricReader( - exporterOptions, - sp.GetRequiredService>().Get(finalOptionsName), - sp); - }); - } + OtlpExporterOptions.RegisterOtlpExporterOptionsFactory(services); - /// - /// Adds to the . - /// - /// builder to use. - /// Callback action for - /// configuring and . - /// The instance of to chain the calls. - public static MeterProviderBuilder AddOtlpExporter( - this MeterProviderBuilder builder, - Action configureExporterAndMetricReader) - => AddOtlpExporter(builder, name: null, configureExporterAndMetricReader); - - /// - /// Adds to the . - /// - /// builder to use. - /// Name which is used when retrieving options. - /// Callback action for - /// configuring and . - /// The instance of to chain the calls. - public static MeterProviderBuilder AddOtlpExporter( - this MeterProviderBuilder builder, - string name, - Action configureExporterAndMetricReader) + services.AddOptions(finalOptionsName).Configure( + (readerOptions, config) => + { + var otlpTemporalityPreference = config[OtlpMetricExporterTemporalityPreferenceEnvVarKey]; + if (!string.IsNullOrWhiteSpace(otlpTemporalityPreference) + && Enum.TryParse(otlpTemporalityPreference, ignoreCase: true, out var enumValue)) + { + readerOptions.TemporalityPreference = enumValue; + } + }); + }); + + return builder.AddReader(sp => { - Guard.ThrowIfNull(builder); + OtlpExporterOptions exporterOptions; - name ??= Options.DefaultName; - - builder.ConfigureServices(services => + if (name == null) { - OtlpExporterOptions.RegisterOtlpExporterOptionsFactory(services); - }); - - return builder.AddReader(sp => + // If we are NOT using named options we create a new + // instance always. The reason for this is + // OtlpExporterOptions is shared by all signals. Without a + // name, delegates for all signals will mix together. See: + // https://github.com/open-telemetry/opentelemetry-dotnet/issues/4043 + exporterOptions = sp.GetRequiredService>().Create(finalOptionsName); + + // Configuration delegate is executed inline on the fresh instance. + configure?.Invoke(exporterOptions); + } + else { - var exporterOptions = sp.GetRequiredService>().Get(name); - var metricReaderOptions = sp.GetRequiredService>().Get(name); + // When using named options we can properly utilize Options + // API to create or reuse an instance. + exporterOptions = sp.GetRequiredService>().Get(finalOptionsName); + } - configureExporterAndMetricReader?.Invoke(exporterOptions, metricReaderOptions); + return BuildOtlpExporterMetricReader( + exporterOptions, + sp.GetRequiredService>().Get(finalOptionsName), + sp); + }); + } - return BuildOtlpExporterMetricReader(exporterOptions, metricReaderOptions, sp); - }); - } + /// + /// Adds to the . + /// + /// builder to use. + /// Callback action for + /// configuring and . + /// The instance of to chain the calls. + public static MeterProviderBuilder AddOtlpExporter( + this MeterProviderBuilder builder, + Action configureExporterAndMetricReader) + => AddOtlpExporter(builder, name: null, configureExporterAndMetricReader); - internal static MetricReader BuildOtlpExporterMetricReader( - OtlpExporterOptions exporterOptions, - MetricReaderOptions metricReaderOptions, - IServiceProvider serviceProvider, - Func, BaseExporter> configureExporterInstance = null) - { - exporterOptions.TryEnableIHttpClientFactoryIntegration(serviceProvider, "OtlpMetricExporter"); + /// + /// Adds to the . + /// + /// builder to use. + /// Optional name which is used when retrieving options. + /// Optional callback action + /// for configuring and . + /// The instance of to chain the calls. + public static MeterProviderBuilder AddOtlpExporter( + this MeterProviderBuilder builder, + string? name, + Action? configureExporterAndMetricReader) + { + Guard.ThrowIfNull(builder); + + var finalOptionsName = name ?? Options.DefaultName; - BaseExporter metricExporter = new OtlpMetricExporter(exporterOptions); + builder.ConfigureServices(services => + { + OtlpExporterOptions.RegisterOtlpExporterOptionsFactory(services); - if (configureExporterInstance != null) + services.AddOptions(finalOptionsName).Configure( + (readerOptions, config) => + { + var otlpTemporalityPreference = config[OtlpMetricExporterTemporalityPreferenceEnvVarKey]; + if (!string.IsNullOrWhiteSpace(otlpTemporalityPreference) + && Enum.TryParse(otlpTemporalityPreference, ignoreCase: true, out var enumValue)) + { + readerOptions.TemporalityPreference = enumValue; + } + }); + }); + + return builder.AddReader(sp => + { + OtlpExporterOptions exporterOptions; + if (name == null) { - metricExporter = configureExporterInstance(metricExporter); + // If we are NOT using named options we create a new + // instance always. The reason for this is + // OtlpExporterOptions is shared by all signals. Without a + // name, delegates for all signals will mix together. + exporterOptions = sp.GetRequiredService>().Create(finalOptionsName); } + else + { + exporterOptions = sp.GetRequiredService>().Get(finalOptionsName); + } + + var metricReaderOptions = sp.GetRequiredService>().Get(finalOptionsName); + + configureExporterAndMetricReader?.Invoke(exporterOptions, metricReaderOptions); + + return BuildOtlpExporterMetricReader(exporterOptions, metricReaderOptions, sp); + }); + } - return PeriodicExportingMetricReaderHelper.CreatePeriodicExportingMetricReader( - metricExporter, - metricReaderOptions); + internal static MetricReader BuildOtlpExporterMetricReader( + OtlpExporterOptions exporterOptions, + MetricReaderOptions metricReaderOptions, + IServiceProvider serviceProvider, + Func, BaseExporter>? configureExporterInstance = null) + { + exporterOptions.TryEnableIHttpClientFactoryIntegration(serviceProvider, "OtlpMetricExporter"); + + BaseExporter metricExporter = new OtlpMetricExporter(exporterOptions); + + if (configureExporterInstance != null) + { + metricExporter = configureExporterInstance(metricExporter); } + + return PeriodicExportingMetricReaderHelper.CreatePeriodicExportingMetricReader( + metricExporter, + metricReaderOptions); } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs index 6c8ade95d52..f017d075428 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs @@ -1,120 +1,93 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; -using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; using OpenTelemetry.Internal; using OtlpCollector = OpenTelemetry.Proto.Collector.Trace.V1; using OtlpResource = OpenTelemetry.Proto.Resource.V1; -namespace OpenTelemetry.Exporter +namespace OpenTelemetry.Exporter; + +/// +/// Exporter consuming and exporting the data using +/// the OpenTelemetry protocol (OTLP). +/// +public class OtlpTraceExporter : BaseExporter { + private readonly SdkLimitOptions sdkLimitOptions; + private readonly OtlpExporterTransmissionHandler transmissionHandler; + + private OtlpResource.Resource processResource; + /// - /// Exporter consuming and exporting the data using - /// the OpenTelemetry protocol (OTLP). + /// Initializes a new instance of the class. /// - public class OtlpTraceExporter : BaseExporter + /// Configuration options for the export. + public OtlpTraceExporter(OtlpExporterOptions options) + : this(options, sdkLimitOptions: new(), transmissionHandler: null) { - private readonly SdkLimitOptions sdkLimitOptions; - private readonly IExportClient exportClient; + } - private OtlpResource.Resource processResource; + /// + /// Initializes a new instance of the class. + /// + /// . + /// . + /// . + internal OtlpTraceExporter( + OtlpExporterOptions exporterOptions, + SdkLimitOptions sdkLimitOptions, + OtlpExporterTransmissionHandler transmissionHandler = null) + { + Debug.Assert(exporterOptions != null, "exporterOptions was null"); + Debug.Assert(sdkLimitOptions != null, "sdkLimitOptions was null"); - /// - /// Initializes a new instance of the class. - /// - /// Configuration options for the export. - public OtlpTraceExporter(OtlpExporterOptions options) - : this(options, new(), null) - { - } + this.sdkLimitOptions = sdkLimitOptions; - /// - /// Initializes a new instance of the class. - /// - /// . - /// . - /// Client used for sending export request. - internal OtlpTraceExporter( - OtlpExporterOptions exporterOptions, - SdkLimitOptions sdkLimitOptions, - IExportClient exportClient = null) - { - Debug.Assert(exporterOptions != null, "exporterOptions was null"); - Debug.Assert(sdkLimitOptions != null, "sdkLimitOptions was null"); + OtlpKeyValueTransformer.LogUnsupportedAttributeType = OpenTelemetryProtocolExporterEventSource.Log.UnsupportedAttributeType; - this.sdkLimitOptions = sdkLimitOptions; + ConfigurationExtensions.LogInvalidEnvironmentVariable = OpenTelemetryProtocolExporterEventSource.Log.InvalidEnvironmentVariable; - OtlpKeyValueTransformer.LogUnsupportedAttributeType = (string tagValueType, string tagKey) => - { - OpenTelemetryProtocolExporterEventSource.Log.UnsupportedAttributeType(tagValueType, tagKey); - }; + this.transmissionHandler = transmissionHandler ?? exporterOptions.GetTraceExportTransmissionHandler(); + } - ConfigurationExtensions.LogInvalidEnvironmentVariable = (string key, string value) => - { - OpenTelemetryProtocolExporterEventSource.Log.InvalidEnvironmentVariable(key, value); - }; + internal OtlpResource.Resource ProcessResource => this.processResource ??= this.ParentProvider.GetResource().ToOtlpResource(); - if (exportClient != null) - { - this.exportClient = exportClient; - } - else - { - this.exportClient = exporterOptions.GetTraceExportClient(); - } - } + /// + public override ExportResult Export(in Batch activityBatch) + { + // Prevents the exporter's gRPC and HTTP operations from being instrumented. + using var scope = SuppressInstrumentationScope.Begin(); - internal OtlpResource.Resource ProcessResource => this.processResource ??= this.ParentProvider.GetResource().ToOtlpResource(); + var request = new OtlpCollector.ExportTraceServiceRequest(); - /// - public override ExportResult Export(in Batch activityBatch) + try { - // Prevents the exporter's gRPC and HTTP operations from being instrumented. - using var scope = SuppressInstrumentationScope.Begin(); - - var request = new OtlpCollector.ExportTraceServiceRequest(); - - try - { - request.AddBatch(this.sdkLimitOptions, this.ProcessResource, activityBatch); + request.AddBatch(this.sdkLimitOptions, this.ProcessResource, activityBatch); - if (!this.exportClient.SendExportRequest(request)) - { - return ExportResult.Failure; - } - } - catch (Exception ex) + if (!this.transmissionHandler.TrySubmitRequest(request)) { - OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(ex); return ExportResult.Failure; } - finally - { - request.Return(); - } - - return ExportResult.Success; } - - /// - protected override bool OnShutdown(int timeoutMilliseconds) + catch (Exception ex) + { + OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(ex); + return ExportResult.Failure; + } + finally { - return this.exportClient?.Shutdown(timeoutMilliseconds) ?? true; + request.Return(); } + + return ExportResult.Success; + } + + /// + protected override bool OnShutdown(int timeoutMilliseconds) + { + return this.transmissionHandler.Shutdown(timeoutMilliseconds); } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporterHelperExtensions.cs index 31e8cbbfe2f..45231f63604 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporterHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporterHelperExtensions.cs @@ -1,18 +1,7 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable using System.Diagnostics; using Microsoft.Extensions.DependencyInjection; @@ -21,122 +10,121 @@ using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; using OpenTelemetry.Internal; -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +/// +/// Extension methods to simplify registering of the OpenTelemetry Protocol (OTLP) exporter. +/// +public static class OtlpTraceExporterHelperExtensions { /// - /// Extension methods to simplify registering of the OpenTelemetry Protocol (OTLP) exporter. + /// Adds OpenTelemetry Protocol (OTLP) exporter to the TracerProvider. /// - public static class OtlpTraceExporterHelperExtensions - { - /// - /// Adds OpenTelemetry Protocol (OTLP) exporter to the TracerProvider. - /// - /// builder to use. - /// The instance of to chain the calls. - public static TracerProviderBuilder AddOtlpExporter(this TracerProviderBuilder builder) - => AddOtlpExporter(builder, name: null, configure: null); - - /// - /// Adds OpenTelemetry Protocol (OTLP) exporter to the TracerProvider. - /// - /// builder to use. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static TracerProviderBuilder AddOtlpExporter(this TracerProviderBuilder builder, Action configure) - => AddOtlpExporter(builder, name: null, configure); - - /// - /// Adds OpenTelemetry Protocol (OTLP) exporter to the TracerProvider. - /// - /// builder to use. - /// Name which is used when retrieving options. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static TracerProviderBuilder AddOtlpExporter( - this TracerProviderBuilder builder, - string name, - Action configure) - { - Guard.ThrowIfNull(builder); + /// builder to use. + /// The instance of to chain the calls. + public static TracerProviderBuilder AddOtlpExporter(this TracerProviderBuilder builder) + => AddOtlpExporter(builder, name: null, configure: null); - var finalOptionsName = name ?? Options.DefaultName; - - builder.ConfigureServices(services => - { - if (name != null && configure != null) - { - // If we are using named options we register the - // configuration delegate into options pipeline. - services.Configure(finalOptionsName, configure); - } - - OtlpExporterOptions.RegisterOtlpExporterOptionsFactory(services); - services.RegisterOptionsFactory(configuration => new SdkLimitOptions(configuration)); - }); - - return builder.AddProcessor(sp => - { - OtlpExporterOptions exporterOptions; - - if (name == null) - { - // If we are NOT using named options we create a new - // instance always. The reason for this is - // OtlpExporterOptions is shared by all signals. Without a - // name, delegates for all signals will mix together. See: - // https://github.com/open-telemetry/opentelemetry-dotnet/issues/4043 - exporterOptions = sp.GetRequiredService>().Create(finalOptionsName); - - // Configuration delegate is executed inline on the fresh instance. - configure?.Invoke(exporterOptions); - } - else - { - // When using named options we can properly utilize Options - // API to create or reuse an instance. - exporterOptions = sp.GetRequiredService>().Get(finalOptionsName); - } - - // Note: Not using finalOptionsName here for SdkLimitOptions. - // There should only be one provider for a given service - // collection so SdkLimitOptions is treated as a single default - // instance. - var sdkOptionsManager = sp.GetRequiredService>().CurrentValue; - - return BuildOtlpExporterProcessor(exporterOptions, sdkOptionsManager, sp); - }); - } + /// + /// Adds OpenTelemetry Protocol (OTLP) exporter to the TracerProvider. + /// + /// builder to use. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static TracerProviderBuilder AddOtlpExporter(this TracerProviderBuilder builder, Action configure) + => AddOtlpExporter(builder, name: null, configure); - internal static BaseProcessor BuildOtlpExporterProcessor( - OtlpExporterOptions exporterOptions, - SdkLimitOptions sdkLimitOptions, - IServiceProvider serviceProvider, - Func, BaseExporter> configureExporterInstance = null) - { - exporterOptions.TryEnableIHttpClientFactoryIntegration(serviceProvider, "OtlpTraceExporter"); + /// + /// Adds OpenTelemetry Protocol (OTLP) exporter to the TracerProvider. + /// + /// builder to use. + /// Optional name which is used when retrieving options. + /// Optional callback action for configuring . + /// The instance of to chain the calls. + public static TracerProviderBuilder AddOtlpExporter( + this TracerProviderBuilder builder, + string? name, + Action? configure) + { + Guard.ThrowIfNull(builder); - BaseExporter otlpExporter = new OtlpTraceExporter(exporterOptions, sdkLimitOptions); + var finalOptionsName = name ?? Options.DefaultName; - if (configureExporterInstance != null) + builder.ConfigureServices(services => + { + if (name != null && configure != null) { - otlpExporter = configureExporterInstance(otlpExporter); + // If we are using named options we register the + // configuration delegate into options pipeline. + services.Configure(finalOptionsName, configure); } - if (exporterOptions.ExportProcessorType == ExportProcessorType.Simple) + OtlpExporterOptions.RegisterOtlpExporterOptionsFactory(services); + services.RegisterOptionsFactory(configuration => new SdkLimitOptions(configuration)); + }); + + return builder.AddProcessor(sp => + { + OtlpExporterOptions exporterOptions; + + if (name == null) { - return new SimpleActivityExportProcessor(otlpExporter); + // If we are NOT using named options we create a new + // instance always. The reason for this is + // OtlpExporterOptions is shared by all signals. Without a + // name, delegates for all signals will mix together. See: + // https://github.com/open-telemetry/opentelemetry-dotnet/issues/4043 + exporterOptions = sp.GetRequiredService>().Create(finalOptionsName); + + // Configuration delegate is executed inline on the fresh instance. + configure?.Invoke(exporterOptions); } else { - var batchOptions = exporterOptions.BatchExportProcessorOptions ?? new BatchExportActivityProcessorOptions(); - - return new BatchActivityExportProcessor( - otlpExporter, - batchOptions.MaxQueueSize, - batchOptions.ScheduledDelayMilliseconds, - batchOptions.ExporterTimeoutMilliseconds, - batchOptions.MaxExportBatchSize); + // When using named options we can properly utilize Options + // API to create or reuse an instance. + exporterOptions = sp.GetRequiredService>().Get(finalOptionsName); } + + // Note: Not using finalOptionsName here for SdkLimitOptions. + // There should only be one provider for a given service + // collection so SdkLimitOptions is treated as a single default + // instance. + var sdkOptionsManager = sp.GetRequiredService>().CurrentValue; + + return BuildOtlpExporterProcessor(exporterOptions, sdkOptionsManager, sp); + }); + } + + internal static BaseProcessor BuildOtlpExporterProcessor( + OtlpExporterOptions exporterOptions, + SdkLimitOptions sdkLimitOptions, + IServiceProvider serviceProvider, + Func, BaseExporter>? configureExporterInstance = null) + { + exporterOptions.TryEnableIHttpClientFactoryIntegration(serviceProvider, "OtlpTraceExporter"); + + BaseExporter otlpExporter = new OtlpTraceExporter(exporterOptions, sdkLimitOptions); + + if (configureExporterInstance != null) + { + otlpExporter = configureExporterInstance(otlpExporter); + } + + if (exporterOptions.ExportProcessorType == ExportProcessorType.Simple) + { + return new SimpleActivityExportProcessor(otlpExporter); + } + else + { + var batchOptions = exporterOptions.BatchExportProcessorOptions ?? new BatchExportActivityProcessorOptions(); + + return new BatchActivityExportProcessor( + otlpExporter, + batchOptions.MaxQueueSize, + batchOptions.ScheduledDelayMilliseconds, + batchOptions.ExporterTimeoutMilliseconds, + batchOptions.MaxExportBatchSize); } } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/DirectorySizeTracker.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/DirectorySizeTracker.cs new file mode 100644 index 00000000000..a60715d185b --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/DirectorySizeTracker.cs @@ -0,0 +1,82 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +namespace OpenTelemetry.PersistentStorage.FileSystem; + +/// +/// Tracks the available storage in a specified directory. +/// +internal sealed class DirectorySizeTracker +{ + private readonly long maxSizeInBytes; + private readonly string path; + private long directoryCurrentSizeInBytes; + + public DirectorySizeTracker(long maxSizeInBytes, string path) + { + this.maxSizeInBytes = maxSizeInBytes; + this.path = path; + this.directoryCurrentSizeInBytes = CalculateFolderSize(path); + } + + public void FileAdded(long fileSizeInBytes) => Interlocked.Add(ref this.directoryCurrentSizeInBytes, fileSizeInBytes); + + public void FileRemoved(long fileSizeInBytes) => Interlocked.Add(ref this.directoryCurrentSizeInBytes, fileSizeInBytes * -1); + + /// + /// Checks if the space is available for new blob. + /// + /// + /// This method is not thread safe and may give false positives/negatives. + /// False positive is ok because the file write will eventually fail. + /// False negative is ok as the file write can be retried if needed. + /// This is done in order to avoid acquiring lock while writing/deleting the blobs. + /// + /// Size of blob to be written. + /// True if space is available else false. + public bool IsSpaceAvailable(out long currentSizeInBytes) + { + currentSizeInBytes = Interlocked.Read(ref this.directoryCurrentSizeInBytes); + return currentSizeInBytes < this.maxSizeInBytes; + } + + public void RecountCurrentSize() + { + var size = CalculateFolderSize(this.path); + Interlocked.Exchange(ref this.directoryCurrentSizeInBytes, size); + } + + internal static long CalculateFolderSize(string path) + { + if (!Directory.Exists(path)) + { + return 0; + } + + long directorySize = 0; + try + { + foreach (string file in Directory.EnumerateFiles(path)) + { + if (File.Exists(file)) + { + FileInfo fileInfo = new FileInfo(file); + directorySize += fileInfo.Length; + } + } + + foreach (string dir in Directory.GetDirectories(path)) + { + directorySize += CalculateFolderSize(dir); + } + } + catch (Exception ex) + { + PersistentStorageEventSource.Log.PersistentStorageException(nameof(PersistentStorageHelper), "Error calculating folder size", ex); + } + + return directorySize; + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/FileBlob.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/FileBlob.cs new file mode 100644 index 00000000000..337c3226243 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/FileBlob.cs @@ -0,0 +1,128 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Diagnostics.CodeAnalysis; +using OpenTelemetry.Internal; +using OpenTelemetry.PersistentStorage.Abstractions; + +namespace OpenTelemetry.PersistentStorage.FileSystem; + +/// +/// The allows to save a blob +/// in file storage. +/// + +#if BUILDING_INTERNAL_PERSISTENT_STORAGE +internal sealed class FileBlob : PersistentBlob +#else +public class FileBlob : PersistentBlob +#endif +{ + private readonly DirectorySizeTracker? directorySizeTracker; + + /// + /// Initializes a new instance of the + /// class. + /// + /// Absolute file path of the blob. + public FileBlob(string fullPath) + : this(fullPath, null) + { + } + + internal FileBlob(string fullPath, DirectorySizeTracker? directorySizeTracker) + { + this.FullPath = fullPath; + this.directorySizeTracker = directorySizeTracker; + } + + public string FullPath { get; private set; } + + protected override bool OnTryRead([NotNullWhen(true)] out byte[]? buffer) + { + try + { + buffer = File.ReadAllBytes(this.FullPath); + } + catch (Exception ex) + { + PersistentStorageEventSource.Log.CouldNotReadFileBlob(this.FullPath, ex); + buffer = null; + return false; + } + + return true; + } + + protected override bool OnTryWrite(byte[] buffer, int leasePeriodMilliseconds = 0) + { + Guard.ThrowIfNull(buffer); + + string path = this.FullPath + ".tmp"; + + try + { + PersistentStorageHelper.WriteAllBytes(path, buffer); + + if (leasePeriodMilliseconds > 0) + { + var timestamp = DateTime.UtcNow + TimeSpan.FromMilliseconds(leasePeriodMilliseconds); + this.FullPath += $"@{timestamp:yyyy-MM-ddTHHmmss.fffffffZ}.lock"; + } + + File.Move(path, this.FullPath); + } + catch (Exception ex) + { + PersistentStorageEventSource.Log.CouldNotWriteFileBlob(path, ex); + return false; + } + + this.directorySizeTracker?.FileAdded(buffer.LongLength); + return true; + } + + protected override bool OnTryLease(int leasePeriodMilliseconds) + { + var path = this.FullPath; + var leaseTimestamp = DateTime.UtcNow + TimeSpan.FromMilliseconds(leasePeriodMilliseconds); + if (path.EndsWith(".lock", StringComparison.OrdinalIgnoreCase)) + { + path = path.Substring(0, path.LastIndexOf('@')); + } + + path += $"@{leaseTimestamp:yyyy-MM-ddTHHmmss.fffffffZ}.lock"; + + try + { + File.Move(this.FullPath, path); + } + catch (Exception ex) + { + PersistentStorageEventSource.Log.CouldNotLeaseFileBlob(this.FullPath, ex); + return false; + } + + this.FullPath = path; + + return true; + } + + protected override bool OnTryDelete() + { + try + { + PersistentStorageHelper.RemoveFile(this.FullPath, out var fileSize); + this.directorySizeTracker?.FileRemoved(fileSize); + } + catch (Exception ex) + { + PersistentStorageEventSource.Log.CouldNotDeleteFileBlob(this.FullPath, ex); + return false; + } + + return true; + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/FileBlobProvider.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/FileBlobProvider.cs new file mode 100644 index 00000000000..c9afdf34ce8 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/FileBlobProvider.cs @@ -0,0 +1,218 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Diagnostics.CodeAnalysis; +using System.Timers; +using OpenTelemetry.Internal; +using OpenTelemetry.PersistentStorage.Abstractions; +using Timer = System.Timers.Timer; + +namespace OpenTelemetry.PersistentStorage.FileSystem; + +/// +/// Persistent file storage allows to save data +/// as blobs in file storage. +/// + +#if BUILDING_INTERNAL_PERSISTENT_STORAGE +internal sealed class FileBlobProvider : PersistentBlobProvider, IDisposable +#else +public class FileBlobProvider : PersistentBlobProvider, IDisposable +#endif +{ + internal readonly string DirectoryPath; + private readonly DirectorySizeTracker directorySizeTracker; + private readonly long retentionPeriodInMilliseconds; + private readonly int writeTimeoutInMilliseconds; + private readonly Timer maintenanceTimer; + private bool disposedValue; + + /// + /// Initializes a new instance of the + /// class. + /// + /// + /// Sets file storage folder location where blobs are stored. + /// + /// + /// Maximum allowed storage folder size. + /// Default is 50 MB. + /// + /// + /// Maintenance event runs at specified interval. + /// Removes expired leases and blobs that exceed retention period. + /// Default is 2 minutes. + /// + /// + /// Retention period in milliseconds for the blob. + /// Default is 2 days. + /// + /// + /// Controls the timeout when writing a buffer to blob. + /// Default is 1 minute. + /// + /// + /// path is null. + /// + /// + /// invalid path. + /// + /// + /// path exceeds system defined maximum length. + /// + /// + /// insufficient priviledges for provided path. + /// + /// + /// path contains a colon character (:) that is not part of a drive label ("C:\"). + /// + /// + /// path contains invalid characters. + /// + /// + /// path is either file or network name is not known. + /// + public FileBlobProvider( + string path, + long maxSizeInBytes = 52428800, + int maintenancePeriodInMilliseconds = 120000, + long retentionPeriodInMilliseconds = 172800000, + int writeTimeoutInMilliseconds = 60000) + { + Guard.ThrowIfNull(path); + + // TODO: Validate time period values + this.DirectoryPath = PersistentStorageHelper.CreateSubdirectory(path); + this.directorySizeTracker = new DirectorySizeTracker(maxSizeInBytes, path); + this.retentionPeriodInMilliseconds = retentionPeriodInMilliseconds; + this.writeTimeoutInMilliseconds = writeTimeoutInMilliseconds; + + this.maintenanceTimer = new Timer(maintenancePeriodInMilliseconds); + this.maintenanceTimer.Elapsed += this.OnMaintenanceEvent; + this.maintenanceTimer.AutoReset = true; + this.maintenanceTimer.Enabled = true; + } + + public void Dispose() + { + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected override IEnumerable OnGetBlobs() + { + var retentionDeadline = DateTime.UtcNow - TimeSpan.FromMilliseconds(this.retentionPeriodInMilliseconds); + + foreach (var file in Directory.EnumerateFiles(this.DirectoryPath, "*.blob", SearchOption.TopDirectoryOnly).OrderByDescending(f => f)) + { + DateTime fileDateTime = PersistentStorageHelper.GetDateTimeFromBlobName(file); + if (fileDateTime > retentionDeadline) + { + yield return new FileBlob(file, this.directorySizeTracker); + } + } + } + + protected override bool OnTryCreateBlob(byte[] buffer, int leasePeriodMilliseconds, [NotNullWhen(true)] out PersistentBlob? blob) + { + blob = this.CreateFileBlob(buffer, leasePeriodMilliseconds); + + return blob != null; + } + + protected override bool OnTryCreateBlob(byte[] buffer, [NotNullWhen(true)] out PersistentBlob? blob) + { + blob = this.CreateFileBlob(buffer); + + return blob != null; + } + + protected override bool OnTryGetBlob([NotNullWhen(true)] out PersistentBlob? blob) + { + blob = this.OnGetBlobs().FirstOrDefault(); + + return blob != null; + } + +#if BUILDING_INTERNAL_PERSISTENT_STORAGE + private void Dispose(bool disposing) +#else + protected virtual void Dispose(bool disposing) +#endif + { + if (!this.disposedValue) + { + if (disposing) + { + this.maintenanceTimer.Dispose(); + } + + this.disposedValue = true; + } + } + + private void OnMaintenanceEvent(object? source, ElapsedEventArgs e) + { + try + { + if (!Directory.Exists(this.DirectoryPath)) + { + Directory.CreateDirectory(this.DirectoryPath); + } + } + catch (Exception ex) + { + PersistentStorageEventSource.Log.PersistentStorageException(nameof(FileBlobProvider), $"Error creating directory {this.DirectoryPath}", ex); + return; + } + + PersistentStorageHelper.RemoveExpiredBlobs(this.DirectoryPath, this.retentionPeriodInMilliseconds, this.writeTimeoutInMilliseconds); + + // It is faster to calculate the directory size, instead of removing length of expired files. + this.directorySizeTracker.RecountCurrentSize(); + } + + private bool CheckStorageSize() + { + if (!this.directorySizeTracker.IsSpaceAvailable(out long size)) + { + // TODO: check accuracy of size reporting. + PersistentStorageEventSource.Log.PersistentStorageWarning( + nameof(FileBlobProvider), + $"Persistent storage max capacity has been reached. Currently at {size / 1024} KiB. Please consider increasing the value of storage max size in exporter config."); + return false; + } + + return true; + } + + private PersistentBlob? CreateFileBlob(byte[] buffer, int leasePeriodMilliseconds = 0) + { + if (!this.CheckStorageSize()) + { + return null; + } + + try + { + var blobFilePath = Path.Combine(this.DirectoryPath, PersistentStorageHelper.GetUniqueFileName(".blob")); + var blob = new FileBlob(blobFilePath, this.directorySizeTracker); + + if (blob.TryWrite(buffer, leasePeriodMilliseconds)) + { + return blob; + } + else + { + return null; + } + } + catch (Exception ex) + { + PersistentStorageEventSource.Log.CouldNotCreateFileBlob(ex); + return null; + } + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentBlob.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentBlob.cs new file mode 100644 index 00000000000..2f0912feb75 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentBlob.cs @@ -0,0 +1,115 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Diagnostics.CodeAnalysis; + +namespace OpenTelemetry.PersistentStorage.Abstractions; + +/// +/// Represents a persistent blob. +/// +#if BUILDING_INTERNAL_PERSISTENT_STORAGE +internal abstract class PersistentBlob +#else +public abstract class PersistentBlob +#endif +{ + /// + /// Attempts to read the content from the blob. + /// + /// + /// The content to be read. + /// + /// + /// True if read was successful or else false. + /// + public bool TryRead([NotNullWhen(true)] out byte[]? buffer) + { + try + { + return this.OnTryRead(out buffer); + } + catch (Exception ex) + { + PersistentStorageAbstractionsEventSource.Log.PersistentStorageAbstractionsException(nameof(PersistentBlob), "Failed to read the blob.", ex); + buffer = null; + return false; + } + } + + /// + /// Attempts to write the given content to the blob. + /// + /// + /// The content to be written. + /// + /// + /// The number of milliseconds to lease after the write operation finished. + /// + /// + /// True if the write operation succeeded or else false. + /// + public bool TryWrite(byte[] buffer, int leasePeriodMilliseconds = 0) + { + try + { + return this.OnTryWrite(buffer, leasePeriodMilliseconds); + } + catch (Exception ex) + { + PersistentStorageAbstractionsEventSource.Log.PersistentStorageAbstractionsException(nameof(PersistentBlob), "Failed to write the blob", ex); + return false; + } + } + + /// + /// Attempts to acquire lease on the blob. + /// + /// + /// The number of milliseconds to lease. + /// + /// + /// true if lease is acquired or else false. + /// + public bool TryLease(int leasePeriodMilliseconds) + { + try + { + return this.OnTryLease(leasePeriodMilliseconds); + } + catch (Exception ex) + { + PersistentStorageAbstractionsEventSource.Log.PersistentStorageAbstractionsException(nameof(PersistentBlob), "Failed to lease the blob", ex); + return false; + } + } + + /// + /// Attempts to delete the blob. + /// + /// + /// True if delete was successful else false. + /// + public bool TryDelete() + { + try + { + return this.OnTryDelete(); + } + catch (Exception ex) + { + PersistentStorageAbstractionsEventSource.Log.PersistentStorageAbstractionsException(nameof(PersistentBlob), "Failed to delete the blob", ex); + return false; + } + } + + protected abstract bool OnTryRead([NotNullWhen(true)] out byte[]? buffer); + + protected abstract bool OnTryWrite(byte[] buffer, int leasePeriodMilliseconds = 0); + + protected abstract bool OnTryLease(int leasePeriodMilliseconds); + + protected abstract bool OnTryDelete(); +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentBlobProvider.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentBlobProvider.cs new file mode 100644 index 00000000000..5d4895f8d46 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentBlobProvider.cs @@ -0,0 +1,123 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Diagnostics.CodeAnalysis; + +namespace OpenTelemetry.PersistentStorage.Abstractions; + +/// +/// Represents persistent blob provider. +/// +#if BUILDING_INTERNAL_PERSISTENT_STORAGE +internal abstract class PersistentBlobProvider +#else +public abstract class PersistentBlobProvider +#endif +{ + /// + /// Attempts to create a new blob with the provided data and lease it. + /// + /// + /// The content to be written. + /// + /// + /// The number of milliseconds to lease after the blob is created. + /// + /// + /// Blob if it is created. + /// + /// + /// True if the blob was created or else false. + /// + public bool TryCreateBlob(byte[] buffer, int leasePeriodMilliseconds, [NotNullWhen(true)] out PersistentBlob? blob) + { + try + { + return this.OnTryCreateBlob(buffer, leasePeriodMilliseconds, out blob); + } + catch (Exception ex) + { + PersistentStorageAbstractionsEventSource.Log.PersistentStorageAbstractionsException(nameof(PersistentBlobProvider), "Failed to create and lease the blob", ex); + blob = null; + return false; + } + } + + /// + /// Attempts to create a new blob with the provided data. + /// + /// + /// The content to be written. + /// + /// + /// Blob if it is created. + /// + /// + /// True if the blob was created or else false. + /// + public bool TryCreateBlob(byte[] buffer, [NotNullWhen(true)] out PersistentBlob? blob) + { + try + { + return this.OnTryCreateBlob(buffer, out blob); + } + catch (Exception ex) + { + PersistentStorageAbstractionsEventSource.Log.PersistentStorageAbstractionsException(nameof(PersistentBlobProvider), "Failed to create the blob", ex); + blob = null; + return false; + } + } + + /// + /// Attempts to get a single blob from storage. + /// + /// + /// Blob object if found. + /// + /// + /// True if blob is present or else false. + /// + public bool TryGetBlob([NotNullWhen(true)] out PersistentBlob? blob) + { + try + { + return this.OnTryGetBlob(out blob); + } + catch (Exception ex) + { + PersistentStorageAbstractionsEventSource.Log.PersistentStorageAbstractionsException(nameof(PersistentBlobProvider), "Failed to get a single blob", ex); + blob = null; + return false; + } + } + + /// + /// Reads a sequence of blobs from storage. + /// + /// + /// List of blobs if present in storage or else empty collection. + /// + public IEnumerable GetBlobs() + { + try + { + return this.OnGetBlobs() ?? Enumerable.Empty(); + } + catch (Exception ex) + { + PersistentStorageAbstractionsEventSource.Log.PersistentStorageAbstractionsException(nameof(PersistentBlobProvider), "Failed to get all the blobs", ex); + return Enumerable.Empty(); + } + } + + protected abstract IEnumerable OnGetBlobs(); + + protected abstract bool OnTryCreateBlob(byte[] buffer, int leasePeriodMilliseconds, [NotNullWhen(true)] out PersistentBlob? blob); + + protected abstract bool OnTryCreateBlob(byte[] buffer, [NotNullWhen(true)] out PersistentBlob? blob); + + protected abstract bool OnTryGetBlob([NotNullWhen(true)] out PersistentBlob? blob); +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentStorageAbstractionsEventSource.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentStorageAbstractionsEventSource.cs new file mode 100644 index 00000000000..0c8562ce319 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentStorageAbstractionsEventSource.cs @@ -0,0 +1,33 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics.Tracing; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.PersistentStorage.Abstractions; + +[EventSource(Name = EventSourceName)] +internal sealed class PersistentStorageAbstractionsEventSource : EventSource +{ + public static PersistentStorageAbstractionsEventSource Log = new PersistentStorageAbstractionsEventSource(); +#if BUILDING_INTERNAL_PERSISTENT_STORAGE + private const string EventSourceName = "OpenTelemetry-PersistentStorage-Abstractions-Otlp"; +#else + private const string EventSourceName = "OpenTelemetry-PersistentStorage-Abstractions"; +#endif + + [NonEvent] + public void PersistentStorageAbstractionsException(string className, string message, Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + { + this.PersistentStorageAbstractionsException(className, message, ex.ToInvariantString()); + } + } + + [Event(1, Message = "{0}: {1}: {2}", Level = EventLevel.Error)] + public void PersistentStorageAbstractionsException(string className, string message, string ex) + { + this.WriteEvent(1, className, message, ex); + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentStorageEventSource.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentStorageEventSource.cs new file mode 100644 index 00000000000..241023b2f89 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentStorageEventSource.cs @@ -0,0 +1,165 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics.Tracing; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.PersistentStorage.FileSystem; + +[EventSource(Name = EventSourceName)] +internal sealed class PersistentStorageEventSource : EventSource +{ + public static PersistentStorageEventSource Log = new PersistentStorageEventSource(); +#if BUILDING_INTERNAL_PERSISTENT_STORAGE + private const string EventSourceName = "OpenTelemetry-PersistentStorage-FileSystem-Otlp"; +#else + private const string EventSourceName = "OpenTelemetry-PersistentStorage-FileSystem"; +#endif + + [NonEvent] + public void CouldNotReadFileBlob(string filePath, Exception ex) + { + if (this.IsEnabled(EventLevel.Informational, EventKeywords.All)) + { + this.CouldNotReadFileBlob(filePath, ex.ToInvariantString()); + } + } + + [NonEvent] + public void CouldNotWriteFileBlob(string filePath, Exception ex) + { + if (this.IsEnabled(EventLevel.Informational, EventKeywords.All)) + { + this.CouldNotWriteFileBlob(filePath, ex.ToInvariantString()); + } + } + + [NonEvent] + public void CouldNotLeaseFileBlob(string filePath, Exception ex) + { + if (this.IsEnabled(EventLevel.Informational, EventKeywords.All)) + { + this.CouldNotLeaseFileBlob(filePath, ex.ToInvariantString()); + } + } + + [NonEvent] + public void CouldNotDeleteFileBlob(string filePath, Exception ex) + { + if (this.IsEnabled(EventLevel.Informational, EventKeywords.All)) + { + this.CouldNotDeleteFileBlob(filePath, ex.ToInvariantString()); + } + } + + [NonEvent] + public void CouldNotCreateFileBlob(Exception ex) + { + if (this.IsEnabled(EventLevel.Informational, EventKeywords.All)) + { + this.CouldNotCreateFileBlob(ex.ToInvariantString()); + } + } + + [NonEvent] + public void CouldNotRemoveExpiredBlob(string filePath, Exception ex) + { + if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) + { + this.CouldNotRemoveExpiredBlob(filePath, ex.ToInvariantString()); + } + } + + [NonEvent] + public void CouldNotRemoveTimedOutTmpFile(string filePath, Exception ex) + { + if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) + { + this.CouldNotRemoveTimedOutTmpFile(filePath, ex.ToInvariantString()); + } + } + + [NonEvent] + public void CouldNotRemoveExpiredLease(string srcFilePath, string destFilePath, Exception ex) + { + if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) + { + this.CouldNotRemoveExpiredLease(srcFilePath, destFilePath, ex.ToInvariantString()); + } + } + + [NonEvent] + public void PersistentStorageException(string className, string message, Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + { + this.PersistentStorageException(className, message, ex.ToInvariantString()); + } + } + + [Event(1, Message = "Could not read blob from file '{0}'", Level = EventLevel.Informational)] + public void CouldNotReadFileBlob(string filePath, string ex) + { + this.WriteEvent(1, filePath, ex); + } + + [Event(2, Message = "Could not write blob to file '{0}'", Level = EventLevel.Informational)] + public void CouldNotWriteFileBlob(string filePath, string ex) + { + this.WriteEvent(2, filePath, ex); + } + + [Event(3, Message = "Could not acquire a lease on file '{0}'", Level = EventLevel.Informational)] + public void CouldNotLeaseFileBlob(string filePath, string ex) + { + this.WriteEvent(3, filePath, ex); + } + + [Event(4, Message = "Could not delete file '{0}'", Level = EventLevel.Informational)] + public void CouldNotDeleteFileBlob(string filePath, string ex) + { + this.WriteEvent(4, filePath, ex); + } + + [Event(5, Message = "Could not create file blob", Level = EventLevel.Informational)] + public void CouldNotCreateFileBlob(string ex) + { + this.WriteEvent(5, ex); + } + + [Event(6, Message = "Could not remove expired blob '{0}'", Level = EventLevel.Warning)] + public void CouldNotRemoveExpiredBlob(string filePath, string ex) + { + this.WriteEvent(6, filePath, ex); + } + + [Event(7, Message = "Could not remove timed out file '{0}'", Level = EventLevel.Warning)] + public void CouldNotRemoveTimedOutTmpFile(string filePath, string ex) + { + this.WriteEvent(7, filePath, ex); + } + + [Event(8, Message = "Could not rename '{0}' to '{1}'", Level = EventLevel.Warning)] + public void CouldNotRemoveExpiredLease(string srcFilePath, string destFilePath, string ex) + { + this.WriteEvent(8, srcFilePath, destFilePath, ex); + } + + [Event(9, Message = "{0}: Error Message: {1}. Exception: {3}", Level = EventLevel.Error)] + public void PersistentStorageException(string className, string message, string ex) + { + this.WriteEvent(9, className, message, ex); + } + + [Event(10, Message = "{0}: Warning Message: {1}", Level = EventLevel.Warning)] + public void PersistentStorageWarning(string className, string message) + { + this.WriteEvent(10, className, message); + } + + [Event(11, Message = "{0}: Message: {1}", Level = EventLevel.Informational)] + public void PersistentStorageInformation(string className, string message) + { + this.WriteEvent(11, className, message); + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentStorageHelper.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentStorageHelper.cs new file mode 100644 index 00000000000..538597dc18f --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentStorageHelper.cs @@ -0,0 +1,153 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Globalization; +using System.Runtime.CompilerServices; + +namespace OpenTelemetry.PersistentStorage.FileSystem; + +internal static class PersistentStorageHelper +{ + internal static void RemoveExpiredBlob(DateTime retentionDeadline, string filePath) + { + if (filePath.EndsWith(".blob", StringComparison.OrdinalIgnoreCase)) + { + DateTime fileDateTime = GetDateTimeFromBlobName(filePath); + if (fileDateTime < retentionDeadline) + { + try + { + File.Delete(filePath); + PersistentStorageEventSource.Log.PersistentStorageInformation(nameof(PersistentStorageHelper), "Removing blob as retention deadline expired"); + } + catch (Exception ex) + { + PersistentStorageEventSource.Log.CouldNotRemoveExpiredBlob(filePath, ex); + } + } + } + } + + internal static bool RemoveExpiredLease(DateTime leaseDeadline, string filePath) + { + bool success = false; + + if (filePath.EndsWith(".lock", StringComparison.OrdinalIgnoreCase)) + { + DateTime fileDateTime = GetDateTimeFromLeaseName(filePath); + if (fileDateTime < leaseDeadline) + { + var newFilePath = filePath.Substring(0, filePath.LastIndexOf('@')); + try + { + File.Move(filePath, newFilePath); + success = true; + } + catch (Exception ex) + { + PersistentStorageEventSource.Log.CouldNotRemoveExpiredLease(filePath, newFilePath, ex); + } + } + } + + return success; + } + + internal static bool RemoveTimedOutTmpFiles(DateTime timeoutDeadline, string filePath) + { + bool success = false; + + if (filePath.EndsWith(".tmp", StringComparison.OrdinalIgnoreCase)) + { + DateTime fileDateTime = GetDateTimeFromBlobName(filePath); + if (fileDateTime < timeoutDeadline) + { + try + { + File.Delete(filePath); + success = true; + PersistentStorageEventSource.Log.PersistentStorageInformation(nameof(PersistentStorageHelper), "File write exceeded timeout. Dropping telemetry"); + } + catch (Exception ex) + { + PersistentStorageEventSource.Log.CouldNotRemoveTimedOutTmpFile(filePath, ex); + } + } + } + + return success; + } + + internal static void RemoveExpiredBlobs(string directoryPath, long retentionPeriodInMilliseconds, long writeTimeoutInMilliseconds) + { + var currentUtcDateTime = DateTime.UtcNow; + + var leaseDeadline = currentUtcDateTime; + var retentionDeadline = currentUtcDateTime - TimeSpan.FromMilliseconds(retentionPeriodInMilliseconds); + var timeoutDeadline = currentUtcDateTime - TimeSpan.FromMilliseconds(writeTimeoutInMilliseconds); + + foreach (var file in Directory.EnumerateFiles(directoryPath).OrderByDescending(filename => filename)) + { + var success = RemoveTimedOutTmpFiles(timeoutDeadline, file); + + if (success) + { + continue; + } + + success = RemoveExpiredLease(leaseDeadline, file); + + if (!success) + { + RemoveExpiredBlob(retentionDeadline, file); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void WriteAllBytes(string path, byte[] buffer) + { + File.WriteAllBytes(path, buffer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void RemoveFile(string fileName, out long fileSize) + { + var fileInfo = new FileInfo(fileName); + fileSize = fileInfo.Length; + fileInfo.Delete(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static string GetUniqueFileName(string extension) + { + return string.Format(CultureInfo.InvariantCulture, $"{DateTime.UtcNow:yyyy-MM-ddTHHmmss.fffffffZ}-{Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)}{extension}"); + } + + internal static string CreateSubdirectory(string path) + { + Directory.CreateDirectory(path); + return path; + } + + internal static DateTime GetDateTimeFromBlobName(string filePath) + { + var fileName = Path.GetFileNameWithoutExtension(filePath); + var time = fileName.Substring(0, fileName.LastIndexOf('-')); + + // TODO:Handle possible parsing failure. + DateTime.TryParseExact(time, "yyyy-MM-ddTHHmmss.fffffffZ", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateTime); + return dateTime.ToUniversalTime(); + } + + internal static DateTime GetDateTimeFromLeaseName(string filePath) + { + var fileName = Path.GetFileNameWithoutExtension(filePath); + var startIndex = fileName.LastIndexOf('@') + 1; + var time = fileName.Substring(startIndex, fileName.Length - startIndex); + DateTime.TryParseExact(time, "yyyy-MM-ddTHHmmss.fffffffZ", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateTime); + return dateTime.ToUniversalTime(); + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/README.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/README.md new file mode 100644 index 00000000000..018aef5b4a8 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/README.md @@ -0,0 +1,14 @@ +# Persistent Storage APIs for OTLP Exporter + +The files in this folder have been copied over from +[OpenTelemetry.PersistentStorage.Abstractions](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/tree/58607b7cdeb15207027a6fa4ca56e7fac897bda4/src/OpenTelemetry.PersistentStorage.Abstractions) +and +[OpenTelemetry.PersistentStorage.FileSystem](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/tree/58607b7cdeb15207027a6fa4ca56e7fac897bda4/src/OpenTelemetry.PersistentStorage.FileSystem). +Any code changes in this folder MUST go through changes in the original location +i.e. in the contrib repo. Here is the sequence of steps to be followed when +making changes: + +1. Open a PR for proposed changes in + [Contrib](https://github.com/open-telemetry/opentelemetry-dotnet-contrib). +2. Once the proposed changed is accepted and merged, copy the changes over to + this folder and open a new PR. diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md index d5f213602a8..8dd42301e23 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md @@ -58,37 +58,106 @@ environment variables. ## Enable Log Exporter -// TODO +```csharp +var loggerFactory = LoggerFactory.Create(builder => +{ + builder.AddOpenTelemetry(options => + { + options.AddOtlpExporter(); + }); +}); +``` + +By default, `AddOtlpExporter()` pairs the OTLP Log Exporter with a [batching +processor](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/sdk.md#batching-processor). +See [`TestLogs.cs`](../../examples/Console/TestLogs.cs) for example on how to +customize the `LogRecordExportProcessorOptions` or see the [Environment +Variables](#environment-variables) section below on how to customize using +environment variables. + +> [!NOTE] +> For details on how to configure logging with OpenTelemetry check the +[Console](../../docs/logs/getting-started-console/README.md) or [ASP.NET +Core](../../docs/logs/getting-started-aspnetcore/README.md) tutorial. + +**ILogger Scopes**: OTLP Log Exporter supports exporting `ILogger` scopes as +Attributes. Scopes must be enabled at the SDK level using +[IncludeScopes](../../docs/logs/customizing-the-sdk/Readme.md#includescopes) +setting on `OpenTelemetryLoggerOptions`. + +> [!NOTE] +> Scope attributes with key set as empty string or `{OriginalFormat}` +are ignored by exporter. Duplicate keys are exported as is. ## Configuration You can configure the `OtlpExporter` through `OtlpExporterOptions` and environment variables. -The `OtlpExporterOptions` type setters take precedence over the environment variables. -This can be achieved by providing an `Action` delegate to the -`AddOtlpExporter()` method or using `AddOptions()`. +> [!NOTE] +> The `OtlpExporterOptions` type setters take precedence over the environment variables. -If additional services from the dependency injection are required, they can be -configured like this: +This can be achieved by providing an `Action` delegate to +the `AddOtlpExporter()` method or using the `Configure()` +Options API extension: ```csharp -services.AddOptions().Configure((opts, svc) => { +// Set via delegate using code: +appBuilder.Services.AddOpenTelemetry() + .WithTracing(builder => builder.AddOtlpExporter(o => { + // ... + })); + +// Set via Options API using code: +appBuilder.Services.Configure(o => { // ... }); -``` -TODO: Show metrics specific configuration (i.e MetricReaderOptions). +// Set via Options API using configuration: +appBuilder.Services.Configure( + appBuilder.Configuration.GetSection("OpenTelemetry:otlp")); +``` -## OtlpExporterOptions +If additional services from the dependency injection are required for +configuration they can be accessed through the Options API like this: -* `ExportProcessorType`: Whether the exporter should use [Batch or Simple - exporting - processor](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#built-in-span-processors). - The default is Batch. +```csharp +// Step 1: Register user-created configuration service. +appBuilder.Services.AddSingleton(); + +// Step 2: Use Options API to configure OtlpExporterOptions with user-created service. +appBuilder.Services.AddOptions() + .Configure((o, configService) => { + o.Endpoint = configService.ResolveOtlpExporterEndpoint(); + }); +``` -* `BatchExportProcessorOptions`: Configuration options for the batch exporter. - Only used if ExportProcessorType is set to Batch. +> [!NOTE] +> The `OtlpExporterOptions` class is shared by logging, metrics, and tracing. To +> bind configuration specific to each signal use the `name` parameter on the +> `AddOtlpExporter` extensions: +> +> ```csharp +> // Step 1: Bind options to config using the name parameter. +> appBuilder.Services.Configure("tracing", appBuilder.Configuration.GetSection("OpenTelemetry:tracing:otlp")); +> appBuilder.Services.Configure("metrics", appBuilder.Configuration.GetSection("OpenTelemetry:metrics:otlp")); +> appBuilder.Services.Configure("logging", appBuilder.Configuration.GetSection("OpenTelemetry:logging:otlp")); +> +> // Step 2: Register OtlpExporter using the name parameter. +> appBuilder.Services.AddOpenTelemetry() +> .WithTracing(builder => builder.AddOtlpExporter("tracing", configure: null)) +> .WithMetrics(builder => builder.AddOtlpExporter("metrics", configure: null)); +> +> appBuilder.Logging.AddOpenTelemetry(builder => builder.AddOtlpExporter( +> "logging", +> options => +> { +> // Note: Options can also be set via code but order is important. In the example here the code will apply after configuration. +> options.Endpoint = new Uri("http://localhost/logs"); +> })); +> ``` + +### OtlpExporterOptions * `Protocol`: OTLP transport protocol. Supported values: `OtlpExportProtocol.Grpc` and `OtlpExportProtocol.HttpProtobuf`. @@ -109,9 +178,71 @@ TODO: Show metrics specific configuration (i.e MetricReaderOptions). * `TimeoutMilliseconds` : Max waiting time for the backend to process a batch. +The following options are only applicable to `OtlpTraceExporter`: + +* `ExportProcessorType`: Whether the exporter should use [Batch or Simple + exporting + processor](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#built-in-span-processors). + The default is Batch. + +* `BatchExportProcessorOptions`: Configuration options for the batch exporter. + Only used if ExportProcessorType is set to Batch. + See the [`TestOtlpExporter.cs`](../../examples/Console/TestOtlpExporter.cs) for an example of how to use the exporter. +### LogRecordExportProcessorOptions + +The `LogRecordExportProcessorOptions` class may be used to configure processor & +batch settings for logging: + +```csharp +// Set via delegate using code: +appBuilder.Logging.AddOpenTelemetry(options => +{ + options.AddOtlpExporter((exporterOptions, processorOptions) => + { + processorOptions.BatchExportProcessorOptions.ScheduledDelayMilliseconds = 2000; + processorOptions.BatchExportProcessorOptions.MaxExportBatchSize = 5000; + }); +}); + +// Set via Options API using code: +appBuilder.Services.Configure(o => +{ + o.BatchExportProcessorOptions.ScheduledDelayMilliseconds = 2000; + o.BatchExportProcessorOptions.MaxExportBatchSize = 5000; +}); + +// Set via Options API using configuration: +appBuilder.Services.Configure( + appBuilder.Configuration.GetSection("OpenTelemetry:Logging")); +``` + +### MetricReaderOptions + +The `MetricReaderOptions` class may be used to configure reader settings for +metrics: + +```csharp +// Set via delegate using code: +appBuilder.Services.AddOpenTelemetry() + .WithMetrics(builder => builder.AddOtlpExporter((exporterOptions, readerOptions) => + { + readerOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 10_000; + })); + +// Set via Options API using code: +appBuilder.Services.Configure(o => +{ + o.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 10_000; +}); + +// Set via Options API using configuration: +appBuilder.Services.Configure( + appBuilder.Configuration.GetSection("OpenTelemetry:Metrics")); +``` + ## Environment Variables The following environment variables can be used to override the default @@ -125,27 +256,53 @@ values of the `OtlpExporterOptions` | `OTEL_EXPORTER_OTLP_TIMEOUT` | `TimeoutMilliseconds` | | `OTEL_EXPORTER_OTLP_PROTOCOL` | `Protocol` (`grpc` or `http/protobuf`)| -The following environment variables can be used to override the default -values of the `PeriodicExportingMetricReaderOptions` -(following the [OpenTelemetry specification](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.12.0/specification/sdk-environment-variables.md#periodic-exporting-metricreader). +The following environment variables can be used to override the default values +for `BatchExportProcessorOptions` in case of `OtlpTraceExporter` (following the +[OpenTelemetry +specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md#batch-span-processor)) + +| Environment variable | `OtlpExporterOptions.BatchExportProcessorOptions` property | +| ---------------------------------| ------------------------------------------------------------| +| `OTEL_BSP_SCHEDULE_DELAY` | `ScheduledDelayMilliseconds` | +| `OTEL_BSP_EXPORT_TIMEOUT` | `ExporterTimeoutMilliseconds` | +| `OTEL_BSP_MAX_QUEUE_SIZE` | `MaxQueueSize` | +| `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` | `MaxExportBatchSize` | + +The following environment variables can be used to override the default values +for `BatchExportProcessorOptions` in case of `OtlpLogExporter` (following the +[OpenTelemetry +specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md#batch-logrecord-processor)) + +| Environment variable | `LogRecordExportProcessorOptions.BatchExportProcessorOptions` property | +| ----------------------------------| ------------------------------------------------------------------------| +| `OTEL_BLRP_SCHEDULE_DELAY` | `ScheduledDelayMilliseconds` | +| `OTEL_BLRP_EXPORT_TIMEOUT` | `ExporterTimeoutMilliseconds` | +| `OTEL_BLRP_MAX_QUEUE_SIZE` | `MaxQueueSize` | +| `OTEL_BLRP_MAX_EXPORT_BATCH_SIZE` | `MaxExportBatchSize` | + +The following environment variables can be used to override the default values +of the `PeriodicExportingMetricReaderOptions` (following the [OpenTelemetry +specification](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.12.0/specification/sdk-environment-variables.md#periodic-exporting-metricreader). | Environment variable | `PeriodicExportingMetricReaderOptions` property | | ----------------------------------------------------| ------------------------------------------------| | `OTEL_METRIC_EXPORT_INTERVAL` | `ExportIntervalMilliseconds` | | `OTEL_METRIC_EXPORT_TIMEOUT` | `ExportTimeoutMilliseconds` | -NOTE: `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` is not supported yet [#3756](https://github.com/open-telemetry/opentelemetry-dotnet/issues/3756). +| Environment variable | `MetricReaderOptions` property | +| ----------------------------------------------------| ------------------------------------------------| +| `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` | `PeriodicExportingMetricReaderOptions` | The following environment variables can be used to override the default values of the attribute limits -(following the [OpenTelemetry specification](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.15.0/specification/sdk-environment-variables.md#attribute-limits)). +(following the [OpenTelemetry specification](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.25.0/specification/configuration/sdk-environment-variables.md#attribute-limits)). * `OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT` * `OTEL_ATTRIBUTE_COUNT_LIMIT` The following environment variables can be used to override the default values of the span limits -(following the [OpenTelemetry specification](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.15.0/specification/sdk-environment-variables.md#span-limits)). +(following the [OpenTelemetry specification](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.25.0/specification/configuration/sdk-environment-variables.md#span-limits)). * `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` * `OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT` @@ -154,6 +311,22 @@ values of the span limits * `OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT` * `OTEL_LINK_ATTRIBUTE_COUNT_LIMIT` +The following environment variables can be used to override the default +values of the log record limits +(following the [OpenTelemetry specification](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.25.0/specification/configuration/sdk-environment-variables.md#logrecord-limits)). + +* `OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT` +* `OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT` + +## Environment Variables for Experimental Features + +### Otlp Log Exporter + +* `OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES` + +When set to `true`, it enables export of `LogRecord.EventId.Id` as +`logrecord.event.id` and `LogRecord.EventId.Name` to `logrecord.event.name`. + ## Configure HttpClient The `HttpClientFactory` option is provided on `OtlpExporterOptions` for users @@ -177,6 +350,14 @@ services.AddOpenTelemetry() })); ``` +> [!NOTE] +> `DefaultRequestHeaders` can be used for [HTTP Basic Access +Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication). For +more complex authentication requirements, +[`System.Net.Http.DelegatingHandler`](https://learn.microsoft.com/dotnet/api/system.net.http.delegatinghandler) +can be used to handle token refresh, as explained +[here](https://stackoverflow.com/questions/56204350/how-to-refresh-a-token-using-ihttpclientfactory). + For users using [IHttpClientFactory](https://docs.microsoft.com/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests) you may also customize the named "OtlpTraceExporter" and/or "OtlpMetricExporter" @@ -189,8 +370,9 @@ services.AddHttpClient( client.DefaultRequestHeaders.Add("X-MyCustomHeader", "value")); ``` -Note: The single instance returned by `HttpClientFactory` is reused by all -export requests. +> [!NOTE] +> The single instance returned by `HttpClientFactory` is reused by all export +requests. ## Troubleshooting diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Grpc/.publicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Shipped.txt similarity index 100% rename from src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Grpc/.publicApi/netstandard2.0/PublicAPI.Shipped.txt rename to src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Shipped.txt diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/net6.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Unshipped.txt similarity index 95% rename from src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/net6.0/PublicAPI.Unshipped.txt rename to src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Unshipped.txt index 0dd120b12da..99d1e9e4939 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/net6.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Unshipped.txt @@ -1,6 +1,8 @@ Microsoft.AspNetCore.Builder.PrometheusExporterApplicationBuilderExtensions Microsoft.AspNetCore.Builder.PrometheusExporterEndpointRouteBuilderExtensions OpenTelemetry.Exporter.PrometheusAspNetCoreOptions +OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.DisableTotalNameSuffixForCounters.get -> bool +OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.DisableTotalNameSuffixForCounters.set -> void OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.PrometheusAspNetCoreOptions() -> void OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.ScrapeEndpointPath.get -> string OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.ScrapeEndpointPath.set -> void diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/AssemblyInfo.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/AssemblyInfo.cs index 4b71c3dd52d..59d05ae59c9 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/AssemblyInfo.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/AssemblyInfo.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Runtime.CompilerServices; diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md index c52cb23f52b..56749e55689 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md @@ -2,6 +2,36 @@ ## Unreleased +* Added option to disable _total suffix addition to counter metrics + ([#5305](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5305)) + +* Export OpenMetrics format from Prometheus exporters ([#5107](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5107)) +* For requests with OpenMetrics format, scope info is automatically added + ([#5086](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5086) + [#5182](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5182)) + +## 1.7.0-rc.1 + +Released 2023-Nov-29 + +## 1.7.0-alpha.1 + +Released 2023-Oct-16 + +* Fixed writing boolean values to use the JSON representation + ([#4823](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4823)) + +## 1.6.0-rc.1 + +Released 2023-Aug-21 + +* Added support for unit and name conversion following the [OpenTelemetry Specification](https://github.com/open-telemetry/opentelemetry-specification/blob/065b25024549120800da7cda6ccd9717658ff0df/specification/compatibility/prometheus_and_openmetrics.md?plain=1#L235-L240) + ([#4753](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4753)) + +## 1.6.0-alpha.1 + +Released 2023-Jul-12 + ## 1.5.0-rc.1 Released 2023-May-25 diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj index a42b8d9033d..a94c7fdb1ae 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj @@ -2,7 +2,7 @@ - net6.0 + $(TargetFrameworksForPrometheusAspNetCore) ASP.NET Core middleware for hosting OpenTelemetry .NET Prometheus Exporter $(PackageTags);prometheus;metrics core- @@ -12,21 +12,24 @@ disable - - false + true - - + + + + + diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusAspNetCoreOptions.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusAspNetCoreOptions.cs index 3fb47f3e4a9..75acb565175 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusAspNetCoreOptions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusAspNetCoreOptions.cs @@ -1,47 +1,42 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Exporter.Prometheus; -namespace OpenTelemetry.Exporter +namespace OpenTelemetry.Exporter; + +/// +/// Prometheus exporter options. +/// +public class PrometheusAspNetCoreOptions { + internal const string DefaultScrapeEndpointPath = "/metrics"; + /// - /// Prometheus exporter options. + /// Gets or sets the path to use for the scraping endpoint. Default value: "/metrics". /// - public class PrometheusAspNetCoreOptions - { - internal const string DefaultScrapeEndpointPath = "/metrics"; + public string ScrapeEndpointPath { get; set; } = DefaultScrapeEndpointPath; - /// - /// Gets or sets the path to use for the scraping endpoint. Default value: "/metrics". - /// - public string ScrapeEndpointPath { get; set; } = DefaultScrapeEndpointPath; - - /// - /// Gets or sets the cache duration in milliseconds for scrape responses. Default value: 300. - /// - /// - /// Note: Specify 0 to disable response caching. - /// - public int ScrapeResponseCacheDurationMilliseconds - { - get => this.ExporterOptions.ScrapeResponseCacheDurationMilliseconds; - set => this.ExporterOptions.ScrapeResponseCacheDurationMilliseconds = value; - } + /// + /// Gets or sets a value indicating whether addition of _total suffix for counter metric names is disabled. Default value: . + /// + public bool DisableTotalNameSuffixForCounters + { + get => this.ExporterOptions.DisableTotalNameSuffixForCounters; + set => this.ExporterOptions.DisableTotalNameSuffixForCounters = value; + } - internal PrometheusExporterOptions ExporterOptions { get; } = new(); + /// + /// Gets or sets the cache duration in milliseconds for scrape responses. Default value: 300. + /// + /// + /// Note: Specify 0 to disable response caching. + /// + public int ScrapeResponseCacheDurationMilliseconds + { + get => this.ExporterOptions.ScrapeResponseCacheDurationMilliseconds; + set => this.ExporterOptions.ScrapeResponseCacheDurationMilliseconds = value; } + + internal PrometheusExporterOptions ExporterOptions { get; } = new(); } diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterApplicationBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterApplicationBuilderExtensions.cs index 23990a5a6f7..946f07d05aa 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterApplicationBuilderExtensions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterApplicationBuilderExtensions.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -21,133 +8,132 @@ using OpenTelemetry.Internal; using OpenTelemetry.Metrics; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Provides extension methods for to add +/// Prometheus scraping endpoint. +/// +public static class PrometheusExporterApplicationBuilderExtensions { /// - /// Provides extension methods for to add - /// Prometheus scraping endpoint. + /// Adds OpenTelemetry Prometheus scraping endpoint middleware to an + /// instance. + /// + /// Note: A branched pipeline is created for the route + /// specified by . + /// The to add + /// middleware to. + /// A reference to the original for chaining calls. + public static IApplicationBuilder UseOpenTelemetryPrometheusScrapingEndpoint(this IApplicationBuilder app) + => UseOpenTelemetryPrometheusScrapingEndpoint(app, meterProvider: null, predicate: null, path: null, configureBranchedPipeline: null, optionsName: null); + + /// + /// Adds OpenTelemetry Prometheus scraping endpoint middleware to an + /// instance. /// - public static class PrometheusExporterApplicationBuilderExtensions + /// Note: A branched pipeline is created for the supplied + /// . + /// The to add + /// middleware to. + /// Path to use for the branched pipeline. + /// A reference to the original for chaining calls. + public static IApplicationBuilder UseOpenTelemetryPrometheusScrapingEndpoint(this IApplicationBuilder app, string path) { - /// - /// Adds OpenTelemetry Prometheus scraping endpoint middleware to an - /// instance. - /// - /// Note: A branched pipeline is created for the route - /// specified by . - /// The to add - /// middleware to. - /// A reference to the original for chaining calls. - public static IApplicationBuilder UseOpenTelemetryPrometheusScrapingEndpoint(this IApplicationBuilder app) - => UseOpenTelemetryPrometheusScrapingEndpoint(app, meterProvider: null, predicate: null, path: null, configureBranchedPipeline: null, optionsName: null); + Guard.ThrowIfNull(path); + return UseOpenTelemetryPrometheusScrapingEndpoint(app, meterProvider: null, predicate: null, path: path, configureBranchedPipeline: null, optionsName: null); + } - /// - /// Adds OpenTelemetry Prometheus scraping endpoint middleware to an - /// instance. - /// - /// Note: A branched pipeline is created for the supplied - /// . - /// The to add - /// middleware to. - /// Path to use for the branched pipeline. - /// A reference to the original for chaining calls. - public static IApplicationBuilder UseOpenTelemetryPrometheusScrapingEndpoint(this IApplicationBuilder app, string path) - { - Guard.ThrowIfNull(path); - return UseOpenTelemetryPrometheusScrapingEndpoint(app, meterProvider: null, predicate: null, path: path, configureBranchedPipeline: null, optionsName: null); - } + /// + /// Adds OpenTelemetry Prometheus scraping endpoint middleware to an + /// instance. + /// + /// Note: A branched pipeline is created for the supplied + /// . + /// The to add + /// middleware to. + /// Predicate for deciding if a given + /// should be branched. + /// A reference to the original for chaining calls. + public static IApplicationBuilder UseOpenTelemetryPrometheusScrapingEndpoint(this IApplicationBuilder app, Func predicate) + { + Guard.ThrowIfNull(predicate); + return UseOpenTelemetryPrometheusScrapingEndpoint(app, meterProvider: null, predicate: predicate, path: null, configureBranchedPipeline: null, optionsName: null); + } - /// - /// Adds OpenTelemetry Prometheus scraping endpoint middleware to an - /// instance. - /// - /// Note: A branched pipeline is created for the supplied - /// . - /// The to add - /// middleware to. - /// Predicate for deciding if a given - /// should be branched. - /// A reference to the original for chaining calls. - public static IApplicationBuilder UseOpenTelemetryPrometheusScrapingEndpoint(this IApplicationBuilder app, Func predicate) - { - Guard.ThrowIfNull(predicate); - return UseOpenTelemetryPrometheusScrapingEndpoint(app, meterProvider: null, predicate: predicate, path: null, configureBranchedPipeline: null, optionsName: null); - } + /// + /// Adds OpenTelemetry Prometheus scraping endpoint middleware to an + /// instance. + /// + /// Note: A branched pipeline is created based on the or . If neither nor are provided then + /// is + /// used. + /// The to add + /// middleware to. + /// Optional + /// containing a Prometheus exporter otherwise the primary SDK provider + /// will be resolved using application services. + /// Optional predicate for deciding if a given + /// should be branched. If supplied is ignored. + /// Optional path to use for the branched pipeline. + /// Ignored if is supplied. + /// Optional callback to + /// configure the branched pipeline. Called before registration of the + /// Prometheus middleware. + /// Optional name used to retrieve . + /// A reference to the original for chaining calls. + public static IApplicationBuilder UseOpenTelemetryPrometheusScrapingEndpoint( + this IApplicationBuilder app, + MeterProvider meterProvider, + Func predicate, + string path, + Action configureBranchedPipeline, + string optionsName) + { + // Note: Order is important here. MeterProvider is accessed before + // GetOptions so that any changes made to + // PrometheusAspNetCoreOptions in deferred AddPrometheusExporter + // configure actions are reflected. + meterProvider ??= app.ApplicationServices.GetRequiredService(); - /// - /// Adds OpenTelemetry Prometheus scraping endpoint middleware to an - /// instance. - /// - /// Note: A branched pipeline is created based on the or . If neither nor are provided then - /// is - /// used. - /// The to add - /// middleware to. - /// Optional - /// containing a Prometheus exporter otherwise the primary SDK provider - /// will be resolved using application services. - /// Optional predicate for deciding if a given - /// should be branched. If supplied is ignored. - /// Optional path to use for the branched pipeline. - /// Ignored if is supplied. - /// Optional callback to - /// configure the branched pipeline. Called before registration of the - /// Prometheus middleware. - /// Optional name used to retrieve . - /// A reference to the original for chaining calls. - public static IApplicationBuilder UseOpenTelemetryPrometheusScrapingEndpoint( - this IApplicationBuilder app, - MeterProvider meterProvider, - Func predicate, - string path, - Action configureBranchedPipeline, - string optionsName) + if (predicate == null) { - // Note: Order is important here. MeterProvider is accessed before - // GetOptions so that any changes made to - // PrometheusAspNetCoreOptions in deferred AddPrometheusExporter - // configure actions are reflected. - meterProvider ??= app.ApplicationServices.GetRequiredService(); - - if (predicate == null) + if (path == null) { - if (path == null) - { - var options = app.ApplicationServices.GetRequiredService>().Get(optionsName ?? Options.DefaultName); + var options = app.ApplicationServices.GetRequiredService>().Get(optionsName ?? Options.DefaultName); - path = options.ScrapeEndpointPath ?? PrometheusAspNetCoreOptions.DefaultScrapeEndpointPath; - } - - if (!path.StartsWith("/")) - { - path = $"/{path}"; - } + path = options.ScrapeEndpointPath ?? PrometheusAspNetCoreOptions.DefaultScrapeEndpointPath; + } - return app.Map( - new PathString(path), - builder => - { - configureBranchedPipeline?.Invoke(builder); - builder.UseMiddleware(meterProvider); - }); + if (!path.StartsWith("/")) + { + path = $"/{path}"; } - return app.MapWhen( - context => predicate(context), + return app.Map( + new PathString(path), builder => { configureBranchedPipeline?.Invoke(builder); builder.UseMiddleware(meterProvider); }); } + + return app.MapWhen( + context => predicate(context), + builder => + { + configureBranchedPipeline?.Invoke(builder); + builder.UseMiddleware(meterProvider); + }); } } diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterEndpointRouteBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterEndpointRouteBuilderExtensions.cs index 030ed53f902..4e70f2f76c9 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterEndpointRouteBuilderExtensions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterEndpointRouteBuilderExtensions.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; @@ -22,91 +9,90 @@ using OpenTelemetry.Internal; using OpenTelemetry.Metrics; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Provides extension methods for to add +/// Prometheus scraping endpoint. +/// +public static class PrometheusExporterEndpointRouteBuilderExtensions { /// - /// Provides extension methods for to add - /// Prometheus scraping endpoint. + /// Adds OpenTelemetry Prometheus scraping endpoint middleware to an + /// instance. /// - public static class PrometheusExporterEndpointRouteBuilderExtensions - { - /// - /// Adds OpenTelemetry Prometheus scraping endpoint middleware to an - /// instance. - /// - /// Note: A branched pipeline is created for the route - /// specified by . - /// The to add - /// middleware to. - /// A convention routes for the Prometheus scraping endpoint. - public static IEndpointConventionBuilder MapPrometheusScrapingEndpoint(this IEndpointRouteBuilder endpoints) - => MapPrometheusScrapingEndpoint(endpoints, path: null, meterProvider: null, configureBranchedPipeline: null, optionsName: null); + /// Note: A branched pipeline is created for the route + /// specified by . + /// The to add + /// middleware to. + /// A convention routes for the Prometheus scraping endpoint. + public static IEndpointConventionBuilder MapPrometheusScrapingEndpoint(this IEndpointRouteBuilder endpoints) + => MapPrometheusScrapingEndpoint(endpoints, path: null, meterProvider: null, configureBranchedPipeline: null, optionsName: null); - /// - /// Adds OpenTelemetry Prometheus scraping endpoint middleware to an - /// instance. - /// - /// The to add - /// middleware to. - /// The path to use for the branched pipeline. - /// A convention routes for the Prometheus scraping endpoint. - public static IEndpointConventionBuilder MapPrometheusScrapingEndpoint(this IEndpointRouteBuilder endpoints, string path) - { - Guard.ThrowIfNull(path); - return MapPrometheusScrapingEndpoint(endpoints, path, meterProvider: null, configureBranchedPipeline: null, optionsName: null); - } + /// + /// Adds OpenTelemetry Prometheus scraping endpoint middleware to an + /// instance. + /// + /// The to add + /// middleware to. + /// The path to use for the branched pipeline. + /// A convention routes for the Prometheus scraping endpoint. + public static IEndpointConventionBuilder MapPrometheusScrapingEndpoint(this IEndpointRouteBuilder endpoints, string path) + { + Guard.ThrowIfNull(path); + return MapPrometheusScrapingEndpoint(endpoints, path, meterProvider: null, configureBranchedPipeline: null, optionsName: null); + } - /// - /// Adds OpenTelemetry Prometheus scraping endpoint middleware to an - /// instance. - /// - /// The to add - /// middleware to. - /// Optional path to use for the branched pipeline. - /// If not provided then - /// is used. - /// Optional - /// containing a Prometheus exporter otherwise the primary SDK provider - /// will be resolved using application services. - /// Optional callback to - /// configure the branched pipeline. Called before registration of the - /// Prometheus middleware. - /// Optional name used to retrieve . - /// A convention routes for the Prometheus scraping endpoint. - public static IEndpointConventionBuilder MapPrometheusScrapingEndpoint( - this IEndpointRouteBuilder endpoints, - string path, - MeterProvider meterProvider, - Action configureBranchedPipeline, - string optionsName) - { - var builder = endpoints.CreateApplicationBuilder(); + /// + /// Adds OpenTelemetry Prometheus scraping endpoint middleware to an + /// instance. + /// + /// The to add + /// middleware to. + /// Optional path to use for the branched pipeline. + /// If not provided then + /// is used. + /// Optional + /// containing a Prometheus exporter otherwise the primary SDK provider + /// will be resolved using application services. + /// Optional callback to + /// configure the branched pipeline. Called before registration of the + /// Prometheus middleware. + /// Optional name used to retrieve . + /// A convention routes for the Prometheus scraping endpoint. + public static IEndpointConventionBuilder MapPrometheusScrapingEndpoint( + this IEndpointRouteBuilder endpoints, + string path, + MeterProvider meterProvider, + Action configureBranchedPipeline, + string optionsName) + { + var builder = endpoints.CreateApplicationBuilder(); - // Note: Order is important here. MeterProvider is accessed before - // GetOptions so that any changes made to - // PrometheusAspNetCoreOptions in deferred AddPrometheusExporter - // configure actions are reflected. - meterProvider ??= endpoints.ServiceProvider.GetRequiredService(); + // Note: Order is important here. MeterProvider is accessed before + // GetOptions so that any changes made to + // PrometheusAspNetCoreOptions in deferred AddPrometheusExporter + // configure actions are reflected. + meterProvider ??= endpoints.ServiceProvider.GetRequiredService(); - if (path == null) - { - var options = endpoints.ServiceProvider.GetRequiredService>().Get(optionsName ?? Options.DefaultName); + if (path == null) + { + var options = endpoints.ServiceProvider.GetRequiredService>().Get(optionsName ?? Options.DefaultName); - path = options.ScrapeEndpointPath ?? PrometheusAspNetCoreOptions.DefaultScrapeEndpointPath; - } + path = options.ScrapeEndpointPath ?? PrometheusAspNetCoreOptions.DefaultScrapeEndpointPath; + } - if (!path.StartsWith("/")) - { - path = $"/{path}"; - } + if (!path.StartsWith("/")) + { + path = $"/{path}"; + } - configureBranchedPipeline?.Invoke(builder); + configureBranchedPipeline?.Invoke(builder); - builder.UseMiddleware(meterProvider); + builder.UseMiddleware(meterProvider); - return endpoints.Map(new PathString(path), builder.Build()); - } + return endpoints.Map(new PathString(path), builder.Build()); } } diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMeterProviderBuilderExtensions.cs index cf0607b99ed..6a075fc1b99 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMeterProviderBuilderExtensions.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -20,69 +7,68 @@ using OpenTelemetry.Exporter.Prometheus; using OpenTelemetry.Internal; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// Extension methods to simplify registering a PrometheusExporter. +/// +public static class PrometheusExporterMeterProviderBuilderExtensions { /// - /// Extension methods to simplify registering a PrometheusExporter. + /// Adds to the . + /// + /// builder to use. + /// The instance of to chain the calls. + public static MeterProviderBuilder AddPrometheusExporter(this MeterProviderBuilder builder) + => AddPrometheusExporter(builder, name: null, configure: null); + + /// + /// Adds to the . /// - public static class PrometheusExporterMeterProviderBuilderExtensions + /// builder to use. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static MeterProviderBuilder AddPrometheusExporter( + this MeterProviderBuilder builder, + Action configure) + => AddPrometheusExporter(builder, name: null, configure); + + /// + /// Adds to the . + /// + /// builder to use. + /// Name which is used when retrieving options. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static MeterProviderBuilder AddPrometheusExporter( + this MeterProviderBuilder builder, + string name, + Action configure) { - /// - /// Adds to the . - /// - /// builder to use. - /// The instance of to chain the calls. - public static MeterProviderBuilder AddPrometheusExporter(this MeterProviderBuilder builder) - => AddPrometheusExporter(builder, name: null, configure: null); + Guard.ThrowIfNull(builder); - /// - /// Adds to the . - /// - /// builder to use. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static MeterProviderBuilder AddPrometheusExporter( - this MeterProviderBuilder builder, - Action configure) - => AddPrometheusExporter(builder, name: null, configure); + name ??= Options.DefaultName; - /// - /// Adds to the . - /// - /// builder to use. - /// Name which is used when retrieving options. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static MeterProviderBuilder AddPrometheusExporter( - this MeterProviderBuilder builder, - string name, - Action configure) + if (configure != null) { - Guard.ThrowIfNull(builder); - - name ??= Options.DefaultName; + builder.ConfigureServices(services => services.Configure(name, configure)); + } - if (configure != null) - { - builder.ConfigureServices(services => services.Configure(name, configure)); - } + return builder.AddReader(sp => + { + var options = sp.GetRequiredService>().Get(name); - return builder.AddReader(sp => - { - var options = sp.GetRequiredService>().Get(name); + return BuildPrometheusExporterMetricReader(options); + }); + } - return BuildPrometheusExporterMetricReader(options); - }); - } + private static MetricReader BuildPrometheusExporterMetricReader(PrometheusAspNetCoreOptions options) + { + var exporter = new PrometheusExporter(options.ExporterOptions); - private static MetricReader BuildPrometheusExporterMetricReader(PrometheusAspNetCoreOptions options) + return new BaseExportingMetricReader(exporter) { - var exporter = new PrometheusExporter(options.ExporterOptions); - - return new BaseExportingMetricReader(exporter) - { - TemporalityPreference = MetricReaderTemporalityPreference.Cumulative, - }; - } + TemporalityPreference = MetricReaderTemporalityPreference.Cumulative, + }; } } diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs index 08fc1634cde..7e815c3e18c 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs @@ -1,102 +1,117 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; using OpenTelemetry.Exporter.Prometheus; using OpenTelemetry.Internal; using OpenTelemetry.Metrics; -namespace OpenTelemetry.Exporter +namespace OpenTelemetry.Exporter; + +/// +/// ASP.NET Core middleware for exposing a Prometheus metrics scraping endpoint. +/// +internal sealed class PrometheusExporterMiddleware { + private readonly PrometheusExporter exporter; + /// - /// ASP.NET Core middleware for exposing a Prometheus metrics scraping endpoint. + /// Initializes a new instance of the class. /// - internal sealed class PrometheusExporterMiddleware + /// . + /// . + public PrometheusExporterMiddleware(MeterProvider meterProvider, RequestDelegate next) { - private readonly PrometheusExporter exporter; - - /// - /// Initializes a new instance of the class. - /// - /// . - /// . - public PrometheusExporterMiddleware(MeterProvider meterProvider, RequestDelegate next) + Guard.ThrowIfNull(meterProvider); + + if (!meterProvider.TryFindExporter(out PrometheusExporter exporter)) { - Guard.ThrowIfNull(meterProvider); + throw new ArgumentException("A PrometheusExporter could not be found configured on the provided MeterProvider."); + } - if (!meterProvider.TryFindExporter(out PrometheusExporter exporter)) - { - throw new ArgumentException("A PrometheusExporter could not be found configured on the provided MeterProvider."); - } + this.exporter = exporter; + } - this.exporter = exporter; - } + internal PrometheusExporterMiddleware(PrometheusExporter exporter) + { + this.exporter = exporter; + } - internal PrometheusExporterMiddleware(PrometheusExporter exporter) - { - this.exporter = exporter; - } + /// + /// Invoke. + /// + /// context. + /// Task. + public async Task InvokeAsync(HttpContext httpContext) + { + Debug.Assert(httpContext != null, "httpContext should not be null"); - /// - /// Invoke. - /// - /// context. - /// Task. - public async Task InvokeAsync(HttpContext httpContext) - { - Debug.Assert(httpContext != null, "httpContext should not be null"); + var response = httpContext.Response; - var response = httpContext.Response; + try + { + var openMetricsRequested = AcceptsOpenMetrics(httpContext.Request); + var collectionResponse = await this.exporter.CollectionManager.EnterCollect(openMetricsRequested).ConfigureAwait(false); try { - var collectionResponse = await this.exporter.CollectionManager.EnterCollect().ConfigureAwait(false); - try + if (collectionResponse.View.Count > 0) { - if (collectionResponse.View.Count > 0) - { - response.StatusCode = 200; - response.Headers.Add("Last-Modified", collectionResponse.GeneratedAtUtc.ToString("R")); - response.ContentType = "text/plain; charset=utf-8; version=0.0.4"; - - await response.Body.WriteAsync(collectionResponse.View.Array, 0, collectionResponse.View.Count).ConfigureAwait(false); - } - else - { - // It's not expected to have no metrics to collect, but it's not necessarily a failure, either. - response.StatusCode = 200; - PrometheusExporterEventSource.Log.NoMetrics(); - } + response.StatusCode = 200; +#if NET8_0_OR_GREATER + response.Headers.Append("Last-Modified", collectionResponse.GeneratedAtUtc.ToString("R")); +#else + response.Headers.Add("Last-Modified", collectionResponse.GeneratedAtUtc.ToString("R")); +#endif + response.ContentType = openMetricsRequested + ? "application/openmetrics-text; version=1.0.0; charset=utf-8" + : "text/plain; charset=utf-8; version=0.0.4"; + + await response.Body.WriteAsync(collectionResponse.View.Array, 0, collectionResponse.View.Count).ConfigureAwait(false); } - finally + else { - this.exporter.CollectionManager.ExitCollect(); + // It's not expected to have no metrics to collect, but it's not necessarily a failure, either. + response.StatusCode = 200; + PrometheusExporterEventSource.Log.NoMetrics(); } } - catch (Exception ex) + finally { - PrometheusExporterEventSource.Log.FailedExport(ex); - if (!response.HasStarted) - { - response.StatusCode = 500; - } + this.exporter.CollectionManager.ExitCollect(); } + } + catch (Exception ex) + { + PrometheusExporterEventSource.Log.FailedExport(ex); + if (!response.HasStarted) + { + response.StatusCode = 500; + } + } + + this.exporter.OnExport = null; + } - this.exporter.OnExport = null; + private static bool AcceptsOpenMetrics(HttpRequest request) + { + var acceptHeader = request.Headers.Accept; + + if (StringValues.IsNullOrEmpty(acceptHeader)) + { + return false; } + + foreach (var header in acceptHeader) + { + if (PrometheusHeadersParser.AcceptsOpenMetrics(header)) + { + return true; + } + } + + return false; } } diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/README.md b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/README.md index c93544a74b1..d8629061f4c 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/README.md +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/README.md @@ -7,7 +7,7 @@ An [OpenTelemetry Prometheus exporter](https://github.com/open-telemetry/opentel for configuring an ASP.NET Core application with an endpoint for Prometheus to scrape. -> **Note** +> [!NOTE] > This exporter does not support [OpenMetrics format](https://github.com/OpenObservability/OpenMetrics), and consequently, does not support Exemplars. For using Exemplars, use the [OTLP diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/net6.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Shipped.txt similarity index 100% rename from src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/net6.0/PublicAPI.Shipped.txt rename to src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Shipped.txt diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Unshipped.txt similarity index 87% rename from src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/net462/PublicAPI.Unshipped.txt rename to src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Unshipped.txt index 9bc2e72461d..d05f12424ea 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/net462/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ OpenTelemetry.Exporter.PrometheusHttpListenerOptions +OpenTelemetry.Exporter.PrometheusHttpListenerOptions.DisableTotalNameSuffixForCounters.get -> bool +OpenTelemetry.Exporter.PrometheusHttpListenerOptions.DisableTotalNameSuffixForCounters.set -> void OpenTelemetry.Exporter.PrometheusHttpListenerOptions.UriPrefixes.get -> System.Collections.Generic.IReadOnlyCollection OpenTelemetry.Exporter.PrometheusHttpListenerOptions.UriPrefixes.set -> void OpenTelemetry.Exporter.PrometheusHttpListenerOptions.PrometheusHttpListenerOptions() -> void diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt deleted file mode 100644 index cc20c38b13e..00000000000 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,10 +0,0 @@ -OpenTelemetry.Exporter.PrometheusHttpListenerOptions -OpenTelemetry.Exporter.PrometheusHttpListenerOptions.PrometheusHttpListenerOptions() -> void -OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ScrapeEndpointPath.get -> string -OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ScrapeEndpointPath.set -> void -OpenTelemetry.Exporter.PrometheusHttpListenerOptions.UriPrefixes.get -> System.Collections.Generic.IReadOnlyCollection -OpenTelemetry.Exporter.PrometheusHttpListenerOptions.UriPrefixes.set -> void -OpenTelemetry.Metrics.PrometheusHttpListenerMeterProviderBuilderExtensions -static OpenTelemetry.Metrics.PrometheusHttpListenerMeterProviderBuilderExtensions.AddPrometheusHttpListener(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.PrometheusHttpListenerMeterProviderBuilderExtensions.AddPrometheusHttpListener(this OpenTelemetry.Metrics.MeterProviderBuilder builder, string name, System.Action configure) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.PrometheusHttpListenerMeterProviderBuilderExtensions.AddPrometheusHttpListener(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action configure) -> OpenTelemetry.Metrics.MeterProviderBuilder diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/AssemblyInfo.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/AssemblyInfo.cs index b5aa26ab08f..7d6b4b08289 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/AssemblyInfo.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/AssemblyInfo.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Runtime.CompilerServices; diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md index a44e143d48b..8deaf5e9a56 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md @@ -2,6 +2,42 @@ ## Unreleased +* Added option to disable _total suffix addition to counter metrics + ([#5305](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5305)) + +* Export OpenMetrics format from Prometheus exporters + ([#5107](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5107)) + +* For requests with OpenMetrics format, scope info is automatically added + ([#5086](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5086) + [#5182](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5182)) + +* **Breaking change** Updated the `PrometheusHttpListener` to throw an exception + if it can't be started. + ([#5304](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5304)) + +## 1.7.0-rc.1 + +Released 2023-Nov-29 + +## 1.7.0-alpha.1 + +Released 2023-Oct-16 + +* Fixed writing boolean values to use the JSON representation + ([#4823](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4823)) + +## 1.6.0-rc.1 + +Released 2023-Aug-21 + +* Added support for unit and name conversion following the [OpenTelemetry Specification](https://github.com/open-telemetry/opentelemetry-specification/blob/065b25024549120800da7cda6ccd9717658ff0df/specification/compatibility/prometheus_and_openmetrics.md?plain=1#L235-L240) + ([#4753](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4753)) + +## 1.6.0-alpha.1 + +Released 2023-Jul-12 + ## 1.5.0-rc.1 Released 2023-May-25 diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs index de55d5de9f7..d92c48f1f2d 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs @@ -1,212 +1,229 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Runtime.CompilerServices; using OpenTelemetry.Metrics; -namespace OpenTelemetry.Exporter.Prometheus +namespace OpenTelemetry.Exporter.Prometheus; + +internal sealed class PrometheusCollectionManager { - internal sealed class PrometheusCollectionManager + private const int MaxCachedMetrics = 1024; + + private readonly PrometheusExporter exporter; + private readonly int scrapeResponseCacheDurationMilliseconds; + private readonly Func, ExportResult> onCollectRef; + private readonly Dictionary metricsCache; + private readonly HashSet scopes; + private int metricsCacheCount; + private byte[] buffer = new byte[85000]; // encourage the object to live in LOH (large object heap) + private int globalLockState; + private ArraySegment previousDataView; + private DateTime? previousDataViewGeneratedAtUtc; + private int readerCount; + private bool collectionRunning; + private TaskCompletionSource collectionTcs; + + public PrometheusCollectionManager(PrometheusExporter exporter) { - private readonly PrometheusExporter exporter; - private readonly int scrapeResponseCacheDurationMilliseconds; - private readonly Func, ExportResult> onCollectRef; - private byte[] buffer = new byte[85000]; // encourage the object to live in LOH (large object heap) - private int globalLockState; - private ArraySegment previousDataView; - private DateTime? previousDataViewGeneratedAtUtc; - private int readerCount; - private bool collectionRunning; - private TaskCompletionSource collectionTcs; - - public PrometheusCollectionManager(PrometheusExporter exporter) - { - this.exporter = exporter; - this.scrapeResponseCacheDurationMilliseconds = this.exporter.ScrapeResponseCacheDurationMilliseconds; - this.onCollectRef = this.OnCollect; - } + this.exporter = exporter; + this.scrapeResponseCacheDurationMilliseconds = this.exporter.ScrapeResponseCacheDurationMilliseconds; + this.onCollectRef = this.OnCollect; + this.metricsCache = new Dictionary(); + this.scopes = new HashSet(); + } #if NET6_0_OR_GREATER - public ValueTask EnterCollect() + public ValueTask EnterCollect(bool openMetricsRequested) #else - public Task EnterCollect() + public Task EnterCollect(bool openMetricsRequested) #endif - { - this.EnterGlobalLock(); + { + this.EnterGlobalLock(); - // If we are within {ScrapeResponseCacheDurationMilliseconds} of the - // last successful collect, return the previous view. - if (this.previousDataViewGeneratedAtUtc.HasValue - && this.scrapeResponseCacheDurationMilliseconds > 0 - && this.previousDataViewGeneratedAtUtc.Value.AddMilliseconds(this.scrapeResponseCacheDurationMilliseconds) >= DateTime.UtcNow) - { - Interlocked.Increment(ref this.readerCount); - this.ExitGlobalLock(); + // If we are within {ScrapeResponseCacheDurationMilliseconds} of the + // last successful collect, return the previous view. + if (this.previousDataViewGeneratedAtUtc.HasValue + && this.scrapeResponseCacheDurationMilliseconds > 0 + && this.previousDataViewGeneratedAtUtc.Value.AddMilliseconds(this.scrapeResponseCacheDurationMilliseconds) >= DateTime.UtcNow) + { + Interlocked.Increment(ref this.readerCount); + this.ExitGlobalLock(); #if NET6_0_OR_GREATER - return new ValueTask(new CollectionResponse(this.previousDataView, this.previousDataViewGeneratedAtUtc.Value, fromCache: true)); + return new ValueTask(new CollectionResponse(this.previousDataView, this.previousDataViewGeneratedAtUtc.Value, fromCache: true)); #else - return Task.FromResult(new CollectionResponse(this.previousDataView, this.previousDataViewGeneratedAtUtc.Value, fromCache: true)); + return Task.FromResult(new CollectionResponse(this.previousDataView, this.previousDataViewGeneratedAtUtc.Value, fromCache: true)); #endif - } + } - // If a collection is already running, return a task to wait on the result. - if (this.collectionRunning) + // If a collection is already running, return a task to wait on the result. + if (this.collectionRunning) + { + if (this.collectionTcs == null) { - if (this.collectionTcs == null) - { - this.collectionTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - } + this.collectionTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + } - Interlocked.Increment(ref this.readerCount); - this.ExitGlobalLock(); + Interlocked.Increment(ref this.readerCount); + this.ExitGlobalLock(); #if NET6_0_OR_GREATER - return new ValueTask(this.collectionTcs.Task); + return new ValueTask(this.collectionTcs.Task); #else - return this.collectionTcs.Task; + return this.collectionTcs.Task; #endif - } + } - this.WaitForReadersToComplete(); + this.WaitForReadersToComplete(); - // Start a collection on the current thread. - this.collectionRunning = true; - this.previousDataViewGeneratedAtUtc = null; - Interlocked.Increment(ref this.readerCount); - this.ExitGlobalLock(); + // Start a collection on the current thread. + this.collectionRunning = true; + this.previousDataViewGeneratedAtUtc = null; + Interlocked.Increment(ref this.readerCount); + this.ExitGlobalLock(); - CollectionResponse response; - var result = this.ExecuteCollect(); - if (result) - { - this.previousDataViewGeneratedAtUtc = DateTime.UtcNow; - response = new CollectionResponse(this.previousDataView, this.previousDataViewGeneratedAtUtc.Value, fromCache: false); - } - else - { - response = default; - } + CollectionResponse response; + var result = this.ExecuteCollect(openMetricsRequested); + if (result) + { + this.previousDataViewGeneratedAtUtc = DateTime.UtcNow; + response = new CollectionResponse(this.previousDataView, this.previousDataViewGeneratedAtUtc.Value, fromCache: false); + } + else + { + response = default; + } - this.EnterGlobalLock(); + this.EnterGlobalLock(); - this.collectionRunning = false; + this.collectionRunning = false; - if (this.collectionTcs != null) - { - this.collectionTcs.SetResult(response); - this.collectionTcs = null; - } + if (this.collectionTcs != null) + { + this.collectionTcs.SetResult(response); + this.collectionTcs = null; + } - this.ExitGlobalLock(); + this.ExitGlobalLock(); #if NET6_0_OR_GREATER - return new ValueTask(response); + return new ValueTask(response); #else - return Task.FromResult(response); + return Task.FromResult(response); #endif - } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void ExitCollect() - { - Interlocked.Decrement(ref this.readerCount); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ExitCollect() + { + Interlocked.Decrement(ref this.readerCount); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void EnterGlobalLock() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnterGlobalLock() + { + SpinWait lockWait = default; + while (true) { - SpinWait lockWait = default; - while (true) + if (Interlocked.CompareExchange(ref this.globalLockState, 1, this.globalLockState) != 0) { - if (Interlocked.CompareExchange(ref this.globalLockState, 1, this.globalLockState) != 0) - { - lockWait.SpinOnce(); - continue; - } - - break; + lockWait.SpinOnce(); + continue; } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ExitGlobalLock() - { - this.globalLockState = 0; + break; } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ExitGlobalLock() + { + Interlocked.Exchange(ref this.globalLockState, 0); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void WaitForReadersToComplete() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WaitForReadersToComplete() + { + SpinWait readWait = default; + while (true) { - SpinWait readWait = default; - while (true) + if (Interlocked.CompareExchange(ref this.readerCount, 0, this.readerCount) != 0) { - if (Interlocked.CompareExchange(ref this.readerCount, 0, this.readerCount) != 0) - { - readWait.SpinOnce(); - continue; - } - - break; + readWait.SpinOnce(); + continue; } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool ExecuteCollect() - { - this.exporter.OnExport = this.onCollectRef; - var result = this.exporter.Collect(Timeout.Infinite); - this.exporter.OnExport = null; - return result; + break; } + } - private ExportResult OnCollect(Batch metrics) - { - var cursor = 0; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool ExecuteCollect(bool openMetricsRequested) + { + this.exporter.OnExport = this.onCollectRef; + this.exporter.OpenMetricsRequested = openMetricsRequested; + var result = this.exporter.Collect(Timeout.Infinite); + this.exporter.OnExport = null; + return result; + } + + private ExportResult OnCollect(Batch metrics) + { + var cursor = 0; - try + try + { + if (this.exporter.OpenMetricsRequested) { + this.scopes.Clear(); + foreach (var metric in metrics) { - while (true) + if (PrometheusSerializer.CanWriteMetric(metric)) { - try - { - cursor = PrometheusSerializer.WriteMetric(this.buffer, cursor, metric); - break; - } - catch (IndexOutOfRangeException) + if (this.scopes.Add(metric.MeterName)) { - if (!this.IncreaseBufferSize()) + try + { + cursor = PrometheusSerializer.WriteScopeInfo(this.buffer, cursor, metric.MeterName); + + break; + } + catch (IndexOutOfRangeException) { - // there are two cases we might run into the following condition: - // 1. we have many metrics to be exported - in this case we probably want - // to put some upper limit and allow the user to configure it. - // 2. we got an IndexOutOfRangeException which was triggered by some other - // code instead of the buffer[cursor++] - in this case we should give up - // at certain point rather than allocating like crazy. - throw; + if (!this.IncreaseBufferSize()) + { + // there are two cases we might run into the following condition: + // 1. we have many metrics to be exported - in this case we probably want + // to put some upper limit and allow the user to configure it. + // 2. we got an IndexOutOfRangeException which was triggered by some other + // code instead of the buffer[cursor++] - in this case we should give up + // at certain point rather than allocating like crazy. + throw; + } } } } } + } + + foreach (var metric in metrics) + { + if (!PrometheusSerializer.CanWriteMetric(metric)) + { + continue; + } while (true) { try { - cursor = PrometheusSerializer.WriteEof(this.buffer, cursor); + cursor = PrometheusSerializer.WriteMetric( + this.buffer, + cursor, + metric, + this.GetPrometheusMetric(metric), + this.exporter.OpenMetricsRequested); + break; } catch (IndexOutOfRangeException) @@ -217,47 +234,81 @@ private ExportResult OnCollect(Batch metrics) } } } - - this.previousDataView = new ArraySegment(this.buffer, 0, cursor); - return ExportResult.Success; } - catch (Exception) + + while (true) { - this.previousDataView = new ArraySegment(Array.Empty(), 0, 0); - return ExportResult.Failure; + try + { + cursor = PrometheusSerializer.WriteEof(this.buffer, cursor); + break; + } + catch (IndexOutOfRangeException) + { + if (!this.IncreaseBufferSize()) + { + throw; + } + } } - } - private bool IncreaseBufferSize() + this.previousDataView = new ArraySegment(this.buffer, 0, cursor); + return ExportResult.Success; + } + catch (Exception) { - var newBufferSize = this.buffer.Length * 2; - - if (newBufferSize > 100 * 1024 * 1024) - { - return false; - } + this.previousDataView = new ArraySegment(Array.Empty(), 0, 0); + return ExportResult.Failure; + } + } - var newBuffer = new byte[newBufferSize]; - this.buffer.CopyTo(newBuffer, 0); - this.buffer = newBuffer; + private bool IncreaseBufferSize() + { + var newBufferSize = this.buffer.Length * 2; - return true; + if (newBufferSize > 100 * 1024 * 1024) + { + return false; } - public readonly struct CollectionResponse + var newBuffer = new byte[newBufferSize]; + this.buffer.CopyTo(newBuffer, 0); + this.buffer = newBuffer; + + return true; + } + + private PrometheusMetric GetPrometheusMetric(Metric metric) + { + // Optimize writing metrics with bounded cache that has pre-calculated Prometheus names. + if (!this.metricsCache.TryGetValue(metric, out var prometheusMetric)) { - public CollectionResponse(ArraySegment view, DateTime generatedAtUtc, bool fromCache) + prometheusMetric = PrometheusMetric.Create(metric, this.exporter.DisableTotalNameSuffixForCounters); + + // Add to the cache if there is space. + if (this.metricsCacheCount < MaxCachedMetrics) { - this.View = view; - this.GeneratedAtUtc = generatedAtUtc; - this.FromCache = fromCache; + this.metricsCache[metric] = prometheusMetric; + this.metricsCacheCount++; } + } - public ArraySegment View { get; } - - public DateTime GeneratedAtUtc { get; } + return prometheusMetric; + } - public bool FromCache { get; } + public readonly struct CollectionResponse + { + public CollectionResponse(ArraySegment view, DateTime generatedAtUtc, bool fromCache) + { + this.View = view; + this.GeneratedAtUtc = generatedAtUtc; + this.FromCache = fromCache; } + + public ArraySegment View { get; } + + public DateTime GeneratedAtUtc { get; } + + public bool FromCache { get; } } } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs index c4aff58aef1..578d0329ada 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs @@ -1,88 +1,79 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Internal; using OpenTelemetry.Metrics; -namespace OpenTelemetry.Exporter.Prometheus +namespace OpenTelemetry.Exporter.Prometheus; + +/// +/// Exporter of OpenTelemetry metrics to Prometheus. +/// +[ExportModes(ExportModes.Pull)] +internal sealed class PrometheusExporter : BaseExporter, IPullMetricExporter { + private Func funcCollect; + private Func, ExportResult> funcExport; + private bool disposed; + /// - /// Exporter of OpenTelemetry metrics to Prometheus. + /// Initializes a new instance of the class. /// - [ExportModes(ExportModes.Pull)] - internal sealed class PrometheusExporter : BaseExporter, IPullMetricExporter + /// . + public PrometheusExporter(PrometheusExporterOptions options) { - private Func funcCollect; - private Func, ExportResult> funcExport; - private bool disposed; - - /// - /// Initializes a new instance of the class. - /// - /// . - public PrometheusExporter(PrometheusExporterOptions options) - { - Guard.ThrowIfNull(options); + Guard.ThrowIfNull(options); - this.ScrapeResponseCacheDurationMilliseconds = options.ScrapeResponseCacheDurationMilliseconds; + this.ScrapeResponseCacheDurationMilliseconds = options.ScrapeResponseCacheDurationMilliseconds; + this.DisableTotalNameSuffixForCounters = options.DisableTotalNameSuffixForCounters; - this.CollectionManager = new PrometheusCollectionManager(this); - } + this.CollectionManager = new PrometheusCollectionManager(this); + } - /// - /// Gets or sets the Collect delegate. - /// - public Func Collect - { - get => this.funcCollect; - set => this.funcCollect = value; - } + /// + /// Gets or sets the Collect delegate. + /// + public Func Collect + { + get => this.funcCollect; + set => this.funcCollect = value; + } - internal Func, ExportResult> OnExport - { - get => this.funcExport; - set => this.funcExport = value; - } + internal Func, ExportResult> OnExport + { + get => this.funcExport; + set => this.funcExport = value; + } - internal Action OnDispose { get; set; } + internal Action OnDispose { get; set; } - internal PrometheusCollectionManager CollectionManager { get; } + internal PrometheusCollectionManager CollectionManager { get; } - internal int ScrapeResponseCacheDurationMilliseconds { get; } + internal int ScrapeResponseCacheDurationMilliseconds { get; } - /// - public override ExportResult Export(in Batch metrics) - { - return this.OnExport(metrics); - } + internal bool DisableTotalNameSuffixForCounters { get; } + + internal bool OpenMetricsRequested { get; set; } - /// - protected override void Dispose(bool disposing) + /// + public override ExportResult Export(in Batch metrics) + { + return this.OnExport(metrics); + } + + /// + protected override void Dispose(bool disposing) + { + if (!this.disposed) { - if (!this.disposed) + if (disposing) { - if (disposing) - { - this.OnDispose?.Invoke(); - } - - this.disposed = true; + this.OnDispose?.Invoke(); } - base.Dispose(disposing); + this.disposed = true; } + + base.Dispose(disposing); } } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporterEventSource.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporterEventSource.cs index d5241f3819d..a55cc6ef553 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporterEventSource.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporterEventSource.cs @@ -1,81 +1,67 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Tracing; using OpenTelemetry.Internal; -namespace OpenTelemetry.Exporter.Prometheus +namespace OpenTelemetry.Exporter.Prometheus; + +/// +/// EventSource events emitted from the project. +/// +[EventSource(Name = "OpenTelemetry-Exporter-Prometheus")] +internal sealed class PrometheusExporterEventSource : EventSource { - /// - /// EventSource events emitted from the project. - /// - [EventSource(Name = "OpenTelemetry-Exporter-Prometheus")] - internal sealed class PrometheusExporterEventSource : EventSource - { - public static PrometheusExporterEventSource Log = new(); + public static PrometheusExporterEventSource Log = new(); - [NonEvent] - public void FailedExport(Exception ex) + [NonEvent] + public void FailedExport(Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.FailedExport(ex.ToInvariantString()); - } + this.FailedExport(ex.ToInvariantString()); } + } - [NonEvent] - public void FailedShutdown(Exception ex) + [NonEvent] + public void FailedShutdown(Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.FailedShutdown(ex.ToInvariantString()); - } + this.FailedShutdown(ex.ToInvariantString()); } + } - [NonEvent] - public void CanceledExport(Exception ex) + [NonEvent] + public void CanceledExport(Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.CanceledExport(ex.ToInvariantString()); - } + this.CanceledExport(ex.ToInvariantString()); } + } - [Event(1, Message = "Failed to export metrics: '{0}'", Level = EventLevel.Error)] - public void FailedExport(string exception) - { - this.WriteEvent(1, exception); - } + [Event(1, Message = "Failed to export metrics: '{0}'", Level = EventLevel.Error)] + public void FailedExport(string exception) + { + this.WriteEvent(1, exception); + } - [Event(2, Message = "Canceled to export metrics: '{0}'", Level = EventLevel.Error)] - public void CanceledExport(string exception) - { - this.WriteEvent(2, exception); - } + [Event(2, Message = "Canceled to export metrics: '{0}'", Level = EventLevel.Error)] + public void CanceledExport(string exception) + { + this.WriteEvent(2, exception); + } - [Event(3, Message = "Failed to shutdown Metrics server '{0}'", Level = EventLevel.Error)] - public void FailedShutdown(string exception) - { - this.WriteEvent(3, exception); - } + [Event(3, Message = "Failed to shutdown Metrics server '{0}'", Level = EventLevel.Error)] + public void FailedShutdown(string exception) + { + this.WriteEvent(3, exception); + } - [Event(4, Message = "No metrics are available for export.", Level = EventLevel.Warning)] - public void NoMetrics() - { - this.WriteEvent(4); - } + [Event(4, Message = "No metrics are available for export.", Level = EventLevel.Warning)] + public void NoMetrics() + { + this.WriteEvent(4); } } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporterOptions.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporterOptions.cs index 4dca687a128..ec14b88b182 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporterOptions.cs @@ -1,45 +1,36 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Internal; -namespace OpenTelemetry.Exporter.Prometheus +namespace OpenTelemetry.Exporter.Prometheus; + +/// +/// Prometheus exporter options. +/// +internal sealed class PrometheusExporterOptions { + private int scrapeResponseCacheDurationMilliseconds = 300; + /// - /// Prometheus exporter options. + /// Gets or sets the cache duration in milliseconds for scrape responses. Default value: 300. /// - internal sealed class PrometheusExporterOptions + /// + /// Note: Specify 0 to disable response caching. + /// + public int ScrapeResponseCacheDurationMilliseconds { - private int scrapeResponseCacheDurationMilliseconds = 300; - - /// - /// Gets or sets the cache duration in milliseconds for scrape responses. Default value: 300. - /// - /// - /// Note: Specify 0 to disable response caching. - /// - public int ScrapeResponseCacheDurationMilliseconds + get => this.scrapeResponseCacheDurationMilliseconds; + set { - get => this.scrapeResponseCacheDurationMilliseconds; - set - { - Guard.ThrowIfOutOfRange(value, min: 0); + Guard.ThrowIfOutOfRange(value, min: 0); - this.scrapeResponseCacheDurationMilliseconds = value; - } + this.scrapeResponseCacheDurationMilliseconds = value; } } + + /// + /// Gets or sets a value indicating whether addition of _total suffix for counter metric names is disabled. Default value: . + /// + public bool DisableTotalNameSuffixForCounters { get; set; } } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusHeadersParser.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusHeadersParser.cs new file mode 100644 index 00000000000..81576b723dc --- /dev/null +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusHeadersParser.cs @@ -0,0 +1,47 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Exporter.Prometheus; + +internal static class PrometheusHeadersParser +{ + private const string OpenMetricsMediaType = "application/openmetrics-text"; + + internal static bool AcceptsOpenMetrics(string contentType) + { + var value = contentType.AsSpan(); + + while (value.Length > 0) + { + var headerValue = SplitNext(ref value, ','); + var mediaType = SplitNext(ref headerValue, ';'); + + if (mediaType.Equals(OpenMetricsMediaType.AsSpan(), StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + private static ReadOnlySpan SplitNext(ref ReadOnlySpan span, char character) + { + var index = span.IndexOf(character); + + if (index == -1) + { + var part = span; + span = span.Slice(span.Length); + + return part; + } + else + { + var part = span.Slice(0, index); + span = span.Slice(index + 1); + + return part; + } + } +} diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs new file mode 100644 index 00000000000..a39a426136a --- /dev/null +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs @@ -0,0 +1,280 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Text; +using OpenTelemetry.Metrics; + +namespace OpenTelemetry.Exporter.Prometheus; + +internal sealed class PrometheusMetric +{ + /* Counter becomes counter + Gauge becomes gauge + Histogram becomes histogram + UpDownCounter becomes gauge + * https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#otlp-metric-points-to-prometheus + */ + private static readonly PrometheusType[] MetricTypes = new PrometheusType[] + { + PrometheusType.Untyped, PrometheusType.Counter, PrometheusType.Gauge, PrometheusType.Summary, PrometheusType.Histogram, PrometheusType.Histogram, PrometheusType.Histogram, PrometheusType.Histogram, PrometheusType.Gauge, + }; + + public PrometheusMetric(string name, string unit, PrometheusType type, bool disableTotalNameSuffixForCounters) + { + // The metric name is + // required to match the regex: `[a-zA-Z_:]([a-zA-Z0-9_:])*`. Invalid characters + // in the metric name MUST be replaced with the `_` character. Multiple + // consecutive `_` characters MUST be replaced with a single `_` character. + // https://github.com/open-telemetry/opentelemetry-specification/blob/b2f923fb1650dde1f061507908b834035506a796/specification/compatibility/prometheus_and_openmetrics.md#L230-L233 + var sanitizedName = SanitizeMetricName(name); + + string sanitizedUnit = null; + if (!string.IsNullOrEmpty(unit)) + { + sanitizedUnit = GetUnit(unit); + + // The resulting unit SHOULD be added to the metric as + // [OpenMetrics UNIT metadata](https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#metricfamily) + // and as a suffix to the metric name unless the metric name already contains the + // unit, or the unit MUST be omitted. The unit suffix comes before any + // type-specific suffixes. + // https://github.com/open-telemetry/opentelemetry-specification/blob/b2f923fb1650dde1f061507908b834035506a796/specification/compatibility/prometheus_and_openmetrics.md#L242-L246 + if (!sanitizedName.Contains(sanitizedUnit)) + { + sanitizedName = sanitizedName + "_" + sanitizedUnit; + } + } + + // If the metric name for monotonic Sum metric points does not end in a suffix of `_total` a suffix of `_total` MUST be added by default, otherwise the name MUST remain unchanged. + // Exporters SHOULD provide a configuration option to disable the addition of `_total` suffixes. + // https://github.com/open-telemetry/opentelemetry-specification/blob/b2f923fb1650dde1f061507908b834035506a796/specification/compatibility/prometheus_and_openmetrics.md#L286 + if (type == PrometheusType.Counter && !sanitizedName.EndsWith("_total") && !disableTotalNameSuffixForCounters) + { + sanitizedName += "_total"; + } + + // Special case: Converting "1" to "ratio". + // https://github.com/open-telemetry/opentelemetry-specification/blob/b2f923fb1650dde1f061507908b834035506a796/specification/compatibility/prometheus_and_openmetrics.md#L239 + if (type == PrometheusType.Gauge && unit == "1" && !sanitizedName.Contains("ratio")) + { + sanitizedName += "_ratio"; + } + + this.Name = sanitizedName; + this.Unit = sanitizedUnit; + this.Type = type; + } + + public string Name { get; } + + public string Unit { get; } + + public PrometheusType Type { get; } + + public static PrometheusMetric Create(Metric metric, bool disableTotalNameSuffixForCounters) + { + return new PrometheusMetric(metric.Name, metric.Unit, GetPrometheusType(metric), disableTotalNameSuffixForCounters); + } + + internal static string SanitizeMetricName(string metricName) + { + StringBuilder sb = null; + var lastCharUnderscore = false; + + for (var i = 0; i < metricName.Length; i++) + { + var c = metricName[i]; + + if (i == 0 && char.IsNumber(c)) + { + sb ??= CreateStringBuilder(metricName); + sb.Append('_'); + lastCharUnderscore = true; + continue; + } + + if (!char.IsLetterOrDigit(c) && c != ':') + { + if (!lastCharUnderscore) + { + lastCharUnderscore = true; + sb ??= CreateStringBuilder(metricName); + sb.Append('_'); + } + } + else + { + sb ??= CreateStringBuilder(metricName); + sb.Append(c); + lastCharUnderscore = false; + } + } + + return sb?.ToString() ?? metricName; + + static StringBuilder CreateStringBuilder(string name) => new StringBuilder(name.Length); + } + + internal static string RemoveAnnotations(string unit) + { + // UCUM standard says the curly braces shouldn't be nested: + // https://ucum.org/ucum#section-Character-Set-and-Lexical-Rules + // What should happen if they are nested isn't defined. + // Right now the remove annotations code doesn't attempt to balance multiple start and end braces. + StringBuilder sb = null; + + var hasOpenBrace = false; + var startOpenBraceIndex = 0; + var lastWriteIndex = 0; + + for (var i = 0; i < unit.Length; i++) + { + var c = unit[i]; + if (c == '{') + { + if (!hasOpenBrace) + { + hasOpenBrace = true; + startOpenBraceIndex = i; + } + } + else if (c == '}') + { + if (hasOpenBrace) + { + sb ??= new StringBuilder(); + sb.Append(unit, lastWriteIndex, startOpenBraceIndex - lastWriteIndex); + hasOpenBrace = false; + lastWriteIndex = i + 1; + } + } + } + + if (lastWriteIndex == 0) + { + return unit; + } + + sb.Append(unit, lastWriteIndex, unit.Length - lastWriteIndex); + return sb.ToString(); + } + + private static string GetUnit(string unit) + { + // Dropping the portions of the Unit within brackets (e.g. {packet}). Brackets MUST NOT be included in the resulting unit. A "count of foo" is considered unitless in Prometheus. + // https://github.com/open-telemetry/opentelemetry-specification/blob/b2f923fb1650dde1f061507908b834035506a796/specification/compatibility/prometheus_and_openmetrics.md#L238 + var updatedUnit = RemoveAnnotations(unit); + + // Converting "foo/bar" to "foo_per_bar". + // https://github.com/open-telemetry/opentelemetry-specification/blob/b2f923fb1650dde1f061507908b834035506a796/specification/compatibility/prometheus_and_openmetrics.md#L240C3-L240C41 + if (TryProcessRateUnits(updatedUnit, out var updatedPerUnit)) + { + updatedUnit = updatedPerUnit; + } + else + { + // Converting from abbreviations to full words (e.g. "ms" to "milliseconds"). + // https://github.com/open-telemetry/opentelemetry-specification/blob/b2f923fb1650dde1f061507908b834035506a796/specification/compatibility/prometheus_and_openmetrics.md#L237 + updatedUnit = MapUnit(updatedUnit.AsSpan()); + } + + return updatedUnit; + } + + private static bool TryProcessRateUnits(string updatedUnit, out string updatedPerUnit) + { + updatedPerUnit = null; + + for (int i = 0; i < updatedUnit.Length; i++) + { + if (updatedUnit[i] == '/') + { + // Only convert rate expressed units if it's a valid expression. + if (i == updatedUnit.Length - 1) + { + return false; + } + + updatedPerUnit = MapUnit(updatedUnit.AsSpan(0, i)) + "_per_" + MapPerUnit(updatedUnit.AsSpan(i + 1, updatedUnit.Length - i - 1)); + return true; + } + } + + return false; + } + + private static PrometheusType GetPrometheusType(Metric metric) + { + int metricType = (int)metric.MetricType >> 4; + return MetricTypes[metricType]; + } + + // The map to translate OTLP units to Prometheus units + // OTLP metrics use the c/s notation as specified at https://ucum.org/ucum.html + // (See also https://github.com/open-telemetry/semantic-conventions/blob/main/docs/general/metrics.md#instrument-units) + // Prometheus best practices for units: https://prometheus.io/docs/practices/naming/#base-units + // OpenMetrics specification for units: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#units-and-base-units + private static string MapUnit(ReadOnlySpan unit) + { + return unit switch + { + // Time + "d" => "days", + "h" => "hours", + "min" => "minutes", + "s" => "seconds", + "ms" => "milliseconds", + "us" => "microseconds", + "ns" => "nanoseconds", + + // Bytes + "By" => "bytes", + "KiBy" => "kibibytes", + "MiBy" => "mebibytes", + "GiBy" => "gibibytes", + "TiBy" => "tibibytes", + "KBy" => "kilobytes", + "MBy" => "megabytes", + "GBy" => "gigabytes", + "TBy" => "terabytes", + "B" => "bytes", + "KB" => "kilobytes", + "MB" => "megabytes", + "GB" => "gigabytes", + "TB" => "terabytes", + + // SI + "m" => "meters", + "V" => "volts", + "A" => "amperes", + "J" => "joules", + "W" => "watts", + "g" => "grams", + + // Misc + "Cel" => "celsius", + "Hz" => "hertz", + "1" => string.Empty, + "%" => "percent", + "$" => "dollars", + _ => unit.ToString(), + }; + } + + // The map that translates the "per" unit + // Example: s => per second (singular) + private static string MapPerUnit(ReadOnlySpan perUnit) + { + return perUnit switch + { + "s" => "second", + "m" => "minute", + "h" => "hour", + "d" => "day", + "w" => "week", + "mo" => "month", + "y" => "year", + _ => perUnit.ToString(), + }; + } +} diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs index e1fc5ed79ad..69365d4e0ff 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs @@ -1,80 +1,34 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Globalization; using System.Runtime.CompilerServices; using OpenTelemetry.Internal; +using OpenTelemetry.Metrics; -namespace OpenTelemetry.Exporter.Prometheus +namespace OpenTelemetry.Exporter.Prometheus; + +/// +/// Basic PrometheusSerializer which has no OpenTelemetry dependency. +/// +internal static partial class PrometheusSerializer { - /// - /// Basic PrometheusSerializer which has no OpenTelemetry dependency. - /// - internal static partial class PrometheusSerializer - { #pragma warning disable SA1310 // Field name should not contain an underscore - private const byte ASCII_QUOTATION_MARK = 0x22; // '"' - private const byte ASCII_FULL_STOP = 0x2E; // '.' - private const byte ASCII_HYPHEN_MINUS = 0x2D; // '-' - private const byte ASCII_REVERSE_SOLIDUS = 0x5C; // '\\' - private const byte ASCII_LINEFEED = 0x0A; // `\n` + private const byte ASCII_QUOTATION_MARK = 0x22; // '"' + private const byte ASCII_FULL_STOP = 0x2E; // '.' + private const byte ASCII_HYPHEN_MINUS = 0x2D; // '-' + private const byte ASCII_REVERSE_SOLIDUS = 0x5C; // '\\' + private const byte ASCII_LINEFEED = 0x0A; // `\n` #pragma warning restore SA1310 // Field name should not contain an underscore - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteDouble(byte[] buffer, int cursor, double value) - { - if (MathHelper.IsFinite(value)) - { -#if NET6_0_OR_GREATER - Span span = stackalloc char[128]; - - var result = value.TryFormat(span, out var cchWritten, "G", CultureInfo.InvariantCulture); - Debug.Assert(result, $"{nameof(result)} should be true."); - - for (int i = 0; i < cchWritten; i++) - { - buffer[cursor++] = unchecked((byte)span[i]); - } -#else - cursor = WriteAsciiStringNoEscape(buffer, cursor, value.ToString(CultureInfo.InvariantCulture)); -#endif - } - else if (double.IsPositiveInfinity(value)) - { - cursor = WriteAsciiStringNoEscape(buffer, cursor, "+Inf"); - } - else if (double.IsNegativeInfinity(value)) - { - cursor = WriteAsciiStringNoEscape(buffer, cursor, "-Inf"); - } - else - { - Debug.Assert(double.IsNaN(value), $"{nameof(value)} should be NaN."); - cursor = WriteAsciiStringNoEscape(buffer, cursor, "Nan"); - } - - return cursor; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteLong(byte[] buffer, int cursor, long value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteDouble(byte[] buffer, int cursor, double value) + { + if (MathHelper.IsFinite(value)) { #if NET6_0_OR_GREATER - Span span = stackalloc char[20]; + Span span = stackalloc char[128]; var result = value.TryFormat(span, out var cchWritten, "G", CultureInfo.InvariantCulture); Debug.Assert(result, $"{nameof(result)} should be true."); @@ -86,265 +40,371 @@ public static int WriteLong(byte[] buffer, int cursor, long value) #else cursor = WriteAsciiStringNoEscape(buffer, cursor, value.ToString(CultureInfo.InvariantCulture)); #endif + } + else if (double.IsPositiveInfinity(value)) + { + cursor = WriteAsciiStringNoEscape(buffer, cursor, "+Inf"); + } + else if (double.IsNegativeInfinity(value)) + { + cursor = WriteAsciiStringNoEscape(buffer, cursor, "-Inf"); + } + else + { + Debug.Assert(double.IsNaN(value), $"{nameof(value)} should be NaN."); + cursor = WriteAsciiStringNoEscape(buffer, cursor, "Nan"); + } - return cursor; + return cursor; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteLong(byte[] buffer, int cursor, long value) + { +#if NET6_0_OR_GREATER + Span span = stackalloc char[20]; + + var result = value.TryFormat(span, out var cchWritten, "G", CultureInfo.InvariantCulture); + Debug.Assert(result, $"{nameof(result)} should be true."); + + for (int i = 0; i < cchWritten; i++) + { + buffer[cursor++] = unchecked((byte)span[i]); } +#else + cursor = WriteAsciiStringNoEscape(buffer, cursor, value.ToString(CultureInfo.InvariantCulture)); +#endif + + return cursor; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteAsciiStringNoEscape(byte[] buffer, int cursor, string value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteAsciiStringNoEscape(byte[] buffer, int cursor, string value) + { + for (int i = 0; i < value.Length; i++) + { + buffer[cursor++] = unchecked((byte)value[i]); + } + + return cursor; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteUnicodeNoEscape(byte[] buffer, int cursor, ushort ordinal) + { + if (ordinal <= 0x7F) + { + buffer[cursor++] = unchecked((byte)ordinal); + } + else if (ordinal <= 0x07FF) + { + buffer[cursor++] = unchecked((byte)(0b_1100_0000 | (ordinal >> 6))); + buffer[cursor++] = unchecked((byte)(0b_1000_0000 | (ordinal & 0b_0011_1111))); + } + else if (ordinal <= 0xFFFF) + { + buffer[cursor++] = unchecked((byte)(0b_1110_0000 | (ordinal >> 12))); + buffer[cursor++] = unchecked((byte)(0b_1000_0000 | ((ordinal >> 6) & 0b_0011_1111))); + buffer[cursor++] = unchecked((byte)(0b_1000_0000 | (ordinal & 0b_0011_1111))); + } + else + { + Debug.Assert(ordinal <= 0xFFFF, ".NET string should not go beyond Unicode BMP."); + } + + return cursor; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteUnicodeString(byte[] buffer, int cursor, string value) + { + for (int i = 0; i < value.Length; i++) { - for (int i = 0; i < value.Length; i++) + var ordinal = (ushort)value[i]; + switch (ordinal) { - buffer[cursor++] = unchecked((byte)value[i]); + case ASCII_REVERSE_SOLIDUS: + buffer[cursor++] = ASCII_REVERSE_SOLIDUS; + buffer[cursor++] = ASCII_REVERSE_SOLIDUS; + break; + case ASCII_LINEFEED: + buffer[cursor++] = ASCII_REVERSE_SOLIDUS; + buffer[cursor++] = unchecked((byte)'n'); + break; + default: + cursor = WriteUnicodeNoEscape(buffer, cursor, ordinal); + break; } + } - return cursor; + return cursor; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteLabelKey(byte[] buffer, int cursor, string value) + { + Debug.Assert(!string.IsNullOrEmpty(value), $"{nameof(value)} should not be null or empty."); + + var ordinal = (ushort)value[0]; + + if (ordinal >= (ushort)'0' && ordinal <= (ushort)'9') + { + buffer[cursor++] = unchecked((byte)'_'); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteUnicodeNoEscape(byte[] buffer, int cursor, ushort ordinal) + for (int i = 0; i < value.Length; i++) { - if (ordinal <= 0x7F) + ordinal = (ushort)value[i]; + + if ((ordinal >= (ushort)'A' && ordinal <= (ushort)'Z') || + (ordinal >= (ushort)'a' && ordinal <= (ushort)'z') || + (ordinal >= (ushort)'0' && ordinal <= (ushort)'9')) { buffer[cursor++] = unchecked((byte)ordinal); } - else if (ordinal <= 0x07FF) - { - buffer[cursor++] = unchecked((byte)(0b_1100_0000 | (ordinal >> 6))); - buffer[cursor++] = unchecked((byte)(0b_1000_0000 | (ordinal & 0b_0011_1111))); - } - else if (ordinal <= 0xFFFF) - { - buffer[cursor++] = unchecked((byte)(0b_1110_0000 | (ordinal >> 12))); - buffer[cursor++] = unchecked((byte)(0b_1000_0000 | ((ordinal >> 6) & 0b_0011_1111))); - buffer[cursor++] = unchecked((byte)(0b_1000_0000 | (ordinal & 0b_0011_1111))); - } else { - Debug.Assert(ordinal <= 0xFFFF, ".NET string should not go beyond Unicode BMP."); + buffer[cursor++] = unchecked((byte)'_'); } - - return cursor; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteUnicodeString(byte[] buffer, int cursor, string value) + return cursor; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteLabelValue(byte[] buffer, int cursor, string value) + { + Debug.Assert(value != null, $"{nameof(value)} should not be null."); + + for (int i = 0; i < value.Length; i++) { - for (int i = 0; i < value.Length; i++) + var ordinal = (ushort)value[i]; + switch (ordinal) { - var ordinal = (ushort)value[i]; - switch (ordinal) - { - case ASCII_REVERSE_SOLIDUS: - buffer[cursor++] = ASCII_REVERSE_SOLIDUS; - buffer[cursor++] = ASCII_REVERSE_SOLIDUS; - break; - case ASCII_LINEFEED: - buffer[cursor++] = ASCII_REVERSE_SOLIDUS; - buffer[cursor++] = unchecked((byte)'n'); - break; - default: - cursor = WriteUnicodeNoEscape(buffer, cursor, ordinal); - break; - } + case ASCII_QUOTATION_MARK: + buffer[cursor++] = ASCII_REVERSE_SOLIDUS; + buffer[cursor++] = ASCII_QUOTATION_MARK; + break; + case ASCII_REVERSE_SOLIDUS: + buffer[cursor++] = ASCII_REVERSE_SOLIDUS; + buffer[cursor++] = ASCII_REVERSE_SOLIDUS; + break; + case ASCII_LINEFEED: + buffer[cursor++] = ASCII_REVERSE_SOLIDUS; + buffer[cursor++] = unchecked((byte)'n'); + break; + default: + cursor = WriteUnicodeNoEscape(buffer, cursor, ordinal); + break; } - - return cursor; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteLabelKey(byte[] buffer, int cursor, string value) - { - Debug.Assert(!string.IsNullOrEmpty(value), $"{nameof(value)} should not be null or empty."); + return cursor; + } - var ordinal = (ushort)value[0]; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteLabel(byte[] buffer, int cursor, string labelKey, object labelValue) + { + cursor = WriteLabelKey(buffer, cursor, labelKey); + buffer[cursor++] = unchecked((byte)'='); + buffer[cursor++] = unchecked((byte)'"'); - if (ordinal >= (ushort)'0' && ordinal <= (ushort)'9') - { - buffer[cursor++] = unchecked((byte)'_'); - } + // In Prometheus, a label with an empty label value is considered equivalent to a label that does not exist. + cursor = WriteLabelValue(buffer, cursor, GetLabelValueString(labelValue)); + buffer[cursor++] = unchecked((byte)'"'); + + return cursor; - for (int i = 0; i < value.Length; i++) + static string GetLabelValueString(object labelValue) + { + // TODO: Attribute values should be written as their JSON representation. Extra logic may need to be added here to correctly convert other .NET types. + // More detail: https://github.com/open-telemetry/opentelemetry-dotnet/issues/4822#issuecomment-1707328495 + if (labelValue is bool b) { - ordinal = (ushort)value[i]; - - if ((ordinal >= (ushort)'A' && ordinal <= (ushort)'Z') || - (ordinal >= (ushort)'a' && ordinal <= (ushort)'z') || - (ordinal >= (ushort)'0' && ordinal <= (ushort)'9')) - { - buffer[cursor++] = unchecked((byte)ordinal); - } - else - { - buffer[cursor++] = unchecked((byte)'_'); - } + return b ? "true" : "false"; } - return cursor; + return labelValue?.ToString() ?? string.Empty; } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteLabelValue(byte[] buffer, int cursor, string value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteMetricName(byte[] buffer, int cursor, PrometheusMetric metric) + { + // Metric name has already been escaped. + for (int i = 0; i < metric.Name.Length; i++) { - Debug.Assert(value != null, $"{nameof(value)} should not be null."); + var ordinal = (ushort)metric.Name[i]; + buffer[cursor++] = unchecked((byte)ordinal); + } - for (int i = 0; i < value.Length; i++) - { - var ordinal = (ushort)value[i]; - switch (ordinal) - { - case ASCII_QUOTATION_MARK: - buffer[cursor++] = ASCII_REVERSE_SOLIDUS; - buffer[cursor++] = ASCII_QUOTATION_MARK; - break; - case ASCII_REVERSE_SOLIDUS: - buffer[cursor++] = ASCII_REVERSE_SOLIDUS; - buffer[cursor++] = ASCII_REVERSE_SOLIDUS; - break; - case ASCII_LINEFEED: - buffer[cursor++] = ASCII_REVERSE_SOLIDUS; - buffer[cursor++] = unchecked((byte)'n'); - break; - default: - cursor = WriteUnicodeNoEscape(buffer, cursor, ordinal); - break; - } - } + return cursor; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteEof(byte[] buffer, int cursor) + { + cursor = WriteAsciiStringNoEscape(buffer, cursor, "# EOF"); + buffer[cursor++] = ASCII_LINEFEED; + + return cursor; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteHelpMetadata(byte[] buffer, int cursor, PrometheusMetric metric, string metricDescription) + { + if (string.IsNullOrEmpty(metricDescription)) + { return cursor; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteLabel(byte[] buffer, int cursor, string labelKey, object labelValue) + cursor = WriteAsciiStringNoEscape(buffer, cursor, "# HELP "); + cursor = WriteMetricName(buffer, cursor, metric); + + if (!string.IsNullOrEmpty(metricDescription)) { - cursor = WriteLabelKey(buffer, cursor, labelKey); - buffer[cursor++] = unchecked((byte)'='); - buffer[cursor++] = unchecked((byte)'"'); + buffer[cursor++] = unchecked((byte)' '); + cursor = WriteUnicodeString(buffer, cursor, metricDescription); + } - // In Prometheus, a label with an empty label value is considered equivalent to a label that does not exist. - cursor = WriteLabelValue(buffer, cursor, labelValue?.ToString() ?? string.Empty); - buffer[cursor++] = unchecked((byte)'"'); + buffer[cursor++] = ASCII_LINEFEED; - return cursor; - } + return cursor; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteMetricName(byte[] buffer, int cursor, string metricName, string metricUnit = null) - { - Debug.Assert(!string.IsNullOrEmpty(metricName), $"{nameof(metricName)} should not be null or empty."); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteTypeMetadata(byte[] buffer, int cursor, PrometheusMetric metric) + { + var metricType = MapPrometheusType(metric.Type); - for (int i = 0; i < metricName.Length; i++) - { - var ordinal = (ushort)metricName[i]; - buffer[cursor++] = ordinal switch - { - ASCII_FULL_STOP or ASCII_HYPHEN_MINUS => unchecked((byte)'_'), - _ => unchecked((byte)ordinal), - }; - } + Debug.Assert(!string.IsNullOrEmpty(metricType), $"{nameof(metricType)} should not be null or empty."); - if (!string.IsNullOrEmpty(metricUnit)) - { - buffer[cursor++] = unchecked((byte)'_'); + cursor = WriteAsciiStringNoEscape(buffer, cursor, "# TYPE "); + cursor = WriteMetricName(buffer, cursor, metric); + buffer[cursor++] = unchecked((byte)' '); + cursor = WriteAsciiStringNoEscape(buffer, cursor, metricType); - for (int i = 0; i < metricUnit.Length; i++) - { - var ordinal = (ushort)metricUnit[i]; - - if ((ordinal >= (ushort)'A' && ordinal <= (ushort)'Z') || - (ordinal >= (ushort)'a' && ordinal <= (ushort)'z') || - (ordinal >= (ushort)'0' && ordinal <= (ushort)'9')) - { - buffer[cursor++] = unchecked((byte)ordinal); - } - else - { - buffer[cursor++] = unchecked((byte)'_'); - } - } - } + buffer[cursor++] = ASCII_LINEFEED; + + return cursor; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteUnitMetadata(byte[] buffer, int cursor, PrometheusMetric metric) + { + if (string.IsNullOrEmpty(metric.Unit)) + { return cursor; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteEof(byte[] buffer, int cursor) + cursor = WriteAsciiStringNoEscape(buffer, cursor, "# UNIT "); + cursor = WriteMetricName(buffer, cursor, metric); + + buffer[cursor++] = unchecked((byte)' '); + + // Unit name has already been escaped. + for (int i = 0; i < metric.Unit.Length; i++) { - cursor = WriteAsciiStringNoEscape(buffer, cursor, "# EOF"); - buffer[cursor++] = ASCII_LINEFEED; + var ordinal = (ushort)metric.Unit[i]; + buffer[cursor++] = unchecked((byte)ordinal); + } + + buffer[cursor++] = ASCII_LINEFEED; + return cursor; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteScopeInfo(byte[] buffer, int cursor, string scopeName) + { + if (string.IsNullOrEmpty(scopeName)) + { return cursor; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteHelpMetadata(byte[] buffer, int cursor, string metricName, string metricUnit, string metricDescription) + cursor = WriteAsciiStringNoEscape(buffer, cursor, "# TYPE otel_scope_info info"); + buffer[cursor++] = ASCII_LINEFEED; + + cursor = WriteAsciiStringNoEscape(buffer, cursor, "# HELP otel_scope_info Scope metadata"); + buffer[cursor++] = ASCII_LINEFEED; + + cursor = WriteAsciiStringNoEscape(buffer, cursor, "otel_scope_info"); + buffer[cursor++] = unchecked((byte)'{'); + cursor = WriteLabel(buffer, cursor, "otel_scope_name", scopeName); + buffer[cursor++] = unchecked((byte)'}'); + buffer[cursor++] = unchecked((byte)' '); + buffer[cursor++] = unchecked((byte)'1'); + buffer[cursor++] = ASCII_LINEFEED; + + return cursor; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteTimestamp(byte[] buffer, int cursor, long value, bool useOpenMetrics) + { + if (useOpenMetrics) { - if (string.IsNullOrEmpty(metricDescription)) - { - return cursor; - } + cursor = WriteLong(buffer, cursor, value / 1000); + buffer[cursor++] = unchecked((byte)'.'); - cursor = WriteAsciiStringNoEscape(buffer, cursor, "# HELP "); - cursor = WriteMetricName(buffer, cursor, metricName, metricUnit); + long millis = value % 1000; - if (!string.IsNullOrEmpty(metricDescription)) + if (millis < 100) { - buffer[cursor++] = unchecked((byte)' '); - cursor = WriteUnicodeString(buffer, cursor, metricDescription); + buffer[cursor++] = unchecked((byte)'0'); } - buffer[cursor++] = ASCII_LINEFEED; + if (millis < 10) + { + buffer[cursor++] = unchecked((byte)'0'); + } - return cursor; + return WriteLong(buffer, cursor, millis); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteTypeMetadata(byte[] buffer, int cursor, string metricName, string metricUnit, string metricType) - { - Debug.Assert(!string.IsNullOrEmpty(metricType), $"{nameof(metricType)} should not be null or empty."); + return WriteLong(buffer, cursor, value); + } - cursor = WriteAsciiStringNoEscape(buffer, cursor, "# TYPE "); - cursor = WriteMetricName(buffer, cursor, metricName, metricUnit); - buffer[cursor++] = unchecked((byte)' '); - cursor = WriteAsciiStringNoEscape(buffer, cursor, metricType); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteTags(byte[] buffer, int cursor, Metric metric, ReadOnlyTagCollection tags, bool writeEnclosingBraces = true) + { + if (writeEnclosingBraces) + { + buffer[cursor++] = unchecked((byte)'{'); + } - buffer[cursor++] = ASCII_LINEFEED; + cursor = WriteLabel(buffer, cursor, "otel_scope_name", metric.MeterName); + buffer[cursor++] = unchecked((byte)','); - return cursor; + if (!string.IsNullOrEmpty(metric.MeterVersion)) + { + cursor = WriteLabel(buffer, cursor, "otel_scope_version", metric.MeterVersion); + buffer[cursor++] = unchecked((byte)','); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteUnitMetadata(byte[] buffer, int cursor, string metricName, string metricUnit) + foreach (var tag in tags) { - if (string.IsNullOrEmpty(metricUnit)) - { - return cursor; - } - - cursor = WriteAsciiStringNoEscape(buffer, cursor, "# UNIT "); - cursor = WriteMetricName(buffer, cursor, metricName, metricUnit); - - buffer[cursor++] = unchecked((byte)' '); + cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value); + buffer[cursor++] = unchecked((byte)','); + } - for (int i = 0; i < metricUnit.Length; i++) - { - var ordinal = (ushort)metricUnit[i]; - - if ((ordinal >= (ushort)'A' && ordinal <= (ushort)'Z') || - (ordinal >= (ushort)'a' && ordinal <= (ushort)'z') || - (ordinal >= (ushort)'0' && ordinal <= (ushort)'9')) - { - buffer[cursor++] = unchecked((byte)ordinal); - } - else - { - buffer[cursor++] = unchecked((byte)'_'); - } - } + if (writeEnclosingBraces) + { + buffer[cursor - 1] = unchecked((byte)'}'); // Note: We write the '}' over the last written comma, which is extra. + } - buffer[cursor++] = ASCII_LINEFEED; + return cursor; + } - return cursor; - } + private static string MapPrometheusType(PrometheusType type) + { + return type switch + { + PrometheusType.Gauge => "gauge", + PrometheusType.Counter => "counter", + PrometheusType.Summary => "summary", + PrometheusType.Histogram => "histogram", + _ => "untyped", + }; } } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs index 3094129cc7e..1523ef7c160 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs @@ -1,210 +1,145 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Metrics; -namespace OpenTelemetry.Exporter.Prometheus +namespace OpenTelemetry.Exporter.Prometheus; + +/// +/// OpenTelemetry additions to the PrometheusSerializer. +/// +internal static partial class PrometheusSerializer { - /// - /// OpenTelemetry additions to the PrometheusSerializer. - /// - internal static partial class PrometheusSerializer + public static bool CanWriteMetric(Metric metric) { - /* Counter becomes counter - Gauge becomes gauge - Histogram becomes histogram - UpDownCounter becomes gauge - * https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#otlp-metric-points-to-prometheus - */ - private static readonly string[] MetricTypes = new string[] + if (metric.MetricType == MetricType.ExponentialHistogram) { - "untyped", "counter", "gauge", "summary", "histogram", "histogram", "histogram", "histogram", "gauge", - }; + // Exponential histograms are not yet support by Prometheus. + // They are ignored for now. + return false; + } - public static int WriteMetric(byte[] buffer, int cursor, Metric metric) - { - if (metric.MetricType == MetricType.ExponentialHistogram) - { - // Exponential histograms are not yet support by Prometheus. - // They are ignored for now. - return cursor; - } + return true; + } - int metricType = (int)metric.MetricType >> 4; - cursor = WriteTypeMetadata(buffer, cursor, metric.Name, metric.Unit, MetricTypes[metricType]); - cursor = WriteUnitMetadata(buffer, cursor, metric.Name, metric.Unit); - cursor = WriteHelpMetadata(buffer, cursor, metric.Name, metric.Unit, metric.Description); + public static int WriteMetric(byte[] buffer, int cursor, Metric metric, PrometheusMetric prometheusMetric, bool openMetricsRequested = false) + { + cursor = WriteTypeMetadata(buffer, cursor, prometheusMetric); + cursor = WriteUnitMetadata(buffer, cursor, prometheusMetric); + cursor = WriteHelpMetadata(buffer, cursor, prometheusMetric, metric.Description); - if (!metric.MetricType.IsHistogram()) + if (!metric.MetricType.IsHistogram()) + { + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) { - foreach (ref readonly var metricPoint in metric.GetMetricPoints()) - { - var tags = metricPoint.Tags; - var timestamp = metricPoint.EndTime.ToUnixTimeMilliseconds(); - - // Counter and Gauge - cursor = WriteMetricName(buffer, cursor, metric.Name, metric.Unit); - - if (tags.Count > 0) - { - buffer[cursor++] = unchecked((byte)'{'); - - foreach (var tag in tags) - { - cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value); - buffer[cursor++] = unchecked((byte)','); - } + var timestamp = metricPoint.EndTime.ToUnixTimeMilliseconds(); - buffer[cursor - 1] = unchecked((byte)'}'); // Note: We write the '}' over the last written comma, which is extra. - } + // Counter and Gauge + cursor = WriteMetricName(buffer, cursor, prometheusMetric); + cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags); - buffer[cursor++] = unchecked((byte)' '); + buffer[cursor++] = unchecked((byte)' '); - // TODO: MetricType is same for all MetricPoints - // within a given Metric, so this check can avoided - // for each MetricPoint - if (((int)metric.MetricType & 0b_0000_1111) == 0x0a /* I8 */) + // TODO: MetricType is same for all MetricPoints + // within a given Metric, so this check can avoided + // for each MetricPoint + if (((int)metric.MetricType & 0b_0000_1111) == 0x0a /* I8 */) + { + if (metric.MetricType.IsSum()) { - if (metric.MetricType.IsSum()) - { - cursor = WriteLong(buffer, cursor, metricPoint.GetSumLong()); - } - else - { - cursor = WriteLong(buffer, cursor, metricPoint.GetGaugeLastValueLong()); - } + cursor = WriteLong(buffer, cursor, metricPoint.GetSumLong()); } else { - if (metric.MetricType.IsSum()) - { - cursor = WriteDouble(buffer, cursor, metricPoint.GetSumDouble()); - } - else - { - cursor = WriteDouble(buffer, cursor, metricPoint.GetGaugeLastValueDouble()); - } + cursor = WriteLong(buffer, cursor, metricPoint.GetGaugeLastValueLong()); } - - buffer[cursor++] = unchecked((byte)' '); - - cursor = WriteLong(buffer, cursor, timestamp); - - buffer[cursor++] = ASCII_LINEFEED; } - } - else - { - foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + else { - var tags = metricPoint.Tags; - var timestamp = metricPoint.EndTime.ToUnixTimeMilliseconds(); - - long totalCount = 0; - foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets()) + if (metric.MetricType.IsSum()) { - totalCount += histogramMeasurement.BucketCount; - - cursor = WriteMetricName(buffer, cursor, metric.Name, metric.Unit); - cursor = WriteAsciiStringNoEscape(buffer, cursor, "_bucket{"); + cursor = WriteDouble(buffer, cursor, metricPoint.GetSumDouble()); + } + else + { + cursor = WriteDouble(buffer, cursor, metricPoint.GetGaugeLastValueDouble()); + } + } - foreach (var tag in tags) - { - cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value); - buffer[cursor++] = unchecked((byte)','); - } + buffer[cursor++] = unchecked((byte)' '); - cursor = WriteAsciiStringNoEscape(buffer, cursor, "le=\""); + cursor = WriteTimestamp(buffer, cursor, timestamp, openMetricsRequested); - if (histogramMeasurement.ExplicitBound != double.PositiveInfinity) - { - cursor = WriteDouble(buffer, cursor, histogramMeasurement.ExplicitBound); - } - else - { - cursor = WriteAsciiStringNoEscape(buffer, cursor, "+Inf"); - } + buffer[cursor++] = ASCII_LINEFEED; + } + } + else + { + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + { + var tags = metricPoint.Tags; + var timestamp = metricPoint.EndTime.ToUnixTimeMilliseconds(); - cursor = WriteAsciiStringNoEscape(buffer, cursor, "\"} "); + long totalCount = 0; + foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets()) + { + totalCount += histogramMeasurement.BucketCount; - cursor = WriteLong(buffer, cursor, totalCount); - buffer[cursor++] = unchecked((byte)' '); + cursor = WriteMetricName(buffer, cursor, prometheusMetric); + cursor = WriteAsciiStringNoEscape(buffer, cursor, "_bucket{"); + cursor = WriteTags(buffer, cursor, metric, tags, writeEnclosingBraces: false); - cursor = WriteLong(buffer, cursor, timestamp); + cursor = WriteAsciiStringNoEscape(buffer, cursor, "le=\""); - buffer[cursor++] = ASCII_LINEFEED; + if (histogramMeasurement.ExplicitBound != double.PositiveInfinity) + { + cursor = WriteDouble(buffer, cursor, histogramMeasurement.ExplicitBound); } - - // Histogram sum - cursor = WriteMetricName(buffer, cursor, metric.Name, metric.Unit); - cursor = WriteAsciiStringNoEscape(buffer, cursor, "_sum"); - - if (tags.Count > 0) + else { - buffer[cursor++] = unchecked((byte)'{'); - - foreach (var tag in tags) - { - cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value); - buffer[cursor++] = unchecked((byte)','); - } - - buffer[cursor - 1] = unchecked((byte)'}'); // Note: We write the '}' over the last written comma, which is extra. + cursor = WriteAsciiStringNoEscape(buffer, cursor, "+Inf"); } - buffer[cursor++] = unchecked((byte)' '); + cursor = WriteAsciiStringNoEscape(buffer, cursor, "\"} "); - cursor = WriteDouble(buffer, cursor, metricPoint.GetHistogramSum()); + cursor = WriteLong(buffer, cursor, totalCount); buffer[cursor++] = unchecked((byte)' '); - cursor = WriteLong(buffer, cursor, timestamp); + cursor = WriteTimestamp(buffer, cursor, timestamp, openMetricsRequested); buffer[cursor++] = ASCII_LINEFEED; + } - // Histogram count - cursor = WriteMetricName(buffer, cursor, metric.Name, metric.Unit); - cursor = WriteAsciiStringNoEscape(buffer, cursor, "_count"); + // Histogram sum + cursor = WriteMetricName(buffer, cursor, prometheusMetric); + cursor = WriteAsciiStringNoEscape(buffer, cursor, "_sum"); + cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags); - if (tags.Count > 0) - { - buffer[cursor++] = unchecked((byte)'{'); + buffer[cursor++] = unchecked((byte)' '); - foreach (var tag in tags) - { - cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value); - buffer[cursor++] = unchecked((byte)','); - } + cursor = WriteDouble(buffer, cursor, metricPoint.GetHistogramSum()); + buffer[cursor++] = unchecked((byte)' '); - buffer[cursor - 1] = unchecked((byte)'}'); // Note: We write the '}' over the last written comma, which is extra. - } + cursor = WriteTimestamp(buffer, cursor, timestamp, openMetricsRequested); - buffer[cursor++] = unchecked((byte)' '); + buffer[cursor++] = ASCII_LINEFEED; - cursor = WriteLong(buffer, cursor, metricPoint.GetHistogramCount()); - buffer[cursor++] = unchecked((byte)' '); + // Histogram count + cursor = WriteMetricName(buffer, cursor, prometheusMetric); + cursor = WriteAsciiStringNoEscape(buffer, cursor, "_count"); + cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags); - cursor = WriteLong(buffer, cursor, timestamp); + buffer[cursor++] = unchecked((byte)' '); - buffer[cursor++] = ASCII_LINEFEED; - } - } + cursor = WriteLong(buffer, cursor, metricPoint.GetHistogramCount()); + buffer[cursor++] = unchecked((byte)' '); - buffer[cursor++] = ASCII_LINEFEED; + cursor = WriteTimestamp(buffer, cursor, timestamp, openMetricsRequested); - return cursor; + buffer[cursor++] = ASCII_LINEFEED; + } } + + return cursor; } } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusType.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusType.cs new file mode 100644 index 00000000000..ac3f73ff5e9 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusType.cs @@ -0,0 +1,32 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Exporter.Prometheus; + +internal enum PrometheusType +{ + /// + /// Not mapped. + /// + Untyped, + + /// + /// Mapped from Gauge and UpDownCounter. + /// + Gauge, + + /// + /// Mapped from Counter. + /// + Counter, + + /// + /// Not mapped. + /// + Summary, + + /// + /// Mapped from Histogram. + /// + Histogram, +} diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/OpenTelemetry.Exporter.Prometheus.HttpListener.csproj b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/OpenTelemetry.Exporter.Prometheus.HttpListener.csproj index 4d2c35e9451..2301edd4068 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/OpenTelemetry.Exporter.Prometheus.HttpListener.csproj +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/OpenTelemetry.Exporter.Prometheus.HttpListener.csproj @@ -1,8 +1,7 @@ - - netstandard2.0;net462 + $(TargetFrameworksForLibraries) Stand-alone HttpListener for hosting OpenTelemetry .NET Prometheus Exporter $(PackageTags);prometheus;metrics core- @@ -11,10 +10,10 @@ disable - - false + true @@ -22,9 +21,9 @@ - - - + + + diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs index 040ada21173..6f9663ba429 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs @@ -1,191 +1,193 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Net; using OpenTelemetry.Exporter.Prometheus; using OpenTelemetry.Internal; -namespace OpenTelemetry.Exporter +namespace OpenTelemetry.Exporter; + +internal sealed class PrometheusHttpListener : IDisposable { - internal sealed class PrometheusHttpListener : IDisposable + private readonly PrometheusExporter exporter; + private readonly HttpListener httpListener = new(); + private readonly object syncObject = new(); + + private CancellationTokenSource tokenSource; + private Task workerThread; + + /// + /// Initializes a new instance of the class. + /// + /// The exporter instance. + /// The configured HttpListener options. + public PrometheusHttpListener(PrometheusExporter exporter, PrometheusHttpListenerOptions options) { - private readonly PrometheusExporter exporter; - private readonly HttpListener httpListener = new(); - private readonly object syncObject = new(); - - private CancellationTokenSource tokenSource; - private Task workerThread; - - /// - /// Initializes a new instance of the class. - /// - /// The exporter instance. - /// The configured HttpListener options. - public PrometheusHttpListener(PrometheusExporter exporter, PrometheusHttpListenerOptions options) - { - Guard.ThrowIfNull(exporter); - Guard.ThrowIfNull(options); + Guard.ThrowIfNull(exporter); + Guard.ThrowIfNull(options); - this.exporter = exporter; + this.exporter = exporter; - string path = options.ScrapeEndpointPath; + string path = options.ScrapeEndpointPath; - if (!path.StartsWith("/")) - { - path = $"/{path}"; - } + if (!path.StartsWith("/")) + { + path = $"/{path}"; + } - if (!path.EndsWith("/")) - { - path = $"{path}/"; - } + if (!path.EndsWith("/")) + { + path = $"{path}/"; + } - foreach (string uriPrefix in options.UriPrefixes) - { - this.httpListener.Prefixes.Add($"{uriPrefix.TrimEnd('/')}{path}"); - } + foreach (string uriPrefix in options.UriPrefixes) + { + this.httpListener.Prefixes.Add($"{uriPrefix.TrimEnd('/')}{path}"); } + } - /// - /// Start the HttpListener. - /// - /// An optional that can be used to stop the HTTP listener. - public void Start(CancellationToken token = default) + /// + /// Start the HttpListener. + /// + /// An optional that can be used to stop the HTTP listener. + public void Start(CancellationToken token = default) + { + lock (this.syncObject) { - lock (this.syncObject) + if (this.tokenSource != null) { - if (this.tokenSource != null) - { - return; - } + return; + } + + this.httpListener.Start(); - // link the passed in token if not null - this.tokenSource = token == default ? - new CancellationTokenSource() : - CancellationTokenSource.CreateLinkedTokenSource(token); + // link the passed in token if not null + this.tokenSource = token == default ? + new CancellationTokenSource() : + CancellationTokenSource.CreateLinkedTokenSource(token); - this.workerThread = Task.Factory.StartNew(this.WorkerProc, default, TaskCreationOptions.LongRunning, TaskScheduler.Default); - } + this.workerThread = Task.Factory.StartNew(this.WorkerProc, default, TaskCreationOptions.LongRunning, TaskScheduler.Default); } + } - /// - /// Gracefully stop the PrometheusHttpListener. - /// - public void Stop() + /// + /// Gracefully stop the PrometheusHttpListener. + /// + public void Stop() + { + lock (this.syncObject) { - lock (this.syncObject) + if (this.tokenSource == null) { - if (this.tokenSource == null) - { - return; - } - - this.tokenSource.Cancel(); - this.workerThread.Wait(); - this.tokenSource = null; + return; } + + this.tokenSource.Cancel(); + this.workerThread.Wait(); + this.tokenSource = null; } + } - /// - public void Dispose() - { - this.Stop(); + /// + public void Dispose() + { + this.Stop(); - if (this.httpListener != null && this.httpListener.IsListening) - { - this.httpListener.Close(); - } + if (this.httpListener.IsListening) + { + this.httpListener.Close(); } + } + + private static bool AcceptsOpenMetrics(HttpListenerRequest request) + { + var acceptHeader = request.Headers["Accept"]; - private void WorkerProc() + if (string.IsNullOrEmpty(acceptHeader)) { - this.httpListener.Start(); + return false; + } - try + return PrometheusHeadersParser.AcceptsOpenMetrics(acceptHeader); + } + + private void WorkerProc() + { + try + { + using var scope = SuppressInstrumentationScope.Begin(); + while (!this.tokenSource.IsCancellationRequested) { - using var scope = SuppressInstrumentationScope.Begin(); - while (!this.tokenSource.IsCancellationRequested) - { - var ctxTask = this.httpListener.GetContextAsync(); - ctxTask.Wait(this.tokenSource.Token); - var ctx = ctxTask.Result; + var ctxTask = this.httpListener.GetContextAsync(); + ctxTask.Wait(this.tokenSource.Token); + var ctx = ctxTask.Result; - Task.Run(() => this.ProcessRequestAsync(ctx)); - } + Task.Run(() => this.ProcessRequestAsync(ctx)); } - catch (OperationCanceledException ex) + } + catch (OperationCanceledException ex) + { + PrometheusExporterEventSource.Log.CanceledExport(ex); + } + finally + { + try { - PrometheusExporterEventSource.Log.CanceledExport(ex); + this.httpListener.Stop(); + this.httpListener.Close(); } - finally + catch (Exception exFromFinally) { - try - { - this.httpListener.Stop(); - this.httpListener.Close(); - } - catch (Exception exFromFinally) - { - PrometheusExporterEventSource.Log.FailedShutdown(exFromFinally); - } + PrometheusExporterEventSource.Log.FailedShutdown(exFromFinally); } } + } - private async Task ProcessRequestAsync(HttpListenerContext context) + private async Task ProcessRequestAsync(HttpListenerContext context) + { + try { + var openMetricsRequested = AcceptsOpenMetrics(context.Request); + var collectionResponse = await this.exporter.CollectionManager.EnterCollect(openMetricsRequested).ConfigureAwait(false); + try { - var collectionResponse = await this.exporter.CollectionManager.EnterCollect().ConfigureAwait(false); - try + context.Response.Headers.Add("Server", string.Empty); + if (collectionResponse.View.Count > 0) { - context.Response.Headers.Add("Server", string.Empty); - if (collectionResponse.View.Count > 0) - { - context.Response.StatusCode = 200; - context.Response.Headers.Add("Last-Modified", collectionResponse.GeneratedAtUtc.ToString("R")); - context.Response.ContentType = "text/plain; charset=utf-8; version=0.0.4"; - - await context.Response.OutputStream.WriteAsync(collectionResponse.View.Array, 0, collectionResponse.View.Count).ConfigureAwait(false); - } - else - { - // It's not expected to have no metrics to collect, but it's not necessarily a failure, either. - context.Response.StatusCode = 200; - PrometheusExporterEventSource.Log.NoMetrics(); - } + context.Response.StatusCode = 200; + context.Response.Headers.Add("Last-Modified", collectionResponse.GeneratedAtUtc.ToString("R")); + context.Response.ContentType = openMetricsRequested + ? "application/openmetrics-text; version=1.0.0; charset=utf-8" + : "text/plain; charset=utf-8; version=0.0.4"; + + await context.Response.OutputStream.WriteAsync(collectionResponse.View.Array, 0, collectionResponse.View.Count).ConfigureAwait(false); } - finally + else { - this.exporter.CollectionManager.ExitCollect(); + // It's not expected to have no metrics to collect, but it's not necessarily a failure, either. + context.Response.StatusCode = 200; + PrometheusExporterEventSource.Log.NoMetrics(); } } - catch (Exception ex) + finally { - PrometheusExporterEventSource.Log.FailedExport(ex); - - context.Response.StatusCode = 500; + this.exporter.CollectionManager.ExitCollect(); } + } + catch (Exception ex) + { + PrometheusExporterEventSource.Log.FailedExport(ex); - try - { - context.Response.Close(); - } - catch - { - } + context.Response.StatusCode = 500; + } + + try + { + context.Response.Close(); + } + catch + { } } } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerMeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerMeterProviderBuilderExtensions.cs index 1c8c63f4ce0..929774a11f9 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerMeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerMeterProviderBuilderExtensions.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -20,91 +7,94 @@ using OpenTelemetry.Exporter.Prometheus; using OpenTelemetry.Internal; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// Extension methods to simplify registering a PrometheusHttpListener. +/// +public static class PrometheusHttpListenerMeterProviderBuilderExtensions { /// - /// Extension methods to simplify registering a PrometheusHttpListener. + /// Adds PrometheusHttpListener to MeterProviderBuilder. /// - public static class PrometheusHttpListenerMeterProviderBuilderExtensions - { - /// - /// Adds PrometheusHttpListener to MeterProviderBuilder. - /// - /// builder to use. - /// The instance of to chain calls. - public static MeterProviderBuilder AddPrometheusHttpListener(this MeterProviderBuilder builder) - => AddPrometheusHttpListener(builder, name: null, configure: null); + /// builder to use. + /// The instance of to chain calls. + public static MeterProviderBuilder AddPrometheusHttpListener(this MeterProviderBuilder builder) + => AddPrometheusHttpListener(builder, name: null, configure: null); - /// - /// Adds PrometheusHttpListener to MeterProviderBuilder. - /// - /// builder to use. - /// Callback action for configuring . - /// The instance of to chain calls. - public static MeterProviderBuilder AddPrometheusHttpListener( - this MeterProviderBuilder builder, - Action configure) - => AddPrometheusHttpListener(builder, name: null, configure); + /// + /// Adds PrometheusHttpListener to MeterProviderBuilder. + /// + /// builder to use. + /// Callback action for configuring . + /// The instance of to chain calls. + public static MeterProviderBuilder AddPrometheusHttpListener( + this MeterProviderBuilder builder, + Action configure) + => AddPrometheusHttpListener(builder, name: null, configure); - /// - /// Adds PrometheusHttpListener to MeterProviderBuilder. - /// - /// builder to use. - /// Name which is used when retrieving options. - /// Callback action for configuring . - /// The instance of to chain calls. - public static MeterProviderBuilder AddPrometheusHttpListener( - this MeterProviderBuilder builder, - string name, - Action configure) - { - Guard.ThrowIfNull(builder); + /// + /// Adds PrometheusHttpListener to MeterProviderBuilder. + /// + /// builder to use. + /// Name which is used when retrieving options. + /// Callback action for configuring . + /// The instance of to chain calls. + public static MeterProviderBuilder AddPrometheusHttpListener( + this MeterProviderBuilder builder, + string name, + Action configure) + { + Guard.ThrowIfNull(builder); - name ??= Options.DefaultName; + name ??= Options.DefaultName; - if (configure != null) - { - builder.ConfigureServices(services => services.Configure(name, configure)); - } + if (configure != null) + { + builder.ConfigureServices(services => services.Configure(name, configure)); + } - return builder.AddReader(sp => - { - var options = sp.GetRequiredService>().Get(name); + return builder.AddReader(sp => + { + var options = sp.GetRequiredService>().Get(name); - return BuildPrometheusHttpListenerMetricReader(options); - }); - } + return BuildPrometheusHttpListenerMetricReader(options); + }); + } - private static MetricReader BuildPrometheusHttpListenerMetricReader( - PrometheusHttpListenerOptions options) + private static MetricReader BuildPrometheusHttpListenerMetricReader( + PrometheusHttpListenerOptions options) + { + var exporter = new PrometheusExporter(new PrometheusExporterOptions { - var exporter = new PrometheusExporter(new PrometheusExporterOptions { ScrapeResponseCacheDurationMilliseconds = 0 }); + ScrapeResponseCacheDurationMilliseconds = 0, + DisableTotalNameSuffixForCounters = options.DisableTotalNameSuffixForCounters, + }); - var reader = new BaseExportingMetricReader(exporter) - { - TemporalityPreference = MetricReaderTemporalityPreference.Cumulative, - }; + var reader = new BaseExportingMetricReader(exporter) + { + TemporalityPreference = MetricReaderTemporalityPreference.Cumulative, + }; + try + { + var listener = new PrometheusHttpListener(exporter, options); + exporter.OnDispose = () => listener.Dispose(); + listener.Start(); + } + catch (Exception ex) + { try { - var listener = new PrometheusHttpListener(exporter, options); - exporter.OnDispose = () => listener.Dispose(); - listener.Start(); + reader.Dispose(); } - catch (Exception ex) + catch { - try - { - reader.Dispose(); - } - catch - { - } - - throw new InvalidOperationException("PrometheusExporter HttpListener could not be started.", ex); } - return reader; + throw new InvalidOperationException("PrometheusExporter HttpListener could not be started.", ex); } + + return reader; } } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerOptions.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerOptions.cs index 77367c9b707..d0c6bd2edf0 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerOptions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerOptions.cs @@ -1,53 +1,44 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Internal; -namespace OpenTelemetry.Exporter +namespace OpenTelemetry.Exporter; + +/// +/// options. +/// +public class PrometheusHttpListenerOptions { + private IReadOnlyCollection uriPrefixes = new[] { "http://localhost:9464/" }; + + /// + /// Gets or sets the path to use for the scraping endpoint. Default value: "/metrics". + /// + public string ScrapeEndpointPath { get; set; } = "/metrics"; + + /// + /// Gets or sets a value indicating whether addition of _total suffix for counter metric names is disabled. Default value: . + /// + public bool DisableTotalNameSuffixForCounters { get; set; } + /// - /// options. + /// Gets or sets the URI (Uniform Resource Identifier) prefixes to use for the http listener. + /// Default value: ["http://localhost:9464/"]. /// - public class PrometheusHttpListenerOptions + public IReadOnlyCollection UriPrefixes { - private IReadOnlyCollection uriPrefixes = new[] { "http://localhost:9464/" }; - - /// - /// Gets or sets the path to use for the scraping endpoint. Default value: "/metrics". - /// - public string ScrapeEndpointPath { get; set; } = "/metrics"; - - /// - /// Gets or sets the URI (Uniform Resource Identifier) prefixes to use for the http listener. - /// Default value: ["http://localhost:9464/"]. - /// - public IReadOnlyCollection UriPrefixes + get => this.uriPrefixes; + set { - get => this.uriPrefixes; - set - { - Guard.ThrowIfNull(value); + Guard.ThrowIfNull(value); - if (value.Count == 0) - { - throw new ArgumentException("Empty list provided.", nameof(this.UriPrefixes)); - } - - this.uriPrefixes = value; + if (value.Count == 0) + { + throw new ArgumentException("Empty list provided.", nameof(this.UriPrefixes)); } + + this.uriPrefixes = value; } } } diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/README.md b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/README.md index 8f3877fb312..60517b42f7f 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/README.md +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/README.md @@ -7,7 +7,8 @@ An [OpenTelemetry Prometheus exporter](https://github.com/open-telemetry/opentel that configures an [HttpListener](https://docs.microsoft.com/dotnet/api/system.net.httplistener) instance for Prometheus to scrape. -**Warning**: this component is intended for dev inner-loop, there is no plan to +> [!WARNING] +> This component is intended for dev inner-loop, there is no plan to make it production ready. Production environments should use [OpenTelemetry.Exporter.Prometheus.AspNetCore](../OpenTelemetry.Exporter.Prometheus.AspNetCore/README.md), or a combination of diff --git a/src/OpenTelemetry.Exporter.Zipkin/.publicApi/net462/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.Zipkin/.publicApi/PublicAPI.Shipped.txt similarity index 100% rename from src/OpenTelemetry.Exporter.Zipkin/.publicApi/net462/PublicAPI.Shipped.txt rename to src/OpenTelemetry.Exporter.Zipkin/.publicApi/PublicAPI.Shipped.txt diff --git a/src/OpenTelemetry.Exporter.Zipkin/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Zipkin/.publicApi/PublicAPI.Unshipped.txt similarity index 100% rename from src/OpenTelemetry.Exporter.Zipkin/.publicApi/net462/PublicAPI.Unshipped.txt rename to src/OpenTelemetry.Exporter.Zipkin/.publicApi/PublicAPI.Unshipped.txt diff --git a/src/OpenTelemetry.Exporter.Zipkin/.publicApi/net6.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.Zipkin/.publicApi/net6.0/PublicAPI.Shipped.txt deleted file mode 100644 index 9e2e613e196..00000000000 --- a/src/OpenTelemetry.Exporter.Zipkin/.publicApi/net6.0/PublicAPI.Shipped.txt +++ /dev/null @@ -1,21 +0,0 @@ -OpenTelemetry.Exporter.ZipkinExporter -OpenTelemetry.Exporter.ZipkinExporter.ZipkinExporter(OpenTelemetry.Exporter.ZipkinExporterOptions options, System.Net.Http.HttpClient client = null) -> void -OpenTelemetry.Exporter.ZipkinExporterOptions -OpenTelemetry.Exporter.ZipkinExporterOptions.BatchExportProcessorOptions.get -> OpenTelemetry.BatchExportProcessorOptions -OpenTelemetry.Exporter.ZipkinExporterOptions.BatchExportProcessorOptions.set -> void -OpenTelemetry.Exporter.ZipkinExporterOptions.Endpoint.get -> System.Uri -OpenTelemetry.Exporter.ZipkinExporterOptions.Endpoint.set -> void -OpenTelemetry.Exporter.ZipkinExporterOptions.ExportProcessorType.get -> OpenTelemetry.ExportProcessorType -OpenTelemetry.Exporter.ZipkinExporterOptions.ExportProcessorType.set -> void -OpenTelemetry.Exporter.ZipkinExporterOptions.HttpClientFactory.get -> System.Func -OpenTelemetry.Exporter.ZipkinExporterOptions.HttpClientFactory.set -> void -OpenTelemetry.Exporter.ZipkinExporterOptions.MaxPayloadSizeInBytes.get -> int? -OpenTelemetry.Exporter.ZipkinExporterOptions.MaxPayloadSizeInBytes.set -> void -OpenTelemetry.Exporter.ZipkinExporterOptions.UseShortTraceIds.get -> bool -OpenTelemetry.Exporter.ZipkinExporterOptions.UseShortTraceIds.set -> void -OpenTelemetry.Exporter.ZipkinExporterOptions.ZipkinExporterOptions() -> void -OpenTelemetry.Trace.ZipkinExporterHelperExtensions -override OpenTelemetry.Exporter.ZipkinExporter.Export(in OpenTelemetry.Batch batch) -> OpenTelemetry.ExportResult -static OpenTelemetry.Trace.ZipkinExporterHelperExtensions.AddZipkinExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.ZipkinExporterHelperExtensions.AddZipkinExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.ZipkinExporterHelperExtensions.AddZipkinExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Exporter.Zipkin/.publicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.Zipkin/.publicApi/netstandard2.0/PublicAPI.Shipped.txt deleted file mode 100644 index 9e2e613e196..00000000000 --- a/src/OpenTelemetry.Exporter.Zipkin/.publicApi/netstandard2.0/PublicAPI.Shipped.txt +++ /dev/null @@ -1,21 +0,0 @@ -OpenTelemetry.Exporter.ZipkinExporter -OpenTelemetry.Exporter.ZipkinExporter.ZipkinExporter(OpenTelemetry.Exporter.ZipkinExporterOptions options, System.Net.Http.HttpClient client = null) -> void -OpenTelemetry.Exporter.ZipkinExporterOptions -OpenTelemetry.Exporter.ZipkinExporterOptions.BatchExportProcessorOptions.get -> OpenTelemetry.BatchExportProcessorOptions -OpenTelemetry.Exporter.ZipkinExporterOptions.BatchExportProcessorOptions.set -> void -OpenTelemetry.Exporter.ZipkinExporterOptions.Endpoint.get -> System.Uri -OpenTelemetry.Exporter.ZipkinExporterOptions.Endpoint.set -> void -OpenTelemetry.Exporter.ZipkinExporterOptions.ExportProcessorType.get -> OpenTelemetry.ExportProcessorType -OpenTelemetry.Exporter.ZipkinExporterOptions.ExportProcessorType.set -> void -OpenTelemetry.Exporter.ZipkinExporterOptions.HttpClientFactory.get -> System.Func -OpenTelemetry.Exporter.ZipkinExporterOptions.HttpClientFactory.set -> void -OpenTelemetry.Exporter.ZipkinExporterOptions.MaxPayloadSizeInBytes.get -> int? -OpenTelemetry.Exporter.ZipkinExporterOptions.MaxPayloadSizeInBytes.set -> void -OpenTelemetry.Exporter.ZipkinExporterOptions.UseShortTraceIds.get -> bool -OpenTelemetry.Exporter.ZipkinExporterOptions.UseShortTraceIds.set -> void -OpenTelemetry.Exporter.ZipkinExporterOptions.ZipkinExporterOptions() -> void -OpenTelemetry.Trace.ZipkinExporterHelperExtensions -override OpenTelemetry.Exporter.ZipkinExporter.Export(in OpenTelemetry.Batch batch) -> OpenTelemetry.ExportResult -static OpenTelemetry.Trace.ZipkinExporterHelperExtensions.AddZipkinExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.ZipkinExporterHelperExtensions.AddZipkinExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.ZipkinExporterHelperExtensions.AddZipkinExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Exporter.Zipkin/AssemblyInfo.cs b/src/OpenTelemetry.Exporter.Zipkin/AssemblyInfo.cs index 6b8a569923c..7ce3243549d 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/AssemblyInfo.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/AssemblyInfo.cs @@ -1,18 +1,6 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + using System.Runtime.CompilerServices; #if SIGNED diff --git a/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md b/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md index f2e21b66c06..babc9483100 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md @@ -2,6 +2,30 @@ ## Unreleased +## 1.7.0 + +Released 2023-Dec-08 + +## 1.7.0-rc.1 + +Released 2023-Nov-29 + +## 1.7.0-alpha.1 + +Released 2023-Oct-16 + +## 1.6.0 + +Released 2023-Sep-05 + +## 1.6.0-rc.1 + +Released 2023-Aug-21 + +## 1.6.0-alpha.1 + +Released 2023-Jul-12 + ## 1.5.1 Released 2023-Jun-26 diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinActivityConversionExtensions.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinActivityConversionExtensions.cs index 19baeb94188..768d308659d 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinActivityConversionExtensions.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinActivityConversionExtensions.cs @@ -1,279 +1,265 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Collections.Concurrent; using System.Diagnostics; using OpenTelemetry.Internal; using OpenTelemetry.Trace; -namespace OpenTelemetry.Exporter.Zipkin.Implementation +namespace OpenTelemetry.Exporter.Zipkin.Implementation; + +internal static class ZipkinActivityConversionExtensions { - internal static class ZipkinActivityConversionExtensions + internal const string ZipkinErrorFlagTagName = "error"; + private const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; + private const long UnixEpochTicks = 621355968000000000L; // = DateTimeOffset.FromUnixTimeMilliseconds(0).Ticks + private const long UnixEpochMicroseconds = UnixEpochTicks / TicksPerMicrosecond; + + private static readonly ConcurrentDictionary<(string, int), ZipkinEndpoint> RemoteEndpointCache = new(); + + internal static ZipkinSpan ToZipkinSpan(this Activity activity, ZipkinEndpoint localEndpoint, bool useShortTraceIds = false) { - internal const string ZipkinErrorFlagTagName = "error"; - private const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; - private const long UnixEpochTicks = 621355968000000000L; // = DateTimeOffset.FromUnixTimeMilliseconds(0).Ticks - private const long UnixEpochMicroseconds = UnixEpochTicks / TicksPerMicrosecond; + var context = activity.Context; - private static readonly ConcurrentDictionary<(string, int), ZipkinEndpoint> RemoteEndpointCache = new(); + string parentId = activity.ParentSpanId == default ? + null + : EncodeSpanId(activity.ParentSpanId); - internal static ZipkinSpan ToZipkinSpan(this Activity activity, ZipkinEndpoint localEndpoint, bool useShortTraceIds = false) + var tagState = new TagEnumerationState { - var context = activity.Context; + Tags = PooledList>.Create(), + }; - string parentId = activity.ParentSpanId == default ? - null - : EncodeSpanId(activity.ParentSpanId); + tagState.EnumerateTags(activity); - var tagState = new TagEnumerationState + // When status is set on Activity using the native Status field in activity, + // which was first introduced in System.Diagnostic.DiagnosticSource 6.0.0. + if (activity.Status != ActivityStatusCode.Unset) + { + if (activity.Status == ActivityStatusCode.Ok) { - Tags = PooledList>.Create(), - }; - - tagState.EnumerateTags(activity); + PooledList>.Add( + ref tagState.Tags, + new KeyValuePair( + SpanAttributeConstants.StatusCodeKey, + "OK")); + } - // When status is set on Activity using the native Status field in activity, - // which was first introduced in System.Diagnostic.DiagnosticSource 6.0.0. - if (activity.Status != ActivityStatusCode.Unset) + // activity.Status is Error + else { - if (activity.Status == ActivityStatusCode.Ok) - { - PooledList>.Add( + PooledList>.Add( ref tagState.Tags, new KeyValuePair( SpanAttributeConstants.StatusCodeKey, - "OK")); - } + "ERROR")); - // activity.Status is Error - else - { - PooledList>.Add( - ref tagState.Tags, - new KeyValuePair( - SpanAttributeConstants.StatusCodeKey, - "ERROR")); - - // Error flag rule from https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk_exporters/zipkin.md#status - PooledList>.Add( - ref tagState.Tags, - new KeyValuePair( - ZipkinErrorFlagTagName, - activity.StatusDescription ?? string.Empty)); - } + // Error flag rule from https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk_exporters/zipkin.md#status + PooledList>.Add( + ref tagState.Tags, + new KeyValuePair( + ZipkinErrorFlagTagName, + activity.StatusDescription ?? string.Empty)); } + } - // In the case when both activity status and status tag were set, - // activity status takes precedence over status tag. - else if (tagState.StatusCode.HasValue && tagState.StatusCode != StatusCode.Unset) + // In the case when both activity status and status tag were set, + // activity status takes precedence over status tag. + else if (tagState.StatusCode.HasValue && tagState.StatusCode != StatusCode.Unset) + { + PooledList>.Add( + ref tagState.Tags, + new KeyValuePair( + SpanAttributeConstants.StatusCodeKey, + StatusHelper.GetTagValueForStatusCode(tagState.StatusCode.Value))); + + // Error flag rule from https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk_exporters/zipkin.md#status + if (tagState.StatusCode == StatusCode.Error) { PooledList>.Add( ref tagState.Tags, new KeyValuePair( - SpanAttributeConstants.StatusCodeKey, - StatusHelper.GetTagValueForStatusCode(tagState.StatusCode.Value))); - - // Error flag rule from https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk_exporters/zipkin.md#status - if (tagState.StatusCode == StatusCode.Error) - { - PooledList>.Add( - ref tagState.Tags, - new KeyValuePair( - ZipkinErrorFlagTagName, - tagState.StatusDescription ?? string.Empty)); - } + ZipkinErrorFlagTagName, + tagState.StatusDescription ?? string.Empty)); } + } - var activitySource = activity.Source; - if (!string.IsNullOrEmpty(activitySource.Name)) + var activitySource = activity.Source; + if (!string.IsNullOrEmpty(activitySource.Name)) + { + PooledList>.Add(ref tagState.Tags, new KeyValuePair("otel.library.name", activitySource.Name)); + if (!string.IsNullOrEmpty(activitySource.Version)) { - PooledList>.Add(ref tagState.Tags, new KeyValuePair("otel.library.name", activitySource.Name)); - if (!string.IsNullOrEmpty(activitySource.Version)) - { - PooledList>.Add(ref tagState.Tags, new KeyValuePair("otel.library.version", activitySource.Version)); - } + PooledList>.Add(ref tagState.Tags, new KeyValuePair("otel.library.version", activitySource.Version)); } + } - ZipkinEndpoint remoteEndpoint = null; - if (activity.Kind == ActivityKind.Client || activity.Kind == ActivityKind.Producer) - { - PeerServiceResolver.Resolve(ref tagState, out string peerServiceName, out bool addAsTag); + ZipkinEndpoint remoteEndpoint = null; + if (activity.Kind == ActivityKind.Client || activity.Kind == ActivityKind.Producer) + { + PeerServiceResolver.Resolve(ref tagState, out string peerServiceName, out bool addAsTag); - if (peerServiceName != null) + if (peerServiceName != null) + { + remoteEndpoint = RemoteEndpointCache.GetOrAdd((peerServiceName, default), ZipkinEndpoint.Create); + if (addAsTag) { - remoteEndpoint = RemoteEndpointCache.GetOrAdd((peerServiceName, default), ZipkinEndpoint.Create); - if (addAsTag) - { - PooledList>.Add(ref tagState.Tags, new KeyValuePair(SemanticConventions.AttributePeerService, peerServiceName)); - } + PooledList>.Add(ref tagState.Tags, new KeyValuePair(SemanticConventions.AttributePeerService, peerServiceName)); } } - - EventEnumerationState eventState = default; - eventState.EnumerateEvents(activity); - - return new ZipkinSpan( - EncodeTraceId(context.TraceId, useShortTraceIds), - parentId, - EncodeSpanId(context.SpanId), - ToActivityKind(activity), - activity.DisplayName, - activity.StartTimeUtc.ToEpochMicroseconds(), - duration: activity.Duration.ToEpochMicroseconds(), - localEndpoint, - remoteEndpoint, - eventState.Annotations, - tagState.Tags, - null, - null); } - internal static string EncodeSpanId(ActivitySpanId spanId) - { - return spanId.ToHexString(); - } - - internal static long ToEpochMicroseconds(this DateTimeOffset dateTimeOffset) - { - // Truncate sub-microsecond precision before offsetting by the Unix Epoch to avoid - // the last digit being off by one for dates that result in negative Unix times - long microseconds = dateTimeOffset.Ticks / TicksPerMicrosecond; - return microseconds - UnixEpochMicroseconds; - } + EventEnumerationState eventState = default; + eventState.EnumerateEvents(activity); + + return new ZipkinSpan( + EncodeTraceId(context.TraceId, useShortTraceIds), + parentId, + EncodeSpanId(context.SpanId), + ToActivityKind(activity), + activity.DisplayName, + activity.StartTimeUtc.ToEpochMicroseconds(), + duration: activity.Duration.ToEpochMicroseconds(), + localEndpoint, + remoteEndpoint, + eventState.Annotations, + tagState.Tags, + null, + null); + } - internal static long ToEpochMicroseconds(this TimeSpan timeSpan) - { - return timeSpan.Ticks / TicksPerMicrosecond; - } + internal static string EncodeSpanId(ActivitySpanId spanId) + { + return spanId.ToHexString(); + } - internal static long ToEpochMicroseconds(this DateTime utcDateTime) - { - // Truncate sub-microsecond precision before offsetting by the Unix Epoch to avoid - // the last digit being off by one for dates that result in negative Unix times - long microseconds = utcDateTime.Ticks / TicksPerMicrosecond; - return microseconds - UnixEpochMicroseconds; - } + internal static long ToEpochMicroseconds(this DateTimeOffset dateTimeOffset) + { + // Truncate sub-microsecond precision before offsetting by the Unix Epoch to avoid + // the last digit being off by one for dates that result in negative Unix times + long microseconds = dateTimeOffset.Ticks / TicksPerMicrosecond; + return microseconds - UnixEpochMicroseconds; + } - private static string EncodeTraceId(ActivityTraceId traceId, bool useShortTraceIds) - { - var id = traceId.ToHexString(); + internal static long ToEpochMicroseconds(this TimeSpan timeSpan) + { + return timeSpan.Ticks / TicksPerMicrosecond; + } - if (id.Length > 16 && useShortTraceIds) - { - id = id.Substring(id.Length - 16, 16); - } + internal static long ToEpochMicroseconds(this DateTime utcDateTime) + { + // Truncate sub-microsecond precision before offsetting by the Unix Epoch to avoid + // the last digit being off by one for dates that result in negative Unix times + long microseconds = utcDateTime.Ticks / TicksPerMicrosecond; + return microseconds - UnixEpochMicroseconds; + } - return id; - } + private static string EncodeTraceId(ActivityTraceId traceId, bool useShortTraceIds) + { + var id = traceId.ToHexString(); - private static string ToActivityKind(Activity activity) + if (id.Length > 16 && useShortTraceIds) { - return activity.Kind switch - { - ActivityKind.Server => "SERVER", - ActivityKind.Producer => "PRODUCER", - ActivityKind.Consumer => "CONSUMER", - ActivityKind.Client => "CLIENT", - _ => null, - }; + id = id.Substring(id.Length - 16, 16); } - internal struct TagEnumerationState : PeerServiceResolver.IPeerServiceState + return id; + } + + private static string ToActivityKind(Activity activity) + { + return activity.Kind switch { - public PooledList> Tags; + ActivityKind.Server => "SERVER", + ActivityKind.Producer => "PRODUCER", + ActivityKind.Consumer => "CONSUMER", + ActivityKind.Client => "CLIENT", + _ => null, + }; + } - public string PeerService { get; set; } + internal struct TagEnumerationState : PeerServiceResolver.IPeerServiceState + { + public PooledList> Tags; - public int? PeerServicePriority { get; set; } + public string PeerService { get; set; } - public string HostName { get; set; } + public int? PeerServicePriority { get; set; } - public string IpAddress { get; set; } + public string HostName { get; set; } - public long Port { get; set; } + public string IpAddress { get; set; } - public StatusCode? StatusCode { get; set; } + public long Port { get; set; } - public string StatusDescription { get; set; } + public StatusCode? StatusCode { get; set; } - public void EnumerateTags(Activity activity) + public string StatusDescription { get; set; } + + public void EnumerateTags(Activity activity) + { + foreach (ref readonly var tag in activity.EnumerateTagObjects()) { - foreach (ref readonly var tag in activity.EnumerateTagObjects()) + if (tag.Value == null) { - if (tag.Value == null) + continue; + } + + string key = tag.Key; + + if (tag.Value is string strVal) + { + PeerServiceResolver.InspectTag(ref this, key, strVal); + + if (key == SpanAttributeConstants.StatusCodeKey) { + this.StatusCode = StatusHelper.GetStatusCodeForTagValue(strVal); continue; } - - string key = tag.Key; - - if (tag.Value is string strVal) + else if (key == SpanAttributeConstants.StatusDescriptionKey) { - PeerServiceResolver.InspectTag(ref this, key, strVal); - - if (key == SpanAttributeConstants.StatusCodeKey) - { - this.StatusCode = StatusHelper.GetStatusCodeForTagValue(strVal); - continue; - } - else if (key == SpanAttributeConstants.StatusDescriptionKey) - { - // Description is sent as `error` but only if StatusCode is Error. See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk_exporters/zipkin.md#status - this.StatusDescription = strVal; - continue; - } - else if (key == ZipkinErrorFlagTagName) - { - // Ignore `error` tag if it exists, it will be added based on StatusCode + StatusDescription. - continue; - } + // Description is sent as `error` but only if StatusCode is Error. See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk_exporters/zipkin.md#status + this.StatusDescription = strVal; + continue; } - else if (tag.Value is int intVal && tag.Key == SemanticConventions.AttributeNetPeerPort) + else if (key == ZipkinErrorFlagTagName) { - PeerServiceResolver.InspectTag(ref this, key, intVal); + // Ignore `error` tag if it exists, it will be added based on StatusCode + StatusDescription. + continue; } - - PooledList>.Add(ref this.Tags, tag); } + else if (tag.Value is int intVal && tag.Key == SemanticConventions.AttributeNetPeerPort) + { + PeerServiceResolver.InspectTag(ref this, key, intVal); + } + + PooledList>.Add(ref this.Tags, tag); } } + } - private struct EventEnumerationState - { - public bool Created; + private struct EventEnumerationState + { + public bool Created; - public PooledList Annotations; + public PooledList Annotations; - public void EnumerateEvents(Activity activity) + public void EnumerateEvents(Activity activity) + { + var enumerator = activity.EnumerateEvents(); + + if (enumerator.MoveNext()) { - var enumerator = activity.EnumerateEvents(); + this.Annotations = PooledList.Create(); + this.Created = true; - if (enumerator.MoveNext()) + do { - this.Annotations = PooledList.Create(); - this.Created = true; + ref readonly var @event = ref enumerator.Current; - do - { - ref readonly var @event = ref enumerator.Current; - - PooledList.Add(ref this.Annotations, new ZipkinAnnotation(@event.Timestamp.ToEpochMicroseconds(), @event.Name)); - } - while (enumerator.MoveNext()); + PooledList.Add(ref this.Annotations, new ZipkinAnnotation(@event.Timestamp.ToEpochMicroseconds(), @event.Name)); } + while (enumerator.MoveNext()); } } } diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinAnnotation.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinAnnotation.cs index 391f4458f8c..263faa48338 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinAnnotation.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinAnnotation.cs @@ -1,32 +1,19 @@ -// // 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. -// -namespace OpenTelemetry.Exporter.Zipkin.Implementation +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Exporter.Zipkin.Implementation; + +internal readonly struct ZipkinAnnotation { - internal readonly struct ZipkinAnnotation + public ZipkinAnnotation( + long timestamp, + string value) { - public ZipkinAnnotation( - long timestamp, - string value) - { - this.Timestamp = timestamp; - this.Value = value; - } + this.Timestamp = timestamp; + this.Value = value; + } - public long Timestamp { get; } + public long Timestamp { get; } - public string Value { get; } - } + public string Value { get; } } diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinEndpoint.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinEndpoint.cs index 8572218a8ce..c6c9908b38e 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinEndpoint.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinEndpoint.cs @@ -1,103 +1,89 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Text.Json; -namespace OpenTelemetry.Exporter.Zipkin.Implementation +namespace OpenTelemetry.Exporter.Zipkin.Implementation; + +internal sealed class ZipkinEndpoint { - internal sealed class ZipkinEndpoint + public ZipkinEndpoint(string serviceName) + : this(serviceName, null, null, null, null) { - public ZipkinEndpoint(string serviceName) - : this(serviceName, null, null, null, null) - { - } + } - public ZipkinEndpoint( - string serviceName, - string ipv4, - string ipv6, - int? port, - Dictionary tags) - { - this.ServiceName = serviceName; - this.Ipv4 = ipv4; - this.Ipv6 = ipv6; - this.Port = port; - this.Tags = tags; - } + public ZipkinEndpoint( + string serviceName, + string ipv4, + string ipv6, + int? port, + Dictionary tags) + { + this.ServiceName = serviceName; + this.Ipv4 = ipv4; + this.Ipv6 = ipv6; + this.Port = port; + this.Tags = tags; + } + + public string ServiceName { get; } - public string ServiceName { get; } + public string Ipv4 { get; } - public string Ipv4 { get; } + public string Ipv6 { get; } - public string Ipv6 { get; } + public int? Port { get; } - public int? Port { get; } + public Dictionary Tags { get; } - public Dictionary Tags { get; } + public static ZipkinEndpoint Create(string serviceName) + { + return new ZipkinEndpoint(serviceName); + } - public static ZipkinEndpoint Create(string serviceName) + public static ZipkinEndpoint Create((string Name, int Port) serviceNameAndPort) + { + var serviceName = serviceNameAndPort.Port == default + ? serviceNameAndPort.Name + : $"{serviceNameAndPort.Name}:{serviceNameAndPort.Port}"; + + return new ZipkinEndpoint(serviceName); + } + + public ZipkinEndpoint Clone(string serviceName) + { + return new ZipkinEndpoint( + serviceName, + this.Ipv4, + this.Ipv6, + this.Port, + this.Tags); + } + + public void Write(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + + if (this.ServiceName != null) { - return new ZipkinEndpoint(serviceName); + writer.WriteString(ZipkinSpanJsonHelper.ServiceNamePropertyName, this.ServiceName); } - public static ZipkinEndpoint Create((string Name, int Port) serviceNameAndPort) + if (this.Ipv4 != null) { - var serviceName = serviceNameAndPort.Port == default - ? serviceNameAndPort.Name - : $"{serviceNameAndPort.Name}:{serviceNameAndPort.Port}"; - - return new ZipkinEndpoint(serviceName); + writer.WriteString(ZipkinSpanJsonHelper.Ipv4PropertyName, this.Ipv4); } - public ZipkinEndpoint Clone(string serviceName) + if (this.Ipv6 != null) { - return new ZipkinEndpoint( - serviceName, - this.Ipv4, - this.Ipv6, - this.Port, - this.Tags); + writer.WriteString(ZipkinSpanJsonHelper.Ipv6PropertyName, this.Ipv6); } - public void Write(Utf8JsonWriter writer) + if (this.Port.HasValue) { - writer.WriteStartObject(); - - if (this.ServiceName != null) - { - writer.WriteString(ZipkinSpanJsonHelper.ServiceNamePropertyName, this.ServiceName); - } - - if (this.Ipv4 != null) - { - writer.WriteString(ZipkinSpanJsonHelper.Ipv4PropertyName, this.Ipv4); - } - - if (this.Ipv6 != null) - { - writer.WriteString(ZipkinSpanJsonHelper.Ipv6PropertyName, this.Ipv6); - } - - if (this.Port.HasValue) - { - writer.WriteNumber(ZipkinSpanJsonHelper.PortPropertyName, this.Port.Value); - } - - writer.WriteEndObject(); + writer.WriteNumber(ZipkinSpanJsonHelper.PortPropertyName, this.Port.Value); } + + writer.WriteEndObject(); } } diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinExporterEventSource.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinExporterEventSource.cs index a7c86b118f8..8c5a3d3a0d5 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinExporterEventSource.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinExporterEventSource.cs @@ -1,57 +1,43 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Tracing; using OpenTelemetry.Internal; -namespace OpenTelemetry.Exporter.Zipkin.Implementation +namespace OpenTelemetry.Exporter.Zipkin.Implementation; + +/// +/// EventSource events emitted from the project. +/// +[EventSource(Name = "OpenTelemetry-Exporter-Zipkin")] +internal sealed class ZipkinExporterEventSource : EventSource { - /// - /// EventSource events emitted from the project. - /// - [EventSource(Name = "OpenTelemetry-Exporter-Zipkin")] - internal sealed class ZipkinExporterEventSource : EventSource - { - public static ZipkinExporterEventSource Log = new(); + public static ZipkinExporterEventSource Log = new(); - [NonEvent] - public void FailedExport(Exception ex) + [NonEvent] + public void FailedExport(Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.FailedExport(ex.ToInvariantString()); - } + this.FailedExport(ex.ToInvariantString()); } + } - [Event(1, Message = "Failed to export activities: '{0}'", Level = EventLevel.Error)] - public void FailedExport(string exception) - { - this.WriteEvent(1, exception); - } + [Event(1, Message = "Failed to export activities: '{0}'", Level = EventLevel.Error)] + public void FailedExport(string exception) + { + this.WriteEvent(1, exception); + } - [Event(2, Message = "Unsupported attribute type '{0}' for '{1}'. Attribute will not be exported.", Level = EventLevel.Warning)] - public void UnsupportedAttributeType(string type, string key) - { - this.WriteEvent(2, type.ToString(), key); - } + [Event(2, Message = "Unsupported attribute type '{0}' for '{1}'. Attribute will not be exported.", Level = EventLevel.Warning)] + public void UnsupportedAttributeType(string type, string key) + { + this.WriteEvent(2, type.ToString(), key); + } - [Event(3, Message = "{0} environment variable has an invalid value: '{1}'", Level = EventLevel.Warning)] - public void InvalidEnvironmentVariable(string key, string value) - { - this.WriteEvent(3, key, value); - } + [Event(3, Message = "{0} environment variable has an invalid value: '{1}'", Level = EventLevel.Warning)] + public void InvalidEnvironmentVariable(string key, string value) + { + this.WriteEvent(3, key, value); } } diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpan.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpan.cs index c6cde5019c3..e28fcda6a60 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpan.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpan.cs @@ -1,202 +1,188 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Globalization; using System.Text.Json; using OpenTelemetry.Internal; -namespace OpenTelemetry.Exporter.Zipkin.Implementation +namespace OpenTelemetry.Exporter.Zipkin.Implementation; + +internal readonly struct ZipkinSpan { - internal readonly struct ZipkinSpan + public ZipkinSpan( + string traceId, + string parentId, + string id, + string kind, + string name, + long? timestamp, + long? duration, + ZipkinEndpoint localEndpoint, + ZipkinEndpoint remoteEndpoint, + in PooledList annotations, + in PooledList> tags, + bool? debug, + bool? shared) { - public ZipkinSpan( - string traceId, - string parentId, - string id, - string kind, - string name, - long? timestamp, - long? duration, - ZipkinEndpoint localEndpoint, - ZipkinEndpoint remoteEndpoint, - in PooledList annotations, - in PooledList> tags, - bool? debug, - bool? shared) - { - Guard.ThrowIfNullOrWhitespace(traceId); - Guard.ThrowIfNullOrWhitespace(id); - - this.TraceId = traceId; - this.ParentId = parentId; - this.Id = id; - this.Kind = kind; - this.Name = name; - this.Timestamp = timestamp; - this.Duration = duration; - this.LocalEndpoint = localEndpoint; - this.RemoteEndpoint = remoteEndpoint; - this.Annotations = annotations; - this.Tags = tags; - this.Debug = debug; - this.Shared = shared; - } + Guard.ThrowIfNullOrWhitespace(traceId); + Guard.ThrowIfNullOrWhitespace(id); + + this.TraceId = traceId; + this.ParentId = parentId; + this.Id = id; + this.Kind = kind; + this.Name = name; + this.Timestamp = timestamp; + this.Duration = duration; + this.LocalEndpoint = localEndpoint; + this.RemoteEndpoint = remoteEndpoint; + this.Annotations = annotations; + this.Tags = tags; + this.Debug = debug; + this.Shared = shared; + } + + public string TraceId { get; } - public string TraceId { get; } + public string ParentId { get; } - public string ParentId { get; } + public string Id { get; } - public string Id { get; } + public string Kind { get; } - public string Kind { get; } + public string Name { get; } - public string Name { get; } + public long? Timestamp { get; } - public long? Timestamp { get; } + public long? Duration { get; } - public long? Duration { get; } + public ZipkinEndpoint LocalEndpoint { get; } - public ZipkinEndpoint LocalEndpoint { get; } + public ZipkinEndpoint RemoteEndpoint { get; } - public ZipkinEndpoint RemoteEndpoint { get; } + public PooledList Annotations { get; } - public PooledList Annotations { get; } + public PooledList> Tags { get; } - public PooledList> Tags { get; } + public bool? Debug { get; } - public bool? Debug { get; } + public bool? Shared { get; } - public bool? Shared { get; } + public void Return() + { + this.Annotations.Return(); + this.Tags.Return(); + } + + public void Write(Utf8JsonWriter writer) + { + writer.WriteStartObject(); - public void Return() + writer.WriteString(ZipkinSpanJsonHelper.TraceIdPropertyName, this.TraceId); + + if (this.Name != null) { - this.Annotations.Return(); - this.Tags.Return(); + writer.WriteString(ZipkinSpanJsonHelper.NamePropertyName, this.Name); } - public void Write(Utf8JsonWriter writer) + if (this.ParentId != null) { - writer.WriteStartObject(); - - writer.WriteString(ZipkinSpanJsonHelper.TraceIdPropertyName, this.TraceId); + writer.WriteString(ZipkinSpanJsonHelper.ParentIdPropertyName, this.ParentId); + } - if (this.Name != null) - { - writer.WriteString(ZipkinSpanJsonHelper.NamePropertyName, this.Name); - } + writer.WriteString(ZipkinSpanJsonHelper.IdPropertyName, this.Id); - if (this.ParentId != null) - { - writer.WriteString(ZipkinSpanJsonHelper.ParentIdPropertyName, this.ParentId); - } + if (this.Kind != null) + { + writer.WriteString(ZipkinSpanJsonHelper.KindPropertyName, this.Kind); + } - writer.WriteString(ZipkinSpanJsonHelper.IdPropertyName, this.Id); + if (this.Timestamp.HasValue) + { + writer.WriteNumber(ZipkinSpanJsonHelper.TimestampPropertyName, this.Timestamp.Value); + } - if (this.Kind != null) - { - writer.WriteString(ZipkinSpanJsonHelper.KindPropertyName, this.Kind); - } + if (this.Duration.HasValue) + { + writer.WriteNumber(ZipkinSpanJsonHelper.DurationPropertyName, this.Duration.Value); + } - if (this.Timestamp.HasValue) - { - writer.WriteNumber(ZipkinSpanJsonHelper.TimestampPropertyName, this.Timestamp.Value); - } + if (this.Debug.HasValue) + { + writer.WriteBoolean(ZipkinSpanJsonHelper.DebugPropertyName, this.Debug.Value); + } - if (this.Duration.HasValue) - { - writer.WriteNumber(ZipkinSpanJsonHelper.DurationPropertyName, this.Duration.Value); - } + if (this.Shared.HasValue) + { + writer.WriteBoolean(ZipkinSpanJsonHelper.SharedPropertyName, this.Shared.Value); + } - if (this.Debug.HasValue) - { - writer.WriteBoolean(ZipkinSpanJsonHelper.DebugPropertyName, this.Debug.Value); - } + if (this.LocalEndpoint != null) + { + writer.WritePropertyName(ZipkinSpanJsonHelper.LocalEndpointPropertyName); + this.LocalEndpoint.Write(writer); + } - if (this.Shared.HasValue) - { - writer.WriteBoolean(ZipkinSpanJsonHelper.SharedPropertyName, this.Shared.Value); - } + if (this.RemoteEndpoint != null) + { + writer.WritePropertyName(ZipkinSpanJsonHelper.RemoteEndpointPropertyName); + this.RemoteEndpoint.Write(writer); + } - if (this.LocalEndpoint != null) - { - writer.WritePropertyName(ZipkinSpanJsonHelper.LocalEndpointPropertyName); - this.LocalEndpoint.Write(writer); - } + if (!this.Annotations.IsEmpty) + { + writer.WritePropertyName(ZipkinSpanJsonHelper.AnnotationsPropertyName); + writer.WriteStartArray(); - if (this.RemoteEndpoint != null) + foreach (var annotation in this.Annotations) { - writer.WritePropertyName(ZipkinSpanJsonHelper.RemoteEndpointPropertyName); - this.RemoteEndpoint.Write(writer); - } + writer.WriteStartObject(); - if (!this.Annotations.IsEmpty) - { - writer.WritePropertyName(ZipkinSpanJsonHelper.AnnotationsPropertyName); - writer.WriteStartArray(); + writer.WriteNumber(ZipkinSpanJsonHelper.TimestampPropertyName, annotation.Timestamp); - foreach (var annotation in this.Annotations) - { - writer.WriteStartObject(); + writer.WriteString(ZipkinSpanJsonHelper.ValuePropertyName, annotation.Value); - writer.WriteNumber(ZipkinSpanJsonHelper.TimestampPropertyName, annotation.Timestamp); + writer.WriteEndObject(); + } - writer.WriteString(ZipkinSpanJsonHelper.ValuePropertyName, annotation.Value); + writer.WriteEndArray(); + } - writer.WriteEndObject(); - } + if (!this.Tags.IsEmpty || this.LocalEndpoint.Tags != null) + { + writer.WritePropertyName(ZipkinSpanJsonHelper.TagsPropertyName); + writer.WriteStartObject(); - writer.WriteEndArray(); - } + // this will be used when we convert int, double, int[], double[] to string + var originalUICulture = Thread.CurrentThread.CurrentUICulture; + Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; - if (!this.Tags.IsEmpty || this.LocalEndpoint.Tags != null) + try { - writer.WritePropertyName(ZipkinSpanJsonHelper.TagsPropertyName); - writer.WriteStartObject(); - - // this will be used when we convert int, double, int[], double[] to string - var originalUICulture = Thread.CurrentThread.CurrentUICulture; - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; - - try + foreach (var tag in this.LocalEndpoint.Tags ?? Enumerable.Empty>()) { - foreach (var tag in this.LocalEndpoint.Tags ?? Enumerable.Empty>()) + if (ZipkinTagTransformer.Instance.TryTransformTag(tag, out var result)) { - if (ZipkinTagTransformer.Instance.TryTransformTag(tag, out var result)) - { - writer.WriteString(tag.Key, result); - } + writer.WriteString(tag.Key, result); } + } - foreach (var tag in this.Tags) + foreach (var tag in this.Tags) + { + if (ZipkinTagTransformer.Instance.TryTransformTag(tag, out var result)) { - if (ZipkinTagTransformer.Instance.TryTransformTag(tag, out var result)) - { - writer.WriteString(tag.Key, result); - } + writer.WriteString(tag.Key, result); } } - finally - { - Thread.CurrentThread.CurrentUICulture = originalUICulture; - } - - writer.WriteEndObject(); + } + finally + { + Thread.CurrentThread.CurrentUICulture = originalUICulture; } writer.WriteEndObject(); } + + writer.WriteEndObject(); } } diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpanJsonHelper.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpanJsonHelper.cs index c654b84500f..ee378e4cec6 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpanJsonHelper.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpanJsonHelper.cs @@ -1,59 +1,45 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Text.Json; -namespace OpenTelemetry.Exporter.Zipkin.Implementation +namespace OpenTelemetry.Exporter.Zipkin.Implementation; + +internal static class ZipkinSpanJsonHelper { - internal static class ZipkinSpanJsonHelper - { - public static readonly JsonEncodedText TraceIdPropertyName = JsonEncodedText.Encode("traceId"); + public static readonly JsonEncodedText TraceIdPropertyName = JsonEncodedText.Encode("traceId"); - public static readonly JsonEncodedText NamePropertyName = JsonEncodedText.Encode("name"); + public static readonly JsonEncodedText NamePropertyName = JsonEncodedText.Encode("name"); - public static readonly JsonEncodedText ParentIdPropertyName = JsonEncodedText.Encode("parentId"); + public static readonly JsonEncodedText ParentIdPropertyName = JsonEncodedText.Encode("parentId"); - public static readonly JsonEncodedText IdPropertyName = JsonEncodedText.Encode("id"); + public static readonly JsonEncodedText IdPropertyName = JsonEncodedText.Encode("id"); - public static readonly JsonEncodedText KindPropertyName = JsonEncodedText.Encode("kind"); + public static readonly JsonEncodedText KindPropertyName = JsonEncodedText.Encode("kind"); - public static readonly JsonEncodedText TimestampPropertyName = JsonEncodedText.Encode("timestamp"); + public static readonly JsonEncodedText TimestampPropertyName = JsonEncodedText.Encode("timestamp"); - public static readonly JsonEncodedText DurationPropertyName = JsonEncodedText.Encode("duration"); + public static readonly JsonEncodedText DurationPropertyName = JsonEncodedText.Encode("duration"); - public static readonly JsonEncodedText DebugPropertyName = JsonEncodedText.Encode("debug"); + public static readonly JsonEncodedText DebugPropertyName = JsonEncodedText.Encode("debug"); - public static readonly JsonEncodedText SharedPropertyName = JsonEncodedText.Encode("shared"); + public static readonly JsonEncodedText SharedPropertyName = JsonEncodedText.Encode("shared"); - public static readonly JsonEncodedText LocalEndpointPropertyName = JsonEncodedText.Encode("localEndpoint"); + public static readonly JsonEncodedText LocalEndpointPropertyName = JsonEncodedText.Encode("localEndpoint"); - public static readonly JsonEncodedText RemoteEndpointPropertyName = JsonEncodedText.Encode("remoteEndpoint"); + public static readonly JsonEncodedText RemoteEndpointPropertyName = JsonEncodedText.Encode("remoteEndpoint"); - public static readonly JsonEncodedText AnnotationsPropertyName = JsonEncodedText.Encode("annotations"); + public static readonly JsonEncodedText AnnotationsPropertyName = JsonEncodedText.Encode("annotations"); - public static readonly JsonEncodedText ValuePropertyName = JsonEncodedText.Encode("value"); + public static readonly JsonEncodedText ValuePropertyName = JsonEncodedText.Encode("value"); - public static readonly JsonEncodedText TagsPropertyName = JsonEncodedText.Encode("tags"); + public static readonly JsonEncodedText TagsPropertyName = JsonEncodedText.Encode("tags"); - public static readonly JsonEncodedText ServiceNamePropertyName = JsonEncodedText.Encode("serviceName"); + public static readonly JsonEncodedText ServiceNamePropertyName = JsonEncodedText.Encode("serviceName"); - public static readonly JsonEncodedText Ipv4PropertyName = JsonEncodedText.Encode("ipv4"); + public static readonly JsonEncodedText Ipv4PropertyName = JsonEncodedText.Encode("ipv4"); - public static readonly JsonEncodedText Ipv6PropertyName = JsonEncodedText.Encode("ipv6"); + public static readonly JsonEncodedText Ipv6PropertyName = JsonEncodedText.Encode("ipv6"); - public static readonly JsonEncodedText PortPropertyName = JsonEncodedText.Encode("port"); - } + public static readonly JsonEncodedText PortPropertyName = JsonEncodedText.Encode("port"); } diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinTagTransformer.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinTagTransformer.cs index 10162d8d91b..95ec122bcf9 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinTagTransformer.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinTagTransformer.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Internal; @@ -35,5 +22,5 @@ private ZipkinTagTransformer() protected override string TransformStringTag(string key, string value) => value; protected override string TransformArrayTag(string key, Array array) - => this.TransformStringTag(key, System.Text.Json.JsonSerializer.Serialize(array)); + => this.TransformStringTag(key, TagTransformerJsonHelper.JsonSerializeArrayTag(array)); } diff --git a/src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj b/src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj index b18fa655057..b75959d8bdf 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj +++ b/src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj @@ -1,7 +1,6 @@ - - net6.0;netstandard2.0;net462; + $(TargetFrameworksForLibraries) Zipkin exporter for OpenTelemetry .NET $(PackageTags);Zipkin;distributed-tracing core- @@ -11,25 +10,32 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporter.cs b/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporter.cs index 2f732a4fcbb..6c964e64393 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporter.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporter.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Net; @@ -27,243 +14,242 @@ using OpenTelemetry.Internal; using OpenTelemetry.Resources; -namespace OpenTelemetry.Exporter +namespace OpenTelemetry.Exporter; + +/// +/// Zipkin exporter. +/// +public class ZipkinExporter : BaseExporter { + private readonly ZipkinExporterOptions options; + private readonly int maxPayloadSizeInBytes; + private readonly HttpClient httpClient; + /// - /// Zipkin exporter. + /// Initializes a new instance of the class. /// - public class ZipkinExporter : BaseExporter + /// Configuration options. + /// Http client to use to upload telemetry. + public ZipkinExporter(ZipkinExporterOptions options, HttpClient client = null) { - private readonly ZipkinExporterOptions options; - private readonly int maxPayloadSizeInBytes; - private readonly HttpClient httpClient; - - /// - /// Initializes a new instance of the class. - /// - /// Configuration options. - /// Http client to use to upload telemetry. - public ZipkinExporter(ZipkinExporterOptions options, HttpClient client = null) - { - Guard.ThrowIfNull(options); + Guard.ThrowIfNull(options); - this.options = options; - this.maxPayloadSizeInBytes = (!options.MaxPayloadSizeInBytes.HasValue || options.MaxPayloadSizeInBytes <= 0) ? ZipkinExporterOptions.DefaultMaxPayloadSizeInBytes : options.MaxPayloadSizeInBytes.Value; - this.httpClient = client ?? options.HttpClientFactory?.Invoke() ?? throw new InvalidOperationException("ZipkinExporter was missing HttpClientFactory or it returned null."); + this.options = options; + this.maxPayloadSizeInBytes = (!options.MaxPayloadSizeInBytes.HasValue || options.MaxPayloadSizeInBytes <= 0) ? ZipkinExporterOptions.DefaultMaxPayloadSizeInBytes : options.MaxPayloadSizeInBytes.Value; + this.httpClient = client ?? options.HttpClientFactory?.Invoke() ?? throw new InvalidOperationException("ZipkinExporter was missing HttpClientFactory or it returned null."); - ZipkinTagTransformer.LogUnsupportedAttributeType = (string tagValueType, string tagKey) => - { - ZipkinExporterEventSource.Log.UnsupportedAttributeType(tagValueType, tagKey); - }; + ZipkinTagTransformer.LogUnsupportedAttributeType = (string tagValueType, string tagKey) => + { + ZipkinExporterEventSource.Log.UnsupportedAttributeType(tagValueType, tagKey); + }; - ConfigurationExtensions.LogInvalidEnvironmentVariable = (string key, string value) => - { - ZipkinExporterEventSource.Log.InvalidEnvironmentVariable(key, value); - }; - } + ConfigurationExtensions.LogInvalidEnvironmentVariable = (string key, string value) => + { + ZipkinExporterEventSource.Log.InvalidEnvironmentVariable(key, value); + }; + } - internal ZipkinEndpoint LocalEndpoint { get; private set; } + internal ZipkinEndpoint LocalEndpoint { get; private set; } - /// - public override ExportResult Export(in Batch batch) - { - // Prevent Zipkin's HTTP operations from being instrumented. - using var scope = SuppressInstrumentationScope.Begin(); + /// + public override ExportResult Export(in Batch batch) + { + // Prevent Zipkin's HTTP operations from being instrumented. + using var scope = SuppressInstrumentationScope.Begin(); - try + try + { + if (this.LocalEndpoint == null) { - if (this.LocalEndpoint == null) - { - this.SetLocalEndpointFromResource(this.ParentProvider.GetResource()); - } + this.SetLocalEndpointFromResource(this.ParentProvider.GetResource()); + } - var requestUri = this.options.Endpoint; + var requestUri = this.options.Endpoint; - using var request = new HttpRequestMessage(HttpMethod.Post, requestUri) - { - Content = new JsonContent(this, batch), - }; + using var request = new HttpRequestMessage(HttpMethod.Post, requestUri) + { + Content = new JsonContent(this, batch), + }; #if NET6_0_OR_GREATER - using var response = this.httpClient.Send(request, CancellationToken.None); + using var response = this.httpClient.Send(request, CancellationToken.None); #else - using var response = this.httpClient.SendAsync(request, CancellationToken.None).GetAwaiter().GetResult(); + using var response = this.httpClient.SendAsync(request, CancellationToken.None).GetAwaiter().GetResult(); #endif - response.EnsureSuccessStatusCode(); + response.EnsureSuccessStatusCode(); - return ExportResult.Success; - } - catch (Exception ex) - { - ZipkinExporterEventSource.Log.FailedExport(ex); + return ExportResult.Success; + } + catch (Exception ex) + { + ZipkinExporterEventSource.Log.FailedExport(ex); - return ExportResult.Failure; - } + return ExportResult.Failure; } + } - internal void SetLocalEndpointFromResource(Resource resource) + internal void SetLocalEndpointFromResource(Resource resource) + { + var hostName = ResolveHostName(); + + string ipv4 = null; + string ipv6 = null; + if (!string.IsNullOrEmpty(hostName)) { - var hostName = ResolveHostName(); + ipv4 = ResolveHostAddress(hostName, AddressFamily.InterNetwork); + ipv6 = ResolveHostAddress(hostName, AddressFamily.InterNetworkV6); + } - string ipv4 = null; - string ipv6 = null; - if (!string.IsNullOrEmpty(hostName)) + string serviceName = null; + foreach (var label in resource.Attributes) + { + if (label.Key == ResourceSemanticConventions.AttributeServiceName) { - ipv4 = ResolveHostAddress(hostName, AddressFamily.InterNetwork); - ipv6 = ResolveHostAddress(hostName, AddressFamily.InterNetworkV6); + serviceName = label.Value as string; + break; } + } - string serviceName = null; - foreach (var label in resource.Attributes) - { - if (label.Key == ResourceSemanticConventions.AttributeServiceName) - { - serviceName = label.Value as string; - break; - } - } + if (string.IsNullOrEmpty(serviceName)) + { + serviceName = (string)this.ParentProvider.GetDefaultResource().Attributes.Where( + pair => pair.Key == ResourceSemanticConventions.AttributeServiceName).FirstOrDefault().Value; + } - if (string.IsNullOrEmpty(serviceName)) - { - serviceName = (string)this.ParentProvider.GetDefaultResource().Attributes.Where( - pair => pair.Key == ResourceSemanticConventions.AttributeServiceName).FirstOrDefault().Value; - } + this.LocalEndpoint = new ZipkinEndpoint( + serviceName, + ipv4, + ipv6, + port: null, + tags: null); + } - this.LocalEndpoint = new ZipkinEndpoint( - serviceName, - ipv4, - ipv6, - port: null, - tags: null); - } + private static string ResolveHostAddress(string hostName, AddressFamily family) + { + string result = null; - private static string ResolveHostAddress(string hostName, AddressFamily family) + try { - string result = null; + var results = Dns.GetHostAddresses(hostName); - try + if (results != null && results.Length > 0) { - var results = Dns.GetHostAddresses(hostName); - - if (results != null && results.Length > 0) + foreach (var addr in results) { - foreach (var addr in results) + if (addr.AddressFamily.Equals(family)) { - if (addr.AddressFamily.Equals(family)) - { - var sanitizedAddress = new IPAddress(addr.GetAddressBytes()); // Construct address sans ScopeID - result = sanitizedAddress.ToString(); + var sanitizedAddress = new IPAddress(addr.GetAddressBytes()); // Construct address sans ScopeID + result = sanitizedAddress.ToString(); - break; - } + break; } } } - catch (Exception) - { - // Ignore - } - - return result; } + catch (Exception) + { + // Ignore + } + + return result; + } + + private static string ResolveHostName() + { + string result = null; - private static string ResolveHostName() + try { - string result = null; + result = Dns.GetHostName(); - try + if (!string.IsNullOrEmpty(result)) { - result = Dns.GetHostName(); + var response = Dns.GetHostEntry(result); - if (!string.IsNullOrEmpty(result)) + if (response != null) { - var response = Dns.GetHostEntry(result); - - if (response != null) - { - return response.HostName; - } + return response.HostName; } } - catch (Exception) - { - // Ignore - } - - return result; } + catch (Exception) + { + // Ignore + } + + return result; + } - private sealed class JsonContent : HttpContent + private sealed class JsonContent : HttpContent + { + private static readonly MediaTypeHeaderValue JsonHeader = new("application/json") { - private static readonly MediaTypeHeaderValue JsonHeader = new("application/json") - { - CharSet = "utf-8", - }; + CharSet = "utf-8", + }; - private readonly ZipkinExporter exporter; - private readonly Batch batch; - private Utf8JsonWriter writer; + private readonly ZipkinExporter exporter; + private readonly Batch batch; + private Utf8JsonWriter writer; - public JsonContent(ZipkinExporter exporter, in Batch batch) - { - this.exporter = exporter; - this.batch = batch; + public JsonContent(ZipkinExporter exporter, in Batch batch) + { + this.exporter = exporter; + this.batch = batch; - this.Headers.ContentType = JsonHeader; - } + this.Headers.ContentType = JsonHeader; + } #if NET6_0_OR_GREATER - protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken) - { - this.SerializeToStreamInternal(stream); - } + protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken) + { + this.SerializeToStreamInternal(stream); + } #endif - protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) + protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + this.SerializeToStreamInternal(stream); + return Task.CompletedTask; + } + + protected override bool TryComputeLength(out long length) + { + // We can't know the length of the content being pushed to the output stream. + length = -1; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SerializeToStreamInternal(Stream stream) + { + if (this.writer == null) { - this.SerializeToStreamInternal(stream); - return Task.CompletedTask; + this.writer = new Utf8JsonWriter(stream); } - - protected override bool TryComputeLength(out long length) + else { - // We can't know the length of the content being pushed to the output stream. - length = -1; - return false; + this.writer.Reset(stream); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SerializeToStreamInternal(Stream stream) + this.writer.WriteStartArray(); + + foreach (var activity in this.batch) { - if (this.writer == null) - { - this.writer = new Utf8JsonWriter(stream); - } - else - { - this.writer.Reset(stream); - } + var zipkinSpan = activity.ToZipkinSpan(this.exporter.LocalEndpoint, this.exporter.options.UseShortTraceIds); - this.writer.WriteStartArray(); + zipkinSpan.Write(this.writer); - foreach (var activity in this.batch) + zipkinSpan.Return(); + if (this.writer.BytesPending >= this.exporter.maxPayloadSizeInBytes) { - var zipkinSpan = activity.ToZipkinSpan(this.exporter.LocalEndpoint, this.exporter.options.UseShortTraceIds); - - zipkinSpan.Write(this.writer); - - zipkinSpan.Return(); - if (this.writer.BytesPending >= this.exporter.maxPayloadSizeInBytes) - { - this.writer.Flush(); - } + this.writer.Flush(); } + } - this.writer.WriteEndArray(); + this.writer.WriteEndArray(); - this.writer.Flush(); - } + this.writer.Flush(); } } } diff --git a/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterHelperExtensions.cs index 28fe9e58dc5..ca858c649ab 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterHelperExtensions.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #if NETFRAMEWORK using System.Net.Http; @@ -24,114 +11,113 @@ using OpenTelemetry.Exporter; using OpenTelemetry.Internal; -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +/// +/// Extension methods to simplify registering of Zipkin exporter. +/// +public static class ZipkinExporterHelperExtensions { /// - /// Extension methods to simplify registering of Zipkin exporter. + /// Adds Zipkin exporter to the TracerProvider. /// - public static class ZipkinExporterHelperExtensions - { - /// - /// Adds Zipkin exporter to the TracerProvider. - /// - /// builder to use. - /// The instance of to chain the calls. - public static TracerProviderBuilder AddZipkinExporter(this TracerProviderBuilder builder) - => AddZipkinExporter(builder, name: null, configure: null); + /// builder to use. + /// The instance of to chain the calls. + public static TracerProviderBuilder AddZipkinExporter(this TracerProviderBuilder builder) + => AddZipkinExporter(builder, name: null, configure: null); - /// - /// Adds Zipkin exporter to the TracerProvider. - /// - /// builder to use. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static TracerProviderBuilder AddZipkinExporter(this TracerProviderBuilder builder, Action configure) - => AddZipkinExporter(builder, name: null, configure); + /// + /// Adds Zipkin exporter to the TracerProvider. + /// + /// builder to use. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static TracerProviderBuilder AddZipkinExporter(this TracerProviderBuilder builder, Action configure) + => AddZipkinExporter(builder, name: null, configure); - /// - /// Adds Zipkin exporter to the TracerProvider. - /// - /// builder to use. - /// Name which is used when retrieving options. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static TracerProviderBuilder AddZipkinExporter( - this TracerProviderBuilder builder, - string name, - Action configure) - { - Guard.ThrowIfNull(builder); + /// + /// Adds Zipkin exporter to the TracerProvider. + /// + /// builder to use. + /// Name which is used when retrieving options. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static TracerProviderBuilder AddZipkinExporter( + this TracerProviderBuilder builder, + string name, + Action configure) + { + Guard.ThrowIfNull(builder); - name ??= Options.DefaultName; + name ??= Options.DefaultName; - builder.ConfigureServices(services => + builder.ConfigureServices(services => + { + if (configure != null) { - if (configure != null) - { - services.Configure(name, configure); - } + services.Configure(name, configure); + } - services.RegisterOptionsFactory( - (sp, configuration, name) => new ZipkinExporterOptions( - configuration, - sp.GetRequiredService>().Get(name))); - }); + services.RegisterOptionsFactory( + (sp, configuration, name) => new ZipkinExporterOptions( + configuration, + sp.GetRequiredService>().Get(name))); + }); - return builder.AddProcessor(sp => - { - var options = sp.GetRequiredService>().Get(name); + return builder.AddProcessor(sp => + { + var options = sp.GetRequiredService>().Get(name); - return BuildZipkinExporterProcessor(builder, options, sp); - }); - } + return BuildZipkinExporterProcessor(builder, options, sp); + }); + } - private static BaseProcessor BuildZipkinExporterProcessor( - TracerProviderBuilder builder, - ZipkinExporterOptions options, - IServiceProvider serviceProvider) + private static BaseProcessor BuildZipkinExporterProcessor( + TracerProviderBuilder builder, + ZipkinExporterOptions options, + IServiceProvider serviceProvider) + { + if (options.HttpClientFactory == ZipkinExporterOptions.DefaultHttpClientFactory) { - if (options.HttpClientFactory == ZipkinExporterOptions.DefaultHttpClientFactory) + options.HttpClientFactory = () => { - options.HttpClientFactory = () => + Type httpClientFactoryType = Type.GetType("System.Net.Http.IHttpClientFactory, Microsoft.Extensions.Http", throwOnError: false); + if (httpClientFactoryType != null) { - Type httpClientFactoryType = Type.GetType("System.Net.Http.IHttpClientFactory, Microsoft.Extensions.Http", throwOnError: false); - if (httpClientFactoryType != null) + object httpClientFactory = serviceProvider.GetService(httpClientFactoryType); + if (httpClientFactory != null) { - object httpClientFactory = serviceProvider.GetService(httpClientFactoryType); - if (httpClientFactory != null) + MethodInfo createClientMethod = httpClientFactoryType.GetMethod( + "CreateClient", + BindingFlags.Public | BindingFlags.Instance, + binder: null, + new Type[] { typeof(string) }, + modifiers: null); + if (createClientMethod != null) { - MethodInfo createClientMethod = httpClientFactoryType.GetMethod( - "CreateClient", - BindingFlags.Public | BindingFlags.Instance, - binder: null, - new Type[] { typeof(string) }, - modifiers: null); - if (createClientMethod != null) - { - return (HttpClient)createClientMethod.Invoke(httpClientFactory, new object[] { "ZipkinExporter" }); - } + return (HttpClient)createClientMethod.Invoke(httpClientFactory, new object[] { "ZipkinExporter" }); } } + } - return new HttpClient(); - }; - } + return new HttpClient(); + }; + } - var zipkinExporter = new ZipkinExporter(options); + var zipkinExporter = new ZipkinExporter(options); - if (options.ExportProcessorType == ExportProcessorType.Simple) - { - return new SimpleActivityExportProcessor(zipkinExporter); - } - else - { - return new BatchActivityExportProcessor( - zipkinExporter, - options.BatchExportProcessorOptions.MaxQueueSize, - options.BatchExportProcessorOptions.ScheduledDelayMilliseconds, - options.BatchExportProcessorOptions.ExporterTimeoutMilliseconds, - options.BatchExportProcessorOptions.MaxExportBatchSize); - } + if (options.ExportProcessorType == ExportProcessorType.Simple) + { + return new SimpleActivityExportProcessor(zipkinExporter); + } + else + { + return new BatchActivityExportProcessor( + zipkinExporter, + options.BatchExportProcessorOptions.MaxQueueSize, + options.BatchExportProcessorOptions.ScheduledDelayMilliseconds, + options.BatchExportProcessorOptions.ExporterTimeoutMilliseconds, + options.BatchExportProcessorOptions.MaxExportBatchSize); } } } diff --git a/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterOptions.cs b/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterOptions.cs index e7051137925..6e04c9d4a79 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterOptions.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; #if NETFRAMEWORK @@ -22,87 +9,86 @@ using OpenTelemetry.Internal; using OpenTelemetry.Trace; -namespace OpenTelemetry.Exporter +namespace OpenTelemetry.Exporter; + +/// +/// Zipkin span exporter options. +/// OTEL_EXPORTER_ZIPKIN_ENDPOINT +/// environment variables are parsed during object construction. +/// +public sealed class ZipkinExporterOptions { + internal const int DefaultMaxPayloadSizeInBytes = 4096; + internal const string ZipkinEndpointEnvVar = "OTEL_EXPORTER_ZIPKIN_ENDPOINT"; + internal const string DefaultZipkinEndpoint = "http://localhost:9411/api/v2/spans"; + + internal static readonly Func DefaultHttpClientFactory = () => new HttpClient(); + /// - /// Zipkin span exporter options. - /// OTEL_EXPORTER_ZIPKIN_ENDPOINT - /// environment variables are parsed during object construction. + /// Initializes a new instance of the class. + /// Initializes zipkin endpoint. /// - public sealed class ZipkinExporterOptions + public ZipkinExporterOptions() + : this(new ConfigurationBuilder().AddEnvironmentVariables().Build(), new()) { - internal const int DefaultMaxPayloadSizeInBytes = 4096; - internal const string ZipkinEndpointEnvVar = "OTEL_EXPORTER_ZIPKIN_ENDPOINT"; - internal const string DefaultZipkinEndpoint = "http://localhost:9411/api/v2/spans"; + } - internal static readonly Func DefaultHttpClientFactory = () => new HttpClient(); + internal ZipkinExporterOptions( + IConfiguration configuration, + BatchExportActivityProcessorOptions defaultBatchOptions) + { + Debug.Assert(configuration != null, "configuration was null"); + Debug.Assert(defaultBatchOptions != null, "defaultBatchOptions was null"); - /// - /// Initializes a new instance of the class. - /// Initializes zipkin endpoint. - /// - public ZipkinExporterOptions() - : this(new ConfigurationBuilder().AddEnvironmentVariables().Build(), new()) + if (configuration.TryGetUriValue(ZipkinEndpointEnvVar, out var endpoint)) { + this.Endpoint = endpoint; } - internal ZipkinExporterOptions( - IConfiguration configuration, - BatchExportActivityProcessorOptions defaultBatchOptions) - { - Debug.Assert(configuration != null, "configuration was null"); - Debug.Assert(defaultBatchOptions != null, "defaultBatchOptions was null"); - - if (configuration.TryGetUriValue(ZipkinEndpointEnvVar, out var endpoint)) - { - this.Endpoint = endpoint; - } - - this.BatchExportProcessorOptions = defaultBatchOptions; - } + this.BatchExportProcessorOptions = defaultBatchOptions; + } - /// - /// Gets or sets Zipkin endpoint address. See https://zipkin.io/zipkin-api/#/default/post_spans. - /// Typically https://zipkin-server-name:9411/api/v2/spans. - /// - public Uri Endpoint { get; set; } = new Uri(DefaultZipkinEndpoint); + /// + /// Gets or sets Zipkin endpoint address. See https://zipkin.io/zipkin-api/#/default/post_spans. + /// Typically https://zipkin-server-name:9411/api/v2/spans. + /// + public Uri Endpoint { get; set; } = new Uri(DefaultZipkinEndpoint); - /// - /// Gets or sets a value indicating whether short trace id should be used. - /// - public bool UseShortTraceIds { get; set; } + /// + /// Gets or sets a value indicating whether short trace id should be used. + /// + public bool UseShortTraceIds { get; set; } - /// - /// Gets or sets the maximum payload size in bytes. Default value: 4096. - /// - public int? MaxPayloadSizeInBytes { get; set; } = DefaultMaxPayloadSizeInBytes; + /// + /// Gets or sets the maximum payload size in bytes. Default value: 4096. + /// + public int? MaxPayloadSizeInBytes { get; set; } = DefaultMaxPayloadSizeInBytes; - /// - /// Gets or sets the export processor type to be used with Zipkin Exporter. The default value is . - /// - public ExportProcessorType ExportProcessorType { get; set; } = ExportProcessorType.Batch; + /// + /// Gets or sets the export processor type to be used with Zipkin Exporter. The default value is . + /// + public ExportProcessorType ExportProcessorType { get; set; } = ExportProcessorType.Batch; - /// - /// Gets or sets the BatchExportProcessor options. Ignored unless ExportProcessorType is BatchExporter. - /// - public BatchExportProcessorOptions BatchExportProcessorOptions { get; set; } + /// + /// Gets or sets the BatchExportProcessor options. Ignored unless ExportProcessorType is BatchExporter. + /// + public BatchExportProcessorOptions BatchExportProcessorOptions { get; set; } - /// - /// Gets or sets the factory function called to create the instance that will be used at runtime to - /// transmit spans over HTTP. The returned instance will be reused for - /// all export invocations. - /// - /// - /// Note: The default behavior when using the extension is if an IHttpClientFactory - /// instance can be resolved through the application then an will be - /// created through the factory with the name "ZipkinExporter" otherwise - /// an will be instantiated directly. - /// - public Func HttpClientFactory { get; set; } = DefaultHttpClientFactory; - } + /// + /// Gets or sets the factory function called to create the instance that will be used at runtime to + /// transmit spans over HTTP. The returned instance will be reused for + /// all export invocations. + /// + /// + /// Note: The default behavior when using the extension is if an IHttpClientFactory + /// instance can be resolved through the application then an will be + /// created through the factory with the name "ZipkinExporter" otherwise + /// an will be instantiated directly. + /// + public Func HttpClientFactory { get; set; } = DefaultHttpClientFactory; } diff --git a/src/OpenTelemetry.Extensions.Hosting/.publicApi/Experimental/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Extensions.Hosting/.publicApi/Experimental/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..f83d7ca4a0f --- /dev/null +++ b/src/OpenTelemetry.Extensions.Hosting/.publicApi/Experimental/PublicAPI.Unshipped.txt @@ -0,0 +1,3 @@ +OpenTelemetry.OpenTelemetryBuilder.WithLogging() -> OpenTelemetry.OpenTelemetryBuilder! +OpenTelemetry.OpenTelemetryBuilder.WithLogging(System.Action! configure) -> OpenTelemetry.OpenTelemetryBuilder! +OpenTelemetry.OpenTelemetryBuilder.WithLogging(System.Action? configureBuilder, System.Action? configureOptions) -> OpenTelemetry.OpenTelemetryBuilder! diff --git a/src/OpenTelemetry.Extensions.Hosting/.publicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Extensions.Hosting/.publicApi/Stable/PublicAPI.Shipped.txt similarity index 100% rename from src/OpenTelemetry.Extensions.Hosting/.publicApi/netstandard2.0/PublicAPI.Shipped.txt rename to src/OpenTelemetry.Extensions.Hosting/.publicApi/Stable/PublicAPI.Shipped.txt diff --git a/src/OpenTelemetry.Exporter.Zipkin/.publicApi/net6.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Extensions.Hosting/.publicApi/Stable/PublicAPI.Unshipped.txt similarity index 100% rename from src/OpenTelemetry.Exporter.Zipkin/.publicApi/net6.0/PublicAPI.Unshipped.txt rename to src/OpenTelemetry.Extensions.Hosting/.publicApi/Stable/PublicAPI.Unshipped.txt diff --git a/src/OpenTelemetry.Extensions.Hosting/AssemblyInfo.cs b/src/OpenTelemetry.Extensions.Hosting/AssemblyInfo.cs index abf21ce8c15..d36465c0505 100644 --- a/src/OpenTelemetry.Extensions.Hosting/AssemblyInfo.cs +++ b/src/OpenTelemetry.Extensions.Hosting/AssemblyInfo.cs @@ -1,34 +1,18 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("OpenTelemetry.Extensions.Hosting.Tests" + AssemblyInfo.PublicKey)] -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2" + AssemblyInfo.MoqPublicKey)] #if SIGNED -internal static class AssemblyInfo +file static class AssemblyInfo { public const string PublicKey = ", PublicKey=002400000480000094000000060200000024000052534131000400000100010051C1562A090FB0C9F391012A32198B5E5D9A60E9B80FA2D7B434C9E5CCB7259BD606E66F9660676AFC6692B8CDC6793D190904551D2103B7B22FA636DCBB8208839785BA402EA08FC00C8F1500CCEF28BBF599AA64FFB1E1D5DC1BF3420A3777BADFE697856E9D52070A50C3EA5821C80BEF17CA3ACFFA28F89DD413F096F898"; - public const string MoqPublicKey = ", PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7"; } #else -internal static class AssemblyInfo +file static class AssemblyInfo { public const string PublicKey = ""; - public const string MoqPublicKey = ""; } #endif diff --git a/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md b/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md index 73c09094a4f..97f90b4cf03 100644 --- a/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md +++ b/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md @@ -2,6 +2,63 @@ ## Unreleased +* `OpenTelemetryBuilder` has been marked obsolete. Component authors using + `OpenTelemetryBuilder` for cross-cutting signal configuration extensions + should switch to targeting `IOpenTelemetryBuilder` instead. + ([#5265](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5265)) + +## 1.7.0 + +Released 2023-Dec-08 + +## 1.7.0-rc.1 + +Released 2023-Nov-29 + +* Updated `Microsoft.Extensions.Hosting.Abstractions` package + 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)) + +* The `OpenTelemetryBuilder.WithLogging` experimental API method will now + register an `ILoggerProvider` named 'OpenTelemetry' into the + `IServiceCollection` to enable `ILoggerFactory` integration. + ([#5072](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5072)) + +## 1.7.0-alpha.1 + +Released 2023-Oct-16 + +* Changed the behavior of the `OpenTelemetryBuilder.AddOpenTelemetry` extension + to INSERT OpenTelemetry services at the beginning of the `IServiceCollection` + in an attempt to provide a better experience for end users capturing telemetry + in hosted services. Note that this does not guarantee that OpenTelemetry + services will be initialized while other hosted services start, so it is + possible to miss telemetry until OpenTelemetry services are fully initialized. + ([#4883](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4883)) + +## 1.6.0 + +Released 2023-Sep-05 + +## 1.6.0-rc.1 + +Released 2023-Aug-21 + +* **Experimental (pre-release builds only):** Added [Logs Bridge + API](https://github.com/open-telemetry/opentelemetry-specification/blob/976432b74c565e8a84af3570e9b82cb95e1d844c/specification/logs/bridge-api.md) + implementation (`OpenTelemetryBuilder.WithLogging`). + ([#4735](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4735)) + +## 1.6.0-alpha.1 + +Released 2023-Jul-12 + ## 1.5.1 Released 2023-Jun-26 diff --git a/src/OpenTelemetry.Extensions.Hosting/Implementation/HostingExtensionsEventSource.cs b/src/OpenTelemetry.Extensions.Hosting/Implementation/HostingExtensionsEventSource.cs index ac54416399b..1d31393b0fa 100644 --- a/src/OpenTelemetry.Extensions.Hosting/Implementation/HostingExtensionsEventSource.cs +++ b/src/OpenTelemetry.Extensions.Hosting/Implementation/HostingExtensionsEventSource.cs @@ -1,47 +1,33 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Tracing; -namespace OpenTelemetry.Extensions.Hosting.Implementation +namespace OpenTelemetry.Extensions.Hosting.Implementation; + +/// +/// EventSource events emitted from the project. +/// +[EventSource(Name = "OpenTelemetry-Extensions-Hosting")] +internal sealed class HostingExtensionsEventSource : EventSource { - /// - /// EventSource events emitted from the project. - /// - [EventSource(Name = "OpenTelemetry-Extensions-Hosting")] - internal sealed class HostingExtensionsEventSource : EventSource - { - public static HostingExtensionsEventSource Log = new(); + public static HostingExtensionsEventSource Log = new(); - [Event(1, Message = "OpenTelemetry TracerProvider was not found in application services. Tracing will remain disabled.", Level = EventLevel.Warning)] - public void TracerProviderNotRegistered() - { - this.WriteEvent(1); - } + [Event(1, Message = "OpenTelemetry TracerProvider was not found in application services. Tracing will remain disabled.", Level = EventLevel.Warning)] + public void TracerProviderNotRegistered() + { + this.WriteEvent(1); + } - [Event(2, Message = "OpenTelemetry MeterProvider was not found in application services. Metrics will remain disabled.", Level = EventLevel.Warning)] - public void MeterProviderNotRegistered() - { - this.WriteEvent(2); - } + [Event(2, Message = "OpenTelemetry MeterProvider was not found in application services. Metrics will remain disabled.", Level = EventLevel.Warning)] + public void MeterProviderNotRegistered() + { + this.WriteEvent(2); + } - [Event(3, Message = "OpenTelemetry LoggerProvider was not found in application services. Logging will remain disabled.", Level = EventLevel.Warning)] - public void LoggerProviderNotRegistered() - { - this.WriteEvent(3); - } + [Event(3, Message = "OpenTelemetry LoggerProvider was not found in application services. Logging will remain disabled.", Level = EventLevel.Warning)] + public void LoggerProviderNotRegistered() + { + this.WriteEvent(3); } } diff --git a/src/OpenTelemetry.Extensions.Hosting/Implementation/TelemetryHostedService.cs b/src/OpenTelemetry.Extensions.Hosting/Implementation/TelemetryHostedService.cs index 60023e38b7b..ba2ac37ef01 100644 --- a/src/OpenTelemetry.Extensions.Hosting/Implementation/TelemetryHostedService.cs +++ b/src/OpenTelemetry.Extensions.Hosting/Implementation/TelemetryHostedService.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using Microsoft.Extensions.DependencyInjection; diff --git a/src/OpenTelemetry.Extensions.Hosting/OpenTelemetry.Extensions.Hosting.csproj b/src/OpenTelemetry.Extensions.Hosting/OpenTelemetry.Extensions.Hosting.csproj index 133f193acc6..93cdb8cf92e 100644 --- a/src/OpenTelemetry.Extensions.Hosting/OpenTelemetry.Extensions.Hosting.csproj +++ b/src/OpenTelemetry.Extensions.Hosting/OpenTelemetry.Extensions.Hosting.csproj @@ -1,8 +1,7 @@ - - netstandard2.0 + $(TargetFrameworksForLibraries) Contains extensions to start OpenTelemetry in applications using Microsoft.Extensions.Hosting OpenTelemetry core- @@ -18,8 +17,11 @@ - - + + + + diff --git a/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryBuilder.cs b/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryBuilder.cs index 1f50f76637a..2ddb9b50055 100644 --- a/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryBuilder.cs +++ b/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryBuilder.cs @@ -1,20 +1,12 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; +using Microsoft.Extensions.Logging; using OpenTelemetry.Internal; using OpenTelemetry.Logs; using OpenTelemetry.Metrics; @@ -27,7 +19,8 @@ namespace OpenTelemetry; /// Contains methods for configuring the OpenTelemetry SDK inside an . /// -public sealed class OpenTelemetryBuilder +[Obsolete("Use IOpenTelemetryBuilder instead this class will be removed in a future version.")] +public sealed class OpenTelemetryBuilder : IOpenTelemetryBuilder { internal OpenTelemetryBuilder(IServiceCollection services) { @@ -38,9 +31,7 @@ internal OpenTelemetryBuilder(IServiceCollection services) this.Services = services; } - /// - /// Gets the behind the builder. - /// + /// public IServiceCollection Services { get; } /// @@ -58,17 +49,7 @@ internal OpenTelemetryBuilder(IServiceCollection services) public OpenTelemetryBuilder ConfigureResource( Action configure) { - Guard.ThrowIfNull(configure); - - this.Services.ConfigureOpenTelemetryMeterProvider( - (sp, builder) => builder.ConfigureResource(configure)); - - this.Services.ConfigureOpenTelemetryTracerProvider( - (sp, builder) => builder.ConfigureResource(configure)); - - this.Services.ConfigureOpenTelemetryLoggerProvider( - (sp, builder) => builder.ConfigureResource(configure)); - + OpenTelemetryBuilderSdkExtensions.ConfigureResource(this, configure); return this; } @@ -76,9 +57,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,12 +82,7 @@ public OpenTelemetryBuilder WithMetrics() /// calls. public OpenTelemetryBuilder WithMetrics(Action configure) { - Guard.ThrowIfNull(configure); - - var builder = new MeterProviderBuilderBase(this.Services); - - configure(builder); - + OpenTelemetryBuilderSdkExtensions.WithMetrics(this, configure); return this; } @@ -127,28 +109,69 @@ public OpenTelemetryBuilder WithTracing() /// calls. public OpenTelemetryBuilder WithTracing(Action configure) { - Guard.ThrowIfNull(configure); - - var builder = new TracerProviderBuilderBase(this.Services); - - configure(builder); - + OpenTelemetryBuilderSdkExtensions.WithTracing(this, configure); return this; } +#if EXPOSE_EXPERIMENTAL_FEATURES /// /// Adds logging services into the builder. /// /// - /// Note: This is safe to be called multiple times and by library authors. + /// WARNING: This is an experimental API which might change or + /// be removed in the future. Use at your own risk. + /// 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. - internal OpenTelemetryBuilder WithLogging() - => this.WithLogging(b => { }); +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + /// + /// Adds logging services into the builder. + /// + /// + /// 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. + internal +#endif + OpenTelemetryBuilder WithLogging() + => this.WithLogging(configureBuilder: null, configureOptions: null); +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds logging services into the builder. + /// + /// + /// + /// configuration callback. + /// The supplied for chaining + /// calls. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else /// /// Adds logging services into the builder. /// @@ -157,13 +180,48 @@ internal OpenTelemetryBuilder WithLogging() /// configuration callback. /// The supplied for chaining /// calls. - internal OpenTelemetryBuilder WithLogging(Action configure) + internal +#endif + OpenTelemetryBuilder WithLogging(Action configure) { Guard.ThrowIfNull(configure); - var builder = new LoggerProviderBuilderBase(this.Services); + return this.WithLogging(configureBuilder: configure, configureOptions: null); + } - configure(builder); +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds logging services into the builder. + /// + /// + /// Optional configuration callback. + /// Optional configuration callback. + /// The supplied for chaining + /// calls. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + /// + /// Adds logging services into the builder. + /// + /// + /// Optional configuration callback. + /// Optional configuration callback. + /// The supplied for chaining + /// calls. + internal +#endif + OpenTelemetryBuilder WithLogging( + Action? configureBuilder, + Action? configureOptions) + { + OpenTelemetryBuilderSdkExtensions.WithLogging(this, configureBuilder, configureOptions); return this; } diff --git a/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryServicesExtensions.cs b/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryServicesExtensions.cs index dff14ca1b38..ac875dd4cc3 100644 --- a/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryServicesExtensions.cs +++ b/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryServicesExtensions.cs @@ -1,20 +1,6 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using OpenTelemetry; using OpenTelemetry.Extensions.Hosting.Implementation; @@ -35,20 +21,34 @@ public static class OpenTelemetryServicesExtensions /// cref="IServiceCollection"/>. /// /// - /// 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 and/or will be created for a given . + /// cref="IServiceCollection"/>. + /// OpenTelemetry SDK services are inserted at the beginning of the + /// and started with the host. For details + /// about the ordering of events and capturing telemetry in + /// s see: . + /// /// /// . /// The supplied for chaining /// calls. +#pragma warning disable CS0618 // Type or member is obsolete + // Note: OpenTelemetryBuilder is obsolete because users should target + // IOpenTelemetryBuilder for extensions but this method is valid and + // expected to be called to obtain a root builder. public static OpenTelemetryBuilder AddOpenTelemetry(this IServiceCollection services) +#pragma warning restore CS0618 // Type or member is obsolete { Guard.ThrowIfNull(services); - services.TryAddEnumerable( - ServiceDescriptor.Singleton()); + if (!services.Any((ServiceDescriptor d) => d.ServiceType == typeof(IHostedService) && d.ImplementationType == typeof(TelemetryHostedService))) + { + services.Insert(0, ServiceDescriptor.Singleton()); + } return new(services); } diff --git a/src/OpenTelemetry.Extensions.Hosting/README.md b/src/OpenTelemetry.Extensions.Hosting/README.md index 9dd0ee96f08..9b804edb14f 100644 --- a/src/OpenTelemetry.Extensions.Hosting/README.md +++ b/src/OpenTelemetry.Extensions.Hosting/README.md @@ -29,13 +29,13 @@ Targeting `Microsoft.Extensions.DependencyInjection.IServiceCollection`: [IServiceCollection](https://learn.microsoft.com/dotnet/api/microsoft.extensions.dependencyinjection.iservicecollection) and then returns an `OpenTelemetryBuilder` class. - > **Note** + > [!NOTE] > `AddOpenTelemetry` should be called by application host code only. Library authors see: [Registration extension method guidance for library authors](../../docs/trace/extending-the-sdk/README.md#registration-extension-method-guidance-for-library-authors). - > **Note** + > [!NOTE] > Multiple calls to `AddOpenTelemetry` will **NOT** result in multiple providers. Only a single `TracerProvider` and/or `MeterProvider` will be created in the target `IServiceCollection`. To establish multiple providers @@ -87,8 +87,8 @@ A fully functional example can be found To dynamically add resources at startup from the dependency injection you can provide an `IResourceDetector`. -To make use of it add it to the dependency injection and they you can use the -`ISerivceProvider` add it to OpenTelemetry: +To make use of it add it to the dependency injection and then you can use the +`IServiceProvider` to add it to OpenTelemetry: ```csharp public class MyResourceDetector : IResourceDetector @@ -141,6 +141,10 @@ and [new](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/examples/AspNetCore) versions of the example application to assist you in your migration. +## Hosted Service Ordering and Telemetry Capture + +TBD + ## References * [OpenTelemetry Project](https://opentelemetry.io/) diff --git a/src/OpenTelemetry.Extensions.Propagators/.publicApi/net462/PublicAPI.Shipped.txt b/src/OpenTelemetry.Extensions.Propagators/.publicApi/PublicAPI.Shipped.txt similarity index 100% rename from src/OpenTelemetry.Extensions.Propagators/.publicApi/net462/PublicAPI.Shipped.txt rename to src/OpenTelemetry.Extensions.Propagators/.publicApi/PublicAPI.Shipped.txt diff --git a/src/OpenTelemetry.Exporter.Zipkin/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Extensions.Propagators/.publicApi/PublicAPI.Unshipped.txt similarity index 100% rename from src/OpenTelemetry.Exporter.Zipkin/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt rename to src/OpenTelemetry.Extensions.Propagators/.publicApi/PublicAPI.Unshipped.txt diff --git a/src/OpenTelemetry.Extensions.Propagators/.publicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Extensions.Propagators/.publicApi/netstandard2.0/PublicAPI.Shipped.txt deleted file mode 100644 index eadd932c939..00000000000 --- a/src/OpenTelemetry.Extensions.Propagators/.publicApi/netstandard2.0/PublicAPI.Shipped.txt +++ /dev/null @@ -1,11 +0,0 @@ -OpenTelemetry.Extensions.Propagators.B3Propagator -OpenTelemetry.Extensions.Propagators.B3Propagator.B3Propagator() -> void -OpenTelemetry.Extensions.Propagators.B3Propagator.B3Propagator(bool singleHeader) -> void -OpenTelemetry.Extensions.Propagators.JaegerPropagator -OpenTelemetry.Extensions.Propagators.JaegerPropagator.JaegerPropagator() -> void -override OpenTelemetry.Extensions.Propagators.B3Propagator.Extract(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Func> getter) -> OpenTelemetry.Context.Propagation.PropagationContext -override OpenTelemetry.Extensions.Propagators.B3Propagator.Fields.get -> System.Collections.Generic.ISet -override OpenTelemetry.Extensions.Propagators.B3Propagator.Inject(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Action setter) -> void -override OpenTelemetry.Extensions.Propagators.JaegerPropagator.Extract(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Func> getter) -> OpenTelemetry.Context.Propagation.PropagationContext -override OpenTelemetry.Extensions.Propagators.JaegerPropagator.Fields.get -> System.Collections.Generic.ISet -override OpenTelemetry.Extensions.Propagators.JaegerPropagator.Inject(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Action setter) -> void diff --git a/src/OpenTelemetry.Extensions.Propagators/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Extensions.Propagators/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/OpenTelemetry.Extensions.Propagators/AssemblyInfo.cs b/src/OpenTelemetry.Extensions.Propagators/AssemblyInfo.cs index 728ed87e54a..a06279f44f2 100644 --- a/src/OpenTelemetry.Extensions.Propagators/AssemblyInfo.cs +++ b/src/OpenTelemetry.Extensions.Propagators/AssemblyInfo.cs @@ -1,18 +1,6 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + using System.Runtime.CompilerServices; #if SIGNED diff --git a/src/OpenTelemetry.Extensions.Propagators/B3Propagator.cs b/src/OpenTelemetry.Extensions.Propagators/B3Propagator.cs index 13559bd1157..37ced1218e8 100644 --- a/src/OpenTelemetry.Extensions.Propagators/B3Propagator.cs +++ b/src/OpenTelemetry.Extensions.Propagators/B3Propagator.cs @@ -1,264 +1,250 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Text; using OpenTelemetry.Context.Propagation; using OpenTelemetry.Internal; -namespace OpenTelemetry.Extensions.Propagators +namespace OpenTelemetry.Extensions.Propagators; + +/// +/// A text map propagator for B3. See https://github.com/openzipkin/b3-propagation. +/// This has been lift-and-shifted as is from the . +/// +public sealed class B3Propagator : TextMapPropagator { + internal const string XB3TraceId = "X-B3-TraceId"; + internal const string XB3SpanId = "X-B3-SpanId"; + internal const string XB3ParentSpanId = "X-B3-ParentSpanId"; + internal const string XB3Sampled = "X-B3-Sampled"; + internal const string XB3Flags = "X-B3-Flags"; + internal const string XB3Combined = "b3"; + internal const char XB3CombinedDelimiter = '-'; + + // Used as the upper ActivityTraceId.SIZE hex characters of the traceID. B3-propagation used to send + // ActivityTraceId.SIZE hex characters (8-bytes traceId) in the past. + internal const string UpperTraceId = "0000000000000000"; + + // Sampled values via the X_B3_SAMPLED header. + internal const string SampledValue = "1"; + + // Some old zipkin implementations may send true/false for the sampled header. Only use this for checking incoming values. + internal const string LegacySampledValue = "true"; + + // "Debug" sampled value. + internal const string FlagsValue = "1"; + + private static readonly HashSet AllFields = new() { XB3TraceId, XB3SpanId, XB3ParentSpanId, XB3Sampled, XB3Flags }; + + private static readonly HashSet SampledValues = new(StringComparer.Ordinal) { SampledValue, LegacySampledValue }; + + private readonly bool singleHeader; + /// - /// A text map propagator for B3. See https://github.com/openzipkin/b3-propagation. - /// This has been lift-and-shifted as is from the . + /// Initializes a new instance of the class. /// - public sealed class B3Propagator : TextMapPropagator + public B3Propagator() + : this(false) { - internal const string XB3TraceId = "X-B3-TraceId"; - internal const string XB3SpanId = "X-B3-SpanId"; - internal const string XB3ParentSpanId = "X-B3-ParentSpanId"; - internal const string XB3Sampled = "X-B3-Sampled"; - internal const string XB3Flags = "X-B3-Flags"; - internal const string XB3Combined = "b3"; - internal const char XB3CombinedDelimiter = '-'; - - // Used as the upper ActivityTraceId.SIZE hex characters of the traceID. B3-propagation used to send - // ActivityTraceId.SIZE hex characters (8-bytes traceId) in the past. - internal const string UpperTraceId = "0000000000000000"; + } - // Sampled values via the X_B3_SAMPLED header. - internal const string SampledValue = "1"; + /// + /// Initializes a new instance of the class. + /// + /// Determines whether to use single or multiple headers when extracting or injecting span context. + public B3Propagator(bool singleHeader) + { + this.singleHeader = singleHeader; + } - // Some old zipkin implementations may send true/false for the sampled header. Only use this for checking incoming values. - internal const string LegacySampledValue = "true"; + /// + public override ISet Fields => AllFields; - // "Debug" sampled value. - internal const string FlagsValue = "1"; + /// + public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) + { + if (context.ActivityContext.IsValid()) + { + // If a valid context has already been extracted, perform a noop. + return context; + } - private static readonly HashSet AllFields = new() { XB3TraceId, XB3SpanId, XB3ParentSpanId, XB3Sampled, XB3Flags }; + if (carrier == null) + { + OpenTelemetryPropagatorsEventSource.Log.FailedToExtractActivityContext(nameof(B3Propagator), "null carrier"); + return context; + } - private static readonly HashSet SampledValues = new(StringComparer.Ordinal) { SampledValue, LegacySampledValue }; + if (getter == null) + { + OpenTelemetryPropagatorsEventSource.Log.FailedToExtractActivityContext(nameof(B3Propagator), "null getter"); + return context; + } - private readonly bool singleHeader; + if (this.singleHeader) + { + return ExtractFromSingleHeader(context, carrier, getter); + } + else + { + return ExtractFromMultipleHeaders(context, carrier, getter); + } + } - /// - /// Initializes a new instance of the class. - /// - public B3Propagator() - : this(false) + /// + public override void Inject(PropagationContext context, T carrier, Action setter) + { + if (context.ActivityContext.TraceId == default || context.ActivityContext.SpanId == default) { + OpenTelemetryPropagatorsEventSource.Log.FailedToInjectActivityContext(nameof(B3Propagator), "invalid context"); + return; } - /// - /// Initializes a new instance of the class. - /// - /// Determines whether to use single or multiple headers when extracting or injecting span context. - public B3Propagator(bool singleHeader) + if (carrier == null) { - this.singleHeader = singleHeader; + OpenTelemetryPropagatorsEventSource.Log.FailedToInjectActivityContext(nameof(B3Propagator), "null carrier"); + return; } - /// - public override ISet Fields => AllFields; + if (setter == null) + { + OpenTelemetryPropagatorsEventSource.Log.FailedToInjectActivityContext(nameof(B3Propagator), "null setter"); + return; + } - /// - public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) + if (this.singleHeader) { - if (context.ActivityContext.IsValid()) + var sb = new StringBuilder(); + sb.Append(context.ActivityContext.TraceId.ToHexString()); + sb.Append(XB3CombinedDelimiter); + sb.Append(context.ActivityContext.SpanId.ToHexString()); + if ((context.ActivityContext.TraceFlags & ActivityTraceFlags.Recorded) != 0) { - // If a valid context has already been extracted, perform a noop. - return context; + sb.Append(XB3CombinedDelimiter); + sb.Append(SampledValue); } - if (carrier == null) + setter(carrier, XB3Combined, sb.ToString()); + } + else + { + setter(carrier, XB3TraceId, context.ActivityContext.TraceId.ToHexString()); + setter(carrier, XB3SpanId, context.ActivityContext.SpanId.ToHexString()); + if ((context.ActivityContext.TraceFlags & ActivityTraceFlags.Recorded) != 0) { - OpenTelemetryPropagatorsEventSource.Log.FailedToExtractActivityContext(nameof(B3Propagator), "null carrier"); - return context; + setter(carrier, XB3Sampled, SampledValue); } + } + } - if (getter == null) + private static PropagationContext ExtractFromMultipleHeaders(PropagationContext context, T carrier, Func> getter) + { + try + { + ActivityTraceId traceId; + var traceIdStr = getter(carrier, XB3TraceId)?.FirstOrDefault(); + if (traceIdStr != null) { - OpenTelemetryPropagatorsEventSource.Log.FailedToExtractActivityContext(nameof(B3Propagator), "null getter"); - return context; - } + if (traceIdStr.Length == 16) + { + // This is an 8-byte traceID. + traceIdStr = UpperTraceId + traceIdStr; + } - if (this.singleHeader) - { - return ExtractFromSingleHeader(context, carrier, getter); + traceId = ActivityTraceId.CreateFromString(traceIdStr.AsSpan()); } else { - return ExtractFromMultipleHeaders(context, carrier, getter); + return context; } - } - /// - public override void Inject(PropagationContext context, T carrier, Action setter) - { - if (context.ActivityContext.TraceId == default || context.ActivityContext.SpanId == default) + ActivitySpanId spanId; + var spanIdStr = getter(carrier, XB3SpanId)?.FirstOrDefault(); + if (spanIdStr != null) { - OpenTelemetryPropagatorsEventSource.Log.FailedToInjectActivityContext(nameof(B3Propagator), "invalid context"); - return; + spanId = ActivitySpanId.CreateFromString(spanIdStr.AsSpan()); } - - if (carrier == null) + else { - OpenTelemetryPropagatorsEventSource.Log.FailedToInjectActivityContext(nameof(B3Propagator), "null carrier"); - return; + return context; } - if (setter == null) + var traceOptions = ActivityTraceFlags.None; + if (SampledValues.Contains(getter(carrier, XB3Sampled)?.FirstOrDefault()) + || FlagsValue.Equals(getter(carrier, XB3Flags)?.FirstOrDefault(), StringComparison.Ordinal)) { - OpenTelemetryPropagatorsEventSource.Log.FailedToInjectActivityContext(nameof(B3Propagator), "null setter"); - return; + traceOptions |= ActivityTraceFlags.Recorded; } - if (this.singleHeader) - { - var sb = new StringBuilder(); - sb.Append(context.ActivityContext.TraceId.ToHexString()); - sb.Append(XB3CombinedDelimiter); - sb.Append(context.ActivityContext.SpanId.ToHexString()); - if ((context.ActivityContext.TraceFlags & ActivityTraceFlags.Recorded) != 0) - { - sb.Append(XB3CombinedDelimiter); - sb.Append(SampledValue); - } - - setter(carrier, XB3Combined, sb.ToString()); - } - else - { - setter(carrier, XB3TraceId, context.ActivityContext.TraceId.ToHexString()); - setter(carrier, XB3SpanId, context.ActivityContext.SpanId.ToHexString()); - if ((context.ActivityContext.TraceFlags & ActivityTraceFlags.Recorded) != 0) - { - setter(carrier, XB3Sampled, SampledValue); - } - } + return new PropagationContext( + new ActivityContext(traceId, spanId, traceOptions, isRemote: true), + context.Baggage); + } + catch (Exception e) + { + OpenTelemetryPropagatorsEventSource.Log.ActivityContextExtractException(nameof(B3Propagator), e); + return context; } + } - private static PropagationContext ExtractFromMultipleHeaders(PropagationContext context, T carrier, Func> getter) + private static PropagationContext ExtractFromSingleHeader(PropagationContext context, T carrier, Func> getter) + { + try { - try + var header = getter(carrier, XB3Combined)?.FirstOrDefault(); + if (string.IsNullOrWhiteSpace(header)) { - ActivityTraceId traceId; - var traceIdStr = getter(carrier, XB3TraceId)?.FirstOrDefault(); - if (traceIdStr != null) - { - if (traceIdStr.Length == 16) - { - // This is an 8-byte traceID. - traceIdStr = UpperTraceId + traceIdStr; - } - - traceId = ActivityTraceId.CreateFromString(traceIdStr.AsSpan()); - } - else - { - return context; - } - - ActivitySpanId spanId; - var spanIdStr = getter(carrier, XB3SpanId)?.FirstOrDefault(); - if (spanIdStr != null) - { - spanId = ActivitySpanId.CreateFromString(spanIdStr.AsSpan()); - } - else - { - return context; - } - - var traceOptions = ActivityTraceFlags.None; - if (SampledValues.Contains(getter(carrier, XB3Sampled)?.FirstOrDefault()) - || FlagsValue.Equals(getter(carrier, XB3Flags)?.FirstOrDefault(), StringComparison.Ordinal)) - { - traceOptions |= ActivityTraceFlags.Recorded; - } - - return new PropagationContext( - new ActivityContext(traceId, spanId, traceOptions, isRemote: true), - context.Baggage); + return context; } - catch (Exception e) + + var parts = header.Split(XB3CombinedDelimiter); + if (parts.Length < 2 || parts.Length > 4) { - OpenTelemetryPropagatorsEventSource.Log.ActivityContextExtractException(nameof(B3Propagator), e); return context; } - } - private static PropagationContext ExtractFromSingleHeader(PropagationContext context, T carrier, Func> getter) - { - try + var traceIdStr = parts[0]; + if (string.IsNullOrWhiteSpace(traceIdStr)) { - var header = getter(carrier, XB3Combined)?.FirstOrDefault(); - if (string.IsNullOrWhiteSpace(header)) - { - return context; - } - - var parts = header.Split(XB3CombinedDelimiter); - if (parts.Length < 2 || parts.Length > 4) - { - return context; - } - - var traceIdStr = parts[0]; - if (string.IsNullOrWhiteSpace(traceIdStr)) - { - return context; - } + return context; + } - if (traceIdStr.Length == 16) - { - // This is an 8-byte traceID. - traceIdStr = UpperTraceId + traceIdStr; - } + if (traceIdStr.Length == 16) + { + // This is an 8-byte traceID. + traceIdStr = UpperTraceId + traceIdStr; + } - var traceId = ActivityTraceId.CreateFromString(traceIdStr.AsSpan()); + var traceId = ActivityTraceId.CreateFromString(traceIdStr.AsSpan()); - var spanIdStr = parts[1]; - if (string.IsNullOrWhiteSpace(spanIdStr)) - { - return context; - } + var spanIdStr = parts[1]; + if (string.IsNullOrWhiteSpace(spanIdStr)) + { + return context; + } - var spanId = ActivitySpanId.CreateFromString(spanIdStr.AsSpan()); + var spanId = ActivitySpanId.CreateFromString(spanIdStr.AsSpan()); - var traceOptions = ActivityTraceFlags.None; - if (parts.Length > 2) + var traceOptions = ActivityTraceFlags.None; + if (parts.Length > 2) + { + var traceFlagsStr = parts[2]; + if (SampledValues.Contains(traceFlagsStr) + || FlagsValue.Equals(traceFlagsStr, StringComparison.Ordinal)) { - var traceFlagsStr = parts[2]; - if (SampledValues.Contains(traceFlagsStr) - || FlagsValue.Equals(traceFlagsStr, StringComparison.Ordinal)) - { - traceOptions |= ActivityTraceFlags.Recorded; - } + traceOptions |= ActivityTraceFlags.Recorded; } - - return new PropagationContext( - new ActivityContext(traceId, spanId, traceOptions, isRemote: true), - context.Baggage); - } - catch (Exception e) - { - OpenTelemetryPropagatorsEventSource.Log.ActivityContextExtractException(nameof(B3Propagator), e); - return context; } + + return new PropagationContext( + new ActivityContext(traceId, spanId, traceOptions, isRemote: true), + context.Baggage); + } + catch (Exception e) + { + OpenTelemetryPropagatorsEventSource.Log.ActivityContextExtractException(nameof(B3Propagator), e); + return context; } } } diff --git a/src/OpenTelemetry.Extensions.Propagators/CHANGELOG.md b/src/OpenTelemetry.Extensions.Propagators/CHANGELOG.md index ae47a662498..bf85dec5460 100644 --- a/src/OpenTelemetry.Extensions.Propagators/CHANGELOG.md +++ b/src/OpenTelemetry.Extensions.Propagators/CHANGELOG.md @@ -2,6 +2,30 @@ ## Unreleased +## 1.7.0 + +Released 2023-Dec-08 + +## 1.7.0-rc.1 + +Released 2023-Nov-29 + +## 1.7.0-alpha.1 + +Released 2023-Oct-16 + +## 1.6.0 + +Released 2023-Sep-05 + +## 1.6.0-rc.1 + +Released 2023-Aug-21 + +## 1.6.0-alpha.1 + +Released 2023-Jul-12 + ## 1.5.1 Released 2023-Jun-26 diff --git a/src/OpenTelemetry.Extensions.Propagators/JaegerPropagator.cs b/src/OpenTelemetry.Extensions.Propagators/JaegerPropagator.cs index c51bc62d2e4..472b719ded0 100644 --- a/src/OpenTelemetry.Extensions.Propagators/JaegerPropagator.cs +++ b/src/OpenTelemetry.Extensions.Propagators/JaegerPropagator.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Context.Propagation; diff --git a/src/OpenTelemetry.Extensions.Propagators/OpenTelemetry.Extensions.Propagators.csproj b/src/OpenTelemetry.Extensions.Propagators/OpenTelemetry.Extensions.Propagators.csproj index c61603b6b75..c099b609a25 100644 --- a/src/OpenTelemetry.Extensions.Propagators/OpenTelemetry.Extensions.Propagators.csproj +++ b/src/OpenTelemetry.Extensions.Propagators/OpenTelemetry.Extensions.Propagators.csproj @@ -1,7 +1,6 @@ - - net462;netstandard2.0 + $(TargetFrameworksForLibraries) OpenTelemetry Extensions Propagators $(PackageTags);distributed-tracing;AspNet;AspNetCore;B3 core- diff --git a/src/OpenTelemetry.Extensions.Propagators/OpenTelemetryPropagatorsEventSource.cs b/src/OpenTelemetry.Extensions.Propagators/OpenTelemetryPropagatorsEventSource.cs index da15e0ee9f0..420679490ef 100644 --- a/src/OpenTelemetry.Extensions.Propagators/OpenTelemetryPropagatorsEventSource.cs +++ b/src/OpenTelemetry.Extensions.Propagators/OpenTelemetryPropagatorsEventSource.cs @@ -1,51 +1,37 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Tracing; -namespace OpenTelemetry.Internal +namespace OpenTelemetry.Internal; + +/// +/// EventSource implementation for OpenTelemetry Propagators. +/// This is used for internal logging of this library. +/// +[EventSource(Name = "OpenTelemetry.Extensions.Propagators")] +internal sealed class OpenTelemetryPropagatorsEventSource : EventSource { - /// - /// EventSource implementation for OpenTelemetry Propagators. - /// This is used for internal logging of this library. - /// - [EventSource(Name = "OpenTelemetry.Extensions.Propagators")] - internal sealed class OpenTelemetryPropagatorsEventSource : EventSource - { - public static OpenTelemetryPropagatorsEventSource Log = new(); + public static OpenTelemetryPropagatorsEventSource Log = new(); - [NonEvent] - public void ActivityContextExtractException(string format, Exception ex) + [NonEvent] + public void ActivityContextExtractException(string format, Exception ex) + { + if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) - { - this.FailedToExtractActivityContext(format, ex.ToInvariantString()); - } + this.FailedToExtractActivityContext(format, ex.ToInvariantString()); } + } - [Event(1, Message = "Failed to extract activity context in format: '{0}', context: '{1}'.", Level = EventLevel.Warning)] - public void FailedToExtractActivityContext(string format, string exception) - { - this.WriteEvent(1, format, exception); - } + [Event(1, Message = "Failed to extract activity context in format: '{0}', context: '{1}'.", Level = EventLevel.Warning)] + public void FailedToExtractActivityContext(string format, string exception) + { + this.WriteEvent(1, format, exception); + } - [Event(2, Message = "Failed to inject activity context in format: '{0}', context: '{1}'.", Level = EventLevel.Warning)] - public void FailedToInjectActivityContext(string format, string error) - { - this.WriteEvent(2, format, error); - } + [Event(2, Message = "Failed to inject activity context in format: '{0}', context: '{1}'.", Level = EventLevel.Warning)] + public void FailedToInjectActivityContext(string format, string error) + { + this.WriteEvent(2, format, error); } } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/PublicAPI.Shipped.txt b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/PublicAPI.Shipped.txt new file mode 100644 index 00000000000..fc47928891a --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/PublicAPI.Shipped.txt @@ -0,0 +1,18 @@ +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions.AspNetCoreTraceInstrumentationOptions() -> void +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions.EnrichWithException.get -> System.Action +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions.EnrichWithException.set -> void +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions.EnrichWithHttpRequest.get -> System.Action +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions.EnrichWithHttpRequest.set -> void +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions.EnrichWithHttpResponse.get -> System.Action +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions.EnrichWithHttpResponse.set -> void +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions.Filter.get -> System.Func +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions.Filter.set -> void +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions.RecordException.get -> bool +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions.RecordException.set -> void +OpenTelemetry.Metrics.AspNetCoreInstrumentationMeterProviderBuilderExtensions +OpenTelemetry.Trace.AspNetCoreInstrumentationTracerProviderBuilderExtensions +static OpenTelemetry.Metrics.AspNetCoreInstrumentationMeterProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder +static OpenTelemetry.Trace.AspNetCoreInstrumentationTracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder +static OpenTelemetry.Trace.AspNetCoreInstrumentationTracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configureAspNetCoreTraceInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder +static OpenTelemetry.Trace.AspNetCoreInstrumentationTracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureAspNetCoreTraceInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Extensions.Hosting/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/PublicAPI.Unshipped.txt similarity index 100% rename from src/OpenTelemetry.Extensions.Hosting/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt rename to src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/PublicAPI.Unshipped.txt diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/net6.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/net6.0/PublicAPI.Unshipped.txt deleted file mode 100644 index b65b99a84c4..00000000000 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/net6.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,29 +0,0 @@ -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.AspNetCoreInstrumentationOptions() -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnableGrpcAspNetCoreSupport.get -> bool -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnableGrpcAspNetCoreSupport.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnrichWithException.get -> System.Action -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnrichWithException.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnrichWithHttpRequest.get -> System.Action -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnrichWithHttpRequest.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnrichWithHttpResponse.get -> System.Action -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnrichWithHttpResponse.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.Filter.get -> System.Func -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.Filter.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.RecordException.get -> bool -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.RecordException.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.AspNetCoreMetricEnrichmentFunc -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.AspNetCoreMetricsInstrumentationOptions() -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.Enrich.get -> OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.AspNetCoreMetricEnrichmentFunc -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.Enrich.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.Filter.get -> System.Func -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.Filter.set -> void -OpenTelemetry.Metrics.MeterProviderBuilderExtensions -OpenTelemetry.Trace.TracerProviderBuilderExtensions -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder, string name, System.Action configureAspNetCoreInstrumentationOptions) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action configureAspNetCoreInstrumentationOptions) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configureAspNetCoreInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureAspNetCoreInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/net7.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/net7.0/PublicAPI.Unshipped.txt deleted file mode 100644 index b65b99a84c4..00000000000 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/net7.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,29 +0,0 @@ -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.AspNetCoreInstrumentationOptions() -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnableGrpcAspNetCoreSupport.get -> bool -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnableGrpcAspNetCoreSupport.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnrichWithException.get -> System.Action -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnrichWithException.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnrichWithHttpRequest.get -> System.Action -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnrichWithHttpRequest.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnrichWithHttpResponse.get -> System.Action -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnrichWithHttpResponse.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.Filter.get -> System.Func -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.Filter.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.RecordException.get -> bool -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.RecordException.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.AspNetCoreMetricEnrichmentFunc -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.AspNetCoreMetricsInstrumentationOptions() -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.Enrich.get -> OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.AspNetCoreMetricEnrichmentFunc -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.Enrich.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.Filter.get -> System.Func -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.Filter.set -> void -OpenTelemetry.Metrics.MeterProviderBuilderExtensions -OpenTelemetry.Trace.TracerProviderBuilderExtensions -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder, string name, System.Action configureAspNetCoreInstrumentationOptions) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action configureAspNetCoreInstrumentationOptions) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configureAspNetCoreInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureAspNetCoreInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt index 68b0a198212..e69de29bb2d 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,27 +0,0 @@ -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.AspNetCoreInstrumentationOptions() -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnrichWithException.get -> System.Action -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnrichWithException.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnrichWithHttpRequest.get -> System.Action -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnrichWithHttpRequest.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnrichWithHttpResponse.get -> System.Action -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnrichWithHttpResponse.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.Filter.get -> System.Func -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.Filter.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.RecordException.get -> bool -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.RecordException.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.AspNetCoreMetricEnrichmentFunc -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.AspNetCoreMetricsInstrumentationOptions() -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.Enrich.get -> OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.AspNetCoreMetricEnrichmentFunc -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.Enrich.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.Filter.get -> System.Func -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.Filter.set -> void -OpenTelemetry.Metrics.MeterProviderBuilderExtensions -OpenTelemetry.Trace.TracerProviderBuilderExtensions -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder, string name, System.Action configureAspNetCoreInstrumentationOptions) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action configureAspNetCoreInstrumentationOptions) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configureAspNetCoreInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureAspNetCoreInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netstandard2.1/PublicAPI.Shipped.txt b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netstandard2.1/PublicAPI.Shipped.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt deleted file mode 100644 index b65b99a84c4..00000000000 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,29 +0,0 @@ -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.AspNetCoreInstrumentationOptions() -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnableGrpcAspNetCoreSupport.get -> bool -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnableGrpcAspNetCoreSupport.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnrichWithException.get -> System.Action -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnrichWithException.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnrichWithHttpRequest.get -> System.Action -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnrichWithHttpRequest.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnrichWithHttpResponse.get -> System.Action -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnrichWithHttpResponse.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.Filter.get -> System.Func -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.Filter.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.RecordException.get -> bool -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.RecordException.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.AspNetCoreMetricEnrichmentFunc -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.AspNetCoreMetricsInstrumentationOptions() -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.Enrich.get -> OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.AspNetCoreMetricEnrichmentFunc -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.Enrich.set -> void -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.Filter.get -> System.Func -OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreMetricsInstrumentationOptions.Filter.set -> void -OpenTelemetry.Metrics.MeterProviderBuilderExtensions -OpenTelemetry.Trace.TracerProviderBuilderExtensions -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder, string name, System.Action configureAspNetCoreInstrumentationOptions) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action configureAspNetCoreInstrumentationOptions) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configureAspNetCoreInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureAspNetCoreInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentation.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentation.cs index 913f1408fa3..d3096792624 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentation.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentation.cs @@ -1,52 +1,38 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + using OpenTelemetry.Instrumentation.AspNetCore.Implementation; -namespace OpenTelemetry.Instrumentation.AspNetCore +namespace OpenTelemetry.Instrumentation.AspNetCore; + +/// +/// Asp.Net Core Requests instrumentation. +/// +internal sealed class AspNetCoreInstrumentation : IDisposable { - /// - /// Asp.Net Core Requests instrumentation. - /// - internal sealed class AspNetCoreInstrumentation : IDisposable + private static readonly HashSet DiagnosticSourceEvents = new() { - private static readonly HashSet DiagnosticSourceEvents = new() - { - "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", - }; + "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start", + "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop", + "Microsoft.AspNetCore.Diagnostics.UnhandledException", + "Microsoft.AspNetCore.Hosting.UnhandledException", + }; - private readonly Func isEnabled = (eventName, _, _) - => DiagnosticSourceEvents.Contains(eventName); + private readonly Func isEnabled = (eventName, _, _) + => DiagnosticSourceEvents.Contains(eventName); - private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; + private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; - public AspNetCoreInstrumentation(HttpInListener httpInListener) - { - this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(httpInListener, this.isEnabled); - this.diagnosticSourceSubscriber.Subscribe(); - } + public AspNetCoreInstrumentation(HttpInListener httpInListener) + { + this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(httpInListener, this.isEnabled, AspNetCoreInstrumentationEventSource.Log.UnknownErrorProcessingEvent); + this.diagnosticSourceSubscriber.Subscribe(); + } - /// - public void Dispose() - { - this.diagnosticSourceSubscriber?.Dispose(); - } + /// + public void Dispose() + { + this.diagnosticSourceSubscriber?.Dispose(); } } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationMeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationMeterProviderBuilderExtensions.cs new file mode 100644 index 00000000000..3da8c1de59d --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationMeterProviderBuilderExtensions.cs @@ -0,0 +1,52 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if !NET8_0_OR_GREATER +using OpenTelemetry.Instrumentation.AspNetCore; +using OpenTelemetry.Instrumentation.AspNetCore.Implementation; +#endif +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Metrics; + +/// +/// Extension methods to simplify registering of ASP.NET Core request instrumentation. +/// +public static class AspNetCoreInstrumentationMeterProviderBuilderExtensions +{ + /// + /// Enables the incoming requests automatic data collection for ASP.NET Core. + /// + /// being configured. + /// The instance of to chain the calls. + public static MeterProviderBuilder AddAspNetCoreInstrumentation( + this MeterProviderBuilder builder) + { + Guard.ThrowIfNull(builder); + +#if NET8_0_OR_GREATER + return builder.ConfigureMeters(); +#else + // Note: Warm-up the status code and method mapping. + _ = TelemetryHelper.BoxedStatusCodes; + _ = RequestMethodHelper.KnownMethods; + + builder.AddMeter(HttpInMetricsListener.InstrumentationName); + + builder.AddInstrumentation(new AspNetCoreMetrics()); + + return builder; +#endif + } + + internal static MeterProviderBuilder ConfigureMeters(this MeterProviderBuilder builder) + { + return builder + .AddMeter("Microsoft.AspNetCore.Hosting") + .AddMeter("Microsoft.AspNetCore.Server.Kestrel") + .AddMeter("Microsoft.AspNetCore.Http.Connections") + .AddMeter("Microsoft.AspNetCore.Routing") + .AddMeter("Microsoft.AspNetCore.Diagnostics") + .AddMeter("Microsoft.AspNetCore.RateLimiting"); + } +} diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationOptions.cs deleted file mode 100644 index 283126e488a..00000000000 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationOptions.cs +++ /dev/null @@ -1,90 +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.Http; - -namespace OpenTelemetry.Instrumentation.AspNetCore -{ - /// - /// Options for requests instrumentation. - /// - public class AspNetCoreInstrumentationOptions - { - /// - /// Gets or sets a filter function that determines whether or not to - /// collect telemetry on a per request basis. - /// - /// - /// Notes: - /// - /// The return value for the filter function is interpreted as: - /// - /// If filter returns , the request is - /// collected. - /// If filter returns or throws an - /// exception the request is NOT collected. - /// - /// - /// - public Func Filter { get; set; } - - /// - /// Gets or sets an action to enrich an Activity. - /// - /// - /// : the activity being enriched. - /// : the HttpRequest object from which additional information can be extracted to enrich the activity. - /// - public Action EnrichWithHttpRequest { get; set; } - - /// - /// Gets or sets an action to enrich an Activity. - /// - /// - /// : the activity being enriched. - /// : the HttpResponse object from which additional information can be extracted to enrich the activity. - /// - public Action EnrichWithHttpResponse { get; set; } - - /// - /// Gets or sets an action to enrich an Activity. - /// - /// - /// : the activity being enriched. - /// : the Exception object from which additional information can be extracted to enrich the activity. - /// - public Action EnrichWithException { get; set; } - - /// - /// Gets or sets a value indicating whether the exception will be recorded as ActivityEvent or not. - /// - /// - /// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/exceptions.md. - /// - public bool RecordException { get; set; } - -#if NETSTANDARD2_1 || NET6_0_OR_GREATER - /// - /// Gets or sets a value indicating whether RPC attributes are added to an Activity when using Grpc.AspNetCore. Default is true. - /// - /// - /// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/rpc.md. - /// - public bool EnableGrpcAspNetCoreSupport { get; set; } = true; -#endif - } -} diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs new file mode 100644 index 00000000000..8ec35718ccd --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs @@ -0,0 +1,123 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OpenTelemetry.Instrumentation.AspNetCore; +using OpenTelemetry.Instrumentation.AspNetCore.Implementation; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Trace; + +/// +/// Extension methods to simplify registering of ASP.NET Core request instrumentation. +/// +public static class AspNetCoreInstrumentationTracerProviderBuilderExtensions +{ + /// + /// Enables the incoming requests automatic data collection for ASP.NET Core. + /// + /// being configured. + /// The instance of to chain the calls. + public static TracerProviderBuilder AddAspNetCoreInstrumentation(this TracerProviderBuilder builder) + => AddAspNetCoreInstrumentation(builder, name: null, configureAspNetCoreTraceInstrumentationOptions: null); + + /// + /// Enables the incoming requests automatic data collection for ASP.NET Core. + /// + /// being configured. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static TracerProviderBuilder AddAspNetCoreInstrumentation( + this TracerProviderBuilder builder, + Action configureAspNetCoreTraceInstrumentationOptions) + => AddAspNetCoreInstrumentation(builder, name: null, configureAspNetCoreTraceInstrumentationOptions); + + /// + /// Enables the incoming requests automatic data collection for ASP.NET Core. + /// + /// being configured. + /// Name which is used when retrieving options. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static TracerProviderBuilder AddAspNetCoreInstrumentation( + this TracerProviderBuilder builder, + string name, + Action configureAspNetCoreTraceInstrumentationOptions) + { + Guard.ThrowIfNull(builder); + + // Note: Warm-up the status code and method mapping. + _ = TelemetryHelper.BoxedStatusCodes; + _ = RequestMethodHelper.KnownMethods; + + name ??= Options.DefaultName; + + builder.ConfigureServices(services => + { + if (configureAspNetCoreTraceInstrumentationOptions != null) + { + services.Configure(name, configureAspNetCoreTraceInstrumentationOptions); + } + + services.RegisterOptionsFactory(configuration => new AspNetCoreTraceInstrumentationOptions(configuration)); + }); + + if (builder is IDeferredTracerProviderBuilder deferredTracerProviderBuilder) + { + deferredTracerProviderBuilder.Configure((sp, builder) => + { + AddAspNetCoreInstrumentationSources(builder, sp); + }); + } + + return builder.AddInstrumentation(sp => + { + var options = sp.GetRequiredService>().Get(name); + + return new AspNetCoreInstrumentation( + new HttpInListener(options)); + }); + } + + // Note: This is used by unit tests. + internal static TracerProviderBuilder AddAspNetCoreInstrumentation( + this TracerProviderBuilder builder, + HttpInListener listener) + { + builder.AddAspNetCoreInstrumentationSources(); + + return builder.AddInstrumentation( + new AspNetCoreInstrumentation(listener)); + } + + private static void AddAspNetCoreInstrumentationSources( + this TracerProviderBuilder builder, + IServiceProvider serviceProvider = null) + { + // For .NET7.0 onwards activity will be created using activitySource. + // https://github.com/dotnet/aspnetcore/blob/bf3352f2422bf16fa3ca49021f0e31961ce525eb/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs#L327 + // For .NET6.0 and below, we will continue to use legacy way. + if (HttpInListener.Net7OrGreater) + { + // TODO: Check with .NET team to see if this can be prevented + // as this allows user to override the ActivitySource. + var activitySourceService = serviceProvider?.GetService(); + if (activitySourceService != null) + { + builder.AddSource(activitySourceService.Name); + } + else + { + // For users not using hosting package? + builder.AddSource(HttpInListener.AspNetCoreActivitySourceName); + } + } + else + { + builder.AddSource(HttpInListener.ActivitySourceName); + builder.AddLegacySource(HttpInListener.ActivityOperationName); // for the activities created by AspNetCore + } + } +} diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs index d1a91d589ac..a819d561a9e 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs @@ -1,62 +1,41 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -using System.Diagnostics.Metrics; -using System.Reflection; +#if !NET8_0_OR_GREATER using OpenTelemetry.Instrumentation.AspNetCore.Implementation; -using OpenTelemetry.Internal; -namespace OpenTelemetry.Instrumentation.AspNetCore +namespace OpenTelemetry.Instrumentation.AspNetCore; + +/// +/// Asp.Net Core Requests instrumentation. +/// +internal sealed class AspNetCoreMetrics : IDisposable { - /// - /// Asp.Net Core Requests instrumentation. - /// - internal sealed class AspNetCoreMetrics : IDisposable + private static readonly HashSet DiagnosticSourceEvents = new() { - internal static readonly AssemblyName AssemblyName = typeof(HttpInListener).Assembly.GetName(); - internal static readonly string InstrumentationName = AssemblyName.Name; - internal static readonly string InstrumentationVersion = AssemblyName.Version.ToString(); - - private static readonly HashSet DiagnosticSourceEvents = new() - { - "Microsoft.AspNetCore.Hosting.HttpRequestIn", - "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start", - "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop", - }; + "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start", + "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop", + "Microsoft.AspNetCore.Diagnostics.UnhandledException", + "Microsoft.AspNetCore.Hosting.UnhandledException", + }; - private readonly Func isEnabled = (eventName, _, _) - => DiagnosticSourceEvents.Contains(eventName); + private readonly Func isEnabled = (eventName, _, _) + => DiagnosticSourceEvents.Contains(eventName); - private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; - private readonly Meter meter; + private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; - internal AspNetCoreMetrics(AspNetCoreMetricsInstrumentationOptions options) - { - Guard.ThrowIfNull(options); - this.meter = new Meter(InstrumentationName, InstrumentationVersion); - var metricsListener = new HttpInMetricsListener("Microsoft.AspNetCore", this.meter, options); - this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(metricsListener, this.isEnabled); - this.diagnosticSourceSubscriber.Subscribe(); - } + internal AspNetCoreMetrics() + { + var metricsListener = new HttpInMetricsListener("Microsoft.AspNetCore"); + this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(metricsListener, this.isEnabled, AspNetCoreInstrumentationEventSource.Log.UnknownErrorProcessingEvent); + this.diagnosticSourceSubscriber.Subscribe(); + } - /// - public void Dispose() - { - this.diagnosticSourceSubscriber?.Dispose(); - this.meter?.Dispose(); - } + /// + public void Dispose() + { + this.diagnosticSourceSubscriber?.Dispose(); } } +#endif diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetricsInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetricsInstrumentationOptions.cs deleted file mode 100644 index 23abdeec670..00000000000 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetricsInstrumentationOptions.cs +++ /dev/null @@ -1,60 +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.Http; - -namespace OpenTelemetry.Instrumentation.AspNetCore -{ - /// - /// Options for metrics requests instrumentation. - /// - public class AspNetCoreMetricsInstrumentationOptions - { - /// - /// Delegate for enrichment of recorded metric with additional tags. - /// - /// The name of the metric being enriched. - /// : the HttpContext object. Both Request and Response are available. - /// : List of current tags. You can add additional tags to this list. - public delegate void AspNetCoreMetricEnrichmentFunc(string name, HttpContext context, ref TagList tags); - - /// - /// Gets or sets a filter function that determines whether or not to - /// collect telemetry on a per request basis. - /// - /// - /// Notes: - /// - /// The first parameter is the name of the metric being - /// filtered. - /// The return value for the filter function is interpreted as: - /// - /// If filter returns , the request is - /// collected. - /// If filter returns or throws an - /// exception the request is NOT collected. - /// - /// - /// - public Func Filter { get; set; } - - /// - /// Gets or sets an function to enrich a recorded metric with additional custom tags. - /// - public AspNetCoreMetricEnrichmentFunc Enrich { get; set; } - } -} diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreTraceInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreTraceInstrumentationOptions.cs new file mode 100644 index 00000000000..f66a5e04344 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreTraceInstrumentationOptions.cs @@ -0,0 +1,94 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Instrumentation.AspNetCore; + +/// +/// Options for requests instrumentation. +/// +public class AspNetCoreTraceInstrumentationOptions +{ + /// + /// Initializes a new instance of the class. + /// + public AspNetCoreTraceInstrumentationOptions() + : this(new ConfigurationBuilder().AddEnvironmentVariables().Build()) + { + } + + internal AspNetCoreTraceInstrumentationOptions(IConfiguration configuration) + { + Debug.Assert(configuration != null, "configuration was null"); + + if (configuration.TryGetBoolValue("OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_ENABLE_GRPC_INSTRUMENTATION", out var enableGrpcInstrumentation)) + { + this.EnableGrpcAspNetCoreSupport = enableGrpcInstrumentation; + } + } + + /// + /// Gets or sets a filter function that determines whether or not to + /// collect telemetry on a per request basis. + /// + /// + /// Notes: + /// + /// The return value for the filter function is interpreted as: + /// + /// If filter returns , the request is + /// collected. + /// If filter returns or throws an + /// exception the request is NOT collected. + /// + /// + /// + public Func Filter { get; set; } + + /// + /// Gets or sets an action to enrich an Activity. + /// + /// + /// : the activity being enriched. + /// : the HttpRequest object from which additional information can be extracted to enrich the activity. + /// + public Action EnrichWithHttpRequest { get; set; } + + /// + /// Gets or sets an action to enrich an Activity. + /// + /// + /// : the activity being enriched. + /// : the HttpResponse object from which additional information can be extracted to enrich the activity. + /// + public Action EnrichWithHttpResponse { get; set; } + + /// + /// Gets or sets an action to enrich an Activity. + /// + /// + /// : the activity being enriched. + /// : the Exception object from which additional information can be extracted to enrich the activity. + /// + public Action EnrichWithException { get; set; } + + /// + /// Gets or sets a value indicating whether the exception will be recorded as ActivityEvent or not. + /// + /// + /// https://github.com/open-telemetry/semantic-conventions/blob/main/docs/exceptions/exceptions-spans.md. + /// + public bool RecordException { get; set; } + + /// + /// Gets or sets a value indicating whether RPC attributes are added to an Activity when using Grpc.AspNetCore. + /// + /// + /// https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-spans.md. + /// + internal bool EnableGrpcAspNetCoreSupport { get; set; } +} diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AssemblyInfo.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AssemblyInfo.cs index 2b1cca81128..2cd1a339fd4 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AssemblyInfo.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AssemblyInfo.cs @@ -1,18 +1,6 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + using System.Runtime.CompilerServices; #if SIGNED diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md index 3ebe52252f3..d3a6f7bd3f4 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md @@ -2,10 +2,283 @@ ## Unreleased -* Updated [Http Semantic Conventions](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.21.0/specification/trace/semantic_conventions/http.md). - This library can emit either old, new, or both attributes. Users can control - which attributes are emitted by setting the environment variable - `OTEL_SEMCONV_STABILITY_OPT_IN`. +## 1.7.1 + +Released 2024-Feb-09 + +* Fixed issue + [#4466](https://github.com/open-telemetry/opentelemetry-dotnet/issues/4466) + where the activity instance returned by `Activity.Current` was different than + instance obtained from `IHttpActivityFeature.Activity`. + ([#5136](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5136)) + +* Fixed an issue where the `http.route` attribute was not set on either the + `Activity` or `http.server.request.duration` metric generated from a + request when an exception handling middleware is invoked. One caveat is that + this fix does not address the problem for the `http.server.request.duration` + metric when running ASP.NET Core 8. ASP.NET Core 8 contains an equivalent fix + which should ship in version 8.0.2 + (see: [dotnet/aspnetcore#52652](https://github.com/dotnet/aspnetcore/pull/52652)). + ([#5135](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5135)) + +* Fixes scenario when the `net6.0` target of this library is loaded into a + .NET 7+ process and the instrumentation does not behave as expected. This + is an unusual scenario that does not affect users consuming this package + normally. This fix is primarily to support the + [opentelemetry-dotnet-instrumentation](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5252) + project. + ([#5252](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5252)) + +## 1.7.0 + +Released 2023-Dec-13 + +## 1.6.0 - First stable release of this library + +Released 2023-Dec-13 + +* Re-introduced support for gRPC instrumentation as an opt-in experimental + feature. From now onwards, gRPC can be enabled by setting + `OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_ENABLE_GRPC_INSTRUMENTATION` flag to + `True`. `OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_ENABLE_GRPC_INSTRUMENTATION` can + be set as an environment variable or via IConfiguration. The change is + introduced in order to support stable release of `http` instrumentation. + Semantic conventions for RPC is still + [experimental](https://github.com/open-telemetry/semantic-conventions/tree/main/docs/rpc) + and hence the package will only support it as an opt-in experimental feature. + Note that the support was removed in `1.6.0-rc.1` version of the package and + versions released before `1.6.0-rc.1` had gRPC instrumentation enabled by + default. + ([#5130](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5130)) + +## 1.6.0-rc.1 + +Released 2023-Dec-01 + +* 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)) + +* Removed `netstandard2.1` target. + ([#5094](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5094)) + +* Removed support for grpc instrumentation to unblock stable release of http + instrumentation. For details, see issue + [#5098](https://github.com/open-telemetry/opentelemetry-dotnet/issues/5098) + ([#5097](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5097)) + +* **Breaking Change** : Renamed `AspNetCoreInstrumentationOptions` to + `AspNetCoreTraceInstrumentationOptions`. + ([#5108](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5108)) + +## 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 + to restore this behavior. + ([#5025](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5025)) + +* Updated `http.request.method` to match specification guidelines. + * For activity, if the method does not belong to one of the [known + values](https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-spans.md#:~:text=http.request.method%20has%20the%20following%20list%20of%20well%2Dknown%20values) + then the request method will be set on an additional tag + `http.request.method.original` and `http.request.method` will be set to + `_OTHER`. + * For metrics, if the original method does not belong to one of the [known + values](https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-spans.md#:~:text=http.request.method%20has%20the%20following%20list%20of%20well%2Dknown%20values) + then `http.request.method` on `http.server.request.duration` metric will be + set to `_OTHER` + + `http.request.method` is set on `http.server.request.duration` metric or + activity when `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable is set to + `http` or `http/dup`. + ([#5001](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5001)) + +* An additional attribute `error.type` will be added to activity and +`http.server.request.duration` metric when the request results in unhandled +exception. The attribute value will be set to full name of exception type. + + The attribute will only be added when `OTEL_SEMCONV_STABILITY_OPT_IN` + environment variable is set to `http` or `http/dup`. + ([#4986](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4986)) + +* 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 + +* Introduced a new metric, `http.server.request.duration` measured in seconds. + The OTel SDK (starting with version 1.6.0) + [applies custom histogram buckets](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4820) + for this metric to comply with the + [Semantic Convention for Http Metrics](https://github.com/open-telemetry/semantic-conventions/blob/2bad9afad58fbd6b33cc683d1ad1f006e35e4a5d/docs/http/http-metrics.md). + This new metric is only available for users who opt-in to the new + semantic convention by configuring the `OTEL_SEMCONV_STABILITY_OPT_IN` + environment variable to either `http` (to emit only the new metric) or + `http/dup` (to emit both the new and old metrics). + ([#4802](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4802)) + * New metric: `http.server.request.duration` + * Unit: `s` (seconds) + * Histogram Buckets: `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` + * Old metric: `http.server.duration` + * Unit: `ms` (milliseconds) + * Histogram Buckets: `0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, + 5000, 7500, 10000` + + Note: the older `http.server.duration` metric and + `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable will eventually be + removed after the HTTP semantic conventions are marked stable. + At which time this instrumentation can publish a stable release. Refer to + the specification for more information regarding the new HTTP semantic + conventions for both + [spans](https://github.com/open-telemetry/semantic-conventions/blob/2bad9afad58fbd6b33cc683d1ad1f006e35e4a5d/docs/http/http-spans.md) + and + [metrics](https://github.com/open-telemetry/semantic-conventions/blob/2bad9afad58fbd6b33cc683d1ad1f006e35e4a5d/docs/http/http-metrics.md). + +* Following metrics will now be enabled by default when targeting `.NET8.0` or + newer framework: + + * **Meter** : `Microsoft.AspNetCore.Hosting` + * `http.server.request.duration` + * `http.server.active_requests` + + * **Meter** : `Microsoft.AspNetCore.Server.Kestrel` + * `kestrel.active_connections` + * `kestrel.connection.duration` + * `kestrel.rejected_connections` + * `kestrel.queued_connections` + * `kestrel.queued_requests` + * `kestrel.upgraded_connections` + * `kestrel.tls_handshake.duration` + * `kestrel.active_tls_handshakes` + + * **Meter** : `Microsoft.AspNetCore.Http.Connections` + * `signalr.server.connection.duration` + * `signalr.server.active_connections` + + * **Meter** : `Microsoft.AspNetCore.Routing` + * `aspnetcore.routing.match_attempts` + + * **Meter** : `Microsoft.AspNetCore.Diagnostics` + * `aspnetcore.diagnostics.exceptions` + + * **Meter** : `Microsoft.AspNetCore.RateLimiting` + * `aspnetcore.rate_limiting.active_request_leases` + * `aspnetcore.rate_limiting.request_lease.duration` + * `aspnetcore.rate_limiting.queued_requests` + * `aspnetcore.rate_limiting.request.time_in_queue` + * `aspnetcore.rate_limiting.requests` + + For details about each individual metric check [ASP.NET Core + docs + page](https://learn.microsoft.com/dotnet/core/diagnostics/built-in-metrics-aspnetcore). + + **NOTES**: + * When targeting `.NET8.0` framework or newer, `http.server.request.duration` metric + will only follow + [v1.22.0](https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-metrics.md#metric-httpclientrequestduration) + semantic conventions specification. Ability to switch behavior to older + conventions using `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable is + not available. + * Users can opt-out of metrics that are not required using + [views](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/metrics/customizing-the-sdk#drop-an-instrument). + + ([#4934](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4934)) + +* Added `network.protocol.name` dimension to `http.server.request.duration` +metric. This change only affects users setting `OTEL_SEMCONV_STABILITY_OPT_IN` +to `http` or `http/dup`. +([#4934](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4934)) + +* **Breaking**: Removed `Enrich` and `Filter` support for **metrics** + instrumentation. With this change, `AspNetCoreMetricsInstrumentationOptions` + is no longer available. + ([#4981](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4981)) + + * `Enrich` migration: + + An enrichment API for the `http.server.request.duration` metric is available + inside AspNetCore for users targeting .NET 8.0 (or newer). For details see: + [Enrich the ASP.NET Core request + metric](https://learn.microsoft.com/aspnet/core/log-mon/metrics/metrics?view=aspnetcore-8.0#enrich-the-aspnet-core-request-metric). + + * `Filter` migration: + + There is no comparable filter mechanism currently available for any .NET + version. Please [share your + feedback](https://github.com/open-telemetry/opentelemetry-dotnet/issues/4982) + if you are impacted by this feature gap. + + > **Note** + > The [View API](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/metrics/customizing-the-sdk#select-specific-tags) + may be used to drop dimensions. + +* Updated description for `http.server.request.duration` metrics to match spec + definition. + ([#4990](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4990)) + +## 1.5.1-beta.1 + +Released 2023-Jul-20 + +* The new HTTP and network semantic conventions can be opted in to by setting + the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable. This allows for a + transition period for users to experiment with the new semantic conventions + and adapt as necessary. The environment variable supports the following + values: + * `http` - emit the new, frozen (proposed for stable) HTTP and networking + attributes, and stop emitting the old experimental HTTP and networking + attributes that the instrumentation emitted previously. + * `http/dup` - emit both the old and the frozen (proposed for stable) HTTP + and networking attributes, allowing for a more seamless transition. + * The default behavior (in the absence of one of these values) is to continue + emitting the same HTTP and network semantic conventions that were emitted in + `1.5.0-beta.1`. + * Note: this option will eventually be removed after the new HTTP and + network semantic conventions are marked stable. At which time this + instrumentation can receive a stable release, and the old HTTP and + network semantic conventions will no longer be supported. Refer to the + specification for more information regarding the new HTTP and network + semantic conventions for both + [spans](https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md) + and + [metrics](https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-metrics.md). + ([#4537](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4537), + [#4606](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4606), + [#4660](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4660)) + +* Fixed an issue affecting NET 7.0+. If custom propagation is being used + and tags are added to an Activity during sampling then that Activity would be dropped. + ([#4637](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4637)) ## 1.5.0-beta.1 diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/AspNetCoreInstrumentationEventSource.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/AspNetCoreInstrumentationEventSource.cs index 9b52e6bd6c7..1ad239f33aa 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/AspNetCoreInstrumentationEventSource.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/AspNetCoreInstrumentationEventSource.cs @@ -1,75 +1,82 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +#if NET6_0_OR_GREATER using System.Diagnostics.CodeAnalysis; +#endif using System.Diagnostics.Tracing; using OpenTelemetry.Internal; -namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation +namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation; + +/// +/// EventSource events emitted from the project. +/// +[EventSource(Name = "OpenTelemetry-Instrumentation-AspNetCore")] +internal sealed class AspNetCoreInstrumentationEventSource : EventSource { - /// - /// EventSource events emitted from the project. - /// - [EventSource(Name = "OpenTelemetry-Instrumentation-AspNetCore")] - internal sealed class AspNetCoreInstrumentationEventSource : EventSource - { - public static AspNetCoreInstrumentationEventSource Log = new(); + public static AspNetCoreInstrumentationEventSource Log = new(); - [NonEvent] - public void RequestFilterException(string handlerName, string eventName, string operationName, Exception ex) + [NonEvent] + public void RequestFilterException(string handlerName, string eventName, string operationName, Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.RequestFilterException(handlerName, eventName, operationName, ex.ToInvariantString()); - } + this.RequestFilterException(handlerName, eventName, operationName, ex.ToInvariantString()); } + } - [NonEvent] - public void EnrichmentException(string handlerName, string eventName, string operationName, Exception ex) + [NonEvent] + public void EnrichmentException(string handlerName, string eventName, string operationName, Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.EnrichmentException(handlerName, eventName, operationName, ex.ToInvariantString()); - } + this.EnrichmentException(handlerName, eventName, operationName, ex.ToInvariantString()); } + } - [Event(1, Message = "Payload is NULL, span will not be recorded. HandlerName: '{0}', EventName: '{1}', OperationName: '{2}'.", Level = EventLevel.Warning)] - public void NullPayload(string handlerName, string eventName, string operationName) + [NonEvent] + public void UnknownErrorProcessingEvent(string handlerName, string eventName, Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - this.WriteEvent(1, handlerName, eventName, operationName); + this.UnknownErrorProcessingEvent(handlerName, eventName, ex.ToInvariantString()); } + } - [Event(2, Message = "Request is filtered out. HandlerName: '{0}', EventName: '{1}', OperationName: '{2}'.", Level = EventLevel.Verbose)] - public void RequestIsFilteredOut(string handlerName, string eventName, string operationName) - { - this.WriteEvent(2, handlerName, eventName, operationName); - } + [Event(1, Message = "Payload is NULL, span will not be recorded. HandlerName: '{0}', EventName: '{1}', OperationName: '{2}'.", Level = EventLevel.Warning)] + public void NullPayload(string handlerName, string eventName, string operationName) + { + this.WriteEvent(1, handlerName, eventName, operationName); + } - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")] - [Event(3, Message = "Filter threw exception, request will not be collected. HandlerName: '{0}', EventName: '{1}', OperationName: '{2}', Exception: {3}.", Level = EventLevel.Error)] - public void RequestFilterException(string handlerName, string eventName, string operationName, string exception) - { - this.WriteEvent(3, handlerName, eventName, operationName, exception); - } + [Event(2, Message = "Request is filtered out. HandlerName: '{0}', EventName: '{1}', OperationName: '{2}'.", Level = EventLevel.Verbose)] + public void RequestIsFilteredOut(string handlerName, string eventName, string operationName) + { + this.WriteEvent(2, handlerName, eventName, operationName); + } - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")] - [Event(4, Message = "Enrich threw exception. HandlerName: '{0}', EventName: '{1}', OperationName: '{2}', Exception: {3}.", Level = EventLevel.Warning)] - public void EnrichmentException(string handlerName, string eventName, string operationName, string exception) - { - this.WriteEvent(4, handlerName, eventName, operationName, exception); - } +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")] +#endif + [Event(3, Message = "Filter threw exception, request will not be collected. HandlerName: '{0}', EventName: '{1}', OperationName: '{2}', Exception: {3}.", Level = EventLevel.Error)] + public void RequestFilterException(string handlerName, string eventName, string operationName, string exception) + { + this.WriteEvent(3, handlerName, eventName, operationName, exception); + } + +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")] +#endif + [Event(4, Message = "Enrich threw exception. HandlerName: '{0}', EventName: '{1}', OperationName: '{2}', Exception: {3}.", Level = EventLevel.Warning)] + public void EnrichmentException(string handlerName, string eventName, string operationName, string exception) + { + this.WriteEvent(4, handlerName, eventName, operationName, exception); + } + + [Event(5, Message = "Unknown error processing event '{1}' from handler '{0}', Exception: {2}", Level = EventLevel.Error)] + public void UnknownErrorProcessingEvent(string handlerName, string eventName, string ex) + { + this.WriteEvent(5, handlerName, eventName, ex); } } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs index 809473ad3aa..97807d41512 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs @@ -1,527 +1,400 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif using System.Reflection; -#if !NETSTANDARD2_0 using System.Runtime.CompilerServices; -#endif using Microsoft.AspNetCore.Http; -#if NET6_0_OR_GREATER -using Microsoft.AspNetCore.Mvc.Diagnostics; +#if !NETSTANDARD +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Routing; #endif using OpenTelemetry.Context.Propagation; -#if !NETSTANDARD2_0 using OpenTelemetry.Instrumentation.GrpcNetClient; -#endif using OpenTelemetry.Internal; using OpenTelemetry.Trace; -using static OpenTelemetry.Internal.HttpSemanticConventionHelper; -namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation +namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation; + +internal class HttpInListener : ListenerHandler { - 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"; - -#if NET7_0_OR_GREATER - // https://github.com/dotnet/aspnetcore/blob/8d6554e655b64da75b71e0e20d6db54a3ba8d2fb/src/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs#L85 - internal static readonly string AspNetCoreActivitySourceName = "Microsoft.AspNetCore"; -#endif + 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 OnUnhandledHostingExceptionEvent = "Microsoft.AspNetCore.Hosting.UnhandledException"; + internal const string OnUnHandledDiagnosticsExceptionEvent = "Microsoft.AspNetCore.Diagnostics.UnhandledException"; - internal static readonly AssemblyName AssemblyName = typeof(HttpInListener).Assembly.GetName(); - internal static readonly string ActivitySourceName = AssemblyName.Name; - internal static readonly Version Version = AssemblyName.Version; - internal static readonly ActivitySource ActivitySource = new(ActivitySourceName, Version.ToString()); + // https://github.com/dotnet/aspnetcore/blob/8d6554e655b64da75b71e0e20d6db54a3ba8d2fb/src/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs#L85 + internal static readonly string AspNetCoreActivitySourceName = "Microsoft.AspNetCore"; - private const string DiagnosticSourceName = "Microsoft.AspNetCore"; - private const string UnknownHostName = "UNKNOWN-HOST"; + internal static readonly AssemblyName AssemblyName = typeof(HttpInListener).Assembly.GetName(); + internal static readonly string ActivitySourceName = AssemblyName.Name; + internal static readonly Version Version = AssemblyName.Version; + internal static readonly ActivitySource ActivitySource = new(ActivitySourceName, Version.ToString()); + internal static readonly bool Net7OrGreater = Environment.Version.Major >= 7; - private static readonly Func> HttpRequestHeaderValuesGetter = (request, name) => request.Headers[name]; -#if !NET6_0_OR_GREATER - private readonly PropertyFetcher beforeActionActionDescriptorFetcher = new("actionDescriptor"); - private readonly PropertyFetcher beforeActionAttributeRouteInfoFetcher = new("AttributeRouteInfo"); - private readonly PropertyFetcher beforeActionTemplateFetcher = new("Template"); -#endif - private readonly PropertyFetcher stopExceptionFetcher = new("Exception"); - private readonly AspNetCoreInstrumentationOptions options; - private readonly HttpSemanticConvention httpSemanticConvention; + private const string DiagnosticSourceName = "Microsoft.AspNetCore"; - public HttpInListener(AspNetCoreInstrumentationOptions options) - : base(DiagnosticSourceName) + private static readonly Func> HttpRequestHeaderValuesGetter = (request, name) => + { + if (request.Headers.TryGetValue(name, out var value)) { - Guard.ThrowIfNull(options); + // This causes allocation as the `StringValues` struct has to be casted to an `IEnumerable` object. + return value; + } - this.options = options; + return Enumerable.Empty(); + }; - this.httpSemanticConvention = GetSemanticConventionOptIn(); - } + private static readonly PropertyFetcher ExceptionPropertyFetcher = new("Exception"); - public override void OnEventWritten(string name, object payload) - { - switch (name) - { - case OnStartEvent: - { - this.OnStartActivity(Activity.Current, payload); - } - - break; - case OnStopEvent: - { - this.OnStopActivity(Activity.Current, payload); - } - - break; - case OnMvcBeforeActionEvent: - { - this.OnMvcBeforeAction(Activity.Current, payload); - } - - break; - case OnUnhandledHostingExceptionEvent: - case OnUnHandledDiagnosticsExceptionEvent: - { - this.OnException(Activity.Current, payload); - } - - break; - } - } + private readonly AspNetCoreTraceInstrumentationOptions options; - public void OnStartActivity(Activity activity, object payload) + public HttpInListener(AspNetCoreTraceInstrumentationOptions options) + : base(DiagnosticSourceName) + { + Guard.ThrowIfNull(options); + + this.options = options; + } + + public override void OnEventWritten(string name, object payload) + { + switch (name) { - // The overall flow of what AspNetCore library does is as below: - // Activity.Start() - // DiagnosticSource.WriteEvent("Start", payload) - // DiagnosticSource.WriteEvent("Stop", payload) - // Activity.Stop() + case OnStartEvent: + { + this.OnStartActivity(Activity.Current, payload); + } - // This method is in the WriteEvent("Start", payload) path. - // By this time, samplers have already run and - // activity.IsAllDataRequested populated accordingly. + break; + case OnStopEvent: + { + this.OnStopActivity(Activity.Current, payload); + } - if (Sdk.SuppressInstrumentation) - { - return; - } + break; + case OnUnhandledHostingExceptionEvent: + case OnUnHandledDiagnosticsExceptionEvent: + { + this.OnException(Activity.Current, payload); + } - HttpContext context = payload as HttpContext; - if (context == null) - { - AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName); - return; - } + break; + } + } - // Ensure context extraction irrespective of sampling decision - var request = context.Request; - var textMapPropagator = Propagators.DefaultTextMapPropagator; - if (textMapPropagator is not TraceContextPropagator) - { - var ctx = textMapPropagator.Extract(default, request, HttpRequestHeaderValuesGetter); + public void OnStartActivity(Activity activity, object payload) + { + // The overall flow of what AspNetCore library does is as below: + // Activity.Start() + // DiagnosticSource.WriteEvent("Start", payload) + // DiagnosticSource.WriteEvent("Stop", payload) + // Activity.Stop() + + // This method is in the WriteEvent("Start", payload) path. + // By this time, samplers have already run and + // activity.IsAllDataRequested populated accordingly. + + var context = payload as HttpContext; + if (context == null) + { + AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName); + return; + } - if (ctx.ActivityContext.IsValid() - && ctx.ActivityContext != new ActivityContext(activity.TraceId, activity.ParentSpanId, activity.ActivityTraceFlags, activity.TraceStateString, true)) + // Ensure context extraction irrespective of sampling decision + var request = context.Request; + var textMapPropagator = Propagators.DefaultTextMapPropagator; + if (textMapPropagator is not TraceContextPropagator) + { + var ctx = textMapPropagator.Extract(default, request, HttpRequestHeaderValuesGetter); + if (ctx.ActivityContext.IsValid() + && !((ctx.ActivityContext.TraceId == activity.TraceId) + && (ctx.ActivityContext.SpanId == activity.ParentSpanId) + && (ctx.ActivityContext.TraceState == activity.TraceStateString))) + { + // Create a new activity with its parent set from the extracted context. + // This makes the new activity as a "sibling" of the activity created by + // Asp.Net Core. + Activity newOne; + if (Net7OrGreater) { - // Create a new activity with its parent set from the extracted context. - // This makes the new activity as a "sibling" of the activity created by - // Asp.Net Core. -#if NET7_0_OR_GREATER // For NET7.0 onwards activity is created using ActivitySource so, // we will use the source of the activity to create the new one. - Activity newOne = activity.Source.CreateActivity(ActivityOperationName, ActivityKind.Server, ctx.ActivityContext); -#else - Activity newOne = new Activity(ActivityOperationName); + newOne = activity.Source.CreateActivity(ActivityOperationName, ActivityKind.Server, ctx.ActivityContext); + } + else + { + newOne = new Activity(ActivityOperationName); newOne.SetParentId(ctx.ActivityContext.TraceId, ctx.ActivityContext.SpanId, ctx.ActivityContext.TraceFlags); -#endif - newOne.TraceStateString = ctx.ActivityContext.TraceState; + } - newOne.SetTag("IsCreatedByInstrumentation", bool.TrueString); + newOne.TraceStateString = ctx.ActivityContext.TraceState; - // Starting the new activity make it the Activity.Current one. - newOne.Start(); + newOne.SetTag("IsCreatedByInstrumentation", bool.TrueString); - // Set IsAllDataRequested to false for the activity created by the framework to only export the sibling activity and not the framework activity - activity.IsAllDataRequested = false; - activity = newOne; - } + // Starting the new activity make it the Activity.Current one. + newOne.Start(); - Baggage.Current = ctx.Baggage; + // Set IsAllDataRequested to false for the activity created by the framework to only export the sibling activity and not the framework activity + activity.IsAllDataRequested = false; + activity = newOne; } - // enrich Activity from payload only if sampling decision - // is favorable. - if (activity.IsAllDataRequested) + Baggage.Current = ctx.Baggage; + } + + // enrich Activity from payload only if sampling decision + // is favorable. + if (activity.IsAllDataRequested) + { + try { - try - { - if (this.options.Filter?.Invoke(context) == false) - { - AspNetCoreInstrumentationEventSource.Log.RequestIsFilteredOut(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName); - activity.IsAllDataRequested = false; - activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; - return; - } - } - catch (Exception ex) + if (this.options.Filter?.Invoke(context) == false) { - AspNetCoreInstrumentationEventSource.Log.RequestFilterException(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName, ex); + AspNetCoreInstrumentationEventSource.Log.RequestIsFilteredOut(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName); activity.IsAllDataRequested = false; activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; return; } + } + catch (Exception ex) + { + AspNetCoreInstrumentationEventSource.Log.RequestFilterException(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName, ex); + activity.IsAllDataRequested = false; + activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; + return; + } -#if !NET7_0_OR_GREATER + if (!Net7OrGreater) + { ActivityInstrumentationHelper.SetActivitySourceProperty(activity, ActivitySource); ActivityInstrumentationHelper.SetKindProperty(activity, ActivityKind.Server); -#endif + } - var path = (request.PathBase.HasValue || request.Path.HasValue) ? (request.PathBase + request.Path).ToString() : "/"; - activity.DisplayName = path; + var path = (request.PathBase.HasValue || request.Path.HasValue) ? (request.PathBase + request.Path).ToString() : "/"; + activity.DisplayName = 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.httpSemanticConvention.HasFlag(HttpSemanticConvention.Old)) - { - if (request.Host.HasValue) - { - activity.SetTag(SemanticConventions.AttributeNetHostName, request.Host.Host); - - 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.Headers.TryGetValue("User-Agent", out var values)) - { - var userAgent = values.Count > 0 ? values[0] : null; - if (!string.IsNullOrEmpty(userAgent)) - { - activity.SetTag(SemanticConventions.AttributeHttpUserAgent, userAgent); - } - } - } + // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md - // see the spec https://github.com/open-telemetry/opentelemetry-specification/blob/v1.21.0/specification/trace/semantic_conventions/http.md - if (this.httpSemanticConvention.HasFlag(HttpSemanticConvention.New)) - { - 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); - } - - activity.SetTag(SemanticConventions.AttributeHttpRequestMethod, request.Method); - 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)) - { - var userAgent = values.Count > 0 ? values[0] : null; - if (!string.IsNullOrEmpty(userAgent)) - { - activity.SetTag(SemanticConventions.AttributeUserAgentOriginal, userAgent); - } - } - } + if (request.Host.HasValue) + { + activity.SetTag(SemanticConventions.AttributeServerAddress, request.Host.Host); - try - { - this.options.EnrichWithHttpRequest?.Invoke(activity, request); - } - catch (Exception ex) + if (request.Host.Port is not null && request.Host.Port != 80 && request.Host.Port != 443) { - AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName, ex); + activity.SetTag(SemanticConventions.AttributeServerPort, request.Host.Port); } } - } - public void OnStopActivity(Activity activity, object payload) - { - if (activity.IsAllDataRequested) + if (request.QueryString.HasValue) { - HttpContext context = payload as HttpContext; - if (context == null) - { - AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInListener), nameof(this.OnStopActivity), activity.OperationName); - return; - } + // QueryString should be sanitized. see: https://github.com/open-telemetry/opentelemetry-dotnet/issues/4571 + activity.SetTag(SemanticConventions.AttributeUrlQuery, request.QueryString.Value); + } - var response = context.Response; + RequestMethodHelper.SetHttpMethodTag(activity, request.Method); - if (this.httpSemanticConvention.HasFlag(HttpSemanticConvention.Old)) - { - activity.SetTag(SemanticConventions.AttributeHttpStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)); - } + activity.SetTag(SemanticConventions.AttributeUrlScheme, request.Scheme); + activity.SetTag(SemanticConventions.AttributeUrlPath, path); + activity.SetTag(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetFlavorTagValueFromProtocol(request.Protocol)); - if (this.httpSemanticConvention.HasFlag(HttpSemanticConvention.New)) - { - activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)); - } - -#if !NETSTANDARD2_0 - if (this.options.EnableGrpcAspNetCoreSupport && TryGetGrpcMethod(activity, out var grpcMethod)) - { - this.AddGrpcAttributes(activity, grpcMethod, context); - } - else if (activity.Status == ActivityStatusCode.Unset) - { - activity.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode(activity.Kind, response.StatusCode)); - } -#else - if (activity.Status == ActivityStatusCode.Unset) - { - activity.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode(activity.Kind, response.StatusCode)); - } -#endif - - try - { - this.options.EnrichWithHttpResponse?.Invoke(activity, response); - } - catch (Exception ex) + if (request.Headers.TryGetValue("User-Agent", out var values)) + { + var userAgent = values.Count > 0 ? values[0] : null; + if (!string.IsNullOrEmpty(userAgent)) { - AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInListener), nameof(this.OnStopActivity), activity.OperationName, ex); + activity.SetTag(SemanticConventions.AttributeUserAgentOriginal, userAgent); } } - if (activity.TryCheckFirstTag("IsCreatedByInstrumentation", out var tagValue) && ReferenceEquals(tagValue, bool.TrueString)) + try { - // If instrumentation started a new Activity, it must - // be stopped here. - activity.SetTag("IsCreatedByInstrumentation", null); - activity.Stop(); - - // After the activity.Stop() code, Activity.Current becomes null. - // If Asp.Net Core uses Activity.Current?.Stop() - it'll not stop the activity - // it created. - // Currently Asp.Net core does not use Activity.Current, instead it stores a - // reference to its activity, and calls .Stop on it. - - // TODO: Should we still restore Activity.Current here? - // If yes, then we need to store the asp.net core activity inside - // the one created by the instrumentation. - // And retrieve it here, and set it to Current. + this.options.EnrichWithHttpRequest?.Invoke(activity, request); } - } - - 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) + catch (Exception ex) { - if (string.Equals(activity.OperationName, ActivityOperationName, StringComparison.Ordinal)) - { - break; - } - - activity = activity.Parent; + AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName, ex); } + } + } - if (activity == null) + public void OnStopActivity(Activity activity, object payload) + { + if (activity.IsAllDataRequested) + { + HttpContext context = payload as HttpContext; + if (context == null) { + AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInListener), nameof(this.OnStopActivity), activity.OperationName); return; } - if (activity.IsAllDataRequested) + var response = context.Response; + +#if !NETSTANDARD + var routePattern = (context.Features.Get()?.Endpoint as RouteEndpoint ?? + context.GetEndpoint() as RouteEndpoint)?.RoutePattern.RawText; + if (!string.IsNullOrEmpty(routePattern)) { -#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; + activity.DisplayName = GetDisplayName(context.Request.Method, routePattern); + activity.SetTag(SemanticConventions.AttributeHttpRoute, routePattern); + } #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; - } - } + activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)); - public void OnException(Activity activity, object payload) - { - if (activity.IsAllDataRequested) + if (this.options.EnableGrpcAspNetCoreSupport && TryGetGrpcMethod(activity, out var grpcMethod)) { - // We need to use reflection here as the payload type is not a defined public type. - if (!this.stopExceptionFetcher.TryFetch(payload, out Exception exc) || exc == null) - { - AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInListener), nameof(this.OnException), activity.OperationName); - return; - } - - if (this.options.RecordException) - { - activity.RecordException(exc); - } + AddGrpcAttributes(activity, grpcMethod, context); + } - activity.SetStatus(ActivityStatusCode.Error, exc.Message); + if (activity.Status == ActivityStatusCode.Unset) + { + activity.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode(activity.Kind, response.StatusCode)); + } - try - { - this.options.EnrichWithException?.Invoke(activity, exc); - } - catch (Exception ex) - { - AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInListener), nameof(this.OnException), activity.OperationName, ex); - } + try + { + this.options.EnrichWithHttpResponse?.Invoke(activity, response); + } + catch (Exception ex) + { + AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInListener), nameof(this.OnStopActivity), activity.OperationName, ex); } } - private static string GetUri(HttpRequest request) + object tagValue; + if (Net7OrGreater) { - // this follows the suggestions from https://github.com/dotnet/aspnetcore/issues/28906 - var scheme = request.Scheme ?? string.Empty; - - // HTTP 1.0 request with NO host header would result in empty Host. - // Use placeholder to avoid incorrect URL like "http:///" - var host = request.Host.Value ?? UnknownHostName; - var pathBase = request.PathBase.Value ?? string.Empty; - var path = request.Path.Value ?? string.Empty; - var queryString = request.QueryString.Value ?? string.Empty; - var length = scheme.Length + Uri.SchemeDelimiter.Length + host.Length + pathBase.Length - + path.Length + queryString.Length; - -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - return string.Create(length, (scheme, host, pathBase, path, queryString), (span, parts) => - { - CopyTo(ref span, parts.scheme); - CopyTo(ref span, Uri.SchemeDelimiter); - CopyTo(ref span, parts.host); - CopyTo(ref span, parts.pathBase); - CopyTo(ref span, parts.path); - CopyTo(ref span, parts.queryString); - - static void CopyTo(ref Span buffer, ReadOnlySpan text) - { - if (!text.IsEmpty) - { - text.CopyTo(buffer); - buffer = buffer.Slice(text.Length); - } - } - }); -#else - return new System.Text.StringBuilder(length) - .Append(scheme) - .Append(Uri.SchemeDelimiter) - .Append(host) - .Append(pathBase) - .Append(path) - .Append(queryString) - .ToString(); -#endif + tagValue = activity.GetTagValue("IsCreatedByInstrumentation"); } - -#if !NETSTANDARD2_0 - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryGetGrpcMethod(Activity activity, out string grpcMethod) + else { - grpcMethod = GrpcTagHelper.GetGrpcMethodFromActivity(activity); - return !string.IsNullOrEmpty(grpcMethod); + _ = activity.TryCheckFirstTag("IsCreatedByInstrumentation", out tagValue); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AddGrpcAttributes(Activity activity, string grpcMethod, HttpContext context) + if (ReferenceEquals(tagValue, bool.TrueString)) { - // The RPC semantic conventions indicate the span name - // should not have a leading forward slash. - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/rpc.md#span-name - activity.DisplayName = grpcMethod.TrimStart('/'); + // If instrumentation started a new Activity, it must + // be stopped here. + activity.SetTag("IsCreatedByInstrumentation", null); + activity.Stop(); + + // After the activity.Stop() code, Activity.Current becomes null. + // If Asp.Net Core uses Activity.Current?.Stop() - it'll not stop the activity + // it created. + // Currently Asp.Net core does not use Activity.Current, instead it stores a + // reference to its activity, and calls .Stop on it. + + // TODO: Should we still restore Activity.Current here? + // If yes, then we need to store the asp.net core activity inside + // the one created by the instrumentation. + // And retrieve it here, and set it to Current. + } + } - activity.SetTag(SemanticConventions.AttributeRpcSystem, GrpcTagHelper.RpcSystemGrpc); - if (context.Connection.RemoteIpAddress != null) + public void OnException(Activity activity, object payload) + { + if (activity.IsAllDataRequested) + { + // We need to use reflection here as the payload type is not a defined public type. + if (!TryFetchException(payload, out Exception exc)) { - // 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()); + AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInListener), nameof(this.OnException), activity.OperationName); + return; } - if (this.httpSemanticConvention.HasFlag(HttpSemanticConvention.Old)) + activity.SetTag(SemanticConventions.AttributeErrorType, exc.GetType().FullName); + + if (this.options.RecordException) { - activity.SetTag(SemanticConventions.AttributeNetPeerPort, context.Connection.RemotePort); + activity.RecordException(exc); } - if (this.httpSemanticConvention.HasFlag(HttpSemanticConvention.New)) + activity.SetStatus(ActivityStatusCode.Error); + + try { - activity.SetTag(SemanticConventions.AttributeServerPort, context.Connection.RemotePort); + this.options.EnrichWithException?.Invoke(activity, exc); } - - bool validConversion = GrpcTagHelper.TryGetGrpcStatusCodeFromActivity(activity, out int status); - if (validConversion) + catch (Exception ex) { - activity.SetStatus(GrpcTagHelper.ResolveSpanStatusForGrpcStatusCode(status)); + AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInListener), nameof(this.OnException), activity.OperationName, ex); } + } - if (GrpcTagHelper.TryParseRpcServiceAndRpcMethod(grpcMethod, out var rpcService, out var rpcMethod)) - { - activity.SetTag(SemanticConventions.AttributeRpcService, rpcService); - activity.SetTag(SemanticConventions.AttributeRpcMethod, rpcMethod); + // See https://github.com/dotnet/aspnetcore/blob/690d78279e940d267669f825aa6627b0d731f64c/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs#L252 + // and https://github.com/dotnet/aspnetcore/blob/690d78279e940d267669f825aa6627b0d731f64c/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs#L174 + // this makes sure that top-level properties on the payload object are always preserved. +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The event source guarantees that top level properties are preserved")] +#endif + static bool TryFetchException(object payload, out Exception exc) + => ExceptionPropertyFetcher.TryFetch(payload, out exc) && exc != null; + } - // Remove the grpc.method tag added by the gRPC .NET library - activity.SetTag(GrpcTagHelper.GrpcMethodTagName, null); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryGetGrpcMethod(Activity activity, out string grpcMethod) + { + grpcMethod = GrpcTagHelper.GetGrpcMethodFromActivity(activity); + return !string.IsNullOrEmpty(grpcMethod); + } - // Remove the grpc.status_code tag added by the gRPC .NET library - activity.SetTag(GrpcTagHelper.GrpcStatusCodeTagName, null); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AddGrpcAttributes(Activity activity, string grpcMethod, HttpContext context) + { + // The RPC semantic conventions indicate the span name + // should not have a leading forward slash. + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-spans.md#span-name + activity.DisplayName = grpcMethod.TrimStart('/'); - if (validConversion) - { - // setting rpc.grpc.status_code - activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, status); - } + activity.SetTag(SemanticConventions.AttributeRpcSystem, GrpcTagHelper.RpcSystemGrpc); + + // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/rpc/rpc-spans.md + + if (context.Connection.RemoteIpAddress != null) + { + 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) + { + activity.SetStatus(GrpcTagHelper.ResolveSpanStatusForGrpcStatusCode(status)); + } + + if (GrpcTagHelper.TryParseRpcServiceAndRpcMethod(grpcMethod, out var rpcService, out var rpcMethod)) + { + activity.SetTag(SemanticConventions.AttributeRpcService, rpcService); + activity.SetTag(SemanticConventions.AttributeRpcMethod, rpcMethod); + + // Remove the grpc.method tag added by the gRPC .NET library + activity.SetTag(GrpcTagHelper.GrpcMethodTagName, null); + + // Remove the grpc.status_code tag added by the gRPC .NET library + activity.SetTag(GrpcTagHelper.GrpcStatusCodeTagName, null); + + if (validConversion) + { + // setting rpc.grpc.status_code + activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, status); } } -#endif + } + + private static string GetDisplayName(string httpMethod, string httpRoute = null) + { + var normalizedMethod = RequestMethodHelper.GetNormalizedHttpMethod(httpMethod); + + return string.IsNullOrEmpty(httpRoute) + ? normalizedMethod + : $"{normalizedMethod} {httpRoute}"; } } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs index 17d9e77738e..e41cd5dc256 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs @@ -1,149 +1,128 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Diagnostics.Metrics; +using System.Reflection; using Microsoft.AspNetCore.Http; +using OpenTelemetry.Internal; + #if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Routing; #endif using OpenTelemetry.Trace; -using static OpenTelemetry.Internal.HttpSemanticConventionHelper; -namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation +namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation; + +internal sealed class HttpInMetricsListener : ListenerHandler { - internal sealed class HttpInMetricsListener : ListenerHandler - { - private const string HttpServerDurationMetricName = "http.server.duration"; - private const string OnStopEvent = "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop"; - private const string EventName = "OnStopActivity"; + internal const string HttpServerRequestDurationMetricName = "http.server.request.duration"; - private readonly Meter meter; - private readonly AspNetCoreMetricsInstrumentationOptions options; - private readonly Histogram httpServerDuration; + internal const string OnUnhandledHostingExceptionEvent = "Microsoft.AspNetCore.Hosting.UnhandledException"; + internal const string OnUnhandledDiagnosticsExceptionEvent = "Microsoft.AspNetCore.Diagnostics.UnhandledException"; - private readonly HttpSemanticConvention httpSemanticConvention; + internal static readonly AssemblyName AssemblyName = typeof(HttpInListener).Assembly.GetName(); + internal static readonly string InstrumentationName = AssemblyName.Name; + internal static readonly string InstrumentationVersion = AssemblyName.Version.ToString(); + internal static readonly Meter Meter = new(InstrumentationName, InstrumentationVersion); - internal HttpInMetricsListener(string name, Meter meter, AspNetCoreMetricsInstrumentationOptions options) - : base(name) - { - this.meter = meter; - this.options = options; - this.httpServerDuration = meter.CreateHistogram(HttpServerDurationMetricName, "ms", "Measures the duration of inbound HTTP requests."); + private const string OnStopEvent = "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop"; - this.httpSemanticConvention = GetSemanticConventionOptIn(); - } + private static readonly PropertyFetcher ExceptionPropertyFetcher = new("Exception"); + private static readonly PropertyFetcher HttpContextPropertyFetcher = new("HttpContext"); + private static readonly object ErrorTypeHttpContextItemsKey = new(); + + private static readonly Histogram HttpServerRequestDuration = Meter.CreateHistogram(HttpServerRequestDurationMetricName, "s", "Duration of HTTP server requests."); - public override void OnEventWritten(string name, object payload) + internal HttpInMetricsListener(string name) + : base(name) + { + } + + public static 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)) { - if (name == OnStopEvent) - { - var context = payload as HttpContext; - if (context == null) - { - AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInMetricsListener), EventName, HttpServerDurationMetricName); - return; - } + AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInMetricsListener), nameof(OnExceptionEventWritten), HttpServerRequestDurationMetricName); + return; + } - try - { - if (this.options.Filter?.Invoke(HttpServerDurationMetricName, context) == false) - { - AspNetCoreInstrumentationEventSource.Log.RequestIsFilteredOut(nameof(HttpInMetricsListener), EventName, HttpServerDurationMetricName); - return; - } - } - catch (Exception ex) - { - AspNetCoreInstrumentationEventSource.Log.RequestFilterException(nameof(HttpInMetricsListener), EventName, HttpServerDurationMetricName, ex); - return; - } + ctx.Items.Add(ErrorTypeHttpContextItemsKey, exc.GetType().FullName); - // 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; - } + // See https://github.com/dotnet/aspnetcore/blob/690d78279e940d267669f825aa6627b0d731f64c/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs#L252 + // and https://github.com/dotnet/aspnetcore/blob/690d78279e940d267669f825aa6627b0d731f64c/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs#L174 + // this makes sure that top-level properties on the payload object are always preserved. +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The ASP.NET Core framework guarantees that top level properties are preserved")] +#endif + static bool TryFetchException(object payload, out Exception exc) + => ExceptionPropertyFetcher.TryFetch(payload, out exc) && exc != null; +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The ASP.NET Core framework guarantees that top level properties are preserved")] +#endif + static bool TryFetchHttpContext(object payload, out HttpContext ctx) + => HttpContextPropertyFetcher.TryFetch(payload, out ctx) && ctx != null; + } - TagList tags = default; + public static void OnStopEventWritten(string name, object payload) + { + var context = payload as HttpContext; + if (context == null) + { + AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInMetricsListener), nameof(OnStopEventWritten), HttpServerRequestDurationMetricName); + return; + } - // see the spec https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/trace/semantic_conventions/http.md - if (this.httpSemanticConvention.HasFlag(HttpSemanticConvention.Old)) - { - 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)); - } - } - } + TagList tags = default; - // see the spec https://github.com/open-telemetry/opentelemetry-specification/blob/v1.21.0/specification/trace/semantic_conventions/http.md - if (this.httpSemanticConvention.HasFlag(HttpSemanticConvention.New)) - { - 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.AttributeHttpRequestMethod, context.Request.Method)); - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(context.Response.StatusCode))); - - if (context.Request.Host.HasValue) - { - tags.Add(new KeyValuePair(SemanticConventions.AttributeServerAddress, 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.AttributeServerPort, context.Request.Host.Port)); - } - } - } + // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md + 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))); + + 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; - if (!string.IsNullOrEmpty(route)) + // Check the exception handler feature first in case the endpoint was overwritten + var route = (context.Features.Get()?.Endpoint as RouteEndpoint ?? + context.GetEndpoint() as RouteEndpoint)?.RoutePattern.RawText; + if (!string.IsNullOrEmpty(route)) + { + tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRoute, route)); + } +#endif + if (context.Items.TryGetValue(ErrorTypeHttpContextItemsKey, out var errorType)) + { + tags.Add(new KeyValuePair(SemanticConventions.AttributeErrorType, errorType)); + } + + // 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. + HttpServerRequestDuration.Record(Activity.Current.Duration.TotalSeconds, tags); + } + + public override void OnEventWritten(string name, object payload) + { + switch (name) + { + case OnUnhandledDiagnosticsExceptionEvent: + case OnUnhandledHostingExceptionEvent: { - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRoute, route)); + OnExceptionEventWritten(name, payload); } -#endif - if (this.options.Enrich != null) + + break; + case OnStopEvent: { - try - { - this.options.Enrich(HttpServerDurationMetricName, context, ref tags); - } - catch (Exception ex) - { - AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInMetricsListener), EventName, HttpServerDurationMetricName, ex); - } + OnStopEventWritten(name, payload); } - // 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); - } + break; } } } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpTagHelper.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpTagHelper.cs index 73114efe0b3..90f37eba3c2 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpTagHelper.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpTagHelper.cs @@ -1,47 +1,33 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation +namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation; + +/// +/// A collection of helper methods to be used when building Http activities. +/// +internal static class HttpTagHelper { /// - /// A collection of helper methods to be used when building Http activities. + /// Gets the OpenTelemetry standard version tag value for a span based on its protocol/>. /// - internal static class HttpTagHelper + /// . + /// Span flavor value. + public static string GetFlavorTagValueFromProtocol(string protocol) { - /// - /// Gets the OpenTelemetry standard version tag value for a span based on its protocol/>. - /// - /// . - /// Span flavor value. - public static string GetFlavorTagValueFromProtocol(string protocol) + switch (protocol) { - switch (protocol) - { - case "HTTP/2": - return "2.0"; + case "HTTP/2": + return "2"; - case "HTTP/3": - return "3.0"; + case "HTTP/3": + return "3"; - case "HTTP/1.1": - return "1.1"; + case "HTTP/1.1": + return "1.1"; - default: - return protocol; - } + default: + return protocol; } } } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/TelemetryHelper.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/TelemetryHelper.cs index 73d84269e82..6f2e1fae8e4 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/TelemetryHelper.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/TelemetryHelper.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation; diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.cs deleted file mode 100644 index 47cbcdf3fc0..00000000000 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.cs +++ /dev/null @@ -1,90 +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 Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using OpenTelemetry.Instrumentation.AspNetCore; -using OpenTelemetry.Instrumentation.AspNetCore.Implementation; -using OpenTelemetry.Internal; - -namespace OpenTelemetry.Metrics -{ - /// - /// Extension methods to simplify registering of ASP.NET Core request instrumentation. - /// - public static class MeterProviderBuilderExtensions - { - /// - /// Enables the incoming requests automatic data collection for ASP.NET Core. - /// - /// being configured. - /// The instance of to chain the calls. - public static MeterProviderBuilder AddAspNetCoreInstrumentation( - this MeterProviderBuilder builder) - => AddAspNetCoreInstrumentation(builder, name: null, configureAspNetCoreInstrumentationOptions: null); - - /// - /// Enables the incoming requests automatic data collection for ASP.NET Core. - /// - /// being configured. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static MeterProviderBuilder AddAspNetCoreInstrumentation( - this MeterProviderBuilder builder, - Action configureAspNetCoreInstrumentationOptions) - => AddAspNetCoreInstrumentation(builder, name: null, configureAspNetCoreInstrumentationOptions); - - /// - /// Enables the incoming requests automatic data collection for ASP.NET Core. - /// - /// being configured. - /// Name which is used when retrieving options. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static MeterProviderBuilder AddAspNetCoreInstrumentation( - this MeterProviderBuilder builder, - string name, - Action configureAspNetCoreInstrumentationOptions) - { - Guard.ThrowIfNull(builder); - - // Note: Warm-up the status code mapping. - _ = TelemetryHelper.BoxedStatusCodes; - - name ??= Options.DefaultName; - - if (configureAspNetCoreInstrumentationOptions != null) - { - builder.ConfigureServices(services => services.Configure(name, configureAspNetCoreInstrumentationOptions)); - } - - builder.AddMeter(AspNetCoreMetrics.InstrumentationName); - - builder.AddInstrumentation(sp => - { - var options = sp.GetRequiredService>().Get(name); - - // 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); - }); - - return builder; - } - } -} diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/OpenTelemetry.Instrumentation.AspNetCore.csproj b/src/OpenTelemetry.Instrumentation.AspNetCore/OpenTelemetry.Instrumentation.AspNetCore.csproj index f2881a4ca1e..c96ded55241 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/OpenTelemetry.Instrumentation.AspNetCore.csproj +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/OpenTelemetry.Instrumentation.AspNetCore.csproj @@ -1,9 +1,10 @@ - net7.0;net6.0;netstandard2.1;netstandard2.0 + $(TargetFrameworksForAspNetCoreInstrumentation) ASP.NET Core instrumentation for OpenTelemetry .NET $(PackageTags);distributed-tracing;AspNetCore + Instrumentation.AspNetCore- true @@ -11,34 +12,33 @@ + - - - + + + + - - + + - - - - + + + - - + + + + - - - - - + diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/README.md b/src/OpenTelemetry.Instrumentation.AspNetCore/README.md index 131f98a4b37..e51a41be4a8 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/README.md +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/README.md @@ -9,18 +9,13 @@ which instruments [ASP.NET Core](https://docs.microsoft.com/aspnet/core) and collect metrics and traces about incoming web requests. This instrumentation also collects traces from incoming gRPC requests using [Grpc.AspNetCore](https://www.nuget.org/packages/Grpc.AspNetCore). +Instrumentation support for gRPC server requests is supported via an +[experimental](#experimental-support-for-grpc-requests) feature flag. -**Note: This component is based on the OpenTelemetry semantic conventions for -[metrics](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/metrics/semantic_conventions) -and -[traces](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/trace/semantic_conventions). -These conventions are -[Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/document-status.md), -and hence, this package is a [pre-release](../../VERSIONING.md#pre-releases). -Until a [stable -version](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/telemetry-stability.md) -is released, there can be breaking changes. You can track the progress from -[milestones](https://github.com/open-telemetry/opentelemetry-dotnet/milestone/23).** +This component is based on the +[v1.23](https://github.com/open-telemetry/semantic-conventions/tree/v1.23.0/docs/http) +of http semantic conventions. For details on the default set of attributes that +are added, checkout [Traces](#traces) and [Metrics](#metrics) sections below. ## Steps to enable OpenTelemetry.Instrumentation.AspNetCore @@ -31,7 +26,7 @@ Add a reference to the package. Also, add any other instrumentations & exporters you will need. ```shell -dotnet add package --prerelease OpenTelemetry.Instrumentation.AspNetCore +dotnet add package OpenTelemetry.Instrumentation.AspNetCore ``` ### Step 2: Enable ASP.NET Core Instrumentation at application startup @@ -66,6 +61,26 @@ public void ConfigureServices(IServiceCollection services) } ``` +Following list of attributes are added by default on activity. See +[http-spans](https://github.com/open-telemetry/semantic-conventions/tree/v1.23.0/docs/http/http-spans.md) +for more details about each individual attribute: + +* `error.type` +* `http.request.method` +* `http.request.method_original` +* `http.response.status_code` +* `http.route` +* `network.protocol.version` +* `user_agent.original` +* `server.address` +* `server.port` +* `url.path` +* `url.query` +* `url.scheme` + +[Enrich Api](#enrich) can be used if any additional attributes are +required on activity. + #### Metrics The following example demonstrates adding ASP.NET Core instrumentation with the @@ -88,30 +103,86 @@ public void ConfigureServices(IServiceCollection services) } ``` -#### List of metrics produced +Following list of attributes are added by default on +`http.server.request.duration` metric. See +[http-metrics](https://github.com/open-telemetry/semantic-conventions/tree/v1.23.0/docs/http/http-metrics.md) +for more details about each individual attribute. `.NET8.0` and above supports +additional metrics, see [list of metrics produced](#list-of-metrics-produced) for +more details. -The instrumentation is implemented based on [metrics semantic -conventions](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#metric-httpserverduration). -Currently, the instrumentation supports the following metric. +* `error.type` +* `http.response.status_code` +* `http.request.method` +* `http.route` +* `network.protocol.version` +* `url.scheme` -| Name | Instrument Type | Unit | Description | -|-------|-----------------|------|-------------| -| `http.server.duration` | Histogram | `ms` | Measures the duration of inbound HTTP requests. | +#### List of metrics produced + +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`. This applies to all +targeted frameworks. ## Advanced configuration +### Tracing + This instrumentation can be configured to change the default behavior by using -`AspNetCoreInstrumentationOptions`, which allows adding [`Filter`](#filter), +`AspNetCoreTraceInstrumentationOptions`, which allows adding [`Filter`](#filter), [`Enrich`](#enrich) as explained below. // TODO: This section could be refined. -When used with [`OpenTelemetry.Extensions.Hosting`](../OpenTelemetry.Extensions.Hosting/README.md), -all configurations to `AspNetCoreInstrumentationOptions` can be done in the `ConfigureServices` +When used with +[`OpenTelemetry.Extensions.Hosting`](../OpenTelemetry.Extensions.Hosting/README.md), +all configurations to `AspNetCoreTraceInstrumentationOptions` can be done in the +`ConfigureServices` method of you applications `Startup` class as shown below. ```csharp // Configure -services.Configure(options => +services.Configure(options => { options.Filter = (httpContext) => { @@ -126,11 +197,11 @@ services.AddOpenTelemetry() .AddConsoleExporter()); ``` -### Filter +#### Filter This instrumentation by default collects all the incoming http requests. It allows filtering of requests by using the `Filter` function in -`AspNetCoreInstrumentationOptions`. This defines the condition for allowable +`AspNetCoreTraceInstrumentationOptions`. This defines the condition for allowable requests. The Filter receives the `HttpContext` of the incoming request, and does not collect telemetry about the request if the Filter returns false or throws exception. @@ -154,7 +225,7 @@ instrumentation. OpenTelemetry has a concept of a [Sampler](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#sampling), and the `Filter` option does the filtering *after* the Sampler is invoked. -### Enrich +#### Enrich This instrumentation library provides `EnrichWithHttpRequest`, `EnrichWithHttpResponse` and `EnrichWithException` options that can be used to @@ -191,12 +262,54 @@ is the general extensibility point to add additional properties to any activity. The `Enrich` option is specific to this instrumentation, and is provided to get access to `HttpRequest` and `HttpResponse`. -### RecordException +#### RecordException This instrumentation automatically sets Activity Status to Error if an unhandled exception is thrown. Additionally, `RecordException` feature may be turned on, to store the exception to the Activity itself as ActivityEvent. +## Activity duration and http.server.request.duration metric calculation + +`Activity.Duration` and `http.server.request.duration` values represents the +time used to handle an inbound HTTP request as measured at the hosting layer of +ASP.NET Core. The time measurement starts once the underlying web host has: + +* Sufficiently parsed the HTTP request headers on the inbound network stream to + identify the new request. +* Initialized the context data structures such as the + [HttpContext](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.httpcontext). + +The time ends when: + +* The ASP.NET Core handler pipeline is finished executing. +* All response data has been sent. +* The context data structures for the request are being disposed. + +## Experimental support for gRPC requests + +gRPC instrumentation can be enabled by setting +`OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_ENABLE_GRPC_INSTRUMENTATION` flag to +`True`. The flag can be set as an environment variable or via IConfiguration as +shown below. + +```csharp +var appBuilder = WebApplication.CreateBuilder(args); + +appBuilder.Configuration.AddInMemoryCollection( + new Dictionary + { + ["OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_ENABLE_GRPC_INSTRUMENTATION"] = "true", + }); + +appBuilder.Services.AddOpenTelemetry() + .WithTracing(tracing => tracing + .AddAspNetCoreInstrumentation()); +``` + + Semantic conventions for RPC are still + [experimental](https://github.com/open-telemetry/semantic-conventions/tree/main/docs/rpc) + and hence the instrumentation only offers it as an experimental feature. + ## Troubleshooting This component uses an diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/TracerProviderBuilderExtensions.cs deleted file mode 100644 index fde28c82ad5..00000000000 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/TracerProviderBuilderExtensions.cs +++ /dev/null @@ -1,130 +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. -// - -#if NET7_0_OR_GREATER -using System.Diagnostics; -#endif -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using OpenTelemetry.Instrumentation.AspNetCore; -using OpenTelemetry.Instrumentation.AspNetCore.Implementation; -using OpenTelemetry.Internal; - -namespace OpenTelemetry.Trace -{ - /// - /// Extension methods to simplify registering of ASP.NET Core request instrumentation. - /// - public static class TracerProviderBuilderExtensions - { - /// - /// Enables the incoming requests automatic data collection for ASP.NET Core. - /// - /// being configured. - /// The instance of to chain the calls. - public static TracerProviderBuilder AddAspNetCoreInstrumentation(this TracerProviderBuilder builder) - => AddAspNetCoreInstrumentation(builder, name: null, configureAspNetCoreInstrumentationOptions: null); - - /// - /// Enables the incoming requests automatic data collection for ASP.NET Core. - /// - /// being configured. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static TracerProviderBuilder AddAspNetCoreInstrumentation( - this TracerProviderBuilder builder, - Action configureAspNetCoreInstrumentationOptions) - => AddAspNetCoreInstrumentation(builder, name: null, configureAspNetCoreInstrumentationOptions); - - /// - /// Enables the incoming requests automatic data collection for ASP.NET Core. - /// - /// being configured. - /// Name which is used when retrieving options. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static TracerProviderBuilder AddAspNetCoreInstrumentation( - this TracerProviderBuilder builder, - string name, - Action configureAspNetCoreInstrumentationOptions) - { - Guard.ThrowIfNull(builder); - - // Note: Warm-up the status code mapping. - _ = TelemetryHelper.BoxedStatusCodes; - - name ??= Options.DefaultName; - - if (configureAspNetCoreInstrumentationOptions != null) - { - builder.ConfigureServices(services => services.Configure(name, configureAspNetCoreInstrumentationOptions)); - } - - if (builder is IDeferredTracerProviderBuilder deferredTracerProviderBuilder) - { - deferredTracerProviderBuilder.Configure((sp, builder) => - { - AddAspNetCoreInstrumentationSources(builder, sp); - }); - } - - return builder.AddInstrumentation(sp => - { - var options = sp.GetRequiredService>().Get(name); - - return new AspNetCoreInstrumentation( - new HttpInListener(options)); - }); - } - - // Note: This is used by unit tests. - internal static TracerProviderBuilder AddAspNetCoreInstrumentation( - this TracerProviderBuilder builder, - HttpInListener listener) - { - builder.AddAspNetCoreInstrumentationSources(); - - return builder.AddInstrumentation( - new AspNetCoreInstrumentation(listener)); - } - - private static void AddAspNetCoreInstrumentationSources( - this TracerProviderBuilder builder, - IServiceProvider serviceProvider = null) - { - // For .NET7.0 onwards activity will be created using activitySource. - // https://github.com/dotnet/aspnetcore/blob/bf3352f2422bf16fa3ca49021f0e31961ce525eb/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs#L327 - // For .NET6.0 and below, we will continue to use legacy way. -#if NET7_0_OR_GREATER - // TODO: Check with .NET team to see if this can be prevented - // as this allows user to override the ActivitySource. - var activitySourceService = serviceProvider?.GetService(); - if (activitySourceService != null) - { - builder.AddSource(activitySourceService.Name); - } - else - { - // For users not using hosting package? - builder.AddSource(HttpInListener.AspNetCoreActivitySourceName); - } -#else - builder.AddSource(HttpInListener.ActivitySourceName); - builder.AddLegacySource(HttpInListener.ActivityOperationName); // for the activities created by AspNetCore -#endif - } - } -} diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/net462/PublicAPI.Shipped.txt b/src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/PublicAPI.Shipped.txt similarity index 100% rename from src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/net462/PublicAPI.Shipped.txt rename to src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/PublicAPI.Shipped.txt diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..32e717dc82b --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/PublicAPI.Unshipped.txt @@ -0,0 +1,12 @@ +OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientTraceInstrumentationOptions +OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientTraceInstrumentationOptions.EnrichWithHttpRequestMessage.get -> System.Action +OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientTraceInstrumentationOptions.EnrichWithHttpRequestMessage.set -> void +OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientTraceInstrumentationOptions.EnrichWithHttpResponseMessage.get -> System.Action +OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientTraceInstrumentationOptions.EnrichWithHttpResponseMessage.set -> void +OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientTraceInstrumentationOptions.GrpcClientTraceInstrumentationOptions() -> void +OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientTraceInstrumentationOptions.SuppressDownstreamInstrumentation.get -> bool +OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientTraceInstrumentationOptions.SuppressDownstreamInstrumentation.set -> void +OpenTelemetry.Trace.TracerProviderBuilderExtensions +static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddGrpcClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder +static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddGrpcClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder +static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddGrpcClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/netstandard2.0/PublicAPI.Shipped.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt deleted file mode 100644 index db708294cc8..00000000000 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,12 +0,0 @@ -OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientInstrumentationOptions -OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientInstrumentationOptions.EnrichWithHttpRequestMessage.get -> System.Action -OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientInstrumentationOptions.EnrichWithHttpRequestMessage.set -> void -OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientInstrumentationOptions.EnrichWithHttpResponseMessage.get -> System.Action -OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientInstrumentationOptions.EnrichWithHttpResponseMessage.set -> void -OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientInstrumentationOptions.GrpcClientInstrumentationOptions() -> void -OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientInstrumentationOptions.SuppressDownstreamInstrumentation.get -> bool -OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientInstrumentationOptions.SuppressDownstreamInstrumentation.set -> void -OpenTelemetry.Trace.TracerProviderBuilderExtensions -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddGrpcClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddGrpcClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddGrpcClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder \ No newline at end of file diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/netstandard2.1/PublicAPI.Shipped.txt b/src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/netstandard2.1/PublicAPI.Shipped.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt deleted file mode 100644 index db708294cc8..00000000000 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,12 +0,0 @@ -OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientInstrumentationOptions -OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientInstrumentationOptions.EnrichWithHttpRequestMessage.get -> System.Action -OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientInstrumentationOptions.EnrichWithHttpRequestMessage.set -> void -OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientInstrumentationOptions.EnrichWithHttpResponseMessage.get -> System.Action -OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientInstrumentationOptions.EnrichWithHttpResponseMessage.set -> void -OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientInstrumentationOptions.GrpcClientInstrumentationOptions() -> void -OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientInstrumentationOptions.SuppressDownstreamInstrumentation.get -> bool -OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientInstrumentationOptions.SuppressDownstreamInstrumentation.set -> void -OpenTelemetry.Trace.TracerProviderBuilderExtensions -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddGrpcClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddGrpcClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddGrpcClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder \ No newline at end of file diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/AssemblyInfo.cs b/src/OpenTelemetry.Instrumentation.GrpcNetClient/AssemblyInfo.cs index 478e6315c82..06eb7f16d20 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/AssemblyInfo.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/AssemblyInfo.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Runtime.CompilerServices; diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.GrpcNetClient/CHANGELOG.md index 1039ef5c9ea..4eba089688b 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/CHANGELOG.md @@ -2,6 +2,61 @@ ## Unreleased +## 1.7.0-beta.1 + +Released 2024-Feb-09 + +* **Breaking Change**: + Please be advised that the + [SuppressDownstreamInstrumentation](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src/OpenTelemetry.Instrumentation.GrpcNetClient#suppressdownstreaminstrumentation) + option no longer works when used in conjunction with the + `OpenTelemetry.Instrumentation.Http` package version `1.6.0` or greater. + This is not a result of a change in the `OpenTelemetry.Instrumentation.GrpcNetClient` + package therefore this also affects versions prior to this release. See this + [issue](https://github.com/open-telemetry/opentelemetry-dotnet/issues/5092) + for details and workaround. +* Removed support for the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable + which toggled the use of the new conventions for the + [server, client, and shared network attributes](https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/general/attributes.md#server-client-and-shared-network-attributes). + Now that this suite of attributes are stable, this instrumentation will only + emit the new attributes. + ([#5259](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5259)) +* **Breaking Change**: Renamed `GrpcClientInstrumentationOptions` to + `GrpcClientTraceInstrumentationOptions`. + ([#5272](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5272)) + +## 1.6.0-beta.3 + +Released 2023-Nov-17 + +## 1.6.0-beta.2 + +Released 2023-Oct-26 + +## 1.5.1-beta.1 + +Released 2023-Jul-20 + +* The new network semantic conventions can be opted in to by setting + the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable. This allows for a + transition period for users to experiment with the new semantic conventions + and adapt as necessary. The environment variable supports the following + values: + * `http` - emit the new, frozen (proposed for stable) networking + attributes, and stop emitting the old experimental networking + attributes that the instrumentation emitted previously. + * `http/dup` - emit both the old and the frozen (proposed for stable) + networking attributes, allowing for a more seamless transition. + * The default behavior (in the absence of one of these values) is to continue + emitting the same network semantic conventions that were emitted in + `1.5.0-beta.1`. + * Note: this option will eventually be removed after the new + network semantic conventions are marked stable. Refer to the + specification for more information regarding the new network + semantic conventions for + [spans](https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/rpc/rpc-spans.md). + ([#4658](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4658)) + ## 1.5.0-beta.1 Released 2023-Jun-05 diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientInstrumentation.cs b/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientInstrumentation.cs index 1e970678603..961ddc17749 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientInstrumentation.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientInstrumentation.cs @@ -1,43 +1,30 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + using OpenTelemetry.Instrumentation.GrpcNetClient.Implementation; -namespace OpenTelemetry.Instrumentation.GrpcNetClient +namespace OpenTelemetry.Instrumentation.GrpcNetClient; + +/// +/// GrpcClient instrumentation. +/// +internal sealed class GrpcClientInstrumentation : IDisposable { + private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; + /// - /// GrpcClient instrumentation. + /// Initializes a new instance of the class. /// - internal sealed class GrpcClientInstrumentation : IDisposable + /// Configuration options for Grpc client instrumentation. + public GrpcClientInstrumentation(GrpcClientTraceInstrumentationOptions options = null) { - private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; - - /// - /// Initializes a new instance of the class. - /// - /// Configuration options for Grpc client instrumentation. - public GrpcClientInstrumentation(GrpcClientInstrumentationOptions options = null) - { - this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(new GrpcClientDiagnosticListener(options), null); - this.diagnosticSourceSubscriber.Subscribe(); - } + this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(new GrpcClientDiagnosticListener(options), isEnabledFilter: null, GrpcInstrumentationEventSource.Log.UnknownErrorProcessingEvent); + this.diagnosticSourceSubscriber.Subscribe(); + } - /// - public void Dispose() - { - this.diagnosticSourceSubscriber.Dispose(); - } + /// + public void Dispose() + { + this.diagnosticSourceSubscriber.Dispose(); } } diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientInstrumentationOptions.cs deleted file mode 100644 index d03c882aa6f..00000000000 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientInstrumentationOptions.cs +++ /dev/null @@ -1,49 +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; - -namespace OpenTelemetry.Instrumentation.GrpcNetClient -{ - /// - /// Options for GrpcClient instrumentation. - /// - public class GrpcClientInstrumentationOptions - { - /// - /// Gets or sets a value indicating whether down stream instrumentation is suppressed (disabled). - /// - public bool SuppressDownstreamInstrumentation { get; set; } - - /// - /// Gets or sets an action to enrich the Activity with . - /// - /// - /// : the activity being enriched. - /// object from which additional information can be extracted to enrich the activity. - /// - public Action EnrichWithHttpRequestMessage { get; set; } - - /// - /// Gets or sets an action to enrich an Activity with . - /// - /// - /// : the activity being enriched. - /// object from which additional information can be extracted to enrich the activity. - /// - public Action EnrichWithHttpResponseMessage { get; set; } - } -} diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientTraceInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientTraceInstrumentationOptions.cs new file mode 100644 index 00000000000..b4cd871980b --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcClientTraceInstrumentationOptions.cs @@ -0,0 +1,35 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; + +namespace OpenTelemetry.Instrumentation.GrpcNetClient; + +/// +/// Options for GrpcClient instrumentation. +/// +public class GrpcClientTraceInstrumentationOptions +{ + /// + /// Gets or sets a value indicating whether down stream instrumentation is suppressed (disabled). + /// + public bool SuppressDownstreamInstrumentation { get; set; } + + /// + /// Gets or sets an action to enrich the Activity with . + /// + /// + /// : the activity being enriched. + /// object from which additional information can be extracted to enrich the activity. + /// + public Action EnrichWithHttpRequestMessage { get; set; } + + /// + /// Gets or sets an action to enrich an Activity with . + /// + /// + /// : the activity being enriched. + /// object from which additional information can be extracted to enrich the activity. + /// + public Action EnrichWithHttpResponseMessage { get; set; } +} diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcTagHelper.cs b/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcTagHelper.cs index 8933480b157..836beb226a8 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcTagHelper.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/GrpcTagHelper.cs @@ -1,90 +1,76 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Text.RegularExpressions; using OpenTelemetry.Trace; -namespace OpenTelemetry.Instrumentation.GrpcNetClient +namespace OpenTelemetry.Instrumentation.GrpcNetClient; + +internal static class GrpcTagHelper { - internal static class GrpcTagHelper - { - public const string RpcSystemGrpc = "grpc"; + public const string RpcSystemGrpc = "grpc"; + + // The Grpc.Net.Client library adds its own tags to the activity. + // These tags are used to source the tags added by the OpenTelemetry instrumentation. + public const string GrpcMethodTagName = "grpc.method"; + public const string GrpcStatusCodeTagName = "grpc.status_code"; - // The Grpc.Net.Client library adds its own tags to the activity. - // These tags are used to source the tags added by the OpenTelemetry instrumentation. - public const string GrpcMethodTagName = "grpc.method"; - public const string GrpcStatusCodeTagName = "grpc.status_code"; + private static readonly Regex GrpcMethodRegex = new(@"^/?(?.*)/(?.*)$", RegexOptions.Compiled); - private static readonly Regex GrpcMethodRegex = new(@"^/?(?.*)/(?.*)$", RegexOptions.Compiled); + public static string GetGrpcMethodFromActivity(Activity activity) + { + return activity.GetTagValue(GrpcMethodTagName) as string; + } - public static string GetGrpcMethodFromActivity(Activity activity) + public static bool TryGetGrpcStatusCodeFromActivity(Activity activity, out int statusCode) + { + statusCode = -1; + var grpcStatusCodeTag = activity.GetTagValue(GrpcStatusCodeTagName); + if (grpcStatusCodeTag == null) { - return activity.GetTagValue(GrpcMethodTagName) as string; + return false; } - public static bool TryGetGrpcStatusCodeFromActivity(Activity activity, out int statusCode) - { - statusCode = -1; - var grpcStatusCodeTag = activity.GetTagValue(GrpcStatusCodeTagName); - if (grpcStatusCodeTag == null) - { - return false; - } + return int.TryParse(grpcStatusCodeTag as string, out statusCode); + } - return int.TryParse(grpcStatusCodeTag as string, out statusCode); + public static bool TryParseRpcServiceAndRpcMethod(string grpcMethod, out string rpcService, out string rpcMethod) + { + var match = GrpcMethodRegex.Match(grpcMethod); + if (match.Success) + { + rpcService = match.Groups["service"].Value; + rpcMethod = match.Groups["method"].Value; + return true; } - - public static bool TryParseRpcServiceAndRpcMethod(string grpcMethod, out string rpcService, out string rpcMethod) + else { - var match = GrpcMethodRegex.Match(grpcMethod); - if (match.Success) - { - rpcService = match.Groups["service"].Value; - rpcMethod = match.Groups["method"].Value; - return true; - } - else - { - rpcService = string.Empty; - rpcMethod = string.Empty; - return false; - } + rpcService = string.Empty; + rpcMethod = string.Empty; + return false; } + } - /// - /// Helper method that populates span properties from RPC status code according - /// to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/rpc.md#status. - /// - /// RPC status code. - /// Resolved span for the Grpc status code. - public static ActivityStatusCode ResolveSpanStatusForGrpcStatusCode(int statusCode) - { - var status = ActivityStatusCode.Error; + /// + /// Helper method that populates span properties from RPC status code according + /// to https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/grpc.md#grpc-attributes. + /// + /// RPC status code. + /// Resolved span for the Grpc status code. + public static ActivityStatusCode ResolveSpanStatusForGrpcStatusCode(int statusCode) + { + var status = ActivityStatusCode.Error; - if (typeof(StatusCanonicalCode).IsEnumDefined(statusCode)) + if (typeof(StatusCanonicalCode).IsEnumDefined(statusCode)) + { + status = ((StatusCanonicalCode)statusCode) switch { - status = ((StatusCanonicalCode)statusCode) switch - { - StatusCanonicalCode.Ok => ActivityStatusCode.Unset, - _ => ActivityStatusCode.Error, - }; - } - - return status; + StatusCanonicalCode.Ok => ActivityStatusCode.Unset, + _ => ActivityStatusCode.Error, + }; } + + return status; } } diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcClientDiagnosticListener.cs b/src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcClientDiagnosticListener.cs index 04183129d15..0804f57363b 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcClientDiagnosticListener.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcClientDiagnosticListener.cs @@ -1,159 +1,186 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif using System.Reflection; using OpenTelemetry.Context.Propagation; using OpenTelemetry.Instrumentation.Http; using OpenTelemetry.Trace; -using static OpenTelemetry.Internal.HttpSemanticConventionHelper; -namespace OpenTelemetry.Instrumentation.GrpcNetClient.Implementation +namespace OpenTelemetry.Instrumentation.GrpcNetClient.Implementation; + +internal sealed class GrpcClientDiagnosticListener : ListenerHandler { - internal sealed class GrpcClientDiagnosticListener : ListenerHandler + internal static readonly AssemblyName AssemblyName = typeof(GrpcClientDiagnosticListener).Assembly.GetName(); + internal static readonly string ActivitySourceName = AssemblyName.Name; + internal static readonly Version Version = AssemblyName.Version; + internal static readonly ActivitySource ActivitySource = new(ActivitySourceName, Version.ToString()); + + private const string OnStartEvent = "Grpc.Net.Client.GrpcOut.Start"; + private const string OnStopEvent = "Grpc.Net.Client.GrpcOut.Stop"; + + private static readonly PropertyFetcher StartRequestFetcher = new("Request"); + private static readonly PropertyFetcher StopResponseFetcher = new("Response"); + + private readonly GrpcClientTraceInstrumentationOptions options; + + public GrpcClientDiagnosticListener(GrpcClientTraceInstrumentationOptions options) + : base("Grpc.Net.Client") + { + this.options = options; + } + + public override void OnEventWritten(string name, object payload) { - internal static readonly AssemblyName AssemblyName = typeof(GrpcClientDiagnosticListener).Assembly.GetName(); - internal static readonly string ActivitySourceName = AssemblyName.Name; - internal static readonly Version Version = AssemblyName.Version; - internal static readonly ActivitySource ActivitySource = new(ActivitySourceName, Version.ToString()); + switch (name) + { + case OnStartEvent: + { + this.OnStartActivity(Activity.Current, payload); + } + + break; + case OnStopEvent: + { + this.OnStopActivity(Activity.Current, payload); + } - private const string OnStartEvent = "Grpc.Net.Client.GrpcOut.Start"; - private const string OnStopEvent = "Grpc.Net.Client.GrpcOut.Stop"; + break; + } + } - private readonly GrpcClientInstrumentationOptions options; - private readonly PropertyFetcher startRequestFetcher = new("Request"); - private readonly PropertyFetcher stopRequestFetcher = new("Response"); + public void OnStartActivity(Activity activity, object payload) + { + // The overall flow of what GrpcClient library does is as below: + // Activity.Start() + // DiagnosticSource.WriteEvent("Start", payload) + // DiagnosticSource.WriteEvent("Stop", payload) + // Activity.Stop() - private readonly HttpSemanticConvention httpSemanticConvention; + // This method is in the WriteEvent("Start", payload) path. + // By this time, samplers have already run and + // activity.IsAllDataRequested populated accordingly. - public GrpcClientDiagnosticListener(GrpcClientInstrumentationOptions options) - : base("Grpc.Net.Client") + if (Sdk.SuppressInstrumentation) { - this.options = options; + return; + } - this.httpSemanticConvention = GetSemanticConventionOptIn(); + // Ensure context propagation irrespective of sampling decision + if (!TryFetchRequest(payload, out HttpRequestMessage request)) + { + GrpcInstrumentationEventSource.Log.NullPayload(nameof(GrpcClientDiagnosticListener), nameof(this.OnStartActivity)); + return; } - public override void OnEventWritten(string name, object payload) + if (this.options.SuppressDownstreamInstrumentation) { - switch (name) - { - case OnStartEvent: - { - this.OnStartActivity(Activity.Current, payload); - } - - break; - case OnStopEvent: - { - this.OnStopActivity(Activity.Current, payload); - } - - break; - } + SuppressInstrumentationScope.Enter(); + + // If we are suppressing downstream instrumentation then inject + // context here. Grpc.Net.Client uses HttpClient, so + // SuppressDownstreamInstrumentation means that the + // OpenTelemetry instrumentation for HttpClient will not be + // invoked. + + // Note that HttpClient natively generates its own activity and + // propagates W3C trace context headers regardless of whether + // OpenTelemetry HttpClient instrumentation is invoked. + // Therefore, injecting here preserves more intuitive span + // parenting - i.e., the entry point span of a downstream + // service would be parented to the span generated by + // Grpc.Net.Client rather than the span generated natively by + // HttpClient. Injecting here also ensures that baggage is + // propagated to downstream services. + // Injecting context here also ensures that the configured + // propagator is used, as HttpClient by itself will only + // do TraceContext propagation. + var textMapPropagator = Propagators.DefaultTextMapPropagator; + textMapPropagator.Inject( + new PropagationContext(activity.Context, Baggage.Current), + request, + HttpRequestMessageContextPropagation.HeaderValueSetter); } - public void OnStartActivity(Activity activity, object payload) + if (activity.IsAllDataRequested) { - // The overall flow of what GrpcClient library does is as below: - // Activity.Start() - // DiagnosticSource.WriteEvent("Start", payload) - // DiagnosticSource.WriteEvent("Stop", payload) - // Activity.Stop() + ActivityInstrumentationHelper.SetActivitySourceProperty(activity, ActivitySource); + ActivityInstrumentationHelper.SetKindProperty(activity, ActivityKind.Client); - // This method is in the WriteEvent("Start", payload) path. - // By this time, samplers have already run and - // activity.IsAllDataRequested populated accordingly. + var grpcMethod = GrpcTagHelper.GetGrpcMethodFromActivity(activity); - if (Sdk.SuppressInstrumentation) - { - return; - } + activity.DisplayName = grpcMethod?.Trim('/'); - // Ensure context propagation irrespective of sampling decision - if (!this.startRequestFetcher.TryFetch(payload, out HttpRequestMessage request) || request == null) + activity.SetTag(SemanticConventions.AttributeRpcSystem, GrpcTagHelper.RpcSystemGrpc); + + if (GrpcTagHelper.TryParseRpcServiceAndRpcMethod(grpcMethod, out var rpcService, out var rpcMethod)) { - GrpcInstrumentationEventSource.Log.NullPayload(nameof(GrpcClientDiagnosticListener), nameof(this.OnStartActivity)); - return; + activity.SetTag(SemanticConventions.AttributeRpcService, rpcService); + activity.SetTag(SemanticConventions.AttributeRpcMethod, rpcMethod); + + // Remove the grpc.method tag added by the gRPC .NET library + activity.SetTag(GrpcTagHelper.GrpcMethodTagName, null); } - if (this.options.SuppressDownstreamInstrumentation) + var uriHostNameType = Uri.CheckHostName(request.RequestUri.Host); + + if (uriHostNameType == UriHostNameType.IPv4 || uriHostNameType == UriHostNameType.IPv6) { - SuppressInstrumentationScope.Enter(); - - // If we are suppressing downstream instrumentation then inject - // context here. Grpc.Net.Client uses HttpClient, so - // SuppressDownstreamInstrumentation means that the - // OpenTelemetry instrumentation for HttpClient will not be - // invoked. - - // Note that HttpClient natively generates its own activity and - // propagates W3C trace context headers regardless of whether - // OpenTelemetry HttpClient instrumentation is invoked. - // Therefore, injecting here preserves more intuitive span - // parenting - i.e., the entry point span of a downstream - // service would be parented to the span generated by - // Grpc.Net.Client rather than the span generated natively by - // HttpClient. Injecting here also ensures that baggage is - // propagated to downstream services. - // Injecting context here also ensures that the configured - // propagator is used, as HttpClient by itself will only - // do TraceContext propagation. - var textMapPropagator = Propagators.DefaultTextMapPropagator; - textMapPropagator.Inject( - new PropagationContext(activity.Context, Baggage.Current), - request, - HttpRequestMessageContextPropagation.HeaderValueSetter); + activity.SetTag(SemanticConventions.AttributeServerSocketAddress, request.RequestUri.Host); } - - if (activity.IsAllDataRequested) + else { - ActivityInstrumentationHelper.SetActivitySourceProperty(activity, ActivitySource); - ActivityInstrumentationHelper.SetKindProperty(activity, ActivityKind.Client); + activity.SetTag(SemanticConventions.AttributeServerAddress, request.RequestUri.Host); + } - var grpcMethod = GrpcTagHelper.GetGrpcMethodFromActivity(activity); + activity.SetTag(SemanticConventions.AttributeServerPort, request.RequestUri.Port); - activity.DisplayName = grpcMethod?.Trim('/'); + try + { + this.options.EnrichWithHttpRequestMessage?.Invoke(activity, request); + } + catch (Exception ex) + { + GrpcInstrumentationEventSource.Log.EnrichmentException(ex); + } + } - activity.SetTag(SemanticConventions.AttributeRpcSystem, GrpcTagHelper.RpcSystemGrpc); + // See https://github.com/grpc/grpc-dotnet/blob/ff1a07b90c498f259e6d9f4a50cdad7c89ecd3c0/src/Grpc.Net.Client/Internal/GrpcCall.cs#L1180-L1183 + // this makes sure that top-level properties on the payload object are always preserved. +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The event source guarantees that top level properties are preserved")] +#endif + static bool TryFetchRequest(object payload, out HttpRequestMessage request) + => StartRequestFetcher.TryFetch(payload, out request) && request != null; + } - if (GrpcTagHelper.TryParseRpcServiceAndRpcMethod(grpcMethod, out var rpcService, out var rpcMethod)) + public void OnStopActivity(Activity activity, object payload) + { + if (activity.IsAllDataRequested) + { + bool validConversion = GrpcTagHelper.TryGetGrpcStatusCodeFromActivity(activity, out int status); + if (validConversion) + { + if (activity.Status == ActivityStatusCode.Unset) { - activity.SetTag(SemanticConventions.AttributeRpcService, rpcService); - activity.SetTag(SemanticConventions.AttributeRpcMethod, rpcMethod); - - // Remove the grpc.method tag added by the gRPC .NET library - activity.SetTag(GrpcTagHelper.GrpcMethodTagName, null); + activity.SetStatus(GrpcTagHelper.ResolveSpanStatusForGrpcStatusCode(status)); } - var uriHostNameType = Uri.CheckHostName(request.RequestUri.Host); - if (uriHostNameType == UriHostNameType.IPv4 || uriHostNameType == UriHostNameType.IPv6) - { - activity.SetTag(SemanticConventions.AttributeNetPeerIp, request.RequestUri.Host); - } - else - { - activity.SetTag(SemanticConventions.AttributeNetPeerName, request.RequestUri.Host); - } + // setting rpc.grpc.status_code + activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, status); + } - activity.SetTag(SemanticConventions.AttributeNetPeerPort, request.RequestUri.Port); + // Remove the grpc.status_code tag added by the gRPC .NET library + activity.SetTag(GrpcTagHelper.GrpcStatusCodeTagName, null); + if (TryFetchResponse(payload, out HttpResponseMessage response)) + { try { - this.options.EnrichWithHttpRequestMessage?.Invoke(activity, request); + this.options.EnrichWithHttpResponseMessage?.Invoke(activity, response); } catch (Exception ex) { @@ -162,37 +189,12 @@ public void OnStartActivity(Activity activity, object payload) } } - public void OnStopActivity(Activity activity, object payload) - { - if (activity.IsAllDataRequested) - { - bool validConversion = GrpcTagHelper.TryGetGrpcStatusCodeFromActivity(activity, out int status); - if (validConversion) - { - if (activity.Status == ActivityStatusCode.Unset) - { - activity.SetStatus(GrpcTagHelper.ResolveSpanStatusForGrpcStatusCode(status)); - } - - // setting rpc.grpc.status_code - activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, status); - } - - // Remove the grpc.status_code tag added by the gRPC .NET library - activity.SetTag(GrpcTagHelper.GrpcStatusCodeTagName, null); - - if (this.stopRequestFetcher.TryFetch(payload, out HttpResponseMessage response) && response != null) - { - try - { - this.options.EnrichWithHttpResponseMessage?.Invoke(activity, response); - } - catch (Exception ex) - { - GrpcInstrumentationEventSource.Log.EnrichmentException(ex); - } - } - } - } + // See https://github.com/grpc/grpc-dotnet/blob/ff1a07b90c498f259e6d9f4a50cdad7c89ecd3c0/src/Grpc.Net.Client/Internal/GrpcCall.cs#L1180-L1183 + // this makes sure that top-level properties on the payload object are always preserved. +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The event source guarantees that top level properties are preserved")] +#endif + static bool TryFetchResponse(object payload, out HttpResponseMessage response) + => StopResponseFetcher.TryFetch(payload, out response) && response != null; } } diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcInstrumentationEventSource.cs b/src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcInstrumentationEventSource.cs index e16c76b8f3c..0bfbd380760 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcInstrumentationEventSource.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcInstrumentationEventSource.cs @@ -1,51 +1,52 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Tracing; using OpenTelemetry.Internal; -namespace OpenTelemetry.Instrumentation.GrpcNetClient.Implementation +namespace OpenTelemetry.Instrumentation.GrpcNetClient.Implementation; + +/// +/// EventSource events emitted from the project. +/// +[EventSource(Name = "OpenTelemetry-Instrumentation-Grpc")] +internal sealed class GrpcInstrumentationEventSource : EventSource { - /// - /// EventSource events emitted from the project. - /// - [EventSource(Name = "OpenTelemetry-Instrumentation-Grpc")] - internal sealed class GrpcInstrumentationEventSource : EventSource + public static GrpcInstrumentationEventSource Log = new(); + + [Event(1, Message = "Payload is NULL in event '{1}' from handler '{0}', span will not be recorded.", Level = EventLevel.Warning)] + public void NullPayload(string handlerName, string eventName) { - public static GrpcInstrumentationEventSource Log = new(); + this.WriteEvent(1, handlerName, eventName); + } - [Event(1, Message = "Payload is NULL in event '{1}' from handler '{0}', span will not be recorded.", Level = EventLevel.Warning)] - public void NullPayload(string handlerName, string eventName) + [NonEvent] + public void EnrichmentException(Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - this.WriteEvent(1, handlerName, eventName); + this.EnrichmentException(ex.ToInvariantString()); } + } - [NonEvent] - public void EnrichmentException(Exception ex) + [NonEvent] + public void UnknownErrorProcessingEvent(string handlerName, string eventName, Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.EnrichmentException(ex.ToInvariantString()); - } + this.UnknownErrorProcessingEvent(handlerName, eventName, ex.ToInvariantString()); } + } - [Event(2, Message = "Enrichment threw exception. Exception {0}.", Level = EventLevel.Error)] - public void EnrichmentException(string exception) - { - this.WriteEvent(2, exception); - } + [Event(2, Message = "Enrichment threw exception. Exception {0}.", Level = EventLevel.Error)] + public void EnrichmentException(string exception) + { + this.WriteEvent(2, exception); + } + + [Event(3, Message = "Unknown error processing event '{1}' from handler '{0}', Exception: {2}", Level = EventLevel.Error)] + public void UnknownErrorProcessingEvent(string handlerName, string eventName, string ex) + { + this.WriteEvent(3, handlerName, eventName, ex); } } diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/OpenTelemetry.Instrumentation.GrpcNetClient.csproj b/src/OpenTelemetry.Instrumentation.GrpcNetClient/OpenTelemetry.Instrumentation.GrpcNetClient.csproj index 11ef412972d..48d9c8e5e65 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/OpenTelemetry.Instrumentation.GrpcNetClient.csproj +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/OpenTelemetry.Instrumentation.GrpcNetClient.csproj @@ -1,9 +1,10 @@ - netstandard2.1;netstandard2.0 + $(TargetFrameworksForGrpcNetClientInstrumentation) gRPC for .NET client instrumentation for OpenTelemetry .NET $(PackageTags);distributed-tracing + Instrumentation.GrpcNetClient- true @@ -12,12 +13,18 @@ - - + + + - + + + + + + diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/README.md b/src/OpenTelemetry.Instrumentation.GrpcNetClient/README.md index bfb6dfbb701..eb6bdd12d92 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/README.md +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/README.md @@ -7,15 +7,16 @@ This is an [Instrumentation Library](https://github.com/open-telemetry/opentelem which instruments [Grpc.Net.Client](https://www.nuget.org/packages/Grpc.Net.Client) and collects traces about outgoing gRPC requests. -**Note: This component is based on the OpenTelemetry semantic conventions for -[traces](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/trace/semantic_conventions). +> [!CAUTION] +> This component is based on the OpenTelemetry semantic conventions for +[traces](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-spans.md). These conventions are [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/document-status.md), and hence, this package is a [pre-release](../../VERSIONING.md#pre-releases). Until a [stable version](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/telemetry-stability.md) is released, there can be breaking changes. You can track the progress from -[milestones](https://github.com/open-telemetry/opentelemetry-dotnet/milestone/23).** +[milestones](https://github.com/open-telemetry/opentelemetry-dotnet/milestone/23). ## Supported .NET Versions @@ -79,6 +80,11 @@ This instrumentation can be configured to change the default behavior by using ### SuppressDownstreamInstrumentation +> [!CAUTION] +> `SuppressDownstreamInstrumentation` no longer works when used in conjunction +with the `OpenTelemetry.Instrumentation.Http` package version `1.6.0` and greater. +This option may change or even be removed in a future release. + This option prevents downstream instrumentation from being invoked. Grpc.Net.Client is built on top of HttpClient. When instrumentation for both libraries is enabled, `SuppressDownstreamInstrumentation` prevents the diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/StatusCanonicalCode.cs b/src/OpenTelemetry.Instrumentation.GrpcNetClient/StatusCanonicalCode.cs index b4f3662ca36..4ddb1ec16cc 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/StatusCanonicalCode.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/StatusCanonicalCode.cs @@ -1,149 +1,135 @@ -// // 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. -// - -namespace OpenTelemetry.Instrumentation.GrpcNetClient +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Instrumentation.GrpcNetClient; + +/// +/// Canonical result code of span execution. +/// +/// +/// This follows the standard GRPC codes. +/// https://github.com/grpc/grpc/blob/master/doc/statuscodes.md. +/// +internal enum StatusCanonicalCode { /// - /// Canonical result code of span execution. - /// - /// - /// This follows the standard GRPC codes. - /// https://github.com/grpc/grpc/blob/master/doc/statuscodes.md. - /// - internal enum StatusCanonicalCode - { - /// - /// The operation completed successfully. - /// - Ok = 0, - - /// - /// The operation was cancelled (typically by the caller). - /// - Cancelled = 1, - - /// - /// Unknown error. An example of where this error may be returned is if a Status value received - /// from another address space belongs to an error-space that is not known in this address space. - /// Also errors raised by APIs that do not return enough error information may be converted to - /// this error. - /// - Unknown = 2, - - /// - /// Client specified an invalid argument. Note that this differs from FAILED_PRECONDITION. - /// INVALID_ARGUMENT indicates arguments that are problematic regardless of the state of the - /// system (e.g., a malformed file name). - /// - InvalidArgument = 3, - - /// - /// Deadline expired before operation could complete. For operations that change the state of the - /// system, this error may be returned even if the operation has completed successfully. For - /// example, a successful response from a server could have been delayed long enough for the - /// deadline to expire. - /// - DeadlineExceeded = 4, - - /// - /// Some requested entity (e.g., file or directory) was not found. - /// - NotFound = 5, - - /// - /// Some entity that we attempted to create (e.g., file or directory) already exists. - /// - AlreadyExists = 6, - - /// - /// The caller does not have permission to execute the specified operation. PERMISSION_DENIED - /// must not be used for rejections caused by exhausting some resource (use RESOURCE_EXHAUSTED - /// instead for those errors). PERMISSION_DENIED must not be used if the caller cannot be - /// identified (use UNAUTHENTICATED instead for those errors). - /// - PermissionDenied = 7, - - /// - /// Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system - /// is out of space. - /// - ResourceExhausted = 8, - - /// - /// Operation was rejected because the system is not in a state required for the operation's - /// execution. For example, directory to be deleted may be non-empty, an rmdir operation is - /// applied to a non-directory, etc. - /// A litmus test that may help a service implementor in deciding between FAILED_PRECONDITION, - /// ABORTED, and UNAVAILABLE: (a) Use UNAVAILABLE if the client can retry just the failing call. - /// (b) Use ABORTED if the client should retry at a higher-level (e.g., restarting a - /// read-modify-write sequence). (c) Use FAILED_PRECONDITION if the client should not retry until - /// the system state has been explicitly fixed. E.g., if an "rmdir" fails because the directory - /// is non-empty, FAILED_PRECONDITION should be returned since the client should not retry unless - /// they have first fixed up the directory by deleting files from it. - /// - FailedPrecondition = 9, - - /// - /// The operation was aborted, typically due to a concurrency issue like sequencer check - /// failures, transaction aborts, etc. - /// - Aborted = 10, - - /// - /// Operation was attempted past the valid range. E.g., seeking or reading past end of file. - /// - /// Unlike INVALID_ARGUMENT, this error indicates a problem that may be fixed if the system - /// state changes. For example, a 32-bit file system will generate INVALID_ARGUMENT if asked to - /// read at an offset that is not in the range [0,2^32-1], but it will generate OUT_OF_RANGE if - /// asked to read from an offset past the current file size. - /// - /// There is a fair bit of overlap between FAILED_PRECONDITION and OUT_OF_RANGE. We recommend - /// using OUT_OF_RANGE (the more specific error) when it applies so that callers who are - /// iterating through a space can easily look for an OUT_OF_RANGE error to detect when they are - /// done. - /// - OutOfRange = 11, - - /// - /// Operation is not implemented or not supported/enabled in this service. - /// - Unimplemented = 12, - - /// - /// Internal errors. Means some invariants expected by underlying system has been broken. If you - /// see one of these errors, something is very broken. - /// - Internal = 13, - - /// - /// The service is currently unavailable. This is a most likely a transient condition and may be - /// corrected by retrying with a backoff. - /// - /// See litmus test above for deciding between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE. - /// - Unavailable = 14, - - /// - /// Unrecoverable data loss or corruption. - /// - DataLoss = 15, - - /// - /// The request does not have valid authentication credentials for the operation. - /// - Unauthenticated = 16, - } + /// The operation completed successfully. + /// + Ok = 0, + + /// + /// The operation was cancelled (typically by the caller). + /// + Cancelled = 1, + + /// + /// Unknown error. An example of where this error may be returned is if a Status value received + /// from another address space belongs to an error-space that is not known in this address space. + /// Also errors raised by APIs that do not return enough error information may be converted to + /// this error. + /// + Unknown = 2, + + /// + /// Client specified an invalid argument. Note that this differs from FAILED_PRECONDITION. + /// INVALID_ARGUMENT indicates arguments that are problematic regardless of the state of the + /// system (e.g., a malformed file name). + /// + InvalidArgument = 3, + + /// + /// Deadline expired before operation could complete. For operations that change the state of the + /// system, this error may be returned even if the operation has completed successfully. For + /// example, a successful response from a server could have been delayed long enough for the + /// deadline to expire. + /// + DeadlineExceeded = 4, + + /// + /// Some requested entity (e.g., file or directory) was not found. + /// + NotFound = 5, + + /// + /// Some entity that we attempted to create (e.g., file or directory) already exists. + /// + AlreadyExists = 6, + + /// + /// The caller does not have permission to execute the specified operation. PERMISSION_DENIED + /// must not be used for rejections caused by exhausting some resource (use RESOURCE_EXHAUSTED + /// instead for those errors). PERMISSION_DENIED must not be used if the caller cannot be + /// identified (use UNAUTHENTICATED instead for those errors). + /// + PermissionDenied = 7, + + /// + /// Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system + /// is out of space. + /// + ResourceExhausted = 8, + + /// + /// Operation was rejected because the system is not in a state required for the operation's + /// execution. For example, directory to be deleted may be non-empty, an rmdir operation is + /// applied to a non-directory, etc. + /// A litmus test that may help a service implementor in deciding between FAILED_PRECONDITION, + /// ABORTED, and UNAVAILABLE: (a) Use UNAVAILABLE if the client can retry just the failing call. + /// (b) Use ABORTED if the client should retry at a higher-level (e.g., restarting a + /// read-modify-write sequence). (c) Use FAILED_PRECONDITION if the client should not retry until + /// the system state has been explicitly fixed. E.g., if an "rmdir" fails because the directory + /// is non-empty, FAILED_PRECONDITION should be returned since the client should not retry unless + /// they have first fixed up the directory by deleting files from it. + /// + FailedPrecondition = 9, + + /// + /// The operation was aborted, typically due to a concurrency issue like sequencer check + /// failures, transaction aborts, etc. + /// + Aborted = 10, + + /// + /// Operation was attempted past the valid range. E.g., seeking or reading past end of file. + /// + /// Unlike INVALID_ARGUMENT, this error indicates a problem that may be fixed if the system + /// state changes. For example, a 32-bit file system will generate INVALID_ARGUMENT if asked to + /// read at an offset that is not in the range [0,2^32-1], but it will generate OUT_OF_RANGE if + /// asked to read from an offset past the current file size. + /// + /// There is a fair bit of overlap between FAILED_PRECONDITION and OUT_OF_RANGE. We recommend + /// using OUT_OF_RANGE (the more specific error) when it applies so that callers who are + /// iterating through a space can easily look for an OUT_OF_RANGE error to detect when they are + /// done. + /// + OutOfRange = 11, + + /// + /// Operation is not implemented or not supported/enabled in this service. + /// + Unimplemented = 12, + + /// + /// Internal errors. Means some invariants expected by underlying system has been broken. If you + /// see one of these errors, something is very broken. + /// + Internal = 13, + + /// + /// The service is currently unavailable. This is a most likely a transient condition and may be + /// corrected by retrying with a backoff. + /// + /// See litmus test above for deciding between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE. + /// + Unavailable = 14, + + /// + /// Unrecoverable data loss or corruption. + /// + DataLoss = 15, + + /// + /// The request does not have valid authentication credentials for the operation. + /// + Unauthenticated = 16, } diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.GrpcNetClient/TracerProviderBuilderExtensions.cs index 00b5b55d505..aa87988869c 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/TracerProviderBuilderExtensions.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -20,63 +7,62 @@ using OpenTelemetry.Instrumentation.GrpcNetClient.Implementation; using OpenTelemetry.Internal; -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +/// +/// Extension methods to simplify registering of gRPC client +/// instrumentation. +/// +public static class TracerProviderBuilderExtensions { /// - /// Extension methods to simplify registering of gRPC client - /// instrumentation. + /// Enables gRPC client instrumentation. /// - public static class TracerProviderBuilderExtensions - { - /// - /// Enables gRPC client instrumentation. - /// - /// being configured. - /// The instance of to chain the calls. - public static TracerProviderBuilder AddGrpcClientInstrumentation(this TracerProviderBuilder builder) - => AddGrpcClientInstrumentation(builder, name: null, configure: null); + /// being configured. + /// The instance of to chain the calls. + public static TracerProviderBuilder AddGrpcClientInstrumentation(this TracerProviderBuilder builder) + => AddGrpcClientInstrumentation(builder, name: null, configure: null); - /// - /// Enables gRPC client instrumentation. - /// - /// being configured. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static TracerProviderBuilder AddGrpcClientInstrumentation( - this TracerProviderBuilder builder, - Action configure) - => AddGrpcClientInstrumentation(builder, name: null, configure); + /// + /// Enables gRPC client instrumentation. + /// + /// being configured. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static TracerProviderBuilder AddGrpcClientInstrumentation( + this TracerProviderBuilder builder, + Action configure) + => AddGrpcClientInstrumentation(builder, name: null, configure); - /// - /// Enables gRPC client instrumentation. - /// - /// being configured. - /// Name which is used when retrieving options. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static TracerProviderBuilder AddGrpcClientInstrumentation( - this TracerProviderBuilder builder, - string name, - Action configure) - { - Guard.ThrowIfNull(builder); + /// + /// Enables gRPC client instrumentation. + /// + /// being configured. + /// Name which is used when retrieving options. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static TracerProviderBuilder AddGrpcClientInstrumentation( + this TracerProviderBuilder builder, + string name, + Action configure) + { + Guard.ThrowIfNull(builder); - name ??= Options.DefaultName; + name ??= Options.DefaultName; - if (configure != null) - { - builder.ConfigureServices(services => services.Configure(name, configure)); - } + if (configure != null) + { + builder.ConfigureServices(services => services.Configure(name, configure)); + } - builder.AddSource(GrpcClientDiagnosticListener.ActivitySourceName); - builder.AddLegacySource("Grpc.Net.Client.GrpcOut"); + builder.AddSource(GrpcClientDiagnosticListener.ActivitySourceName); + builder.AddLegacySource("Grpc.Net.Client.GrpcOut"); - return builder.AddInstrumentation(sp => - { - var options = sp.GetRequiredService>().Get(name); + return builder.AddInstrumentation(sp => + { + var options = sp.GetRequiredService>().Get(name); - return new GrpcClientInstrumentation(options); - }); - } + return new GrpcClientInstrumentation(options); + }); } } diff --git a/src/OpenTelemetry.Instrumentation.Http/.publicApi/PublicAPI.Shipped.txt b/src/OpenTelemetry.Instrumentation.Http/.publicApi/PublicAPI.Shipped.txt new file mode 100644 index 00000000000..a082f2c5bec --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Http/.publicApi/PublicAPI.Shipped.txt @@ -0,0 +1,24 @@ +OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions +OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.EnrichWithException.get -> System.Action +OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.EnrichWithException.set -> void +OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.EnrichWithHttpRequestMessage.get -> System.Action +OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.EnrichWithHttpRequestMessage.set -> void +OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.EnrichWithHttpResponseMessage.get -> System.Action +OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.EnrichWithHttpResponseMessage.set -> void +OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.EnrichWithHttpWebRequest.get -> System.Action +OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.EnrichWithHttpWebRequest.set -> void +OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.EnrichWithHttpWebResponse.get -> System.Action +OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.EnrichWithHttpWebResponse.set -> void +OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.FilterHttpRequestMessage.get -> System.Func +OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.FilterHttpRequestMessage.set -> void +OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.FilterHttpWebRequest.get -> System.Func +OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.FilterHttpWebRequest.set -> void +OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.HttpClientTraceInstrumentationOptions() -> void +OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.RecordException.get -> bool +OpenTelemetry.Instrumentation.Http.HttpClientTraceInstrumentationOptions.RecordException.set -> void +OpenTelemetry.Metrics.HttpClientInstrumentationMeterProviderBuilderExtensions +OpenTelemetry.Trace.HttpClientInstrumentationTracerProviderBuilderExtensions +static OpenTelemetry.Metrics.HttpClientInstrumentationMeterProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder +static OpenTelemetry.Trace.HttpClientInstrumentationTracerProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder +static OpenTelemetry.Trace.HttpClientInstrumentationTracerProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configureHttpClientTraceInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder +static OpenTelemetry.Trace.HttpClientInstrumentationTracerProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureHttpClientTraceInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Extensions.Propagators/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.Http/.publicApi/PublicAPI.Unshipped.txt similarity index 100% rename from src/OpenTelemetry.Extensions.Propagators/.publicApi/net462/PublicAPI.Unshipped.txt rename to src/OpenTelemetry.Instrumentation.Http/.publicApi/PublicAPI.Unshipped.txt diff --git a/src/OpenTelemetry.Instrumentation.Http/.publicApi/net462/PublicAPI.Shipped.txt b/src/OpenTelemetry.Instrumentation.Http/.publicApi/net462/PublicAPI.Shipped.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/OpenTelemetry.Instrumentation.Http/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.Http/.publicApi/net462/PublicAPI.Unshipped.txt deleted file mode 100644 index 2dd55551b0c..00000000000 --- a/src/OpenTelemetry.Instrumentation.Http/.publicApi/net462/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,24 +0,0 @@ -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithException.get -> System.Action -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithException.set -> void -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpRequestMessage.get -> System.Action -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpRequestMessage.set -> void -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpResponseMessage.get -> System.Action -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpResponseMessage.set -> void -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpWebRequest.get -> System.Action -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpWebRequest.set -> void -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpWebResponse.get -> System.Action -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpWebResponse.set -> void -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.FilterHttpRequestMessage.get -> System.Func -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.FilterHttpRequestMessage.set -> void -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.FilterHttpWebRequest.get -> System.Func -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.FilterHttpWebRequest.set -> void -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.HttpClientInstrumentationOptions() -> void -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.RecordException.get -> bool -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.RecordException.set -> void -OpenTelemetry.Metrics.MeterProviderBuilderExtensions -OpenTelemetry.Trace.TracerProviderBuilderExtensions -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configureHttpClientInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureHttpClientInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Instrumentation.Http/.publicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Instrumentation.Http/.publicApi/netstandard2.0/PublicAPI.Shipped.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/OpenTelemetry.Instrumentation.Http/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.Http/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt deleted file mode 100644 index 2dd55551b0c..00000000000 --- a/src/OpenTelemetry.Instrumentation.Http/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,24 +0,0 @@ -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithException.get -> System.Action -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithException.set -> void -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpRequestMessage.get -> System.Action -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpRequestMessage.set -> void -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpResponseMessage.get -> System.Action -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpResponseMessage.set -> void -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpWebRequest.get -> System.Action -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpWebRequest.set -> void -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpWebResponse.get -> System.Action -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.EnrichWithHttpWebResponse.set -> void -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.FilterHttpRequestMessage.get -> System.Func -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.FilterHttpRequestMessage.set -> void -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.FilterHttpWebRequest.get -> System.Func -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.FilterHttpWebRequest.set -> void -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.HttpClientInstrumentationOptions() -> void -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.RecordException.get -> bool -OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.RecordException.set -> void -OpenTelemetry.Metrics.MeterProviderBuilderExtensions -OpenTelemetry.Trace.TracerProviderBuilderExtensions -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configureHttpClientInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureHttpClientInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Instrumentation.Http/AssemblyInfo.cs b/src/OpenTelemetry.Instrumentation.Http/AssemblyInfo.cs index 0ea3b92570f..d1d2362bb31 100644 --- a/src/OpenTelemetry.Instrumentation.Http/AssemblyInfo.cs +++ b/src/OpenTelemetry.Instrumentation.Http/AssemblyInfo.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Runtime.CompilerServices; diff --git a/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md index 51594927f13..b634f2b943a 100644 --- a/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md @@ -2,10 +2,211 @@ ## Unreleased -* Updated [Http Semantic Conventions](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.21.0/specification/trace/semantic_conventions/http.md). - This library can emit either old, new, or both attributes. Users can control - which attributes are emitted by setting the environment variable - `OTEL_SEMCONV_STABILITY_OPT_IN`. +## 1.7.1 + +Released 2024-Feb-09 + +* .NET Framework - fix description for `http.client.request.duration` metric. + ([#5234](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5234)) + +## 1.7.0 + +Released 2023-Dec-13 + +## 1.6.0 - First stable release of this library + +Released 2023-Dec-13 + +## 1.6.0-rc.1 + +Released 2023-Dec-01 + +* 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)) + +* Update activity DisplayName as per the specification. + ([#5078](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5078)) + +* Removed reference to `OpenTelemetry` package. This is a **breaking change** + for users relying on + [SuppressDownstreamInstrumentation](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src/OpenTelemetry.Instrumentation.GrpcNetClient#suppressdownstreaminstrumentation) + option in `OpenTelemetry.Instrumentation.GrpcNetClient`. For details, check + out this + [issue](https://github.com/open-telemetry/opentelemetry-dotnet/issues/5092). + ([#5077](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5077)) + +* **Breaking Change**: Renamed `HttpClientInstrumentationOptions` to + `HttpClientTraceInstrumentationOptions`. + ([#5109](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5109)) + +* **Breaking Change**: Removed `http.user_agent` tag from HttpClient activity. + ([#5110](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5110)) + +* `HttpWebRequest` : Introduced additional values for `error.type` tag on + activity and `http.client.request.duration` metric. + ([#5111](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5111)) + +## 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 + to restore this behavior. + ([#5025](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5025)) + +* Updated `http.request.method` to match specification guidelines. + * For activity, if the method does not belong to one of the [known + values](https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-spans.md#:~:text=http.request.method%20has%20the%20following%20list%20of%20well%2Dknown%20values) + then the request method will be set on an additional tag + `http.request.method.original` and `http.request.method` will be set to + `_OTHER`. + * For metrics, if the original method does not belong to one of the [known + values](https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-spans.md#:~:text=http.request.method%20has%20the%20following%20list%20of%20well%2Dknown%20values) + then `http.request.method` on `http.client.request.duration` metric will be + set to `_OTHER` + + `http.request.method` is set on `http.client.request.duration` metric or + activity when `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable is set to + `http` or `http/dup`. + ([#5003](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5003)) + +* An additional attribute `error.type` will be added to activity and + `http.client.request.duration` metric in case of failed requests as per the + [specification](https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md#common-attributes). + + Users moving to `net8.0` or newer frameworks from lower versions will see + difference in values in case of an exception. `net8.0` or newer frameworks add + the ability to further drill down the exceptions to a specific type through + [HttpRequestError](https://learn.microsoft.com/dotnet/api/system.net.http.httprequesterror?view=net-8.0) + enum. For lower versions, the individual types will be rolled in to a single + type. This could be a **breaking change** if alerts are set based on the values. + + The attribute will only be added when `OTEL_SEMCONV_STABILITY_OPT_IN` + environment variable is set to `http` or `http/dup`. + + ([#5005](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5005)) + ([#5034](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5034)) + +* 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 + +* Introduced a new metric for `HttpClient`, `http.client.request.duration` + measured in seconds. The OTel SDK (starting with version 1.6.0) + [applies custom histogram buckets](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4820) + for this metric to comply with the + [Semantic Convention for Http Metrics](https://github.com/open-telemetry/semantic-conventions/blob/2bad9afad58fbd6b33cc683d1ad1f006e35e4a5d/docs/http/http-metrics.md). + This new metric is only available for users who opt-in to the new + semantic convention by configuring the `OTEL_SEMCONV_STABILITY_OPT_IN` + environment variable to either `http` (to emit only the new metric) or + `http/dup` (to emit both the new and old metrics). + ([#4870](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4870)) + + * New metric: `http.client.request.duration` + * Unit: `s` (seconds) + * Histogram Buckets: `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` + * Old metric: `http.client.duration` + * Unit: `ms` (milliseconds) + * Histogram Buckets: `0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, + 5000, 7500, 10000` + + Note: The older `http.client.duration` metric and + `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable will eventually be + removed after the HTTP semantic conventions are marked stable. At which time + this instrumentation can publish a stable release. Refer to the specification + for more information regarding the new HTTP semantic conventions: + * [http-spans](https://github.com/open-telemetry/semantic-conventions/blob/2bad9afad58fbd6b33cc683d1ad1f006e35e4a5d/docs/http/http-spans.md) + * [http-metrics](https://github.com/open-telemetry/semantic-conventions/blob/2bad9afad58fbd6b33cc683d1ad1f006e35e4a5d/docs/http/http-metrics.md) + +* Added support for publishing `http.client.duration` & + `http.client.request.duration` metrics on .NET Framework for `HttpWebRequest`. + ([#4870](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4870)) + +* Following `HttpClient` metrics will now be enabled by default when targeting + `.NET8.0` framework or newer. + + * **Meter** : `System.Net.Http` + * `http.client.request.duration` + * `http.client.active_requests` + * `http.client.open_connections` + * `http.client.connection.duration` + * `http.client.request.time_in_queue` + + * **Meter** : `System.Net.NameResolution` + * `dns.lookups.duration` + + For details about each individual metric check [System.Net metrics + docs + page](https://learn.microsoft.com/dotnet/core/diagnostics/built-in-metrics-system-net). + + **NOTES**: + * When targeting `.NET8.0` framework or newer, `http.client.request.duration` metric + will only follow + [v1.22.0](https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-metrics.md#metric-httpclientrequestduration) + semantic conventions specification. Ability to switch behavior to older + conventions using `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable is + not available. + * Users can opt-out of metrics that are not required using + [views](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/metrics/customizing-the-sdk#drop-an-instrument). + + ([#4931](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4931)) + +* Added `url.scheme` attribute to `http.client.request.duration` metric. The + metric will be emitted when `OTEL_SEMCONV_STABILITY_OPT_IN` environment + variable is set to `http` or `http/dup`. + ([#4989](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4989)) + +* Updated description for `http.client.request.duration` metrics to match spec + 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 + +* The new HTTP and network semantic conventions can be opted in to by setting + the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable. This allows for a + transition period for users to experiment with the new semantic conventions + and adapt as necessary. The environment variable supports the following + values: + * `http` - emit the new, frozen (proposed for stable) HTTP and networking + attributes, and stop emitting the old experimental HTTP and networking + attributes that the instrumentation emitted previously. + * `http/dup` - emit both the old and the frozen (proposed for stable) HTTP + and networking attributes, allowing for a more seamless transition. + * The default behavior (in the absence of one of these values) is to continue + emitting the same HTTP and network semantic conventions that were emitted in + `1.5.0-beta.1`. + * Note: this option will eventually be removed after the new HTTP and + network semantic conventions are marked stable. At which time this + instrumentation can receive a stable release, and the old HTTP and + network semantic conventions will no longer be supported. Refer to the + specification for more information regarding the new HTTP and network + semantic conventions for both + [spans](https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md) + and + [metrics](https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-metrics.md). + ([#4538](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4538), + [#4639](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4639)) ## 1.5.0-beta.1 diff --git a/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentation.cs b/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentation.cs index 842cd0b9af3..80a84bb57c3 100644 --- a/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentation.cs +++ b/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentation.cs @@ -1,76 +1,63 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + using OpenTelemetry.Instrumentation.Http.Implementation; -namespace OpenTelemetry.Instrumentation.Http +namespace OpenTelemetry.Instrumentation.Http; + +/// +/// HttpClient instrumentation. +/// +internal sealed class HttpClientInstrumentation : IDisposable { - /// - /// HttpClient instrumentation. - /// - internal sealed class HttpClientInstrumentation : IDisposable + private static readonly HashSet ExcludedDiagnosticSourceEventsNet7OrGreater = new() { - private static readonly HashSet ExcludedDiagnosticSourceEventsNet7OrGreater = new() - { - "System.Net.Http.Request", - "System.Net.Http.Response", - "System.Net.Http.HttpRequestOut", - }; + "System.Net.Http.Request", + "System.Net.Http.Response", + "System.Net.Http.HttpRequestOut", + }; - private static readonly HashSet ExcludedDiagnosticSourceEvents = new() - { - "System.Net.Http.Request", - "System.Net.Http.Response", - }; + private static readonly HashSet ExcludedDiagnosticSourceEvents = new() + { + "System.Net.Http.Request", + "System.Net.Http.Response", + }; - private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; + private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; - private readonly Func isEnabled = (eventName, _, _) - => !ExcludedDiagnosticSourceEvents.Contains(eventName); + private readonly Func isEnabled = (eventName, _, _) + => !ExcludedDiagnosticSourceEvents.Contains(eventName); - private readonly Func isEnabledNet7OrGreater = (eventName, _, _) - => !ExcludedDiagnosticSourceEventsNet7OrGreater.Contains(eventName); + private readonly Func isEnabledNet7OrGreater = (eventName, _, _) + => !ExcludedDiagnosticSourceEventsNet7OrGreater.Contains(eventName); - /// - /// Initializes a new instance of the class. - /// - /// Configuration options for HTTP client instrumentation. - public HttpClientInstrumentation(HttpClientInstrumentationOptions options) + /// + /// Initializes a new instance of the class. + /// + /// Configuration options for HTTP client instrumentation. + public HttpClientInstrumentation(HttpClientTraceInstrumentationOptions options) + { + // For .NET7.0 activity will be created using activitySource. + // https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs + // However, in case when activity creation returns null (due to sampling) + // the framework will fall back to creating activity anyways due to active diagnostic source listener + // To prevent this, isEnabled is implemented which will return false always + // so that the sampler's decision is respected. + if (HttpHandlerDiagnosticListener.IsNet7OrGreater) { - // For .NET7.0 activity will be created using activitySource. - // https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs - // However, in case when activity creation returns null (due to sampling) - // the framework will fall back to creating activity anyways due to active diagnostic source listener - // To prevent this, isEnabled is implemented which will return false always - // so that the sampler's decision is respected. - if (HttpHandlerDiagnosticListener.IsNet7OrGreater) - { - this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(new HttpHandlerDiagnosticListener(options), this.isEnabledNet7OrGreater); - } - else - { - this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(new HttpHandlerDiagnosticListener(options), this.isEnabled); - } - - this.diagnosticSourceSubscriber.Subscribe(); + this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(new HttpHandlerDiagnosticListener(options), this.isEnabledNet7OrGreater, HttpInstrumentationEventSource.Log.UnknownErrorProcessingEvent); } - - /// - public void Dispose() + else { - this.diagnosticSourceSubscriber?.Dispose(); + this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(new HttpHandlerDiagnosticListener(options), this.isEnabled, HttpInstrumentationEventSource.Log.UnknownErrorProcessingEvent); } + + this.diagnosticSourceSubscriber.Subscribe(); + } + + /// + public void Dispose() + { + this.diagnosticSourceSubscriber?.Dispose(); } } diff --git a/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentationMeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentationMeterProviderBuilderExtensions.cs new file mode 100644 index 00000000000..24b9524696f --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentationMeterProviderBuilderExtensions.cs @@ -0,0 +1,49 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if !NET8_0_OR_GREATER +#if !NETFRAMEWORK +using OpenTelemetry.Instrumentation.Http; +#endif +using OpenTelemetry.Instrumentation.Http.Implementation; +#endif + +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Metrics; + +/// +/// Extension methods to simplify registering of HttpClient instrumentation. +/// +public static class HttpClientInstrumentationMeterProviderBuilderExtensions +{ + /// + /// Enables HttpClient instrumentation. + /// + /// being configured. + /// The instance of to chain the calls. + public static MeterProviderBuilder AddHttpClientInstrumentation( + this MeterProviderBuilder builder) + { + Guard.ThrowIfNull(builder); + +#if NET8_0_OR_GREATER + return builder + .AddMeter("System.Net.Http") + .AddMeter("System.Net.NameResolution"); +#else + // Note: Warm-up the status code and method mapping. + _ = TelemetryHelper.BoxedStatusCodes; + _ = RequestMethodHelper.KnownMethods; + +#if NETFRAMEWORK + builder.AddMeter(HttpWebRequestActivitySource.MeterName); +#else + builder.AddMeter(HttpHandlerMetricsDiagnosticListener.MeterName); + + builder.AddInstrumentation(new HttpClientMetrics()); +#endif + return builder; +#endif + } +} diff --git a/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentationOptions.cs deleted file mode 100644 index 1084bbdc44d..00000000000 --- a/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentationOptions.cs +++ /dev/null @@ -1,177 +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 System.Net; -#if NETFRAMEWORK -using System.Net.Http; -#endif -using System.Runtime.CompilerServices; -using OpenTelemetry.Instrumentation.Http.Implementation; - -namespace OpenTelemetry.Instrumentation.Http -{ - /// - /// Options for HttpClient instrumentation. - /// - public class HttpClientInstrumentationOptions - { - /// - /// Gets or sets a filter function that determines whether or not to - /// collect telemetry on a per request basis. - /// - /// - /// FilterHttpRequestMessage is only executed on .NET and .NET - /// Core runtimes. and on .NET and .NET Core are both implemented - /// using . - /// Notes: - /// - /// The return value for the filter function is interpreted as: - /// - /// If filter returns , the request is - /// collected. - /// If filter returns or throws an - /// exception the request is NOT collected. - /// - /// - /// - public Func FilterHttpRequestMessage { get; set; } - - /// - /// Gets or sets an action to enrich an with . - /// - /// - /// EnrichWithHttpRequestMessage is only executed on .NET and .NET - /// Core runtimes. and on .NET and .NET Core are both implemented - /// using . - /// - public Action EnrichWithHttpRequestMessage { get; set; } - - /// - /// Gets or sets an action to enrich an with . - /// - /// - /// EnrichWithHttpResponseMessage is only executed on .NET and .NET - /// Core runtimes. and on .NET and .NET Core are both implemented - /// using . - /// - public Action EnrichWithHttpResponseMessage { get; set; } - - /// - /// Gets or sets an action to enrich an with . - /// - /// - /// EnrichWithException is called for all runtimes. - /// - public Action EnrichWithException { get; set; } - - /// - /// Gets or sets a filter function that determines whether or not to - /// collect telemetry on a per request basis. - /// - /// - /// FilterHttpWebRequest is only executed on .NET Framework - /// runtimes. and - /// on .NET Framework are both implemented using . - /// Notes: - /// - /// The return value for the filter function is interpreted as: - /// - /// If filter returns , the request is - /// collected. - /// If filter returns or throws an - /// exception the request is NOT collected. - /// - /// - /// - public Func FilterHttpWebRequest { get; set; } - - /// - /// Gets or sets an action to enrich an with . - /// - /// - /// EnrichWithHttpWebRequest is only executed on .NET Framework - /// runtimes. and - /// on .NET Framework are both implemented using . - /// - public Action EnrichWithHttpWebRequest { get; set; } - - /// - /// Gets or sets an action to enrich an with . - /// - /// - /// EnrichWithHttpWebResponse is only executed on .NET Framework - /// runtimes. and - /// on .NET Framework are both implemented using . - /// - public Action EnrichWithHttpWebResponse { get; set; } - - /// - /// Gets or sets a value indicating whether exception will be recorded - /// as an or not. Default value: . - /// - /// - /// RecordException is supported on all runtimes. - /// For specification details see: . - /// - public bool RecordException { get; set; } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal bool EventFilterHttpRequestMessage(string activityName, object arg1) - { - try - { - return - this.FilterHttpRequestMessage == null || - !TryParseHttpRequestMessage(activityName, arg1, out HttpRequestMessage requestMessage) || - this.FilterHttpRequestMessage(requestMessage); - } - catch (Exception ex) - { - HttpInstrumentationEventSource.Log.RequestFilterException(ex); - return false; - } - } - - internal bool EventFilterHttpWebRequest(HttpWebRequest request) - { - try - { - return this.FilterHttpWebRequest?.Invoke(request) ?? true; - } - catch (Exception ex) - { - HttpInstrumentationEventSource.Log.RequestFilterException(ex); - return false; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryParseHttpRequestMessage(string activityName, object arg1, out HttpRequestMessage requestMessage) - { - return (requestMessage = arg1 as HttpRequestMessage) != null && activityName == "System.Net.Http.HttpRequestOut"; - } - } -} diff --git a/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentationTracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentationTracerProviderBuilderExtensions.cs new file mode 100644 index 00000000000..6015c183f2c --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentationTracerProviderBuilderExtensions.cs @@ -0,0 +1,104 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OpenTelemetry.Instrumentation.Http; +using OpenTelemetry.Instrumentation.Http.Implementation; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Trace; + +/// +/// Extension methods to simplify registering of HttpClient instrumentation. +/// +public static class HttpClientInstrumentationTracerProviderBuilderExtensions +{ + /// + /// Enables HttpClient instrumentation. + /// + /// being configured. + /// The instance of to chain the calls. + public static TracerProviderBuilder AddHttpClientInstrumentation(this TracerProviderBuilder builder) + => AddHttpClientInstrumentation(builder, name: null, configureHttpClientTraceInstrumentationOptions: null); + + /// + /// Enables HttpClient instrumentation. + /// + /// being configured. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static TracerProviderBuilder AddHttpClientInstrumentation( + this TracerProviderBuilder builder, + Action configureHttpClientTraceInstrumentationOptions) + => AddHttpClientInstrumentation(builder, name: null, configureHttpClientTraceInstrumentationOptions); + + /// + /// Enables HttpClient instrumentation. + /// + /// being configured. + /// Name which is used when retrieving options. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static TracerProviderBuilder AddHttpClientInstrumentation( + this TracerProviderBuilder builder, + string name, + Action configureHttpClientTraceInstrumentationOptions) + { + Guard.ThrowIfNull(builder); + + // Note: Warm-up the status code and method mapping. + _ = TelemetryHelper.BoxedStatusCodes; + _ = RequestMethodHelper.KnownMethods; + + name ??= Options.DefaultName; + + builder.ConfigureServices(services => + { + if (configureHttpClientTraceInstrumentationOptions != null) + { + services.Configure(name, configureHttpClientTraceInstrumentationOptions); + } + }); + +#if NETFRAMEWORK + builder.AddSource(HttpWebRequestActivitySource.ActivitySourceName); + + if (builder is IDeferredTracerProviderBuilder deferredTracerProviderBuilder) + { + deferredTracerProviderBuilder.Configure((sp, builder) => + { + var options = sp.GetRequiredService>().Get(name); + + HttpWebRequestActivitySource.TracingOptions = options; + }); + } +#else + AddHttpClientInstrumentationSource(builder); + + builder.AddInstrumentation(sp => + { + var options = sp.GetRequiredService>().Get(name); + + return new HttpClientInstrumentation(options); + }); +#endif + return builder; + } + +#if !NETFRAMEWORK + internal static void AddHttpClientInstrumentationSource( + this TracerProviderBuilder builder) + { + if (HttpHandlerDiagnosticListener.IsNet7OrGreater) + { + builder.AddSource(HttpHandlerDiagnosticListener.HttpClientActivitySourceName); + } + else + { + builder.AddSource(HttpHandlerDiagnosticListener.ActivitySourceName); + builder.AddLegacySource("System.Net.Http.HttpRequestOut"); + } + } +#endif +} diff --git a/src/OpenTelemetry.Instrumentation.Http/HttpClientMetrics.cs b/src/OpenTelemetry.Instrumentation.Http/HttpClientMetrics.cs index 3bf57029d8a..f3773ed8fdc 100644 --- a/src/OpenTelemetry.Instrumentation.Http/HttpClientMetrics.cs +++ b/src/OpenTelemetry.Instrumentation.Http/HttpClientMetrics.cs @@ -1,61 +1,41 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -using System.Diagnostics.Metrics; -using System.Reflection; using OpenTelemetry.Instrumentation.Http.Implementation; -namespace OpenTelemetry.Instrumentation.Http +namespace OpenTelemetry.Instrumentation.Http; + +/// +/// HttpClient instrumentation. +/// +internal sealed class HttpClientMetrics : IDisposable { - /// - /// HttpClient instrumentation. - /// - internal sealed class HttpClientMetrics : IDisposable + private static readonly HashSet ExcludedDiagnosticSourceEvents = new() { - internal static readonly AssemblyName AssemblyName = typeof(HttpClientMetrics).Assembly.GetName(); - internal static readonly string InstrumentationName = AssemblyName.Name; - internal static readonly string InstrumentationVersion = AssemblyName.Version.ToString(); + "System.Net.Http.Request", + "System.Net.Http.Response", + }; - private static readonly HashSet ExcludedDiagnosticSourceEvents = new() - { - "System.Net.Http.Request", - "System.Net.Http.Response", - }; + private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; - private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; - private readonly Meter meter; + private readonly Func isEnabled = (activityName, obj1, obj2) + => !ExcludedDiagnosticSourceEvents.Contains(activityName); - private readonly Func isEnabled = (activityName, obj1, obj2) - => !ExcludedDiagnosticSourceEvents.Contains(activityName); - - /// - /// Initializes a new instance of the class. - /// - public HttpClientMetrics() - { - this.meter = new Meter(InstrumentationName, InstrumentationVersion); - this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(new HttpHandlerMetricsDiagnosticListener("HttpHandlerDiagnosticListener", this.meter), this.isEnabled); - this.diagnosticSourceSubscriber.Subscribe(); - } + /// + /// Initializes a new instance of the class. + /// + public HttpClientMetrics() + { + this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber( + new HttpHandlerMetricsDiagnosticListener("HttpHandlerDiagnosticListener"), + this.isEnabled, + HttpInstrumentationEventSource.Log.UnknownErrorProcessingEvent); + this.diagnosticSourceSubscriber.Subscribe(); + } - /// - public void Dispose() - { - this.diagnosticSourceSubscriber?.Dispose(); - this.meter?.Dispose(); - } + /// + public void Dispose() + { + this.diagnosticSourceSubscriber?.Dispose(); } } diff --git a/src/OpenTelemetry.Instrumentation.Http/HttpClientTraceInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.Http/HttpClientTraceInstrumentationOptions.cs new file mode 100644 index 00000000000..95bb6dab7b0 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Http/HttpClientTraceInstrumentationOptions.cs @@ -0,0 +1,163 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using System.Net; +#if NETFRAMEWORK +using System.Net.Http; +#endif +using System.Runtime.CompilerServices; +using OpenTelemetry.Instrumentation.Http.Implementation; + +namespace OpenTelemetry.Instrumentation.Http; + +/// +/// Options for HttpClient instrumentation. +/// +public class HttpClientTraceInstrumentationOptions +{ + /// + /// Gets or sets a filter function that determines whether or not to + /// collect telemetry on a per request basis. + /// + /// + /// FilterHttpRequestMessage is only executed on .NET and .NET + /// Core runtimes. and on .NET and .NET Core are both implemented + /// using . + /// Notes: + /// + /// The return value for the filter function is interpreted as: + /// + /// If filter returns , the request is + /// collected. + /// If filter returns or throws an + /// exception the request is NOT collected. + /// + /// + /// + public Func FilterHttpRequestMessage { get; set; } + + /// + /// Gets or sets an action to enrich an with . + /// + /// + /// EnrichWithHttpRequestMessage is only executed on .NET and .NET + /// Core runtimes. and on .NET and .NET Core are both implemented + /// using . + /// + public Action EnrichWithHttpRequestMessage { get; set; } + + /// + /// Gets or sets an action to enrich an with . + /// + /// + /// EnrichWithHttpResponseMessage is only executed on .NET and .NET + /// Core runtimes. and on .NET and .NET Core are both implemented + /// using . + /// + public Action EnrichWithHttpResponseMessage { get; set; } + + /// + /// Gets or sets an action to enrich an with . + /// + /// + /// EnrichWithException is called for all runtimes. + /// + public Action EnrichWithException { get; set; } + + /// + /// Gets or sets a filter function that determines whether or not to + /// collect telemetry on a per request basis. + /// + /// + /// FilterHttpWebRequest is only executed on .NET Framework + /// runtimes. and + /// on .NET Framework are both implemented using . + /// Notes: + /// + /// The return value for the filter function is interpreted as: + /// + /// If filter returns , the request is + /// collected. + /// If filter returns or throws an + /// exception the request is NOT collected. + /// + /// + /// + public Func FilterHttpWebRequest { get; set; } + + /// + /// Gets or sets an action to enrich an with . + /// + /// + /// EnrichWithHttpWebRequest is only executed on .NET Framework + /// runtimes. and + /// on .NET Framework are both implemented using . + /// + public Action EnrichWithHttpWebRequest { get; set; } + + /// + /// Gets or sets an action to enrich an with . + /// + /// + /// EnrichWithHttpWebResponse is only executed on .NET Framework + /// runtimes. and + /// on .NET Framework are both implemented using . + /// + public Action EnrichWithHttpWebResponse { get; set; } + + /// + /// Gets or sets a value indicating whether exception will be recorded + /// as an or not. Default value: . + /// + /// + /// RecordException is supported on all runtimes. + /// For specification details see: . + /// + public bool RecordException { get; set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool EventFilterHttpRequestMessage(string activityName, object arg1) + { + try + { + return + this.FilterHttpRequestMessage == null || + !TryParseHttpRequestMessage(activityName, arg1, out HttpRequestMessage requestMessage) || + this.FilterHttpRequestMessage(requestMessage); + } + catch (Exception ex) + { + HttpInstrumentationEventSource.Log.RequestFilterException(ex); + return false; + } + } + + internal bool EventFilterHttpWebRequest(HttpWebRequest request) + { + try + { + return this.FilterHttpWebRequest?.Invoke(request) ?? true; + } + catch (Exception ex) + { + HttpInstrumentationEventSource.Log.RequestFilterException(ex); + return false; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryParseHttpRequestMessage(string activityName, object arg1, out HttpRequestMessage requestMessage) + { + return (requestMessage = arg1 as HttpRequestMessage) != null && activityName == "System.Net.Http.HttpRequestOut"; + } +} diff --git a/src/OpenTelemetry.Instrumentation.Http/HttpRequestMessageContextPropagation.cs b/src/OpenTelemetry.Instrumentation.Http/HttpRequestMessageContextPropagation.cs index 3f8d3ece0e3..91d2a5c18a7 100644 --- a/src/OpenTelemetry.Instrumentation.Http/HttpRequestMessageContextPropagation.cs +++ b/src/OpenTelemetry.Instrumentation.Http/HttpRequestMessageContextPropagation.cs @@ -1,41 +1,17 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #if NETFRAMEWORK using System.Net.Http; #endif -namespace OpenTelemetry.Instrumentation.Http +namespace OpenTelemetry.Instrumentation.Http; + +internal static class HttpRequestMessageContextPropagation { - internal static class HttpRequestMessageContextPropagation + internal static Action HeaderValueSetter => (request, name, value) => { - internal static Func> HeaderValuesGetter => (request, name) => - { - if (request.Headers.TryGetValues(name, out var values)) - { - return values; - } - - return null; - }; - - internal static Action HeaderValueSetter => (request, name, value) => - { - request.Headers.Remove(name); - request.Headers.Add(name, value); - }; - } + request.Headers.Remove(name); + request.Headers.Add(name, value); + }; } diff --git a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs index 7197d772f1f..e0625b420db 100644 --- a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs +++ b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs @@ -1,303 +1,340 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif #if NETFRAMEWORK using System.Net.Http; #endif using System.Reflection; using OpenTelemetry.Context.Propagation; +using OpenTelemetry.Internal; using OpenTelemetry.Trace; -using static OpenTelemetry.Internal.HttpSemanticConventionHelper; -namespace OpenTelemetry.Instrumentation.Http.Implementation +namespace OpenTelemetry.Instrumentation.Http.Implementation; + +internal sealed class HttpHandlerDiagnosticListener : ListenerHandler { - internal sealed class HttpHandlerDiagnosticListener : ListenerHandler + internal static readonly AssemblyName AssemblyName = typeof(HttpHandlerDiagnosticListener).Assembly.GetName(); + internal static readonly bool IsNet7OrGreater; + + // https://github.com/dotnet/runtime/blob/7d034ddbbbe1f2f40c264b323b3ed3d6b3d45e9a/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L19 + internal static readonly string HttpClientActivitySourceName = "System.Net.Http"; + internal static readonly string ActivitySourceName = AssemblyName.Name + ".HttpClient"; + internal static readonly Version Version = AssemblyName.Version; + internal static readonly ActivitySource ActivitySource = new(ActivitySourceName, Version.ToString()); + + private const string OnStartEvent = "System.Net.Http.HttpRequestOut.Start"; + private const string OnStopEvent = "System.Net.Http.HttpRequestOut.Stop"; + private const string OnUnhandledExceptionEvent = "System.Net.Http.Exception"; + + private static readonly PropertyFetcher StartRequestFetcher = new("Request"); + private static readonly PropertyFetcher StopResponseFetcher = new("Response"); + private static readonly PropertyFetcher StopExceptionFetcher = new("Exception"); + private static readonly PropertyFetcher StopRequestStatusFetcher = new("RequestTaskStatus"); + private readonly HttpClientTraceInstrumentationOptions options; + + static HttpHandlerDiagnosticListener() + { + try + { + IsNet7OrGreater = typeof(HttpClient).Assembly.GetName().Version.Major >= 7; + } + catch (Exception) + { + IsNet7OrGreater = false; + } + } + + public HttpHandlerDiagnosticListener(HttpClientTraceInstrumentationOptions options) + : base("HttpHandlerDiagnosticListener") + { + this.options = options; + } + + public override void OnEventWritten(string name, object payload) { - internal static readonly AssemblyName AssemblyName = typeof(HttpHandlerDiagnosticListener).Assembly.GetName(); - internal static readonly bool IsNet7OrGreater; + switch (name) + { + case OnStartEvent: + { + this.OnStartActivity(Activity.Current, payload); + } - // https://github.com/dotnet/runtime/blob/7d034ddbbbe1f2f40c264b323b3ed3d6b3d45e9a/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L19 - internal static readonly string HttpClientActivitySourceName = "System.Net.Http"; - internal static readonly string ActivitySourceName = AssemblyName.Name + ".HttpClient"; - internal static readonly Version Version = AssemblyName.Version; - internal static readonly ActivitySource ActivitySource = new(ActivitySourceName, Version.ToString()); + break; + case OnStopEvent: + { + this.OnStopActivity(Activity.Current, payload); + } + + break; + case OnUnhandledExceptionEvent: + { + this.OnException(Activity.Current, payload); + } - private const string OnStartEvent = "System.Net.Http.HttpRequestOut.Start"; - private const string OnStopEvent = "System.Net.Http.HttpRequestOut.Stop"; - private const string OnUnhandledExceptionEvent = "System.Net.Http.Exception"; + break; + } + } - private readonly PropertyFetcher startRequestFetcher = new("Request"); - private readonly PropertyFetcher stopResponseFetcher = new("Response"); - private readonly PropertyFetcher stopExceptionFetcher = new("Exception"); - private readonly PropertyFetcher stopRequestStatusFetcher = new("RequestTaskStatus"); - private readonly HttpClientInstrumentationOptions options; + public void OnStartActivity(Activity activity, object payload) + { + // The overall flow of what HttpClient library does is as below: + // Activity.Start() + // DiagnosticSource.WriteEvent("Start", payload) + // DiagnosticSource.WriteEvent("Stop", payload) + // Activity.Stop() - private readonly HttpSemanticConvention httpSemanticConvention; + // This method is in the WriteEvent("Start", payload) path. + // By this time, samplers have already run and + // activity.IsAllDataRequested populated accordingly. - static HttpHandlerDiagnosticListener() + if (!TryFetchRequest(payload, out HttpRequestMessage request)) { - try - { - IsNet7OrGreater = typeof(HttpClient).Assembly.GetName().Version.Major >= 7; - } - catch (Exception) - { - IsNet7OrGreater = false; - } + HttpInstrumentationEventSource.Log.NullPayload(nameof(HttpHandlerDiagnosticListener), nameof(this.OnStartActivity)); + return; } - public HttpHandlerDiagnosticListener(HttpClientInstrumentationOptions options) - : base("HttpHandlerDiagnosticListener") + // Propagate context irrespective of sampling decision + var textMapPropagator = Propagators.DefaultTextMapPropagator; + if (textMapPropagator is not TraceContextPropagator) { - this.options = options; + textMapPropagator.Inject(new PropagationContext(activity.Context, Baggage.Current), request, HttpRequestMessageContextPropagation.HeaderValueSetter); + } - this.httpSemanticConvention = GetSemanticConventionOptIn(); + // For .NET7.0 or higher versions, activity is created using activity source. + // However the framework will fallback to creating activity if the sampler's decision is to drop and there is a active diagnostic listener. + // To prevent processing such activities we first check the source name to confirm if it was created using + // activity source or not. + if (IsNet7OrGreater && string.IsNullOrEmpty(activity.Source.Name)) + { + activity.IsAllDataRequested = false; } - public override void OnEventWritten(string name, object payload) + // enrich Activity from payload only if sampling decision + // is favorable. + if (activity.IsAllDataRequested) { - switch (name) + try { - case OnStartEvent: - { - this.OnStartActivity(Activity.Current, payload); - } - - break; - case OnStopEvent: - { - this.OnStopActivity(Activity.Current, payload); - } + if (this.options.EventFilterHttpRequestMessage(activity.OperationName, request) == false) + { + HttpInstrumentationEventSource.Log.RequestIsFilteredOut(activity.OperationName); + activity.IsAllDataRequested = false; + activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; + return; + } + } + catch (Exception ex) + { + HttpInstrumentationEventSource.Log.RequestFilterException(ex); + activity.IsAllDataRequested = false; + activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; + return; + } - break; - case OnUnhandledExceptionEvent: - { - this.OnException(Activity.Current, payload); - } + RequestMethodHelper.SetHttpClientActivityDisplayName(activity, request.Method.Method); - break; + if (!IsNet7OrGreater) + { + ActivityInstrumentationHelper.SetActivitySourceProperty(activity, ActivitySource); + ActivityInstrumentationHelper.SetKindProperty(activity, ActivityKind.Client); } - } - public void OnStartActivity(Activity activity, object payload) - { - // The overall flow of what HttpClient library does is as below: - // Activity.Start() - // DiagnosticSource.WriteEvent("Start", payload) - // DiagnosticSource.WriteEvent("Stop", payload) - // Activity.Stop() - - // This method is in the WriteEvent("Start", payload) path. - // By this time, samplers have already run and - // activity.IsAllDataRequested populated accordingly. + // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md + RequestMethodHelper.SetHttpMethodTag(activity, request.Method.Method); - if (Sdk.SuppressInstrumentation) + activity.SetTag(SemanticConventions.AttributeServerAddress, request.RequestUri.Host); + if (!request.RequestUri.IsDefaultPort) { - return; + activity.SetTag(SemanticConventions.AttributeServerPort, request.RequestUri.Port); } - if (!this.startRequestFetcher.TryFetch(payload, out HttpRequestMessage request) || request == null) + activity.SetTag(SemanticConventions.AttributeUrlFull, HttpTagHelper.GetUriTagValueFromRequestUri(request.RequestUri)); + + try { - HttpInstrumentationEventSource.Log.NullPayload(nameof(HttpHandlerDiagnosticListener), nameof(this.OnStartActivity)); - return; + this.options.EnrichWithHttpRequestMessage?.Invoke(activity, request); } - - // Propagate context irrespective of sampling decision - var textMapPropagator = Propagators.DefaultTextMapPropagator; - if (textMapPropagator is not TraceContextPropagator) + catch (Exception ex) { - textMapPropagator.Inject(new PropagationContext(activity.Context, Baggage.Current), request, HttpRequestMessageContextPropagation.HeaderValueSetter); + HttpInstrumentationEventSource.Log.EnrichmentException(ex); } + } - // For .NET7.0 or higher versions, activity is created using activity source. - // However the framework will fallback to creating activity if the sampler's decision is to drop and there is a active diagnostic listener. - // To prevent processing such activities we first check the source name to confirm if it was created using - // activity source or not. - if (IsNet7OrGreater && string.IsNullOrEmpty(activity.Source.Name)) + // The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved. + // see https://github.com/dotnet/runtime/blob/f9246538e3d49b90b0e9128d7b1defef57cd6911/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325 +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The event source guarantees that top-level properties are preserved")] +#endif + static bool TryFetchRequest(object payload, out HttpRequestMessage request) + { + if (!StartRequestFetcher.TryFetch(payload, out request) || request == null) { - activity.IsAllDataRequested = false; + return false; } - // enrich Activity from payload only if sampling decision - // is favorable. - if (activity.IsAllDataRequested) + return true; + } + } + + public void OnStopActivity(Activity activity, object payload) + { + if (activity.IsAllDataRequested) + { + var requestTaskStatus = GetRequestStatus(payload); + + ActivityStatusCode currentStatusCode = activity.Status; + if (requestTaskStatus != TaskStatus.RanToCompletion) { - try + if (requestTaskStatus == TaskStatus.Canceled) { - if (this.options.EventFilterHttpRequestMessage(activity.OperationName, request) == false) + if (currentStatusCode == ActivityStatusCode.Unset) { - HttpInstrumentationEventSource.Log.RequestIsFilteredOut(activity.OperationName); - activity.IsAllDataRequested = false; - activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; - return; + activity.SetStatus(ActivityStatusCode.Error); } } - catch (Exception ex) + else if (requestTaskStatus != TaskStatus.Faulted) { - HttpInstrumentationEventSource.Log.RequestFilterException(ex); - activity.IsAllDataRequested = false; - activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; - return; - } - - activity.DisplayName = HttpTagHelper.GetOperationNameForHttpMethod(request.Method); - - if (!IsNet7OrGreater) - { - ActivityInstrumentationHelper.SetActivitySourceProperty(activity, ActivitySource); - 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.httpSemanticConvention.HasFlag(HttpSemanticConvention.Old)) - { - 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) + if (currentStatusCode == ActivityStatusCode.Unset) { - activity.SetTag(SemanticConventions.AttributeNetPeerPort, request.RequestUri.Port); + // Faults are handled in OnException and should already have a span.Status of Error w/ Description. + activity.SetStatus(ActivityStatusCode.Error); } - - activity.SetTag(SemanticConventions.AttributeHttpUrl, HttpTagHelper.GetUriTagValueFromRequestUri(request.RequestUri)); - activity.SetTag(SemanticConventions.AttributeHttpFlavor, HttpTagHelper.GetFlavorTagValueFromProtocolVersion(request.Version)); } + } - // see the spec https://github.com/open-telemetry/opentelemetry-specification/blob/v1.21.0/specification/trace/semantic_conventions/http.md - if (this.httpSemanticConvention.HasFlag(HttpSemanticConvention.New)) + if (TryFetchResponse(payload, out HttpResponseMessage response)) + { + if (currentStatusCode == ActivityStatusCode.Unset) { - activity.SetTag(SemanticConventions.AttributeUrlScheme, request.RequestUri.Scheme); - activity.SetTag(SemanticConventions.AttributeHttpRequestMethod, HttpTagHelper.GetNameForHttpMethod(request.Method)); - 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.GetFlavorTagValueFromProtocolVersion(request.Version)); + activity.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode(activity.Kind, (int)response.StatusCode)); + } - if (request.Headers.TryGetValues("User-Agent", out var userAgentValues)) - { - var userAgent = userAgentValues.FirstOrDefault(); - if (!string.IsNullOrEmpty(userAgent)) - { - activity.SetTag(SemanticConventions.AttributeHttpUserAgent, userAgent); - } - } + activity.SetTag(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetProtocolVersionString(response.Version)); + activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)); + if (activity.Status == ActivityStatusCode.Error) + { + activity.SetTag(SemanticConventions.AttributeErrorType, TelemetryHelper.GetStatusCodeString(response.StatusCode)); } try { - this.options.EnrichWithHttpRequestMessage?.Invoke(activity, request); + this.options.EnrichWithHttpResponseMessage?.Invoke(activity, response); } catch (Exception ex) { HttpInstrumentationEventSource.Log.EnrichmentException(ex); } } + + // The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved. + // see https://github.com/dotnet/runtime/blob/f9246538e3d49b90b0e9128d7b1defef57cd6911/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325 +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The event source guarantees that top-level properties are preserved")] +#endif + static TaskStatus GetRequestStatus(object payload) + { + // requestTaskStatus (type is TaskStatus) is a non-nullable enum so we don't need to have a null check here. + // See: https://github.com/dotnet/runtime/blob/79c021d65c280020246d1035b0e87ae36f2d36a9/src/libraries/System.Net.Http/src/HttpDiagnosticsGuide.md?plain=1#L69 + _ = StopRequestStatusFetcher.TryFetch(payload, out var requestTaskStatus); + + return requestTaskStatus; + } } - public void OnStopActivity(Activity activity, object payload) + // The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved. + // see https://github.com/dotnet/runtime/blob/f9246538e3d49b90b0e9128d7b1defef57cd6911/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325 +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The event source guarantees that top-level properties are preserved")] +#endif + static bool TryFetchResponse(object payload, out HttpResponseMessage response) { - if (activity.IsAllDataRequested) + if (StopResponseFetcher.TryFetch(payload, out response) && response != null) { - // https://github.com/dotnet/runtime/blob/master/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs - // requestTaskStatus is not null - _ = this.stopRequestStatusFetcher.TryFetch(payload, out var requestTaskStatus); + return true; + } - ActivityStatusCode currentStatusCode = activity.Status; - if (requestTaskStatus != TaskStatus.RanToCompletion) - { - if (requestTaskStatus == TaskStatus.Canceled) - { - if (currentStatusCode == ActivityStatusCode.Unset) - { - activity.SetStatus(ActivityStatusCode.Error); - } - } - else if (requestTaskStatus != TaskStatus.Faulted) - { - if (currentStatusCode == ActivityStatusCode.Unset) - { - // Faults are handled in OnException and should already have a span.Status of Error w/ Description. - activity.SetStatus(ActivityStatusCode.Error); - } - } - } + return false; + } + } - if (this.stopResponseFetcher.TryFetch(payload, out HttpResponseMessage response) && response != null) - { - if (this.httpSemanticConvention.HasFlag(HttpSemanticConvention.Old)) - { - activity.SetTag(SemanticConventions.AttributeHttpStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)); - } + public void OnException(Activity activity, object payload) + { + if (activity.IsAllDataRequested) + { + if (!TryFetchException(payload, out Exception exc)) + { + HttpInstrumentationEventSource.Log.NullPayload(nameof(HttpHandlerDiagnosticListener), nameof(this.OnException)); + return; + } - if (this.httpSemanticConvention.HasFlag(HttpSemanticConvention.New)) - { - activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)); - } + activity.SetTag(SemanticConventions.AttributeErrorType, GetErrorType(exc)); - if (currentStatusCode == ActivityStatusCode.Unset) - { - activity.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode(activity.Kind, (int)response.StatusCode)); - } + if (this.options.RecordException) + { + activity.RecordException(exc); + } - try - { - this.options.EnrichWithHttpResponseMessage?.Invoke(activity, response); - } - catch (Exception ex) - { - HttpInstrumentationEventSource.Log.EnrichmentException(ex); - } - } + if (exc is HttpRequestException) + { + activity.SetStatus(ActivityStatusCode.Error); + } + + try + { + this.options.EnrichWithException?.Invoke(activity, exc); + } + catch (Exception ex) + { + HttpInstrumentationEventSource.Log.EnrichmentException(ex); } } - public void OnException(Activity activity, object payload) + // The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved. + // see https://github.com/dotnet/runtime/blob/f9246538e3d49b90b0e9128d7b1defef57cd6911/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325 +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The event source guarantees that top-level properties are preserved")] +#endif + static bool TryFetchException(object payload, out Exception exc) { - if (activity.IsAllDataRequested) + if (!StopExceptionFetcher.TryFetch(payload, out exc) || exc == null) { - if (!this.stopExceptionFetcher.TryFetch(payload, out Exception exc) || exc == null) - { - HttpInstrumentationEventSource.Log.NullPayload(nameof(HttpHandlerDiagnosticListener), nameof(this.OnException)); - return; - } - - if (this.options.RecordException) - { - activity.RecordException(exc); - } + return false; + } - if (exc is HttpRequestException) - { - activity.SetStatus(ActivityStatusCode.Error, exc.Message); - } + return true; + } + } - try - { - this.options.EnrichWithException?.Invoke(activity, exc); - } - catch (Exception ex) - { - HttpInstrumentationEventSource.Log.EnrichmentException(ex); - } - } + private static string GetErrorType(Exception exc) + { +#if NET8_0_OR_GREATER + // For net8.0 and above exception type can be found using HttpRequestError. + // https://learn.microsoft.com/dotnet/api/system.net.http.httprequesterror?view=net-8.0 + if (exc is HttpRequestException httpRequestException) + { + return httpRequestException.HttpRequestError switch + { + HttpRequestError.NameResolutionError => "name_resolution_error", + HttpRequestError.ConnectionError => "connection_error", + HttpRequestError.SecureConnectionError => "secure_connection_error", + HttpRequestError.HttpProtocolError => "http_protocol_error", + HttpRequestError.ExtendedConnectNotSupported => "extended_connect_not_supported", + HttpRequestError.VersionNegotiationError => "version_negotiation_error", + HttpRequestError.UserAuthenticationError => "user_authentication_error", + HttpRequestError.ProxyTunnelError => "proxy_tunnel_error", + HttpRequestError.InvalidResponse => "invalid_response", + HttpRequestError.ResponseEnded => "response_ended", + HttpRequestError.ConfigurationLimitExceeded => "configuration_limit_exceeded", + + // Fall back to the exception type name in case of HttpRequestError.Unknown + _ => exc.GetType().FullName, + }; } +#endif + return exc.GetType().FullName; } } diff --git a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerMetricsDiagnosticListener.cs b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerMetricsDiagnosticListener.cs index 338a55dbf92..c6a710e2dd0 100644 --- a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerMetricsDiagnosticListener.cs +++ b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerMetricsDiagnosticListener.cs @@ -1,105 +1,168 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif using System.Diagnostics.Metrics; #if NETFRAMEWORK using System.Net.Http; #endif +using System.Reflection; +using OpenTelemetry.Internal; using OpenTelemetry.Trace; -using static OpenTelemetry.Internal.HttpSemanticConventionHelper; -namespace OpenTelemetry.Instrumentation.Http.Implementation +namespace OpenTelemetry.Instrumentation.Http.Implementation; + +internal sealed class HttpHandlerMetricsDiagnosticListener : ListenerHandler { - internal sealed class HttpHandlerMetricsDiagnosticListener : ListenerHandler - { - internal const string OnStopEvent = "System.Net.Http.HttpRequestOut.Stop"; + internal const string OnStopEvent = "System.Net.Http.HttpRequestOut.Stop"; - private readonly PropertyFetcher stopResponseFetcher = new("Response"); - private readonly PropertyFetcher stopRequestFetcher = new("Request"); - private readonly Histogram httpClientDuration; + internal static readonly AssemblyName AssemblyName = typeof(HttpClientMetrics).Assembly.GetName(); + internal static readonly string MeterName = AssemblyName.Name; + 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 HttpClientRequestDuration = Meter.CreateHistogram("http.client.request.duration", "s", "Duration of HTTP client requests."); - private readonly HttpSemanticConvention httpSemanticConvention; + private static readonly PropertyFetcher StopRequestFetcher = new("Request"); + private static readonly PropertyFetcher StopResponseFetcher = new("Response"); + private static readonly PropertyFetcher StopExceptionFetcher = new("Exception"); + private static readonly PropertyFetcher RequestFetcher = new("Request"); +#if NET6_0_OR_GREATER + private static readonly HttpRequestOptionsKey HttpRequestOptionsErrorKey = new(SemanticConventions.AttributeErrorType); +#endif + + public HttpHandlerMetricsDiagnosticListener(string name) + : base(name) + { + } - public HttpHandlerMetricsDiagnosticListener(string name, Meter meter) - : base(name) + public static void OnStopEventWritten(Activity activity, object payload) + { + if (TryFetchRequest(payload, out HttpRequestMessage request)) { - this.httpClientDuration = meter.CreateHistogram("http.client.duration", "ms", "Measures the duration of outbound HTTP requests."); + // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-metrics.md + TagList tags = default; - this.httpSemanticConvention = GetSemanticConventionOptIn(); - } + var httpMethod = RequestMethodHelper.GetNormalizedHttpMethod(request.Method.Method); + tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, httpMethod)); - public override void OnEventWritten(string name, object payload) - { - if (name == OnStopEvent) + tags.Add(new KeyValuePair(SemanticConventions.AttributeServerAddress, request.RequestUri.Host)); + tags.Add(new KeyValuePair(SemanticConventions.AttributeUrlScheme, request.RequestUri.Scheme)); + + if (!request.RequestUri.IsDefaultPort) + { + tags.Add(new KeyValuePair(SemanticConventions.AttributeServerPort, request.RequestUri.Port)); + } + + if (TryFetchResponse(payload, out HttpResponseMessage response)) { - if (Sdk.SuppressInstrumentation) + tags.Add(new KeyValuePair(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetProtocolVersionString(response.Version))); + 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) { - return; + tags.Add(new KeyValuePair(SemanticConventions.AttributeErrorType, TelemetryHelper.GetStatusCodeString(response.StatusCode))); } + } - var activity = Activity.Current; - if (this.stopRequestFetcher.TryFetch(payload, out HttpRequestMessage request) && request != null) + if (response == null) + { +#if !NET6_0_OR_GREATER + request.Properties.TryGetValue(SemanticConventions.AttributeErrorType, out var errorType); +#else + 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) { - TagList tags = default; - - // see the spec https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/trace/semantic_conventions/http.md - if (this.httpSemanticConvention.HasFlag(HttpSemanticConvention.Old)) - { - 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)); - - if (!request.RequestUri.IsDefaultPort) - { - tags.Add(new KeyValuePair(SemanticConventions.AttributeNetPeerPort, request.RequestUri.Port)); - } - - if (this.stopResponseFetcher.TryFetch(payload, out HttpResponseMessage response) && response != null) - { - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode))); - } - } - - // see the spec https://github.com/open-telemetry/opentelemetry-specification/blob/v1.21.0/specification/trace/semantic_conventions/http.md - if (this.httpSemanticConvention.HasFlag(HttpSemanticConvention.New)) - { - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, HttpTagHelper.GetNameForHttpMethod(request.Method))); - tags.Add(new KeyValuePair(SemanticConventions.AttributeUrlScheme, request.RequestUri.Scheme)); - tags.Add(new KeyValuePair(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetFlavorTagValueFromProtocolVersion(request.Version))); - tags.Add(new KeyValuePair(SemanticConventions.AttributeServerAddress, request.RequestUri.Host)); - - if (!request.RequestUri.IsDefaultPort) - { - tags.Add(new KeyValuePair(SemanticConventions.AttributeServerPort, request.RequestUri.Port)); - } - - if (this.stopResponseFetcher.TryFetch(payload, out HttpResponseMessage response) && response != null) - { - tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpResponseStatusCode, 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. - this.httpClientDuration.Record(activity.Duration.TotalMilliseconds, tags); + 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); + } + + // The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved. + // see https://github.com/dotnet/runtime/blob/f9246538e3d49b90b0e9128d7b1defef57cd6911/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325 +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The System.Net.Http library guarantees that top-level properties are preserved")] +#endif + static bool TryFetchRequest(object payload, out HttpRequestMessage request) => + StopRequestFetcher.TryFetch(payload, out request) && request != null; + + // The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved. + // see https://github.com/dotnet/runtime/blob/f9246538e3d49b90b0e9128d7b1defef57cd6911/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325 +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The System.Net.Http library guarantees that top-level properties are preserved")] +#endif + static bool TryFetchResponse(object payload, out HttpResponseMessage response) => + StopResponseFetcher.TryFetch(payload, out response) && response != null; + } + + public static void OnExceptionEventWritten(Activity activity, object payload) + { + if (!TryFetchException(payload, out Exception exc) || !TryFetchRequest(payload, out HttpRequestMessage request)) + { + HttpInstrumentationEventSource.Log.NullPayload(nameof(HttpHandlerMetricsDiagnosticListener), nameof(OnExceptionEventWritten)); + return; + } + +#if !NET6_0_OR_GREATER + request.Properties.Add(SemanticConventions.AttributeErrorType, exc.GetType().FullName); +#else + request.Options.Set(HttpRequestOptionsErrorKey, exc.GetType().FullName); +#endif + + // The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved. + // see https://github.com/dotnet/runtime/blob/f9246538e3d49b90b0e9128d7b1defef57cd6911/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325 +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The System.Net.Http library guarantees that top-level properties are preserved")] +#endif + static bool TryFetchException(object payload, out Exception exc) + { + if (!StopExceptionFetcher.TryFetch(payload, out exc) || exc == null) + { + return false; + } + + return true; + } + + // The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved. + // see https://github.com/dotnet/runtime/blob/f9246538e3d49b90b0e9128d7b1defef57cd6911/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325 +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The System.Net.Http library guarantees that top-level properties are preserved")] +#endif + static bool TryFetchRequest(object payload, out HttpRequestMessage request) + { + if (!RequestFetcher.TryFetch(payload, out request) || request == null) + { + return false; + } + + return true; + } + } + + public override void OnEventWritten(string name, object payload) + { + if (name == OnStopEvent) + { + OnStopEventWritten(Activity.Current, payload); + } + else if (name == OnUnhandledExceptionEvent) + { + OnExceptionEventWritten(Activity.Current, payload); } } } diff --git a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpInstrumentationEventSource.cs b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpInstrumentationEventSource.cs index 862348a94b1..360fa496701 100644 --- a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpInstrumentationEventSource.cs +++ b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpInstrumentationEventSource.cs @@ -1,102 +1,103 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Tracing; using OpenTelemetry.Internal; -namespace OpenTelemetry.Instrumentation.Http.Implementation +namespace OpenTelemetry.Instrumentation.Http.Implementation; + +/// +/// EventSource events emitted from the project. +/// +[EventSource(Name = "OpenTelemetry-Instrumentation-Http")] +internal sealed class HttpInstrumentationEventSource : EventSource { - /// - /// EventSource events emitted from the project. - /// - [EventSource(Name = "OpenTelemetry-Instrumentation-Http")] - internal sealed class HttpInstrumentationEventSource : EventSource - { - public static HttpInstrumentationEventSource Log = new(); + public static HttpInstrumentationEventSource Log = new(); - [NonEvent] - public void FailedProcessResult(Exception ex) + [NonEvent] + public void FailedProcessResult(Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.FailedProcessResult(ex.ToInvariantString()); - } + this.FailedProcessResult(ex.ToInvariantString()); } + } - [NonEvent] - public void RequestFilterException(Exception ex) + [NonEvent] + public void RequestFilterException(Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.RequestFilterException(ex.ToInvariantString()); - } + this.RequestFilterException(ex.ToInvariantString()); } + } - [NonEvent] - public void ExceptionInitializingInstrumentation(string instrumentationType, Exception ex) + [NonEvent] + public void ExceptionInitializingInstrumentation(string instrumentationType, Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.ExceptionInitializingInstrumentation(instrumentationType, ex.ToInvariantString()); - } + this.ExceptionInitializingInstrumentation(instrumentationType, ex.ToInvariantString()); } + } - [Event(1, Message = "Failed to process result: '{0}'", Level = EventLevel.Error)] - public void FailedProcessResult(string ex) + [NonEvent] + public void UnknownErrorProcessingEvent(string handlerName, string eventName, Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - this.WriteEvent(1, ex); + this.UnknownErrorProcessingEvent(handlerName, eventName, ex.ToInvariantString()); } + } - [Event(2, Message = "Error initializing instrumentation type {0}. Exception : {1}", Level = EventLevel.Error)] - public void ExceptionInitializingInstrumentation(string instrumentationType, string ex) - { - this.WriteEvent(2, instrumentationType, ex); - } + [Event(1, Message = "Failed to process result: '{0}'", Level = EventLevel.Error)] + public void FailedProcessResult(string ex) + { + this.WriteEvent(1, ex); + } - [Event(3, Message = "Payload is NULL in event '{1}' from handler '{0}', span will not be recorded.", Level = EventLevel.Warning)] - public void NullPayload(string handlerName, string eventName) - { - this.WriteEvent(3, handlerName, eventName); - } + [Event(2, Message = "Error initializing instrumentation type {0}. Exception : {1}", Level = EventLevel.Error)] + public void ExceptionInitializingInstrumentation(string instrumentationType, string ex) + { + this.WriteEvent(2, instrumentationType, ex); + } - [Event(4, Message = "Filter threw exception. Request will not be collected. Exception {0}.", Level = EventLevel.Error)] - public void RequestFilterException(string exception) - { - this.WriteEvent(4, exception); - } + [Event(3, Message = "Payload is NULL in event '{1}' from handler '{0}', span will not be recorded.", Level = EventLevel.Warning)] + public void NullPayload(string handlerName, string eventName) + { + this.WriteEvent(3, handlerName, eventName); + } - [NonEvent] - public void EnrichmentException(Exception ex) - { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.EnrichmentException(ex.ToInvariantString()); - } - } + [Event(4, Message = "Filter threw exception. Request will not be collected. Exception {0}.", Level = EventLevel.Error)] + public void RequestFilterException(string exception) + { + this.WriteEvent(4, exception); + } - [Event(5, Message = "Enrich threw exception. Exception {0}.", Level = EventLevel.Error)] - public void EnrichmentException(string exception) + [NonEvent] + public void EnrichmentException(Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - this.WriteEvent(5, exception); + this.EnrichmentException(ex.ToInvariantString()); } + } - [Event(6, Message = "Request is filtered out.", Level = EventLevel.Verbose)] - public void RequestIsFilteredOut(string eventName) - { - this.WriteEvent(6, eventName); - } + [Event(5, Message = "Enrich threw exception. Exception {0}.", Level = EventLevel.Error)] + public void EnrichmentException(string exception) + { + this.WriteEvent(5, exception); + } + + [Event(6, Message = "Request is filtered out.", Level = EventLevel.Verbose)] + public void RequestIsFilteredOut(string eventName) + { + this.WriteEvent(6, eventName); + } + + [Event(7, Message = "Unknown error processing event '{1}' from handler '{0}', Exception: {2}", Level = EventLevel.Error)] + public void UnknownErrorProcessingEvent(string handlerName, string eventName, string ex) + { + this.WriteEvent(7, handlerName, eventName, ex); } } diff --git a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpTagHelper.cs b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpTagHelper.cs index 83d7ea0aedc..82d7fd11bf2 100644 --- a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpTagHelper.cs +++ b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpTagHelper.cs @@ -1,89 +1,34 @@ -// // 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.Collections.Concurrent; -#if NETFRAMEWORK -using System.Net.Http; -#endif +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Instrumentation.Http.Implementation +namespace OpenTelemetry.Instrumentation.Http.Implementation; + +/// +/// A collection of helper methods to be used when building Http activities. +/// +internal static class HttpTagHelper { /// - /// A collection of helper methods to be used when building Http activities. + /// Gets the OpenTelemetry standard uri tag value for a span based on its request . /// - internal static class HttpTagHelper + /// . + /// Span uri value. + public static string GetUriTagValueFromRequestUri(Uri uri) { - 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. - /// - /// Http method. - /// Activity name. - public static string GetOperationNameForHttpMethod(string method) => MethodOperationNameCache.GetOrAdd(method, ConvertMethodToOperationNameRef); - - /// - /// Gets the OpenTelemetry standard operation name for a span based on its . - /// - /// . - /// Span operation name. - public static string GetOperationNameForHttpMethod(HttpMethod method) => HttpMethodOperationNameCache.GetOrAdd(method, ConvertHttpMethodToOperationNameRef); - - /// - /// Gets the OpenTelemetry standard method name for a span based on its . - /// - /// . - /// 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 . - /// - /// . - /// Span uri value. - public static string GetUriTagValueFromRequestUri(Uri uri) + if (string.IsNullOrEmpty(uri.UserInfo)) { - if (string.IsNullOrEmpty(uri.UserInfo)) - { - return uri.OriginalString; - } - - return string.Concat(uri.Scheme, Uri.SchemeDelimiter, uri.Authority, uri.PathAndQuery, uri.Fragment); + return uri.OriginalString; } - private static string ConvertMethodToOperationName(string method) => $"HTTP {method}"; - - private static string ConvertHttpMethodToOperationName(HttpMethod method) => $"HTTP {method}"; - - private static string ConvertHttpMethodToName(HttpMethod method) => method.ToString(); - - private static string ConvertProtocolVersionToString(Version protocolVersion) => protocolVersion.ToString(); + return string.Concat(uri.Scheme, Uri.SchemeDelimiter, uri.Authority, uri.PathAndQuery, uri.Fragment); } + + public static string GetProtocolVersionString(Version httpVersion) => (httpVersion.Major, httpVersion.Minor) switch + { + (1, 0) => "1.0", + (1, 1) => "1.1", + (2, 0) => "2", + (3, 0) => "3", + _ => httpVersion.ToString(), + }; } diff --git a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs index 7150ce8ddde..c27bef64132 100644 --- a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs +++ b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs @@ -1,1085 +1,1198 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #if NETFRAMEWORK using System.Collections; using System.Diagnostics; +using System.Diagnostics.Metrics; using System.Net; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using OpenTelemetry.Context.Propagation; +using OpenTelemetry.Internal; using OpenTelemetry.Trace; -namespace OpenTelemetry.Instrumentation.Http.Implementation +namespace OpenTelemetry.Instrumentation.Http.Implementation; + +/// +/// Hooks into the class reflectively and writes diagnostic events as requests are processed. +/// +/// +/// Inspired from the System.Diagnostics.DiagnosticSource.HttpHandlerDiagnosticListener class which has some bugs and feature gaps. +/// See https://github.com/dotnet/runtime/pull/33732 for details. +/// +internal static class HttpWebRequestActivitySource { - /// - /// Hooks into the class reflectively and writes diagnostic events as requests are processed. - /// - /// - /// Inspired from the System.Diagnostics.DiagnosticSource.HttpHandlerDiagnosticListener class which has some bugs and feature gaps. - /// See https://github.com/dotnet/runtime/pull/33732 for details. - /// - internal static class HttpWebRequestActivitySource + internal static readonly AssemblyName AssemblyName = typeof(HttpWebRequestActivitySource).Assembly.GetName(); + internal static readonly string ActivitySourceName = AssemblyName.Name + ".HttpWebRequest"; + internal static readonly string ActivityName = ActivitySourceName + ".HttpRequestOut"; + internal static readonly string MeterName = AssemblyName.Name; + + internal static readonly Func> HttpWebRequestHeaderValuesGetter = (request, name) => request.Headers.GetValues(name); + internal static readonly Action HttpWebRequestHeaderValuesSetter = (request, name, value) => request.Headers.Add(name, value); + + 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 HttpClientRequestDuration = WebRequestMeter.CreateHistogram("http.client.request.duration", "s", "Duration of HTTP client requests."); + + private static HttpClientTraceInstrumentationOptions tracingOptions; + + // Fields for reflection + private static FieldInfo connectionGroupListField; + private static Type connectionGroupType; + private static FieldInfo connectionListField; + private static Type connectionType; + private static FieldInfo writeListField; + private static Func writeAResultAccessor; + private static Func readAResultAccessor; + + // LazyAsyncResult & ContextAwareResult + private static Func asyncCallbackAccessor; + private static Action asyncCallbackModifier; + private static Func asyncStateAccessor; + private static Action asyncStateModifier; + private static Func endCalledAccessor; + private static Func resultAccessor; + private static Func isContextAwareResultChecker; + + // HttpWebResponse + private static Func httpWebResponseCtor; + private static Func uriAccessor; + private static Func verbAccessor; + private static Func mediaTypeAccessor; + private static Func usesProxySemanticsAccessor; + private static Func coreResponseDataAccessor; + private static Func isWebSocketResponseAccessor; + private static Func connectionGroupNameAccessor; + + static HttpWebRequestActivitySource() { - internal static readonly AssemblyName AssemblyName = typeof(HttpWebRequestActivitySource).Assembly.GetName(); - internal static readonly string ActivitySourceName = AssemblyName.Name + ".HttpWebRequest"; - internal static readonly string ActivityName = ActivitySourceName + ".HttpRequestOut"; - - internal static readonly Func> HttpWebRequestHeaderValuesGetter = (request, name) => request.Headers.GetValues(name); - internal static readonly Action HttpWebRequestHeaderValuesSetter = (request, name, value) => request.Headers.Add(name, value); - - internal static HttpClientInstrumentationOptions Options = new HttpClientInstrumentationOptions(); - - private static readonly Version Version = AssemblyName.Version; - private static readonly ActivitySource WebRequestActivitySource = new ActivitySource(ActivitySourceName, Version.ToString()); - - // Fields for reflection - private static FieldInfo connectionGroupListField; - private static Type connectionGroupType; - private static FieldInfo connectionListField; - private static Type connectionType; - private static FieldInfo writeListField; - private static Func writeAResultAccessor; - private static Func readAResultAccessor; - - // LazyAsyncResult & ContextAwareResult - private static Func asyncCallbackAccessor; - private static Action asyncCallbackModifier; - private static Func asyncStateAccessor; - private static Action asyncStateModifier; - private static Func endCalledAccessor; - private static Func resultAccessor; - private static Func isContextAwareResultChecker; - - // HttpWebResponse - private static Func httpWebResponseCtor; - private static Func uriAccessor; - private static Func verbAccessor; - private static Func mediaTypeAccessor; - private static Func usesProxySemanticsAccessor; - private static Func coreResponseDataAccessor; - private static Func isWebSocketResponseAccessor; - private static Func connectionGroupNameAccessor; - - static HttpWebRequestActivitySource() + try { - try - { - PrepareReflectionObjects(); - PerformInjection(); - } - catch (Exception ex) - { - // If anything went wrong, just no-op. Write an event so at least we can find out. - HttpInstrumentationEventSource.Log.ExceptionInitializingInstrumentation(typeof(HttpWebRequestActivitySource).FullName, ex); - } - } + PrepareReflectionObjects(); + PerformInjection(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void AddRequestTagsAndInstrumentRequest(HttpWebRequest request, Activity activity) + TracingOptions = new HttpClientTraceInstrumentationOptions(); + } + catch (Exception ex) { - activity.DisplayName = HttpTagHelper.GetOperationNameForHttpMethod(request.Method); - - if (activity.IsAllDataRequested) - { - 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)); - - try - { - Options.EnrichWithHttpWebRequest?.Invoke(activity, request); - } - catch (Exception ex) - { - HttpInstrumentationEventSource.Log.EnrichmentException(ex); - } - } + // If anything went wrong, just no-op. Write an event so at least we can find out. + HttpInstrumentationEventSource.Log.ExceptionInitializingInstrumentation(typeof(HttpWebRequestActivitySource).FullName, ex); } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void AddResponseTags(HttpWebResponse response, Activity activity) + internal static HttpClientTraceInstrumentationOptions TracingOptions + { + get => tracingOptions; + set { - if (activity.IsAllDataRequested) - { - activity.SetTag(SemanticConventions.AttributeHttpStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)); - - activity.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode(activity.Kind, (int)response.StatusCode)); - - try - { - Options.EnrichWithHttpWebResponse?.Invoke(activity, response); - } - catch (Exception ex) - { - HttpInstrumentationEventSource.Log.EnrichmentException(ex); - } - } + tracingOptions = value; } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AddRequestTagsAndInstrumentRequest(HttpWebRequest request, Activity activity) + { + RequestMethodHelper.SetHttpClientActivityDisplayName(activity, request.Method); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void AddExceptionTags(Exception exception, Activity activity) + if (activity.IsAllDataRequested) { - if (!activity.IsAllDataRequested) + // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md + RequestMethodHelper.SetHttpMethodTag(activity, request.Method); + + activity.SetTag(SemanticConventions.AttributeServerAddress, request.RequestUri.Host); + if (!request.RequestUri.IsDefaultPort) { - return; + activity.SetTag(SemanticConventions.AttributeServerPort, request.RequestUri.Port); } - ActivityStatusCode status; - string exceptionMessage = null; + activity.SetTag(SemanticConventions.AttributeUrlFull, HttpTagHelper.GetUriTagValueFromRequestUri(request.RequestUri)); - if (exception is WebException wexc) + try { - if (wexc.Response is HttpWebResponse response) - { - activity.SetTag(SemanticConventions.AttributeHttpStatusCode, (int)response.StatusCode); - - status = SpanHelper.ResolveSpanStatusForHttpStatusCode(activity.Kind, (int)response.StatusCode); - } - else - { - switch (wexc.Status) - { - case WebExceptionStatus.Timeout: - case WebExceptionStatus.RequestCanceled: - status = ActivityStatusCode.Error; - break; - case WebExceptionStatus.SendFailure: - case WebExceptionStatus.ConnectFailure: - case WebExceptionStatus.SecureChannelFailure: - case WebExceptionStatus.TrustFailure: - case WebExceptionStatus.ServerProtocolViolation: - case WebExceptionStatus.MessageLengthLimitExceeded: - status = ActivityStatusCode.Error; - exceptionMessage = exception.Message; - break; - default: - status = ActivityStatusCode.Error; - exceptionMessage = exception.Message; - break; - } - } + TracingOptions.EnrichWithHttpWebRequest?.Invoke(activity, request); } - else + catch (Exception ex) { - status = ActivityStatusCode.Error; - exceptionMessage = exception.Message; + HttpInstrumentationEventSource.Log.EnrichmentException(ex); } + } + } - activity.SetStatus(status, exceptionMessage); - if (Options.RecordException) - { - activity.RecordException(exception); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AddResponseTags(HttpWebResponse response, Activity activity) + { + Debug.Assert(activity != null, "Activity must not be null"); + + if (activity.IsAllDataRequested) + { + activity.SetTag(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetProtocolVersionString(response.ProtocolVersion)); + activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)); try { - Options.EnrichWithException?.Invoke(activity, exception); + TracingOptions.EnrichWithHttpWebResponse?.Invoke(activity, response); } catch (Exception ex) { HttpInstrumentationEventSource.Log.EnrichmentException(ex); } } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string GetErrorType(Exception exception) + { + if (exception is WebException wexc) + { + // TODO: consider other Status values from + // https://learn.microsoft.com/dotnet/api/system.net.webexceptionstatus?view=netframework-4.6.2 + return wexc.Status switch + { + WebExceptionStatus.NameResolutionFailure => "name_resolution_failure", + WebExceptionStatus.ConnectFailure => "connect_failure", + WebExceptionStatus.ReceiveFailure => "receive_failure", + WebExceptionStatus.SendFailure => "send_failure", + WebExceptionStatus.PipelineFailure => "pipeline_failure", + WebExceptionStatus.RequestCanceled => "request_cancelled", + WebExceptionStatus.ProtocolError => "protocol_error", + WebExceptionStatus.ConnectionClosed => "connection_closed", + WebExceptionStatus.TrustFailure => "trust_failure", + WebExceptionStatus.SecureChannelFailure => "secure_channel_failure", + WebExceptionStatus.ServerProtocolViolation => "server_protocol_violation", + WebExceptionStatus.KeepAliveFailure => "keep_alive_failure", + WebExceptionStatus.Timeout => "timeout", + WebExceptionStatus.ProxyNameResolutionFailure => "proxy_name_resolution_failure", + WebExceptionStatus.MessageLengthLimitExceeded => "message_length_limit_exceeded", + WebExceptionStatus.RequestProhibitedByCachePolicy => "request_prohibited_by_cache_policy", + WebExceptionStatus.RequestProhibitedByProxy => "request_prohibited_by_proxy", + _ => wexc.GetType().FullName, + }; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void InstrumentRequest(HttpWebRequest request, ActivityContext activityContext) - => Propagators.DefaultTextMapPropagator.Inject(new PropagationContext(activityContext, Baggage.Current), request, HttpWebRequestHeaderValuesSetter); + return exception.GetType().FullName; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsRequestInstrumented(HttpWebRequest request) - => Propagators.DefaultTextMapPropagator.Extract(default, request, HttpWebRequestHeaderValuesGetter) != default; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AddExceptionEvent(Exception exception, Activity activity) + { + Debug.Assert(activity != null, "Activity must not be null"); - private static void ProcessRequest(HttpWebRequest request) + if (!activity.IsAllDataRequested) { - if (!WebRequestActivitySource.HasListeners() || !Options.EventFilterHttpWebRequest(request)) - { - // No subscribers to the ActivitySource or User provider Filter is - // filtering this request. - // Propagation must still be done in such cases, to allow - // downstream services to continue from parent context, if any. - // Eg: Parent could be the Asp.Net activity. - InstrumentRequest(request, Activity.Current?.Context ?? default); - return; - } + return; + } - if (IsRequestInstrumented(request)) - { - // This request was instrumented by previous - // ProcessRequest, such is the case with redirect responses where the same request is sent again. - return; - } + if (TracingOptions.RecordException) + { + activity.RecordException(exception); + } + + try + { + TracingOptions.EnrichWithException?.Invoke(activity, exception); + } + catch (Exception ex) + { + HttpInstrumentationEventSource.Log.EnrichmentException(ex); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InstrumentRequest(HttpWebRequest request, ActivityContext activityContext) + => Propagators.DefaultTextMapPropagator.Inject(new PropagationContext(activityContext, Baggage.Current), request, HttpWebRequestHeaderValuesSetter); - var activity = WebRequestActivitySource.StartActivity(ActivityName, ActivityKind.Client); - var activityContext = Activity.Current?.Context ?? default; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsRequestInstrumented(HttpWebRequest request) + => Propagators.DefaultTextMapPropagator.Extract(default, request, HttpWebRequestHeaderValuesGetter) != default; - // Propagation must still be done in all cases, to allow + private static void ProcessRequest(HttpWebRequest request) + { + // There are subscribers to the ActivitySource and no user-provided filter is + // filtering this request. + var enableTracing = WebRequestActivitySource.HasListeners() + && TracingOptions.EventFilterHttpWebRequest(request); + + 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 // downstream services to continue from parent context, if any. // Eg: Parent could be the Asp.Net activity. - InstrumentRequest(request, activityContext); - if (activity == null) - { - // There is a listener but it decided not to sample the current request. - return; - } + InstrumentRequest(request, Activity.Current?.Context ?? default); + return; + } - IAsyncResult asyncContext = writeAResultAccessor(request); - if (asyncContext != null) - { - // Flow here is for [Begin]GetRequestStream[Async]. + if (IsRequestInstrumented(request)) + { + // This request was instrumented by previous + // ProcessRequest, such is the case with redirect responses where the same request is sent again. + return; + } - AsyncCallbackWrapper callback = new AsyncCallbackWrapper(request, activity, asyncCallbackAccessor(asyncContext)); - asyncCallbackModifier(asyncContext, callback.AsyncCallback); - } - else - { - // Flow here is for [Begin]GetResponse[Async] without a prior call to [Begin]GetRequestStream[Async]. + Activity activity = enableTracing + ? WebRequestActivitySource.StartActivity(ActivityName, ActivityKind.Client) + : null; - asyncContext = readAResultAccessor(request); - AsyncCallbackWrapper callback = new AsyncCallbackWrapper(request, activity, asyncCallbackAccessor(asyncContext)); - asyncCallbackModifier(asyncContext, callback.AsyncCallback); - } + var activityContext = Activity.Current?.Context ?? default; - AddRequestTagsAndInstrumentRequest(request, activity); + // Propagation must still be done in all cases, to allow + // downstream services to continue from parent context, if any. + // Eg: Parent could be the Asp.Net activity. + InstrumentRequest(request, activityContext); + + IAsyncResult asyncContext = writeAResultAccessor(request); + if (asyncContext != null) + { + // Flow here is for [Begin]GetRequestStream[Async]. + + AsyncCallbackWrapper callback = new AsyncCallbackWrapper(request, activity, asyncCallbackAccessor(asyncContext), Stopwatch.GetTimestamp()); + asyncCallbackModifier(asyncContext, callback.AsyncCallback); + } + else + { + // Flow here is for [Begin]GetResponse[Async] without a prior call to [Begin]GetRequestStream[Async]. + + asyncContext = readAResultAccessor(request); + AsyncCallbackWrapper callback = new AsyncCallbackWrapper(request, activity, asyncCallbackAccessor(asyncContext), Stopwatch.GetTimestamp()); + asyncCallbackModifier(asyncContext, callback.AsyncCallback); } - private static void HookOrProcessResult(HttpWebRequest request) + if (activity != null) { - IAsyncResult writeAsyncContext = writeAResultAccessor(request); - if (writeAsyncContext == null || !(asyncCallbackAccessor(writeAsyncContext)?.Target is AsyncCallbackWrapper writeAsyncContextCallback)) - { - // If we already hooked into the read result during ProcessRequest or we hooked up after the fact already we don't need to do anything here. - return; - } + AddRequestTagsAndInstrumentRequest(request, activity); + } + } - // If we got here it means the user called [Begin]GetRequestStream[Async] and we have to hook the read result after the fact. + private static void HookOrProcessResult(HttpWebRequest request) + { + IAsyncResult writeAsyncContext = writeAResultAccessor(request); + if (writeAsyncContext == null || asyncCallbackAccessor(writeAsyncContext)?.Target is not AsyncCallbackWrapper writeAsyncContextCallback) + { + // If we already hooked into the read result during ProcessRequest or we hooked up after the fact already we don't need to do anything here. + return; + } - IAsyncResult readAsyncContext = readAResultAccessor(request); - if (readAsyncContext == null) - { - // We're still trying to establish the connection (no read has started). - return; - } + // If we got here it means the user called [Begin]GetRequestStream[Async] and we have to hook the read result after the fact. - // Clear our saved callback so we know not to process again. - asyncCallbackModifier(writeAsyncContext, null); + IAsyncResult readAsyncContext = readAResultAccessor(request); + if (readAsyncContext == null) + { + // We're still trying to establish the connection (no read has started). + return; + } - if (endCalledAccessor.Invoke(readAsyncContext) || readAsyncContext.CompletedSynchronously) - { - // We need to process the result directly because the read callback has already fired. Force a copy because response has likely already been disposed. - ProcessResult(readAsyncContext, null, writeAsyncContextCallback.Activity, resultAccessor(readAsyncContext), true); - return; - } + // Clear our saved callback so we know not to process again. + asyncCallbackModifier(writeAsyncContext, null); - // Hook into the result callback if it hasn't already fired. - AsyncCallbackWrapper callback = new AsyncCallbackWrapper(writeAsyncContextCallback.Request, writeAsyncContextCallback.Activity, asyncCallbackAccessor(readAsyncContext)); - asyncCallbackModifier(readAsyncContext, callback.AsyncCallback); + if (endCalledAccessor.Invoke(readAsyncContext) || readAsyncContext.CompletedSynchronously) + { + // We need to process the result directly because the read callback has already fired. Force a copy because response has likely already been disposed. + ProcessResult(readAsyncContext, null, writeAsyncContextCallback.Activity, resultAccessor(readAsyncContext), true, request, writeAsyncContextCallback.StartTimestamp); + return; } - private static void ProcessResult(IAsyncResult asyncResult, AsyncCallback asyncCallback, Activity activity, object result, bool forceResponseCopy) + // Hook into the result callback if it hasn't already fired. + AsyncCallbackWrapper callback = new AsyncCallbackWrapper(writeAsyncContextCallback.Request, writeAsyncContextCallback.Activity, asyncCallbackAccessor(readAsyncContext), Stopwatch.GetTimestamp()); + asyncCallbackModifier(readAsyncContext, callback.AsyncCallback); + } + + private static void ProcessResult(IAsyncResult asyncResult, AsyncCallback asyncCallback, Activity activity, object result, bool forceResponseCopy, HttpWebRequest request, long startTimestamp) + { + 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 + // 2. Request was filtered out + // 3. Request was not sampled + // We could be executing on a different thread now so restore the activity if needed. + if (activity != null && Activity.Current != activity) { - // We could be executing on a different thread now so restore the activity if needed. - if (Activity.Current != activity) - { - Activity.Current = activity; - } + Activity.Current = activity; + } - try + try + { + if (result is Exception ex) { - if (result is Exception ex) + errorType = GetErrorType(ex); + if (ex is WebException wexc && wexc.Response is HttpWebResponse response) { - AddExceptionTags(ex, activity); + 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 { - HttpWebResponse response = (HttpWebResponse)result; - - if (forceResponseCopy || (asyncCallback == null && isContextAwareResultChecker(asyncResult))) + activityStatus = ActivityStatusCode.Error; + if (activity != null) { - // For async calls (where asyncResult is ContextAwareResult)... - // If no callback was set assume the user is manually calling BeginGetResponse & EndGetResponse - // in which case they could dispose the HttpWebResponse before our listeners have a chance to work with it. - // Disposed HttpWebResponse throws when accessing properties, so let's make a copy of the data to ensure that doesn't happen. - - HttpWebResponse responseCopy = httpWebResponseCtor( - new object[] - { - uriAccessor(response), verbAccessor(response), coreResponseDataAccessor(response), mediaTypeAccessor(response), - usesProxySemanticsAccessor(response), DecompressionMethods.None, - isWebSocketResponseAccessor(response), connectionGroupNameAccessor(response), - }); + AddExceptionEvent(ex, activity); + } + } + } + else + { + HttpWebResponse response = (HttpWebResponse)result; + if (forceResponseCopy || (asyncCallback == null && isContextAwareResultChecker(asyncResult))) + { + // For async calls (where asyncResult is ContextAwareResult)... + // If no callback was set assume the user is manually calling BeginGetResponse & EndGetResponse + // in which case they could dispose the HttpWebResponse before our listeners have a chance to work with it. + // Disposed HttpWebResponse throws when accessing properties, so let's make a copy of the data to ensure that doesn't happen. + + HttpWebResponse responseCopy = httpWebResponseCtor( + new object[] + { + uriAccessor(response), verbAccessor(response), coreResponseDataAccessor(response), mediaTypeAccessor(response), + usesProxySemanticsAccessor(response), DecompressionMethods.None, + isWebSocketResponseAccessor(response), connectionGroupNameAccessor(response), + }); + + if (activity != null) + { AddResponseTags(responseCopy, activity); } - else + + httpStatusCode = responseCopy.StatusCode; + protocolVersion = responseCopy.ProtocolVersion; + } + else + { + if (activity != null) { AddResponseTags(response, activity); } + + httpStatusCode = response.StatusCode; + protocolVersion = response.ProtocolVersion; } - } - catch (Exception ex) - { - HttpInstrumentationEventSource.Log.FailedProcessResult(ex); - } - activity.Stop(); - } + activityStatus = SpanHelper.ResolveSpanStatusForHttpStatusCode(ActivityKind.Client, (int)httpStatusCode.Value); - private static void PrepareReflectionObjects() + if (activityStatus == ActivityStatusCode.Error) + { + // set the errorType to statusCode for failures. + errorType = TelemetryHelper.GetStatusCodeString(httpStatusCode.Value); + } + } + } + catch (Exception ex) { - // At any point, if the operation failed, it should just throw. The caller should catch all exceptions and swallow. - - Type servicePointType = typeof(ServicePoint); - Assembly systemNetHttpAssembly = servicePointType.Assembly; - connectionGroupListField = servicePointType.GetField("m_ConnectionGroupList", BindingFlags.Instance | BindingFlags.NonPublic); - connectionGroupType = systemNetHttpAssembly?.GetType("System.Net.ConnectionGroup"); - connectionListField = connectionGroupType?.GetField("m_ConnectionList", BindingFlags.Instance | BindingFlags.NonPublic); - connectionType = systemNetHttpAssembly?.GetType("System.Net.Connection"); - writeListField = connectionType?.GetField("m_WriteList", BindingFlags.Instance | BindingFlags.NonPublic); - - writeAResultAccessor = CreateFieldGetter(typeof(HttpWebRequest), "_WriteAResult", BindingFlags.NonPublic | BindingFlags.Instance); - readAResultAccessor = CreateFieldGetter(typeof(HttpWebRequest), "_ReadAResult", BindingFlags.NonPublic | BindingFlags.Instance); + HttpInstrumentationEventSource.Log.FailedProcessResult(ex); + } - // Double checking to make sure we have all the pieces initialized - if (connectionGroupListField == null || - connectionGroupType == null || - connectionListField == null || - connectionType == null || - writeListField == null || - writeAResultAccessor == null || - readAResultAccessor == null || - !PrepareAsyncResultReflectionObjects(systemNetHttpAssembly) || - !PrepareHttpWebResponseReflectionObjects(systemNetHttpAssembly)) + if (activity != null && activity.IsAllDataRequested) + { + activity.SetStatus(activityStatus); + if (errorType != null) { - // If anything went wrong here, just return false. There is nothing we can do. - throw new InvalidOperationException("Unable to initialize all required reflection objects"); + activity.SetTag(SemanticConventions.AttributeErrorType, errorType); } } - private static bool PrepareAsyncResultReflectionObjects(Assembly systemNetHttpAssembly) + activity?.Stop(); + + if (HttpClientRequestDuration.Enabled) { - Type lazyAsyncResultType = systemNetHttpAssembly?.GetType("System.Net.LazyAsyncResult"); - if (lazyAsyncResultType != null) + double durationS; + if (activity != null) { - asyncCallbackAccessor = CreateFieldGetter(lazyAsyncResultType, "m_AsyncCallback", BindingFlags.NonPublic | BindingFlags.Instance); - asyncCallbackModifier = CreateFieldSetter(lazyAsyncResultType, "m_AsyncCallback", BindingFlags.NonPublic | BindingFlags.Instance); - asyncStateAccessor = CreateFieldGetter(lazyAsyncResultType, "m_AsyncState", BindingFlags.NonPublic | BindingFlags.Instance); - asyncStateModifier = CreateFieldSetter(lazyAsyncResultType, "m_AsyncState", BindingFlags.NonPublic | BindingFlags.Instance); - endCalledAccessor = CreateFieldGetter(lazyAsyncResultType, "m_EndCalled", BindingFlags.NonPublic | BindingFlags.Instance); - resultAccessor = CreateFieldGetter(lazyAsyncResultType, "m_Result", BindingFlags.NonPublic | BindingFlags.Instance); + durationS = activity.Duration.TotalSeconds; } - - Type contextAwareResultType = systemNetHttpAssembly?.GetType("System.Net.ContextAwareResult"); - if (contextAwareResultType != null) + else { - isContextAwareResultChecker = CreateTypeChecker(contextAwareResultType); + var endTimestamp = Stopwatch.GetTimestamp(); + durationS = (endTimestamp - startTimestamp) / (double)Stopwatch.Frequency; } - return asyncCallbackAccessor != null - && asyncCallbackModifier != null - && asyncStateAccessor != null - && asyncStateModifier != null - && endCalledAccessor != null - && resultAccessor != null - && isContextAwareResultChecker != null; - } + TagList tags = default; - private static bool PrepareHttpWebResponseReflectionObjects(Assembly systemNetHttpAssembly) - { - Type knownHttpVerbType = systemNetHttpAssembly?.GetType("System.Net.KnownHttpVerb"); - Type coreResponseData = systemNetHttpAssembly?.GetType("System.Net.CoreResponseData"); + var httpMethod = RequestMethodHelper.GetNormalizedHttpMethod(request.Method); + tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, httpMethod)); - if (knownHttpVerbType != null && coreResponseData != null) + tags.Add(SemanticConventions.AttributeServerAddress, request.RequestUri.Host); + tags.Add(SemanticConventions.AttributeUrlScheme, request.RequestUri.Scheme); + if (protocolVersion != null) { - var constructorParameterTypes = new Type[] - { - typeof(Uri), knownHttpVerbType, coreResponseData, typeof(string), - typeof(bool), typeof(DecompressionMethods), - typeof(bool), typeof(string), - }; - - ConstructorInfo ctor = typeof(HttpWebResponse).GetConstructor( - BindingFlags.NonPublic | BindingFlags.Instance, - null, - constructorParameterTypes, - null); - - if (ctor != null) - { - httpWebResponseCtor = CreateTypeInstance(ctor); - } + tags.Add(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetProtocolVersionString(protocolVersion)); } - uriAccessor = CreateFieldGetter("m_Uri", BindingFlags.NonPublic | BindingFlags.Instance); - verbAccessor = CreateFieldGetter("m_Verb", BindingFlags.NonPublic | BindingFlags.Instance); - mediaTypeAccessor = CreateFieldGetter("m_MediaType", BindingFlags.NonPublic | BindingFlags.Instance); - usesProxySemanticsAccessor = CreateFieldGetter("m_UsesProxySemantics", BindingFlags.NonPublic | BindingFlags.Instance); - coreResponseDataAccessor = CreateFieldGetter("m_CoreResponseData", BindingFlags.NonPublic | BindingFlags.Instance); - isWebSocketResponseAccessor = CreateFieldGetter("m_IsWebSocketResponse", BindingFlags.NonPublic | BindingFlags.Instance); - connectionGroupNameAccessor = CreateFieldGetter("m_ConnectionGroupName", BindingFlags.NonPublic | BindingFlags.Instance); - - return httpWebResponseCtor != null - && uriAccessor != null - && verbAccessor != null - && mediaTypeAccessor != null - && usesProxySemanticsAccessor != null - && coreResponseDataAccessor != null - && isWebSocketResponseAccessor != null - && connectionGroupNameAccessor != null; - } - - private static void PerformInjection() - { - FieldInfo servicePointTableField = typeof(ServicePointManager).GetField("s_ServicePointTable", BindingFlags.Static | BindingFlags.NonPublic); - if (servicePointTableField == null) + if (!request.RequestUri.IsDefaultPort) { - // If anything went wrong here, just return false. There is nothing we can do. - throw new InvalidOperationException("Unable to access the ServicePointTable field"); + tags.Add(SemanticConventions.AttributeServerPort, request.RequestUri.Port); } - Hashtable originalTable = servicePointTableField.GetValue(null) as Hashtable; - ServicePointHashtable newTable = new ServicePointHashtable(originalTable ?? new Hashtable()); + if (httpStatusCode.HasValue) + { + tags.Add(SemanticConventions.AttributeHttpResponseStatusCode, (int)httpStatusCode.Value); + } - foreach (DictionaryEntry existingServicePoint in originalTable) + if (errorType != null) { - HookServicePoint(existingServicePoint.Value); + tags.Add(SemanticConventions.AttributeErrorType, errorType); } - servicePointTableField.SetValue(null, newTable); + HttpClientRequestDuration.Record(durationS, tags); } + } - private static void HookServicePoint(object value) + private static void PrepareReflectionObjects() + { + // At any point, if the operation failed, it should just throw. The caller should catch all exceptions and swallow. + + Type servicePointType = typeof(ServicePoint); + Assembly systemNetHttpAssembly = servicePointType.Assembly; + connectionGroupListField = servicePointType.GetField("m_ConnectionGroupList", BindingFlags.Instance | BindingFlags.NonPublic); + connectionGroupType = systemNetHttpAssembly?.GetType("System.Net.ConnectionGroup"); + connectionListField = connectionGroupType?.GetField("m_ConnectionList", BindingFlags.Instance | BindingFlags.NonPublic); + connectionType = systemNetHttpAssembly?.GetType("System.Net.Connection"); + writeListField = connectionType?.GetField("m_WriteList", BindingFlags.Instance | BindingFlags.NonPublic); + + writeAResultAccessor = CreateFieldGetter(typeof(HttpWebRequest), "_WriteAResult", BindingFlags.NonPublic | BindingFlags.Instance); + readAResultAccessor = CreateFieldGetter(typeof(HttpWebRequest), "_ReadAResult", BindingFlags.NonPublic | BindingFlags.Instance); + + // Double checking to make sure we have all the pieces initialized + if (connectionGroupListField == null || + connectionGroupType == null || + connectionListField == null || + connectionType == null || + writeListField == null || + writeAResultAccessor == null || + readAResultAccessor == null || + !PrepareAsyncResultReflectionObjects(systemNetHttpAssembly) || + !PrepareHttpWebResponseReflectionObjects(systemNetHttpAssembly)) { - if (value is WeakReference weakRef - && weakRef.IsAlive - && weakRef.Target is ServicePoint servicePoint) - { - // Replace the ConnectionGroup hashtable inside this ServicePoint object, - // which allows us to intercept each new ConnectionGroup object added under - // this ServicePoint. - Hashtable originalTable = connectionGroupListField.GetValue(servicePoint) as Hashtable; - ConnectionGroupHashtable newTable = new ConnectionGroupHashtable(originalTable ?? new Hashtable()); + // If anything went wrong here, just return false. There is nothing we can do. + throw new InvalidOperationException("Unable to initialize all required reflection objects"); + } + } - foreach (DictionaryEntry existingConnectionGroup in originalTable) - { - HookConnectionGroup(existingConnectionGroup.Value); - } + private static bool PrepareAsyncResultReflectionObjects(Assembly systemNetHttpAssembly) + { + Type lazyAsyncResultType = systemNetHttpAssembly?.GetType("System.Net.LazyAsyncResult"); + if (lazyAsyncResultType != null) + { + asyncCallbackAccessor = CreateFieldGetter(lazyAsyncResultType, "m_AsyncCallback", BindingFlags.NonPublic | BindingFlags.Instance); + asyncCallbackModifier = CreateFieldSetter(lazyAsyncResultType, "m_AsyncCallback", BindingFlags.NonPublic | BindingFlags.Instance); + asyncStateAccessor = CreateFieldGetter(lazyAsyncResultType, "m_AsyncState", BindingFlags.NonPublic | BindingFlags.Instance); + asyncStateModifier = CreateFieldSetter(lazyAsyncResultType, "m_AsyncState", BindingFlags.NonPublic | BindingFlags.Instance); + endCalledAccessor = CreateFieldGetter(lazyAsyncResultType, "m_EndCalled", BindingFlags.NonPublic | BindingFlags.Instance); + resultAccessor = CreateFieldGetter(lazyAsyncResultType, "m_Result", BindingFlags.NonPublic | BindingFlags.Instance); + } - connectionGroupListField.SetValue(servicePoint, newTable); - } + Type contextAwareResultType = systemNetHttpAssembly?.GetType("System.Net.ContextAwareResult"); + if (contextAwareResultType != null) + { + isContextAwareResultChecker = CreateTypeChecker(contextAwareResultType); } - private static void HookConnectionGroup(object value) + return asyncCallbackAccessor != null + && asyncCallbackModifier != null + && asyncStateAccessor != null + && asyncStateModifier != null + && endCalledAccessor != null + && resultAccessor != null + && isContextAwareResultChecker != null; + } + + private static bool PrepareHttpWebResponseReflectionObjects(Assembly systemNetHttpAssembly) + { + Type knownHttpVerbType = systemNetHttpAssembly?.GetType("System.Net.KnownHttpVerb"); + Type coreResponseData = systemNetHttpAssembly?.GetType("System.Net.CoreResponseData"); + + if (knownHttpVerbType != null && coreResponseData != null) { - if (connectionGroupType.IsInstanceOfType(value)) + var constructorParameterTypes = new Type[] { - // Replace the Connection arraylist inside this ConnectionGroup object, - // which allows us to intercept each new Connection object added under - // this ConnectionGroup. - ArrayList originalArrayList = connectionListField.GetValue(value) as ArrayList; - ConnectionArrayList newArrayList = new ConnectionArrayList(originalArrayList ?? new ArrayList()); + typeof(Uri), knownHttpVerbType, coreResponseData, typeof(string), + typeof(bool), typeof(DecompressionMethods), + typeof(bool), typeof(string), + }; - foreach (object connection in originalArrayList) - { - HookConnection(connection); - } + ConstructorInfo ctor = typeof(HttpWebResponse).GetConstructor( + BindingFlags.NonPublic | BindingFlags.Instance, + null, + constructorParameterTypes, + null); - connectionListField.SetValue(value, newArrayList); + if (ctor != null) + { + httpWebResponseCtor = CreateTypeInstance(ctor); } } - private static void HookConnection(object value) - { - if (connectionType.IsInstanceOfType(value)) - { - // Replace the HttpWebRequest arraylist inside this Connection object, - // which allows us to intercept each new HttpWebRequest object added under - // this Connection. - ArrayList originalArrayList = writeListField.GetValue(value) as ArrayList; - HttpWebRequestArrayList newArrayList = new HttpWebRequestArrayList(originalArrayList ?? new ArrayList()); + uriAccessor = CreateFieldGetter("m_Uri", BindingFlags.NonPublic | BindingFlags.Instance); + verbAccessor = CreateFieldGetter("m_Verb", BindingFlags.NonPublic | BindingFlags.Instance); + mediaTypeAccessor = CreateFieldGetter("m_MediaType", BindingFlags.NonPublic | BindingFlags.Instance); + usesProxySemanticsAccessor = CreateFieldGetter("m_UsesProxySemantics", BindingFlags.NonPublic | BindingFlags.Instance); + coreResponseDataAccessor = CreateFieldGetter("m_CoreResponseData", BindingFlags.NonPublic | BindingFlags.Instance); + isWebSocketResponseAccessor = CreateFieldGetter("m_IsWebSocketResponse", BindingFlags.NonPublic | BindingFlags.Instance); + connectionGroupNameAccessor = CreateFieldGetter("m_ConnectionGroupName", BindingFlags.NonPublic | BindingFlags.Instance); + + return httpWebResponseCtor != null + && uriAccessor != null + && verbAccessor != null + && mediaTypeAccessor != null + && usesProxySemanticsAccessor != null + && coreResponseDataAccessor != null + && isWebSocketResponseAccessor != null + && connectionGroupNameAccessor != null; + } - writeListField.SetValue(value, newArrayList); - } + private static void PerformInjection() + { + FieldInfo servicePointTableField = typeof(ServicePointManager).GetField("s_ServicePointTable", BindingFlags.Static | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("Unable to access the ServicePointTable field"); + + Hashtable originalTable = servicePointTableField.GetValue(null) as Hashtable; + ServicePointHashtable newTable = new ServicePointHashtable(originalTable ?? new Hashtable()); + + foreach (DictionaryEntry existingServicePoint in originalTable) + { + HookServicePoint(existingServicePoint.Value); } - private static Func CreateFieldGetter(string fieldName, BindingFlags flags) - where TClass : class + servicePointTableField.SetValue(null, newTable); + } + + private static void HookServicePoint(object value) + { + if (value is WeakReference weakRef + && weakRef.IsAlive + && weakRef.Target is ServicePoint servicePoint) { - FieldInfo field = typeof(TClass).GetField(fieldName, flags); - if (field != null) + // Replace the ConnectionGroup hashtable inside this ServicePoint object, + // which allows us to intercept each new ConnectionGroup object added under + // this ServicePoint. + Hashtable originalTable = connectionGroupListField.GetValue(servicePoint) as Hashtable; + ConnectionGroupHashtable newTable = new ConnectionGroupHashtable(originalTable ?? new Hashtable()); + + foreach (DictionaryEntry existingConnectionGroup in originalTable) { - string methodName = field.ReflectedType.FullName + ".get_" + field.Name; - DynamicMethod getterMethod = new DynamicMethod(methodName, typeof(TField), new[] { typeof(TClass) }, true); - ILGenerator generator = getterMethod.GetILGenerator(); - generator.Emit(OpCodes.Ldarg_0); - generator.Emit(OpCodes.Ldfld, field); - generator.Emit(OpCodes.Ret); - return (Func)getterMethod.CreateDelegate(typeof(Func)); + HookConnectionGroup(existingConnectionGroup.Value); } - return null; + connectionGroupListField.SetValue(servicePoint, newTable); } + } - /// - /// Creates getter for a field defined in private or internal type - /// repesented with classType variable. - /// - private static Func CreateFieldGetter(Type classType, string fieldName, BindingFlags flags) + private static void HookConnectionGroup(object value) + { + if (connectionGroupType.IsInstanceOfType(value)) { - FieldInfo field = classType.GetField(fieldName, flags); - if (field != null) + // Replace the Connection arraylist inside this ConnectionGroup object, + // which allows us to intercept each new Connection object added under + // this ConnectionGroup. + ArrayList originalArrayList = connectionListField.GetValue(value) as ArrayList; + ConnectionArrayList newArrayList = new ConnectionArrayList(originalArrayList ?? new ArrayList()); + + foreach (object connection in originalArrayList) { - string methodName = classType.FullName + ".get_" + field.Name; - DynamicMethod getterMethod = new DynamicMethod(methodName, typeof(TField), new[] { typeof(object) }, true); - ILGenerator generator = getterMethod.GetILGenerator(); - generator.Emit(OpCodes.Ldarg_0); - generator.Emit(OpCodes.Castclass, classType); - generator.Emit(OpCodes.Ldfld, field); - generator.Emit(OpCodes.Ret); - - return (Func)getterMethod.CreateDelegate(typeof(Func)); + HookConnection(connection); } - return null; + connectionListField.SetValue(value, newArrayList); } + } - /// - /// Creates setter for a field defined in private or internal type - /// repesented with classType variable. - /// - private static Action CreateFieldSetter(Type classType, string fieldName, BindingFlags flags) + private static void HookConnection(object value) + { + if (connectionType.IsInstanceOfType(value)) { - FieldInfo field = classType.GetField(fieldName, flags); - if (field != null) - { - string methodName = classType.FullName + ".set_" + field.Name; - DynamicMethod setterMethod = new DynamicMethod(methodName, null, new[] { typeof(object), typeof(TField) }, true); - ILGenerator generator = setterMethod.GetILGenerator(); - generator.Emit(OpCodes.Ldarg_0); - generator.Emit(OpCodes.Castclass, classType); - generator.Emit(OpCodes.Ldarg_1); - generator.Emit(OpCodes.Stfld, field); - generator.Emit(OpCodes.Ret); - - return (Action)setterMethod.CreateDelegate(typeof(Action)); - } + // Replace the HttpWebRequest arraylist inside this Connection object, + // which allows us to intercept each new HttpWebRequest object added under + // this Connection. + ArrayList originalArrayList = writeListField.GetValue(value) as ArrayList; + HttpWebRequestArrayList newArrayList = new HttpWebRequestArrayList(originalArrayList ?? new ArrayList()); - return null; + writeListField.SetValue(value, newArrayList); } + } - /// - /// Creates an "is" method for the private or internal type. - /// - private static Func CreateTypeChecker(Type classType) + private static Func CreateFieldGetter(string fieldName, BindingFlags flags) + where TClass : class + { + FieldInfo field = typeof(TClass).GetField(fieldName, flags); + if (field != null) { - string methodName = classType.FullName + ".typeCheck"; - DynamicMethod setterMethod = new DynamicMethod(methodName, typeof(bool), new[] { typeof(object) }, true); - ILGenerator generator = setterMethod.GetILGenerator(); + string methodName = field.ReflectedType.FullName + ".get_" + field.Name; + DynamicMethod getterMethod = new DynamicMethod(methodName, typeof(TField), new[] { typeof(TClass) }, true); + ILGenerator generator = getterMethod.GetILGenerator(); generator.Emit(OpCodes.Ldarg_0); - generator.Emit(OpCodes.Isinst, classType); - generator.Emit(OpCodes.Ldnull); - generator.Emit(OpCodes.Cgt_Un); + generator.Emit(OpCodes.Ldfld, field); generator.Emit(OpCodes.Ret); - - return (Func)setterMethod.CreateDelegate(typeof(Func)); + return (Func)getterMethod.CreateDelegate(typeof(Func)); } - /// - /// Creates an instance of T using a private or internal ctor. - /// - private static Func CreateTypeInstance(ConstructorInfo constructorInfo) + return null; + } + + /// + /// Creates getter for a field defined in private or internal type + /// repesented with classType variable. + /// + private static Func CreateFieldGetter(Type classType, string fieldName, BindingFlags flags) + { + FieldInfo field = classType.GetField(fieldName, flags); + if (field != null) { - Type classType = typeof(T); - string methodName = classType.FullName + ".ctor"; - DynamicMethod setterMethod = new DynamicMethod(methodName, classType, new Type[] { typeof(object[]) }, true); - ILGenerator generator = setterMethod.GetILGenerator(); + string methodName = classType.FullName + ".get_" + field.Name; + DynamicMethod getterMethod = new DynamicMethod(methodName, typeof(TField), new[] { typeof(object) }, true); + ILGenerator generator = getterMethod.GetILGenerator(); + generator.Emit(OpCodes.Ldarg_0); + generator.Emit(OpCodes.Castclass, classType); + generator.Emit(OpCodes.Ldfld, field); + generator.Emit(OpCodes.Ret); - ParameterInfo[] ctorParams = constructorInfo.GetParameters(); - for (int i = 0; i < ctorParams.Length; i++) - { - generator.Emit(OpCodes.Ldarg_0); - switch (i) - { - case 0: generator.Emit(OpCodes.Ldc_I4_0); break; - case 1: generator.Emit(OpCodes.Ldc_I4_1); break; - case 2: generator.Emit(OpCodes.Ldc_I4_2); break; - case 3: generator.Emit(OpCodes.Ldc_I4_3); break; - case 4: generator.Emit(OpCodes.Ldc_I4_4); break; - case 5: generator.Emit(OpCodes.Ldc_I4_5); break; - case 6: generator.Emit(OpCodes.Ldc_I4_6); break; - case 7: generator.Emit(OpCodes.Ldc_I4_7); break; - case 8: generator.Emit(OpCodes.Ldc_I4_8); break; - default: generator.Emit(OpCodes.Ldc_I4, i); break; - } + return (Func)getterMethod.CreateDelegate(typeof(Func)); + } - generator.Emit(OpCodes.Ldelem_Ref); - Type paramType = ctorParams[i].ParameterType; - generator.Emit(paramType.IsValueType ? OpCodes.Unbox_Any : OpCodes.Castclass, paramType); - } + return null; + } - generator.Emit(OpCodes.Newobj, constructorInfo); + /// + /// Creates setter for a field defined in private or internal type + /// repesented with classType variable. + /// + private static Action CreateFieldSetter(Type classType, string fieldName, BindingFlags flags) + { + FieldInfo field = classType.GetField(fieldName, flags); + if (field != null) + { + string methodName = classType.FullName + ".set_" + field.Name; + DynamicMethod setterMethod = new DynamicMethod(methodName, null, new[] { typeof(object), typeof(TField) }, true); + ILGenerator generator = setterMethod.GetILGenerator(); + generator.Emit(OpCodes.Ldarg_0); + generator.Emit(OpCodes.Castclass, classType); + generator.Emit(OpCodes.Ldarg_1); + generator.Emit(OpCodes.Stfld, field); generator.Emit(OpCodes.Ret); - return (Func)setterMethod.CreateDelegate(typeof(Func)); + return (Action)setterMethod.CreateDelegate(typeof(Action)); } - private class HashtableWrapper : Hashtable, IEnumerable + return null; + } + + /// + /// Creates an "is" method for the private or internal type. + /// + private static Func CreateTypeChecker(Type classType) + { + string methodName = classType.FullName + ".typeCheck"; + DynamicMethod setterMethod = new DynamicMethod(methodName, typeof(bool), new[] { typeof(object) }, true); + ILGenerator generator = setterMethod.GetILGenerator(); + generator.Emit(OpCodes.Ldarg_0); + generator.Emit(OpCodes.Isinst, classType); + generator.Emit(OpCodes.Ldnull); + generator.Emit(OpCodes.Cgt_Un); + generator.Emit(OpCodes.Ret); + + return (Func)setterMethod.CreateDelegate(typeof(Func)); + } + + /// + /// Creates an instance of T using a private or internal ctor. + /// + private static Func CreateTypeInstance(ConstructorInfo constructorInfo) + { + Type classType = typeof(T); + string methodName = classType.FullName + ".ctor"; + DynamicMethod setterMethod = new DynamicMethod(methodName, classType, new Type[] { typeof(object[]) }, true); + ILGenerator generator = setterMethod.GetILGenerator(); + + ParameterInfo[] ctorParams = constructorInfo.GetParameters(); + for (int i = 0; i < ctorParams.Length; i++) { - private readonly Hashtable table; + generator.Emit(OpCodes.Ldarg_0); + switch (i) + { + case 0: generator.Emit(OpCodes.Ldc_I4_0); break; + case 1: generator.Emit(OpCodes.Ldc_I4_1); break; + case 2: generator.Emit(OpCodes.Ldc_I4_2); break; + case 3: generator.Emit(OpCodes.Ldc_I4_3); break; + case 4: generator.Emit(OpCodes.Ldc_I4_4); break; + case 5: generator.Emit(OpCodes.Ldc_I4_5); break; + case 6: generator.Emit(OpCodes.Ldc_I4_6); break; + case 7: generator.Emit(OpCodes.Ldc_I4_7); break; + case 8: generator.Emit(OpCodes.Ldc_I4_8); break; + default: generator.Emit(OpCodes.Ldc_I4, i); break; + } + + generator.Emit(OpCodes.Ldelem_Ref); + Type paramType = ctorParams[i].ParameterType; + generator.Emit(paramType.IsValueType ? OpCodes.Unbox_Any : OpCodes.Castclass, paramType); + } - internal HashtableWrapper(Hashtable table) - : base() - { - this.table = table; - } + generator.Emit(OpCodes.Newobj, constructorInfo); + generator.Emit(OpCodes.Ret); - public override int Count => this.table.Count; + return (Func)setterMethod.CreateDelegate(typeof(Func)); + } - public override bool IsReadOnly => this.table.IsReadOnly; + private class HashtableWrapper : Hashtable, IEnumerable + { + private readonly Hashtable table; - public override bool IsFixedSize => this.table.IsFixedSize; + internal HashtableWrapper(Hashtable table) + : base() + { + this.table = table; + } - public override bool IsSynchronized => this.table.IsSynchronized; + public override int Count => this.table.Count; - public override object SyncRoot => this.table.SyncRoot; + public override bool IsReadOnly => this.table.IsReadOnly; - public override ICollection Keys => this.table.Keys; + public override bool IsFixedSize => this.table.IsFixedSize; - public override ICollection Values => this.table.Values; + public override bool IsSynchronized => this.table.IsSynchronized; - public override object this[object key] - { - get => this.table[key]; - set => this.table[key] = value; - } + public override object SyncRoot => this.table.SyncRoot; - public override void Add(object key, object value) - { - this.table.Add(key, value); - } + public override ICollection Keys => this.table.Keys; - public override void Clear() - { - this.table.Clear(); - } + public override ICollection Values => this.table.Values; - public override bool Contains(object key) - { - return this.table.Contains(key); - } + public override object this[object key] + { + get => this.table[key]; + set => this.table[key] = value; + } - public override bool ContainsKey(object key) - { - return this.table.ContainsKey(key); - } + public override void Add(object key, object value) + { + this.table.Add(key, value); + } - public override bool ContainsValue(object key) - { - return this.table.ContainsValue(key); - } + public override void Clear() + { + this.table.Clear(); + } - public override void CopyTo(Array array, int arrayIndex) - { - this.table.CopyTo(array, arrayIndex); - } + public override bool Contains(object key) + { + return this.table.Contains(key); + } - public override object Clone() - { - return new HashtableWrapper((Hashtable)this.table.Clone()); - } + public override bool ContainsKey(object key) + { + return this.table.ContainsKey(key); + } - IEnumerator IEnumerable.GetEnumerator() - { - return this.table.GetEnumerator(); - } + public override bool ContainsValue(object key) + { + return this.table.ContainsValue(key); + } - public override IDictionaryEnumerator GetEnumerator() - { - return this.table.GetEnumerator(); - } + public override void CopyTo(Array array, int arrayIndex) + { + this.table.CopyTo(array, arrayIndex); + } - public override void Remove(object key) - { - this.table.Remove(key); - } + public override object Clone() + { + return new HashtableWrapper((Hashtable)this.table.Clone()); } - /// - /// Helper class used for ServicePointManager.s_ServicePointTable. The goal here is to - /// intercept each new ServicePoint object being added to ServicePointManager.s_ServicePointTable - /// and replace its ConnectionGroupList hashtable field. - /// - private sealed class ServicePointHashtable : HashtableWrapper + IEnumerator IEnumerable.GetEnumerator() { - public ServicePointHashtable(Hashtable table) - : base(table) - { - } + return this.table.GetEnumerator(); + } - public override object this[object key] - { - get => base[key]; - set - { - HookServicePoint(value); - base[key] = value; - } - } + public override IDictionaryEnumerator GetEnumerator() + { + return this.table.GetEnumerator(); } - /// - /// Helper class used for ServicePoint.m_ConnectionGroupList. The goal here is to - /// intercept each new ConnectionGroup object being added to ServicePoint.m_ConnectionGroupList - /// and replace its m_ConnectionList arraylist field. - /// - private sealed class ConnectionGroupHashtable : HashtableWrapper + public override void Remove(object key) { - public ConnectionGroupHashtable(Hashtable table) - : base(table) - { - } + this.table.Remove(key); + } + } + + /// + /// Helper class used for ServicePointManager.s_ServicePointTable. The goal here is to + /// intercept each new ServicePoint object being added to ServicePointManager.s_ServicePointTable + /// and replace its ConnectionGroupList hashtable field. + /// + private sealed class ServicePointHashtable : HashtableWrapper + { + public ServicePointHashtable(Hashtable table) + : base(table) + { + } - public override object this[object key] + public override object this[object key] + { + get => base[key]; + set { - get => base[key]; - set - { - HookConnectionGroup(value); - base[key] = value; - } + HookServicePoint(value); + base[key] = value; } } + } - /// - /// Helper class used to wrap the array list object. This class itself doesn't actually - /// have the array elements, but rather access another array list that's given at - /// construction time. - /// - private class ArrayListWrapper : ArrayList + /// + /// Helper class used for ServicePoint.m_ConnectionGroupList. The goal here is to + /// intercept each new ConnectionGroup object being added to ServicePoint.m_ConnectionGroupList + /// and replace its m_ConnectionList arraylist field. + /// + private sealed class ConnectionGroupHashtable : HashtableWrapper + { + public ConnectionGroupHashtable(Hashtable table) + : base(table) { - private ArrayList list; + } - internal ArrayListWrapper(ArrayList list) - : base() + public override object this[object key] + { + get => base[key]; + set { - this.list = list; + HookConnectionGroup(value); + base[key] = value; } + } + } - public override int Capacity - { - get => this.list.Capacity; - set => this.list.Capacity = value; - } + /// + /// Helper class used to wrap the array list object. This class itself doesn't actually + /// have the array elements, but rather access another array list that's given at + /// construction time. + /// + private class ArrayListWrapper : ArrayList + { + private ArrayList list; - public override int Count => this.list.Count; + internal ArrayListWrapper(ArrayList list) + : base() + { + this.list = list; + } - public override bool IsReadOnly => this.list.IsReadOnly; + public override int Capacity + { + get => this.list.Capacity; + set => this.list.Capacity = value; + } - public override bool IsFixedSize => this.list.IsFixedSize; + public override int Count => this.list.Count; - public override bool IsSynchronized => this.list.IsSynchronized; + public override bool IsReadOnly => this.list.IsReadOnly; - public override object SyncRoot => this.list.SyncRoot; + public override bool IsFixedSize => this.list.IsFixedSize; - public override object this[int index] - { - get => this.list[index]; - set => this.list[index] = value; - } + public override bool IsSynchronized => this.list.IsSynchronized; - public override int Add(object value) - { - return this.list.Add(value); - } + public override object SyncRoot => this.list.SyncRoot; - public override void AddRange(ICollection c) - { - this.list.AddRange(c); - } + public override object this[int index] + { + get => this.list[index]; + set => this.list[index] = value; + } - public override int BinarySearch(object value) - { - return this.list.BinarySearch(value); - } + public override int Add(object value) + { + return this.list.Add(value); + } - public override int BinarySearch(object value, IComparer comparer) - { - return this.list.BinarySearch(value, comparer); - } + public override void AddRange(ICollection c) + { + this.list.AddRange(c); + } - public override int BinarySearch(int index, int count, object value, IComparer comparer) - { - return this.list.BinarySearch(index, count, value, comparer); - } + public override int BinarySearch(object value) + { + return this.list.BinarySearch(value); + } - public override void Clear() - { - this.list.Clear(); - } + public override int BinarySearch(object value, IComparer comparer) + { + return this.list.BinarySearch(value, comparer); + } - public override object Clone() - { - return new ArrayListWrapper((ArrayList)this.list.Clone()); - } + public override int BinarySearch(int index, int count, object value, IComparer comparer) + { + return this.list.BinarySearch(index, count, value, comparer); + } - public override bool Contains(object item) - { - return this.list.Contains(item); - } + public override void Clear() + { + this.list.Clear(); + } - public override void CopyTo(Array array) - { - this.list.CopyTo(array); - } + public override object Clone() + { + return new ArrayListWrapper((ArrayList)this.list.Clone()); + } - public override void CopyTo(Array array, int index) - { - this.list.CopyTo(array, index); - } + public override bool Contains(object item) + { + return this.list.Contains(item); + } - public override void CopyTo(int index, Array array, int arrayIndex, int count) - { - this.list.CopyTo(index, array, arrayIndex, count); - } + public override void CopyTo(Array array) + { + this.list.CopyTo(array); + } - public override IEnumerator GetEnumerator() - { - return this.list.GetEnumerator(); - } + public override void CopyTo(Array array, int index) + { + this.list.CopyTo(array, index); + } - public override IEnumerator GetEnumerator(int index, int count) - { - return this.list.GetEnumerator(index, count); - } + public override void CopyTo(int index, Array array, int arrayIndex, int count) + { + this.list.CopyTo(index, array, arrayIndex, count); + } - public override int IndexOf(object value) - { - return this.list.IndexOf(value); - } + public override IEnumerator GetEnumerator() + { + return this.list.GetEnumerator(); + } - public override int IndexOf(object value, int startIndex) - { - return this.list.IndexOf(value, startIndex); - } + public override IEnumerator GetEnumerator(int index, int count) + { + return this.list.GetEnumerator(index, count); + } - public override int IndexOf(object value, int startIndex, int count) - { - return this.list.IndexOf(value, startIndex, count); - } + public override int IndexOf(object value) + { + return this.list.IndexOf(value); + } - public override void Insert(int index, object value) - { - this.list.Insert(index, value); - } + public override int IndexOf(object value, int startIndex) + { + return this.list.IndexOf(value, startIndex); + } - public override void InsertRange(int index, ICollection c) - { - this.list.InsertRange(index, c); - } + public override int IndexOf(object value, int startIndex, int count) + { + return this.list.IndexOf(value, startIndex, count); + } - public override int LastIndexOf(object value) - { - return this.list.LastIndexOf(value); - } + public override void Insert(int index, object value) + { + this.list.Insert(index, value); + } - public override int LastIndexOf(object value, int startIndex) - { - return this.list.LastIndexOf(value, startIndex); - } + public override void InsertRange(int index, ICollection c) + { + this.list.InsertRange(index, c); + } - public override int LastIndexOf(object value, int startIndex, int count) - { - return this.list.LastIndexOf(value, startIndex, count); - } + public override int LastIndexOf(object value) + { + return this.list.LastIndexOf(value); + } - public override void Remove(object value) - { - this.list.Remove(value); - } + public override int LastIndexOf(object value, int startIndex) + { + return this.list.LastIndexOf(value, startIndex); + } - public override void RemoveAt(int index) - { - this.list.RemoveAt(index); - } + public override int LastIndexOf(object value, int startIndex, int count) + { + return this.list.LastIndexOf(value, startIndex, count); + } - public override void RemoveRange(int index, int count) - { - this.list.RemoveRange(index, count); - } + public override void Remove(object value) + { + this.list.Remove(value); + } - public override void Reverse(int index, int count) - { - this.list.Reverse(index, count); - } + public override void RemoveAt(int index) + { + this.list.RemoveAt(index); + } - public override void SetRange(int index, ICollection c) - { - this.list.SetRange(index, c); - } + public override void RemoveRange(int index, int count) + { + this.list.RemoveRange(index, count); + } - public override ArrayList GetRange(int index, int count) - { - return this.list.GetRange(index, count); - } + public override void Reverse(int index, int count) + { + this.list.Reverse(index, count); + } - public override void Sort() - { - this.list.Sort(); - } + public override void SetRange(int index, ICollection c) + { + this.list.SetRange(index, c); + } - public override void Sort(IComparer comparer) - { - this.list.Sort(comparer); - } + public override ArrayList GetRange(int index, int count) + { + return this.list.GetRange(index, count); + } - public override void Sort(int index, int count, IComparer comparer) - { - this.list.Sort(index, count, comparer); - } + public override void Sort() + { + this.list.Sort(); + } - public override object[] ToArray() - { - return this.list.ToArray(); - } + public override void Sort(IComparer comparer) + { + this.list.Sort(comparer); + } - public override Array ToArray(Type type) - { - return this.list.ToArray(type); - } + public override void Sort(int index, int count, IComparer comparer) + { + this.list.Sort(index, count, comparer); + } - public override void TrimToSize() - { - this.list.TrimToSize(); - } + public override object[] ToArray() + { + return this.list.ToArray(); + } - public ArrayList Swap() - { - ArrayList old = this.list; - this.list = new ArrayList(old.Capacity); - return old; - } + public override Array ToArray(Type type) + { + return this.list.ToArray(type); } - /// - /// Helper class used for ConnectionGroup.m_ConnectionList. The goal here is to - /// intercept each new Connection object being added to ConnectionGroup.m_ConnectionList - /// and replace its m_WriteList arraylist field. - /// - private sealed class ConnectionArrayList : ArrayListWrapper + public override void TrimToSize() { - public ConnectionArrayList(ArrayList list) - : base(list) - { - } + this.list.TrimToSize(); + } - public override int Add(object value) - { - HookConnection(value); - return base.Add(value); - } + public ArrayList Swap() + { + ArrayList old = this.list; + this.list = new ArrayList(old.Capacity); + return old; } + } - /// - /// Helper class used for Connection.m_WriteList. The goal here is to - /// intercept all new HttpWebRequest objects being added to Connection.m_WriteList - /// and notify the listener about the HttpWebRequest that's about to send a request. - /// It also intercepts all HttpWebRequest objects that are about to get removed from - /// Connection.m_WriteList as they have completed the request. - /// - private sealed class HttpWebRequestArrayList : ArrayListWrapper + /// + /// Helper class used for ConnectionGroup.m_ConnectionList. The goal here is to + /// intercept each new Connection object being added to ConnectionGroup.m_ConnectionList + /// and replace its m_WriteList arraylist field. + /// + private sealed class ConnectionArrayList : ArrayListWrapper + { + public ConnectionArrayList(ArrayList list) + : base(list) { - public HttpWebRequestArrayList(ArrayList list) - : base(list) - { - } + } - public override int Add(object value) - { - // Add before firing events so if some user code cancels/aborts the request it will be found in the outstanding list. - int index = base.Add(value); + public override int Add(object value) + { + HookConnection(value); + return base.Add(value); + } + } - if (value is HttpWebRequest request) - { - ProcessRequest(request); - } + /// + /// Helper class used for Connection.m_WriteList. The goal here is to + /// intercept all new HttpWebRequest objects being added to Connection.m_WriteList + /// and notify the listener about the HttpWebRequest that's about to send a request. + /// It also intercepts all HttpWebRequest objects that are about to get removed from + /// Connection.m_WriteList as they have completed the request. + /// + private sealed class HttpWebRequestArrayList : ArrayListWrapper + { + public HttpWebRequestArrayList(ArrayList list) + : base(list) + { + } - return index; - } + public override int Add(object value) + { + // Add before firing events so if some user code cancels/aborts the request it will be found in the outstanding list. + int index = base.Add(value); - public override void RemoveAt(int index) + if (value is HttpWebRequest request) { - object request = this[index]; + ProcessRequest(request); + } + + return index; + } - base.RemoveAt(index); + public override void RemoveAt(int index) + { + object request = this[index]; - if (request is HttpWebRequest webRequest) - { - HookOrProcessResult(webRequest); - } + base.RemoveAt(index); + + if (request is HttpWebRequest webRequest) + { + HookOrProcessResult(webRequest); } + } - public override void Clear() + public override void Clear() + { + ArrayList oldList = this.Swap(); + for (int i = 0; i < oldList.Count; i++) { - ArrayList oldList = this.Swap(); - for (int i = 0; i < oldList.Count; i++) + if (oldList[i] is HttpWebRequest request) { - if (oldList[i] is HttpWebRequest request) - { - HookOrProcessResult(request); - } + HookOrProcessResult(request); } } } + } - /// - /// A closure object so our state is available when our callback executes. - /// - private sealed class AsyncCallbackWrapper + /// + /// A closure object so our state is available when our callback executes. + /// + private sealed class AsyncCallbackWrapper + { + public AsyncCallbackWrapper(HttpWebRequest request, Activity activity, AsyncCallback originalCallback, long startTimestamp) { - public AsyncCallbackWrapper(HttpWebRequest request, Activity activity, AsyncCallback originalCallback) - { - this.Request = request; - this.Activity = activity; - this.OriginalCallback = originalCallback; - } + this.Request = request; + this.Activity = activity; + this.OriginalCallback = originalCallback; + this.StartTimestamp = startTimestamp; + } - public HttpWebRequest Request { get; } + public HttpWebRequest Request { get; } - public Activity Activity { get; } + public Activity Activity { get; } - public AsyncCallback OriginalCallback { get; } + public AsyncCallback OriginalCallback { get; } - public void AsyncCallback(IAsyncResult asyncResult) - { - object result = resultAccessor(asyncResult); - if (result is Exception || result is HttpWebResponse) - { - ProcessResult(asyncResult, this.OriginalCallback, this.Activity, result, false); - } + public long StartTimestamp { get; } - this.OriginalCallback?.Invoke(asyncResult); + public void AsyncCallback(IAsyncResult asyncResult) + { + object result = resultAccessor(asyncResult); + if (result is Exception || result is HttpWebResponse) + { + ProcessResult( + asyncResult, + this.OriginalCallback, + this.Activity, + result, + forceResponseCopy: false, + this.Request, + this.StartTimestamp); } + + this.OriginalCallback?.Invoke(asyncResult); } } } diff --git a/src/OpenTelemetry.Instrumentation.Http/Implementation/TelemetryHelper.cs b/src/OpenTelemetry.Instrumentation.Http/Implementation/TelemetryHelper.cs index 9b53a5bc5f6..353a49b0e01 100644 --- a/src/OpenTelemetry.Instrumentation.Http/Implementation/TelemetryHelper.cs +++ b/src/OpenTelemetry.Instrumentation.Http/Implementation/TelemetryHelper.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Net; @@ -20,14 +7,14 @@ namespace OpenTelemetry.Instrumentation.Http.Implementation; internal static class TelemetryHelper { - public static readonly object[] BoxedStatusCodes; + public static readonly (object, string)[] BoxedStatusCodes; static TelemetryHelper() { - BoxedStatusCodes = new object[500]; + BoxedStatusCodes = new (object, string)[500]; for (int i = 0, c = 100; i < BoxedStatusCodes.Length; i++, c++) { - BoxedStatusCodes[i] = c; + BoxedStatusCodes[i] = (c, c.ToString()); } } @@ -36,9 +23,20 @@ public static object GetBoxedStatusCode(HttpStatusCode statusCode) int intStatusCode = (int)statusCode; if (intStatusCode >= 100 && intStatusCode < 600) { - return BoxedStatusCodes[intStatusCode - 100]; + return BoxedStatusCodes[intStatusCode - 100].Item1; } - return intStatusCode; + return statusCode; + } + + public static string GetStatusCodeString(HttpStatusCode statusCode) + { + int intStatusCode = (int)statusCode; + if (intStatusCode >= 100 && intStatusCode < 600) + { + return BoxedStatusCodes[intStatusCode - 100].Item2; + } + + return statusCode.ToString(); } } diff --git a/src/OpenTelemetry.Instrumentation.Http/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Http/MeterProviderBuilderExtensions.cs deleted file mode 100644 index b78c800dee7..00000000000 --- a/src/OpenTelemetry.Instrumentation.Http/MeterProviderBuilderExtensions.cs +++ /dev/null @@ -1,54 +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 OpenTelemetry.Instrumentation.Http; -using OpenTelemetry.Instrumentation.Http.Implementation; -using OpenTelemetry.Internal; - -namespace OpenTelemetry.Metrics -{ - /// - /// Extension methods to simplify registering of HttpClient instrumentation. - /// - public static class MeterProviderBuilderExtensions - { - /// - /// Enables HttpClient instrumentation. - /// - /// being configured. - /// The instance of to chain the calls. - public static MeterProviderBuilder AddHttpClientInstrumentation( - this MeterProviderBuilder builder) - { - Guard.ThrowIfNull(builder); - - // Note: Warm-up the status code mapping. - _ = TelemetryHelper.BoxedStatusCodes; - - // TODO: Implement an IDeferredMeterProviderBuilder - - // TODO: Handle HttpClientInstrumentationOptions - // SetHttpFlavor - seems like this would be handled by views - // Filter - makes sense for metric instrumentation - // Enrich - do we want a similar kind of functionality for metrics? - // RecordException - probably doesn't make sense for metric instrumentation - - var instrumentation = new HttpClientMetrics(); - builder.AddMeter(HttpClientMetrics.InstrumentationName); - return builder.AddInstrumentation(() => instrumentation); - } - } -} diff --git a/src/OpenTelemetry.Instrumentation.Http/OpenTelemetry.Instrumentation.Http.csproj b/src/OpenTelemetry.Instrumentation.Http/OpenTelemetry.Instrumentation.Http.csproj index 23a8c183a25..55158028cde 100644 --- a/src/OpenTelemetry.Instrumentation.Http/OpenTelemetry.Instrumentation.Http.csproj +++ b/src/OpenTelemetry.Instrumentation.Http/OpenTelemetry.Instrumentation.Http.csproj @@ -1,10 +1,10 @@ - - netstandard2.0;net462 + $(TargetFrameworksForLibraries) Http instrumentation for OpenTelemetry .NET $(PackageTags);distributed-tracing + Instrumentation.Http- true @@ -12,12 +12,23 @@ - - + + + + + + + + + + + + - + + diff --git a/src/OpenTelemetry.Instrumentation.Http/README.md b/src/OpenTelemetry.Instrumentation.Http/README.md index 7ca9400d4b2..e9177766ec4 100644 --- a/src/OpenTelemetry.Instrumentation.Http/README.md +++ b/src/OpenTelemetry.Instrumentation.Http/README.md @@ -11,18 +11,10 @@ and [System.Net.HttpWebRequest](https://docs.microsoft.com/dotnet/api/system.net.httpwebrequest) and collects metrics and traces about outgoing HTTP requests. -**Note: This component is based on the OpenTelemetry semantic conventions for -[metrics](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/metrics/semantic_conventions) -and -[traces](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/trace/semantic_conventions). -These conventions are -[Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/document-status.md), -and hence, this package is a [pre-release](../../VERSIONING.md#pre-releases). -Until a [stable -version](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/telemetry-stability.md) -is released, there can be [breaking changes](./CHANGELOG.md). You can track the -progress from -[milestones](https://github.com/open-telemetry/opentelemetry-dotnet/milestone/23).** +This component is based on the +[v1.23](https://github.com/open-telemetry/semantic-conventions/tree/v1.23.0/docs/http) +of http semantic conventions. For details on the default set of attributes that +are added, checkout [Traces](#traces) and [Metrics](#metrics) sections below. ## Steps to enable OpenTelemetry.Instrumentation.Http @@ -33,7 +25,7 @@ Add a reference to the package. Also, add any other instrumentations & exporters you will need. ```shell -dotnet add package --prerelease OpenTelemetry.Instrumentation.Http +dotnet add package OpenTelemetry.Instrumentation.Http ``` ### Step 2: Enable HTTP Instrumentation at application startup @@ -65,10 +57,23 @@ public class Program } ``` -#### Metrics +Following list of attributes are added by default on activity. See +[http-spans](https://github.com/open-telemetry/semantic-conventions/tree/v1.23.0/docs/http/http-spans.md) +for more details about each individual attribute: + +* `error.type` +* `http.request.method` +* `http.request.method_original` +* `http.response.status_code` +* `network.protocol.version` +* `server.address` +* `server.port` +* `url.full` -> **Note** -> Metrics are not available for .NET Framework. +[Enrich Api](#enrich-httpclient-api) can be used if any additional attributes are +required on activity. + +#### Metrics The following example demonstrates adding `HttpClient` instrumentation with the extension method `.AddHttpClientInstrumentation()` on `MeterProviderBuilder` to @@ -100,29 +105,84 @@ Refer to this [example](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/blob/main/src/OpenTelemetry.Instrumentation.AspNet/README.md) to see how to enable this instrumentation in an ASP.NET application. -#### List of metrics produced +Following list of attributes are added by default on +`http.client.request.duration` metric. See +[http-metrics](https://github.com/open-telemetry/semantic-conventions/tree/v1.23.0/docs/http/http-metrics.md) +for more details about each individual attribute. `.NET8.0` and above supports +additional metrics, see [list of metrics produced](#list-of-metrics-produced) for +more details. + +* `error.type` +* `http.request.method` +* `http.response.status_code` +* `network.protocol.version` +* `server.address` +* `server.port` +* `url.scheme` -The instrumentation is implemented based on [metrics semantic -conventions](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#metric-httpclientduration). -Currently, the instrumentation supports the following metric. +#### List of metrics produced -| Name | Instrument Type | Unit | Description | -|-------|-----------------|------|-------------| -| `http.client.duration` | Histogram | `ms` | Measures the duration of outbound HTTP requests. | +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`. This applies to all +targeted frameworks. ## Advanced configuration +### Tracing + This instrumentation can be configured to change the default behavior by using -`HttpClientInstrumentationOptions`. It is important to note that there are +`HttpClientTraceInstrumentationOptions`. It is important to note that there are differences between .NET Framework and newer .NET/.NET Core runtimes which govern what options are used. On .NET Framework, `HttpClient` uses the `HttpWebRequest` API. On .NET & .NET Core, `HttpWebRequest` uses the `HttpClient` API. As such, depending on the runtime, only one half of the "filter" & "enrich" options are used. -### .NET & .NET Core +#### .NET & .NET Core -#### Filter HttpClient API +##### Filter HttpClient API This instrumentation by default collects all the outgoing HTTP requests. It allows filtering of requests by using the `FilterHttpRequestMessage` function @@ -154,14 +214,14 @@ to this instrumentation. OpenTelemetry has a concept of a and the `FilterHttpRequestMessage` option does the filtering *after* the Sampler is invoked. -#### Enrich HttpClient API +##### Enrich HttpClient API This instrumentation library provides options that can be used to enrich the activity with additional information. These actions are called only when `activity.IsAllDataRequested` is `true`. It contains the activity itself (which can be enriched) and the actual raw object. -`HttpClientInstrumentationOptions` provides 3 enrich options: +`HttpClientTraceInstrumentationOptions` provides 3 enrich options: `EnrichWithHttpRequestMessage`, `EnrichWithHttpResponseMessage` and `EnrichWithException`. These are based on the raw object that is passed in to the action to enrich the activity. @@ -193,9 +253,9 @@ var tracerProvider = Sdk.CreateTracerProviderBuilder() .Build(); ``` -### .NET Framework +#### .NET Framework -#### Filter HttpWebRequest API +##### Filter HttpWebRequest API This instrumentation by default collects all the outgoing HTTP requests. It allows filtering of requests by using the `FilterHttpWebRequest` function @@ -227,14 +287,14 @@ this instrumentation. OpenTelemetry has a concept of a and the `FilterHttpWebRequest` option does the filtering *after* the Sampler is invoked. -#### Enrich HttpWebRequest API +##### Enrich HttpWebRequest API This instrumentation library provides options that can be used to enrich the activity with additional information. These actions are called only when `activity.IsAllDataRequested` is `true`. It contains the activity itself (which can be enriched) and the actual raw object. -`HttpClientInstrumentationOptions` provides 3 enrich options: +`HttpClientTraceInstrumentationOptions` provides 3 enrich options: `EnrichWithHttpWebRequest`, `EnrichWithHttpWebResponse` and `EnrichWithException`. These are based on the raw object that is passed in to the action to enrich the activity. @@ -271,12 +331,19 @@ general extensibility point to add additional properties to any activity. The `Enrich` option is specific to this instrumentation, and is provided to get access to raw request, response, and exception objects. -### RecordException +#### RecordException This instrumentation automatically sets Activity Status to Error if the Http StatusCode is >= 400. Additionally, `RecordException` feature may be turned on, to store the exception to the Activity itself as ActivityEvent. +## Activity duration and http.client.request.duration metric calculation + +`Activity.Duration` and `http.client.request.duration` values represents the +time the underlying client handler takes to complete the request. Completing the +request includes the time up to reading response headers from the network +stream. It doesn't include the time spent reading the response body. + ## Troubleshooting This component uses an diff --git a/src/OpenTelemetry.Instrumentation.Http/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Http/TracerProviderBuilderExtensions.cs deleted file mode 100644 index bebfd21eb9e..00000000000 --- a/src/OpenTelemetry.Instrumentation.Http/TracerProviderBuilderExtensions.cs +++ /dev/null @@ -1,114 +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 Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using OpenTelemetry.Instrumentation.Http; -using OpenTelemetry.Instrumentation.Http.Implementation; -using OpenTelemetry.Internal; - -namespace OpenTelemetry.Trace -{ - /// - /// Extension methods to simplify registering of HttpClient instrumentation. - /// - public static class TracerProviderBuilderExtensions - { - /// - /// Enables HttpClient instrumentation. - /// - /// being configured. - /// The instance of to chain the calls. - public static TracerProviderBuilder AddHttpClientInstrumentation(this TracerProviderBuilder builder) - => AddHttpClientInstrumentation(builder, name: null, configureHttpClientInstrumentationOptions: null); - - /// - /// Enables HttpClient instrumentation. - /// - /// being configured. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static TracerProviderBuilder AddHttpClientInstrumentation( - this TracerProviderBuilder builder, - Action configureHttpClientInstrumentationOptions) - => AddHttpClientInstrumentation(builder, name: null, configureHttpClientInstrumentationOptions); - - /// - /// Enables HttpClient instrumentation. - /// - /// being configured. - /// Name which is used when retrieving options. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static TracerProviderBuilder AddHttpClientInstrumentation( - this TracerProviderBuilder builder, - string name, - Action configureHttpClientInstrumentationOptions) - { - Guard.ThrowIfNull(builder); - - // Note: Warm-up the status code mapping. - _ = TelemetryHelper.BoxedStatusCodes; - - name ??= Options.DefaultName; - - if (configureHttpClientInstrumentationOptions != null) - { - builder.ConfigureServices(services => services.Configure(name, configureHttpClientInstrumentationOptions)); - } - -#if NETFRAMEWORK - builder.AddSource(HttpWebRequestActivitySource.ActivitySourceName); - - if (builder is IDeferredTracerProviderBuilder deferredTracerProviderBuilder) - { - deferredTracerProviderBuilder.Configure((sp, builder) => - { - var options = sp.GetRequiredService>().Get(name); - - HttpWebRequestActivitySource.Options = options; - }); - } -#else - AddHttpClientInstrumentationSource(builder); - - builder.AddInstrumentation(sp => - { - var options = sp.GetRequiredService>().Get(name); - - return new HttpClientInstrumentation(options); - }); -#endif - return builder; - } - -#if !NETFRAMEWORK - internal static void AddHttpClientInstrumentationSource( - this TracerProviderBuilder builder) - { - if (HttpHandlerDiagnosticListener.IsNet7OrGreater) - { - builder.AddSource(HttpHandlerDiagnosticListener.HttpClientActivitySourceName); - } - else - { - builder.AddSource(HttpHandlerDiagnosticListener.ActivitySourceName); - builder.AddLegacySource("System.Net.Http.HttpRequestOut"); - } - } -#endif - } -} diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/PublicAPI.Shipped.txt similarity index 100% rename from src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/netstandard2.0/PublicAPI.Shipped.txt rename to src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/PublicAPI.Shipped.txt diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..82f785235c9 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/PublicAPI.Unshipped.txt @@ -0,0 +1,18 @@ +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.EnableConnectionLevelAttributes.get -> bool +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.EnableConnectionLevelAttributes.set -> void +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.Enrich.get -> System.Action +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.Enrich.set -> void +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.Filter.get -> System.Func +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.Filter.set -> void +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.RecordException.get -> bool +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.RecordException.set -> void +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.SetDbStatementForStoredProcedure.get -> bool +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.SetDbStatementForStoredProcedure.set -> void +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.SetDbStatementForText.get -> bool +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.SetDbStatementForText.set -> void +OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.SqlClientTraceInstrumentationOptions() -> void +OpenTelemetry.Trace.TracerProviderBuilderExtensions +static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddSqlClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder +static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddSqlClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configureSqlClientTraceInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder +static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddSqlClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureSqlClientTraceInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/net462/PublicAPI.Shipped.txt b/src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/net462/PublicAPI.Shipped.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/net462/PublicAPI.Unshipped.txt deleted file mode 100644 index ee167e8afff..00000000000 --- a/src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/net462/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,18 +0,0 @@ -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.EnableConnectionLevelAttributes.get -> bool -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.EnableConnectionLevelAttributes.set -> void -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.Enrich.get -> System.Action -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.Enrich.set -> void -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.Filter.get -> System.Func -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.Filter.set -> void -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.RecordException.get -> bool -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.RecordException.set -> void -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.SetDbStatementForStoredProcedure.get -> bool -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.SetDbStatementForStoredProcedure.set -> void -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.SetDbStatementForText.get -> bool -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.SetDbStatementForText.set -> void -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.SqlClientInstrumentationOptions() -> void -OpenTelemetry.Trace.TracerProviderBuilderExtensions -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddSqlClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddSqlClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configureSqlClientInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddSqlClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureSqlClientInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/netstandard2.0/PublicAPI.Shipped.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt deleted file mode 100644 index ee167e8afff..00000000000 --- a/src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,18 +0,0 @@ -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.EnableConnectionLevelAttributes.get -> bool -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.EnableConnectionLevelAttributes.set -> void -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.Enrich.get -> System.Action -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.Enrich.set -> void -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.Filter.get -> System.Func -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.Filter.set -> void -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.RecordException.get -> bool -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.RecordException.set -> void -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.SetDbStatementForStoredProcedure.get -> bool -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.SetDbStatementForStoredProcedure.set -> void -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.SetDbStatementForText.get -> bool -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.SetDbStatementForText.set -> void -OpenTelemetry.Instrumentation.SqlClient.SqlClientInstrumentationOptions.SqlClientInstrumentationOptions() -> void -OpenTelemetry.Trace.TracerProviderBuilderExtensions -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddSqlClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddSqlClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configureSqlClientInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddSqlClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureSqlClientInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/AssemblyInfo.cs b/src/OpenTelemetry.Instrumentation.SqlClient/AssemblyInfo.cs index 18cd3732cf1..70e3c273ab2 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/AssemblyInfo.cs +++ b/src/OpenTelemetry.Instrumentation.SqlClient/AssemblyInfo.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Runtime.CompilerServices; diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md index 9d0ba655f9c..a07be3094d7 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md @@ -2,6 +2,59 @@ ## Unreleased +## 1.7.0-beta.1 + +Released 2024-Feb-09 + +* Removed support for the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable + which toggled the use of the new conventions for the + [server, client, and shared network attributes](https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/general/attributes.md#server-client-and-shared-network-attributes). + Now that this suite of attributes are stable, this instrumentation will only + emit the new attributes. + ([#5270](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5270)) +* **Breaking Change**: Renamed `SqlClientInstrumentationOptions` to + `SqlClientTraceInstrumentationOptions`. + ([#5285](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5285)) +* **Breaking Change**: Stop emitting `db.statement_type` attribute. + This attribute was never a part of the [semantic conventions](https://github.com/open-telemetry/semantic-conventions/blob/v1.24.0/docs/database/database-spans.md#call-level-attributes). + ([#5301](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5301)) + +## 1.6.0-beta.3 + +Released 2023-Nov-17 + +* Updated `Microsoft.Extensions.Configuration` and + `Microsoft.Extensions.Options` package version to `8.0.0`. + ([#5051](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5051)) + +## 1.6.0-beta.2 + +Released 2023-Oct-26 + +## 1.5.1-beta.1 + +Released 2023-Jul-20 + +* The new network semantic conventions can be opted in to by setting + the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable. This allows for a + transition period for users to experiment with the new semantic conventions + and adapt as necessary. The environment variable supports the following + values: + * `http` - emit the new, frozen (proposed for stable) networking + attributes, and stop emitting the old experimental networking + attributes that the instrumentation emitted previously. + * `http/dup` - emit both the old and the frozen (proposed for stable) + networking attributes, allowing for a more seamless transition. + * The default behavior (in the absence of one of these values) is to continue + emitting the same network semantic conventions that were emitted in + `1.5.0-beta.1`. + * Note: this option will eventually be removed after the new + network semantic conventions are marked stable. Refer to the + specification for more information regarding the new network + semantic conventions for + [spans](https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/database/database-spans.md). + ([#4644](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4644)) + ## 1.5.0-beta.1 Released 2023-Jun-05 @@ -158,7 +211,7 @@ Released 2021-Jan-29 activities created by the instrumentation. * New setting on SqlClientInstrumentationOptions on .NET Core: `RecordException` can be set to instruct the instrumentation to record SqlExceptions as Activity - [events](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/exceptions.md). + [events](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/exceptions/exceptions-spans.md). ([#1592](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1592)) ## 1.0.0-rc1.1 diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlActivitySourceHelper.cs b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlActivitySourceHelper.cs index 271a9417f7f..287ee0d6dc0 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlActivitySourceHelper.cs +++ b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlActivitySourceHelper.cs @@ -1,41 +1,28 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + using System.Diagnostics; using System.Reflection; using OpenTelemetry.Trace; -namespace OpenTelemetry.Instrumentation.SqlClient.Implementation +namespace OpenTelemetry.Instrumentation.SqlClient.Implementation; + +/// +/// Helper class to hold common properties used by both SqlClientDiagnosticListener on .NET Core +/// and SqlEventSourceListener on .NET Framework. +/// +internal sealed class SqlActivitySourceHelper { - /// - /// Helper class to hold common properties used by both SqlClientDiagnosticListener on .NET Core - /// and SqlEventSourceListener on .NET Framework. - /// - internal sealed class SqlActivitySourceHelper - { - public const string MicrosoftSqlServerDatabaseSystemName = "mssql"; + public const string MicrosoftSqlServerDatabaseSystemName = "mssql"; - public static readonly AssemblyName AssemblyName = typeof(SqlActivitySourceHelper).Assembly.GetName(); - public static readonly string ActivitySourceName = AssemblyName.Name; - public static readonly Version Version = AssemblyName.Version; - public static readonly ActivitySource ActivitySource = new(ActivitySourceName, Version.ToString()); - public static readonly string ActivityName = ActivitySourceName + ".Execute"; + public static readonly AssemblyName AssemblyName = typeof(SqlActivitySourceHelper).Assembly.GetName(); + public static readonly string ActivitySourceName = AssemblyName.Name; + public static readonly Version Version = AssemblyName.Version; + public static readonly ActivitySource ActivitySource = new(ActivitySourceName, Version.ToString()); + public static readonly string ActivityName = ActivitySourceName + ".Execute"; - public static readonly IEnumerable> CreationTags = new[] - { - new KeyValuePair(SemanticConventions.AttributeDbSystem, MicrosoftSqlServerDatabaseSystemName), - }; - } + public static readonly IEnumerable> CreationTags = new[] + { + new KeyValuePair(SemanticConventions.AttributeDbSystem, MicrosoftSqlServerDatabaseSystemName), + }; } diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientDiagnosticListener.cs b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientDiagnosticListener.cs index fe17f8e907d..589ae603f7a 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientDiagnosticListener.cs +++ b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientDiagnosticListener.cs @@ -1,218 +1,203 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + #if !NETFRAMEWORK using System.Data; using System.Diagnostics; using OpenTelemetry.Trace; -using static OpenTelemetry.Internal.HttpSemanticConventionHelper; +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif + +namespace OpenTelemetry.Instrumentation.SqlClient.Implementation; -namespace OpenTelemetry.Instrumentation.SqlClient.Implementation +#if NET6_0_OR_GREATER +[RequiresUnreferencedCode(SqlClientInstrumentation.SqlClientTrimmingUnsupportedMessage)] +#endif +internal sealed class SqlClientDiagnosticListener : ListenerHandler { - internal sealed class SqlClientDiagnosticListener : ListenerHandler + public const string SqlDataBeforeExecuteCommand = "System.Data.SqlClient.WriteCommandBefore"; + public const string SqlMicrosoftBeforeExecuteCommand = "Microsoft.Data.SqlClient.WriteCommandBefore"; + + public const string SqlDataAfterExecuteCommand = "System.Data.SqlClient.WriteCommandAfter"; + public const string SqlMicrosoftAfterExecuteCommand = "Microsoft.Data.SqlClient.WriteCommandAfter"; + + public const string SqlDataWriteCommandError = "System.Data.SqlClient.WriteCommandError"; + public const string SqlMicrosoftWriteCommandError = "Microsoft.Data.SqlClient.WriteCommandError"; + + private readonly PropertyFetcher commandFetcher = new("Command"); + private readonly PropertyFetcher connectionFetcher = new("Connection"); + private readonly PropertyFetcher dataSourceFetcher = new("DataSource"); + private readonly PropertyFetcher databaseFetcher = new("Database"); + private readonly PropertyFetcher commandTypeFetcher = new("CommandType"); + private readonly PropertyFetcher commandTextFetcher = new("CommandText"); + private readonly PropertyFetcher exceptionFetcher = new("Exception"); + private readonly SqlClientTraceInstrumentationOptions options; + + public SqlClientDiagnosticListener(string sourceName, SqlClientTraceInstrumentationOptions options) + : base(sourceName) { - public const string SqlDataBeforeExecuteCommand = "System.Data.SqlClient.WriteCommandBefore"; - public const string SqlMicrosoftBeforeExecuteCommand = "Microsoft.Data.SqlClient.WriteCommandBefore"; - - public const string SqlDataAfterExecuteCommand = "System.Data.SqlClient.WriteCommandAfter"; - public const string SqlMicrosoftAfterExecuteCommand = "Microsoft.Data.SqlClient.WriteCommandAfter"; - - public const string SqlDataWriteCommandError = "System.Data.SqlClient.WriteCommandError"; - public const string SqlMicrosoftWriteCommandError = "Microsoft.Data.SqlClient.WriteCommandError"; - - private readonly PropertyFetcher commandFetcher = new("Command"); - private readonly PropertyFetcher connectionFetcher = new("Connection"); - private readonly PropertyFetcher dataSourceFetcher = new("DataSource"); - private readonly PropertyFetcher databaseFetcher = new("Database"); - private readonly PropertyFetcher commandTypeFetcher = new("CommandType"); - private readonly PropertyFetcher commandTextFetcher = new("CommandText"); - private readonly PropertyFetcher exceptionFetcher = new("Exception"); - private readonly SqlClientInstrumentationOptions options; - - private readonly HttpSemanticConvention httpSemanticConvention; - - public SqlClientDiagnosticListener(string sourceName, SqlClientInstrumentationOptions options) - : base(sourceName) - { - this.options = options ?? new SqlClientInstrumentationOptions(); - - this.httpSemanticConvention = GetSemanticConventionOptIn(); - } + this.options = options ?? new SqlClientTraceInstrumentationOptions(); + } - public override bool SupportsNullActivity => true; + public override bool SupportsNullActivity => true; - public override void OnEventWritten(string name, object payload) + public override void OnEventWritten(string name, object payload) + { + var activity = Activity.Current; + switch (name) { - var activity = Activity.Current; - switch (name) - { - case SqlDataBeforeExecuteCommand: - case SqlMicrosoftBeforeExecuteCommand: + case SqlDataBeforeExecuteCommand: + case SqlMicrosoftBeforeExecuteCommand: + { + // SqlClient does not create an Activity. So the activity coming in here will be null or the root span. + activity = SqlActivitySourceHelper.ActivitySource.StartActivity( + SqlActivitySourceHelper.ActivityName, + ActivityKind.Client, + default(ActivityContext), + SqlActivitySourceHelper.CreationTags); + + if (activity == null) { - // SqlClient does not create an Activity. So the activity coming in here will be null or the root span. - activity = SqlActivitySourceHelper.ActivitySource.StartActivity( - SqlActivitySourceHelper.ActivityName, - ActivityKind.Client, - default(ActivityContext), - SqlActivitySourceHelper.CreationTags); - - if (activity == null) - { - // There is no listener or it decided not to sample the current request. - return; - } + // There is no listener or it decided not to sample the current request. + return; + } - _ = this.commandFetcher.TryFetch(payload, out var command); - if (command == null) - { - SqlClientInstrumentationEventSource.Log.NullPayload(nameof(SqlClientDiagnosticListener), name); - activity.Stop(); - return; - } + _ = this.commandFetcher.TryFetch(payload, out var command); + if (command == null) + { + SqlClientInstrumentationEventSource.Log.NullPayload(nameof(SqlClientDiagnosticListener), name); + activity.Stop(); + return; + } - if (activity.IsAllDataRequested) + if (activity.IsAllDataRequested) + { + try { - try + if (this.options.Filter?.Invoke(command) == false) { - if (this.options.Filter?.Invoke(command) == false) - { - SqlClientInstrumentationEventSource.Log.CommandIsFilteredOut(activity.OperationName); - activity.IsAllDataRequested = false; - activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; - return; - } - } - catch (Exception ex) - { - SqlClientInstrumentationEventSource.Log.CommandFilterException(ex); + SqlClientInstrumentationEventSource.Log.CommandIsFilteredOut(activity.OperationName); activity.IsAllDataRequested = false; activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; return; } + } + catch (Exception ex) + { + SqlClientInstrumentationEventSource.Log.CommandFilterException(ex); + activity.IsAllDataRequested = false; + activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; + return; + } - _ = this.connectionFetcher.TryFetch(command, out var connection); - _ = this.databaseFetcher.TryFetch(connection, out var database); + _ = this.connectionFetcher.TryFetch(command, out var connection); + _ = this.databaseFetcher.TryFetch(connection, out var database); - activity.DisplayName = (string)database; + activity.DisplayName = (string)database; - _ = this.dataSourceFetcher.TryFetch(connection, out var dataSource); - _ = this.commandTextFetcher.TryFetch(command, out var commandText); + _ = this.dataSourceFetcher.TryFetch(connection, out var dataSource); + _ = this.commandTextFetcher.TryFetch(command, out var commandText); - activity.SetTag(SemanticConventions.AttributeDbName, (string)database); + activity.SetTag(SemanticConventions.AttributeDbName, (string)database); - this.options.AddConnectionLevelDetailsToActivity((string)dataSource, activity); + this.options.AddConnectionLevelDetailsToActivity((string)dataSource, activity); - if (this.commandTypeFetcher.TryFetch(command, out CommandType commandType)) + if (this.commandTypeFetcher.TryFetch(command, out CommandType commandType)) + { + switch (commandType) { - switch (commandType) - { - case CommandType.StoredProcedure: - activity.SetTag(SpanAttributeConstants.DatabaseStatementTypeKey, nameof(CommandType.StoredProcedure)); - if (this.options.SetDbStatementForStoredProcedure) - { - activity.SetTag(SemanticConventions.AttributeDbStatement, (string)commandText); - } - - break; - - case CommandType.Text: - activity.SetTag(SpanAttributeConstants.DatabaseStatementTypeKey, nameof(CommandType.Text)); - if (this.options.SetDbStatementForText) - { - activity.SetTag(SemanticConventions.AttributeDbStatement, (string)commandText); - } - - break; - - case CommandType.TableDirect: - activity.SetTag(SpanAttributeConstants.DatabaseStatementTypeKey, nameof(CommandType.TableDirect)); - break; - } - } + case CommandType.StoredProcedure: + if (this.options.SetDbStatementForStoredProcedure) + { + activity.SetTag(SemanticConventions.AttributeDbStatement, (string)commandText); + } - try - { - this.options.Enrich?.Invoke(activity, "OnCustom", command); - } - catch (Exception ex) - { - SqlClientInstrumentationEventSource.Log.EnrichmentException(ex); + break; + + case CommandType.Text: + if (this.options.SetDbStatementForText) + { + activity.SetTag(SemanticConventions.AttributeDbStatement, (string)commandText); + } + + break; + + case CommandType.TableDirect: + break; } } - } - break; - case SqlDataAfterExecuteCommand: - case SqlMicrosoftAfterExecuteCommand: - { - if (activity == null) + try { - SqlClientInstrumentationEventSource.Log.NullActivity(name); - return; + this.options.Enrich?.Invoke(activity, "OnCustom", command); } - - if (activity.Source != SqlActivitySourceHelper.ActivitySource) + catch (Exception ex) { - return; + SqlClientInstrumentationEventSource.Log.EnrichmentException(ex); } + } + } - activity.Stop(); + break; + case SqlDataAfterExecuteCommand: + case SqlMicrosoftAfterExecuteCommand: + { + if (activity == null) + { + SqlClientInstrumentationEventSource.Log.NullActivity(name); + return; } - break; - case SqlDataWriteCommandError: - case SqlMicrosoftWriteCommandError: + if (activity.Source != SqlActivitySourceHelper.ActivitySource) { - if (activity == null) - { - SqlClientInstrumentationEventSource.Log.NullActivity(name); - return; - } + return; + } - if (activity.Source != SqlActivitySourceHelper.ActivitySource) - { - return; - } + activity.Stop(); + } - try + break; + case SqlDataWriteCommandError: + case SqlMicrosoftWriteCommandError: + { + if (activity == null) + { + SqlClientInstrumentationEventSource.Log.NullActivity(name); + return; + } + + if (activity.Source != SqlActivitySourceHelper.ActivitySource) + { + return; + } + + try + { + if (activity.IsAllDataRequested) { - if (activity.IsAllDataRequested) + if (this.exceptionFetcher.TryFetch(payload, out Exception exception) && exception != null) { - if (this.exceptionFetcher.TryFetch(payload, out Exception exception) && exception != null) - { - activity.SetStatus(ActivityStatusCode.Error, exception.Message); + activity.SetStatus(ActivityStatusCode.Error, exception.Message); - if (this.options.RecordException) - { - activity.RecordException(exception); - } - } - else + if (this.options.RecordException) { - SqlClientInstrumentationEventSource.Log.NullPayload(nameof(SqlClientDiagnosticListener), name); + activity.RecordException(exception); } } + else + { + SqlClientInstrumentationEventSource.Log.NullPayload(nameof(SqlClientDiagnosticListener), name); + } } - finally - { - activity.Stop(); - } } + finally + { + activity.Stop(); + } + } - break; - } + break; } } } diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientInstrumentationEventSource.cs b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientInstrumentationEventSource.cs index 4afdbac2da1..ccd86471d78 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientInstrumentationEventSource.cs +++ b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientInstrumentationEventSource.cs @@ -1,99 +1,85 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Tracing; using OpenTelemetry.Internal; -namespace OpenTelemetry.Instrumentation.SqlClient.Implementation +namespace OpenTelemetry.Instrumentation.SqlClient.Implementation; + +/// +/// EventSource events emitted from the project. +/// +[EventSource(Name = "OpenTelemetry-Instrumentation-SqlClient")] +internal sealed class SqlClientInstrumentationEventSource : EventSource { - /// - /// EventSource events emitted from the project. - /// - [EventSource(Name = "OpenTelemetry-Instrumentation-SqlClient")] - internal sealed class SqlClientInstrumentationEventSource : EventSource - { - public static SqlClientInstrumentationEventSource Log = new(); + public static SqlClientInstrumentationEventSource Log = new(); - [NonEvent] - public void UnknownErrorProcessingEvent(string handlerName, string eventName, Exception ex) + [NonEvent] + public void UnknownErrorProcessingEvent(string handlerName, string eventName, Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.UnknownErrorProcessingEvent(handlerName, eventName, ex.ToInvariantString()); - } + this.UnknownErrorProcessingEvent(handlerName, eventName, ex.ToInvariantString()); } + } - [Event(1, Message = "Unknown error processing event '{1}' from handler '{0}', Exception: {2}", Level = EventLevel.Error)] - public void UnknownErrorProcessingEvent(string handlerName, string eventName, string ex) - { - this.WriteEvent(1, handlerName, eventName, ex); - } + [Event(1, Message = "Unknown error processing event '{1}' from handler '{0}', Exception: {2}", Level = EventLevel.Error)] + public void UnknownErrorProcessingEvent(string handlerName, string eventName, string ex) + { + this.WriteEvent(1, handlerName, eventName, ex); + } - [Event(2, Message = "Current Activity is NULL in the '{0}' callback. Span will not be recorded.", Level = EventLevel.Warning)] - public void NullActivity(string eventName) - { - this.WriteEvent(2, eventName); - } + [Event(2, Message = "Current Activity is NULL in the '{0}' callback. Span will not be recorded.", Level = EventLevel.Warning)] + public void NullActivity(string eventName) + { + this.WriteEvent(2, eventName); + } - [Event(3, Message = "Payload is NULL in event '{1}' from handler '{0}', span will not be recorded.", Level = EventLevel.Warning)] - public void NullPayload(string handlerName, string eventName) - { - this.WriteEvent(3, handlerName, eventName); - } + [Event(3, Message = "Payload is NULL in event '{1}' from handler '{0}', span will not be recorded.", Level = EventLevel.Warning)] + public void NullPayload(string handlerName, string eventName) + { + this.WriteEvent(3, handlerName, eventName); + } - [Event(4, Message = "Payload is invalid in event '{1}' from handler '{0}', span will not be recorded.", Level = EventLevel.Warning)] - public void InvalidPayload(string handlerName, string eventName) - { - this.WriteEvent(4, handlerName, eventName); - } + [Event(4, Message = "Payload is invalid in event '{1}' from handler '{0}', span will not be recorded.", Level = EventLevel.Warning)] + public void InvalidPayload(string handlerName, string eventName) + { + this.WriteEvent(4, handlerName, eventName); + } - [NonEvent] - public void EnrichmentException(Exception ex) + [NonEvent] + public void EnrichmentException(Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.EnrichmentException(ex.ToInvariantString()); - } + this.EnrichmentException(ex.ToInvariantString()); } + } - [Event(5, Message = "Enrichment threw exception. Exception {0}.", Level = EventLevel.Error)] - public void EnrichmentException(string exception) - { - this.WriteEvent(5, exception); - } + [Event(5, Message = "Enrichment threw exception. Exception {0}.", Level = EventLevel.Error)] + public void EnrichmentException(string exception) + { + this.WriteEvent(5, exception); + } - [Event(6, Message = "Command is filtered out. Activity {0}", Level = EventLevel.Verbose)] - public void CommandIsFilteredOut(string activityName) - { - this.WriteEvent(6, activityName); - } + [Event(6, Message = "Command is filtered out. Activity {0}", Level = EventLevel.Verbose)] + public void CommandIsFilteredOut(string activityName) + { + this.WriteEvent(6, activityName); + } - [NonEvent] - public void CommandFilterException(Exception ex) + [NonEvent] + public void CommandFilterException(Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.CommandFilterException(ex.ToInvariantString()); - } + this.CommandFilterException(ex.ToInvariantString()); } + } - [Event(7, Message = "Command filter threw exception. Command will not be collected. Exception {0}.", Level = EventLevel.Error)] - public void CommandFilterException(string exception) - { - this.WriteEvent(7, exception); - } + [Event(7, Message = "Command filter threw exception. Command will not be collected. Exception {0}.", Level = EventLevel.Error)] + public void CommandFilterException(string exception) + { + this.WriteEvent(7, exception); } } diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlEventSourceListener.netfx.cs b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlEventSourceListener.netfx.cs index a06c77c428c..111e4878d3c 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlEventSourceListener.netfx.cs +++ b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlEventSourceListener.netfx.cs @@ -1,204 +1,190 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #if NETFRAMEWORK using System.Diagnostics; using System.Diagnostics.Tracing; using OpenTelemetry.Trace; -namespace OpenTelemetry.Instrumentation.SqlClient.Implementation +namespace OpenTelemetry.Instrumentation.SqlClient.Implementation; + +/// +/// On .NET Framework, neither System.Data.SqlClient nor Microsoft.Data.SqlClient emit DiagnosticSource events. +/// Instead they use EventSource: +/// For System.Data.SqlClient see: reference source. +/// For Microsoft.Data.SqlClient see: SqlClientEventSource. +/// +/// We hook into these event sources and process their BeginExecute/EndExecute events. +/// +/// +/// Note that before version 2.0.0, Microsoft.Data.SqlClient used +/// "Microsoft-AdoNet-SystemData" (same as System.Data.SqlClient), but since +/// 2.0.0 has switched to "Microsoft.Data.SqlClient.EventSource". +/// +internal sealed class SqlEventSourceListener : EventListener { - /// - /// On .NET Framework, neither System.Data.SqlClient nor Microsoft.Data.SqlClient emit DiagnosticSource events. - /// Instead they use EventSource: - /// For System.Data.SqlClient see: reference source. - /// For Microsoft.Data.SqlClient see: SqlClientEventSource. - /// - /// We hook into these event sources and process their BeginExecute/EndExecute events. - /// - /// - /// Note that before version 2.0.0, Microsoft.Data.SqlClient used - /// "Microsoft-AdoNet-SystemData" (same as System.Data.SqlClient), but since - /// 2.0.0 has switched to "Microsoft.Data.SqlClient.EventSource". - /// - internal sealed class SqlEventSourceListener : EventListener - { - internal const string AdoNetEventSourceName = "Microsoft-AdoNet-SystemData"; - internal const string MdsEventSourceName = "Microsoft.Data.SqlClient.EventSource"; + internal const string AdoNetEventSourceName = "Microsoft-AdoNet-SystemData"; + internal const string MdsEventSourceName = "Microsoft.Data.SqlClient.EventSource"; + + internal const int BeginExecuteEventId = 1; + internal const int EndExecuteEventId = 2; - internal const int BeginExecuteEventId = 1; - internal const int EndExecuteEventId = 2; + private readonly SqlClientTraceInstrumentationOptions options; + private EventSource adoNetEventSource; + private EventSource mdsEventSource; - private readonly SqlClientInstrumentationOptions options; - private EventSource adoNetEventSource; - private EventSource mdsEventSource; + public SqlEventSourceListener(SqlClientTraceInstrumentationOptions options = null) + { + this.options = options ?? new SqlClientTraceInstrumentationOptions(); + } - public SqlEventSourceListener(SqlClientInstrumentationOptions options = null) + public override void Dispose() + { + if (this.adoNetEventSource != null) { - this.options = options ?? new SqlClientInstrumentationOptions(); + this.DisableEvents(this.adoNetEventSource); } - public override void Dispose() + if (this.mdsEventSource != null) { - if (this.adoNetEventSource != null) - { - this.DisableEvents(this.adoNetEventSource); - } + this.DisableEvents(this.mdsEventSource); + } - if (this.mdsEventSource != null) - { - this.DisableEvents(this.mdsEventSource); - } + base.Dispose(); + } - base.Dispose(); + protected override void OnEventSourceCreated(EventSource eventSource) + { + if (eventSource?.Name.StartsWith(AdoNetEventSourceName, StringComparison.Ordinal) == true) + { + this.adoNetEventSource = eventSource; + this.EnableEvents(eventSource, EventLevel.Informational, EventKeywords.All); } - - protected override void OnEventSourceCreated(EventSource eventSource) + else if (eventSource?.Name.StartsWith(MdsEventSourceName, StringComparison.Ordinal) == true) { - if (eventSource?.Name.StartsWith(AdoNetEventSourceName, StringComparison.Ordinal) == true) - { - this.adoNetEventSource = eventSource; - this.EnableEvents(eventSource, EventLevel.Informational, EventKeywords.All); - } - else if (eventSource?.Name.StartsWith(MdsEventSourceName, StringComparison.Ordinal) == true) - { - this.mdsEventSource = eventSource; - this.EnableEvents(eventSource, EventLevel.Informational, EventKeywords.All); - } - - base.OnEventSourceCreated(eventSource); + this.mdsEventSource = eventSource; + this.EnableEvents(eventSource, EventLevel.Informational, EventKeywords.All); } - protected override void OnEventWritten(EventWrittenEventArgs eventData) + base.OnEventSourceCreated(eventSource); + } + + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + try { - try + if (eventData.EventId == BeginExecuteEventId) { - if (eventData.EventId == BeginExecuteEventId) - { - this.OnBeginExecute(eventData); - } - else if (eventData.EventId == EndExecuteEventId) - { - this.OnEndExecute(eventData); - } + this.OnBeginExecute(eventData); } - catch (Exception exc) + else if (eventData.EventId == EndExecuteEventId) { - SqlClientInstrumentationEventSource.Log.UnknownErrorProcessingEvent(nameof(SqlEventSourceListener), nameof(this.OnEventWritten), exc); + this.OnEndExecute(eventData); } } + catch (Exception exc) + { + SqlClientInstrumentationEventSource.Log.UnknownErrorProcessingEvent(nameof(SqlEventSourceListener), nameof(this.OnEventWritten), exc); + } + } - private void OnBeginExecute(EventWrittenEventArgs eventData) + private void OnBeginExecute(EventWrittenEventArgs eventData) + { + /* + Expected payload: + [0] -> ObjectId + [1] -> DataSource + [2] -> Database + [3] -> CommandText + + Note: + - For "Microsoft-AdoNet-SystemData" v1.0: [3] CommandText = CommandType == CommandType.StoredProcedure ? CommandText : string.Empty; (so it is set for only StoredProcedure command types) + (https://github.com/dotnet/SqlClient/blob/v1.0.19239.1/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs#L6369) + - For "Microsoft-AdoNet-SystemData" v1.1: [3] CommandText = sqlCommand.CommandText (so it is set for all command types) + (https://github.com/dotnet/SqlClient/blob/v1.1.0/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs#L7459) + - For "Microsoft.Data.SqlClient.EventSource" v2.0+: [3] CommandText = sqlCommand.CommandText (so it is set for all command types). + (https://github.com/dotnet/SqlClient/blob/f4568ce68da21db3fe88c0e72e1287368aaa1dc8/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs#L6641) + */ + + if ((eventData?.Payload?.Count ?? 0) < 4) { - /* - Expected payload: - [0] -> ObjectId - [1] -> DataSource - [2] -> Database - [3] -> CommandText - - Note: - - For "Microsoft-AdoNet-SystemData" v1.0: [3] CommandText = CommandType == CommandType.StoredProcedure ? CommandText : string.Empty; (so it is set for only StoredProcedure command types) - (https://github.com/dotnet/SqlClient/blob/v1.0.19239.1/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs#L6369) - - For "Microsoft-AdoNet-SystemData" v1.1: [3] CommandText = sqlCommand.CommandText (so it is set for all command types) - (https://github.com/dotnet/SqlClient/blob/v1.1.0/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs#L7459) - - For "Microsoft.Data.SqlClient.EventSource" v2.0+: [3] CommandText = sqlCommand.CommandText (so it is set for all command types). - (https://github.com/dotnet/SqlClient/blob/f4568ce68da21db3fe88c0e72e1287368aaa1dc8/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs#L6641) - */ - - if ((eventData?.Payload?.Count ?? 0) < 4) - { - SqlClientInstrumentationEventSource.Log.InvalidPayload(nameof(SqlEventSourceListener), nameof(this.OnBeginExecute)); - return; - } + SqlClientInstrumentationEventSource.Log.InvalidPayload(nameof(SqlEventSourceListener), nameof(this.OnBeginExecute)); + return; + } - var activity = SqlActivitySourceHelper.ActivitySource.StartActivity( - SqlActivitySourceHelper.ActivityName, - ActivityKind.Client, - default(ActivityContext), - SqlActivitySourceHelper.CreationTags); + var activity = SqlActivitySourceHelper.ActivitySource.StartActivity( + SqlActivitySourceHelper.ActivityName, + ActivityKind.Client, + default(ActivityContext), + SqlActivitySourceHelper.CreationTags); - if (activity == null) - { - // There is no listener or it decided not to sample the current request. - return; - } + if (activity == null) + { + // There is no listener or it decided not to sample the current request. + return; + } - string databaseName = (string)eventData.Payload[2]; + string databaseName = (string)eventData.Payload[2]; - activity.DisplayName = databaseName; + activity.DisplayName = databaseName; - if (activity.IsAllDataRequested) - { - activity.SetTag(SemanticConventions.AttributeDbName, databaseName); + if (activity.IsAllDataRequested) + { + activity.SetTag(SemanticConventions.AttributeDbName, databaseName); - this.options.AddConnectionLevelDetailsToActivity((string)eventData.Payload[1], activity); + this.options.AddConnectionLevelDetailsToActivity((string)eventData.Payload[1], activity); - string commandText = (string)eventData.Payload[3]; - if (!string.IsNullOrEmpty(commandText) && this.options.SetDbStatementForText) - { - activity.SetTag(SemanticConventions.AttributeDbStatement, commandText); - } + string commandText = (string)eventData.Payload[3]; + if (!string.IsNullOrEmpty(commandText) && this.options.SetDbStatementForText) + { + activity.SetTag(SemanticConventions.AttributeDbStatement, commandText); } } + } - private void OnEndExecute(EventWrittenEventArgs eventData) + private void OnEndExecute(EventWrittenEventArgs eventData) + { + /* + Expected payload: + [0] -> ObjectId + [1] -> CompositeState bitmask (0b001 -> successFlag, 0b010 -> isSqlExceptionFlag , 0b100 -> synchronousFlag) + [2] -> SqlExceptionNumber + */ + + if ((eventData?.Payload?.Count ?? 0) < 3) { - /* - Expected payload: - [0] -> ObjectId - [1] -> CompositeState bitmask (0b001 -> successFlag, 0b010 -> isSqlExceptionFlag , 0b100 -> synchronousFlag) - [2] -> SqlExceptionNumber - */ - - if ((eventData?.Payload?.Count ?? 0) < 3) - { - SqlClientInstrumentationEventSource.Log.InvalidPayload(nameof(SqlEventSourceListener), nameof(this.OnEndExecute)); - return; - } + SqlClientInstrumentationEventSource.Log.InvalidPayload(nameof(SqlEventSourceListener), nameof(this.OnEndExecute)); + return; + } - var activity = Activity.Current; - if (activity?.Source != SqlActivitySourceHelper.ActivitySource) - { - return; - } + var activity = Activity.Current; + if (activity?.Source != SqlActivitySourceHelper.ActivitySource) + { + return; + } - try + try + { + if (activity.IsAllDataRequested) { - if (activity.IsAllDataRequested) + int compositeState = (int)eventData.Payload[1]; + if ((compositeState & 0b001) != 0b001) { - int compositeState = (int)eventData.Payload[1]; - if ((compositeState & 0b001) != 0b001) + if ((compositeState & 0b010) == 0b010) + { + var errorText = $"SqlExceptionNumber {eventData.Payload[2]} thrown."; + activity.SetStatus(ActivityStatusCode.Error, errorText); + } + else { - if ((compositeState & 0b010) == 0b010) - { - var errorText = $"SqlExceptionNumber {eventData.Payload[2]} thrown."; - activity.SetStatus(ActivityStatusCode.Error, errorText); - } - else - { - activity.SetStatus(ActivityStatusCode.Error, "Unknown Sql failure."); - } + activity.SetStatus(ActivityStatusCode.Error, "Unknown Sql failure."); } } } - finally - { - activity.Stop(); - } + } + finally + { + activity.Stop(); } } } diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/OpenTelemetry.Instrumentation.SqlClient.csproj b/src/OpenTelemetry.Instrumentation.SqlClient/OpenTelemetry.Instrumentation.SqlClient.csproj index 9a47a00608f..09d8a5782f6 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/OpenTelemetry.Instrumentation.SqlClient.csproj +++ b/src/OpenTelemetry.Instrumentation.SqlClient/OpenTelemetry.Instrumentation.SqlClient.csproj @@ -1,10 +1,10 @@ - - netstandard2.0;net462 + $(TargetFrameworksForLibraries) SqlClient instrumentation for OpenTelemetry .NET $(PackageTags);distributed-tracing + Instrumentation.SqlClient- true @@ -12,14 +12,19 @@ - - + + - + + + + + + diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/README.md b/src/OpenTelemetry.Instrumentation.SqlClient/README.md index a2ecc18e33f..1df04352509 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/README.md +++ b/src/OpenTelemetry.Instrumentation.SqlClient/README.md @@ -11,14 +11,14 @@ and [System.Data.SqlClient](https://www.nuget.org/packages/System.Data.SqlClient) and collects traces about database operations. -> **Warning** +> [!WARNING] > Instrumentation is not working with `Microsoft.Data.SqlClient` v3.* due to the [issue](https://github.com/dotnet/SqlClient/pull/1258). It was fixed in 4.0 and later. -> -> **Note** + +> [!CAUTION] > This component is based on the OpenTelemetry semantic conventions for -[traces](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/trace/semantic_conventions). +[traces](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md). These conventions are [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/document-status.md), and hence, this package is a [pre-release](../../VERSIONING.md#pre-releases). @@ -75,13 +75,13 @@ For an ASP.NET application, adding instrumentation is typically done in the ## Advanced configuration This instrumentation can be configured to change the default behavior by using -`SqlClientInstrumentationOptions`. +`SqlClientTraceInstrumentationOptions`. ### Capturing database statements -The `SqlClientInstrumentationOptions` class exposes two properties that can be -used to configure how the -[`db.statement`](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md#call-level-attributes) +The `SqlClientTraceInstrumentationOptions` class exposes two properties that can +be used to configure how the +[`db.statement`](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md#call-level-attributes) attribute is captured upon execution of a query but the behavior depends on the runtime used. @@ -93,13 +93,13 @@ control capturing of `CommandType.StoredProcedure` and `CommandType.Text` respectively. `SetDbStatementForStoredProcedure` is _true_ by default and will set -[`db.statement`](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md#call-level-attributes) +[`db.statement`](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md#call-level-attributes) attribute to the stored procedure command name. `SetDbStatementForText` is _false_ by default (to prevent accidental capture of sensitive data that might be part of the SQL statement text). When set to `true`, the instrumentation will set -[`db.statement`](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md#call-level-attributes) +[`db.statement`](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md#call-level-attributes) attribute to the text of the SQL command being executed. To disable capturing stored procedure commands use configuration like below. @@ -127,7 +127,7 @@ using var tracerProvider = Sdk.CreateTracerProviderBuilder() On .NET Framework, the `SetDbStatementForText` property controls whether or not this instrumentation will set the -[`db.statement`](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md#call-level-attributes) +[`db.statement`](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md#call-level-attributes) attribute to the text of the `SqlCommand` being executed. This could either be the name of a stored procedure (when `CommandType.StoredProcedure` is used) or the full text of a `CommandType.Text` query. `SetDbStatementForStoredProcedure` @@ -148,7 +148,7 @@ using var tracerProvider = Sdk.CreateTracerProviderBuilder() .Build(); ``` -> **Note** +> [!NOTE] > When using the built-in `System.Data.SqlClient` only stored procedure command names will ever be captured. When using the `Microsoft.Data.SqlClient` NuGet package (v1.1+) stored procedure command names, full query text, and other @@ -156,7 +156,7 @@ command text will be captured. ### EnableConnectionLevelAttributes -> **Note** +> [!NOTE] > EnableConnectionLevelAttributes is supported on all runtimes. By default, `EnabledConnectionLevelAttributes` is disabled and this @@ -180,7 +180,7 @@ using var tracerProvider = Sdk.CreateTracerProviderBuilder() ### Enrich -> **Note** +> [!NOTE] > Enrich is supported on .NET and .NET Core runtimes only. This option can be used to enrich the activity with additional information from @@ -217,12 +217,12 @@ access to `SqlCommand` object. ### RecordException -> **Note** +> [!NOTE] > RecordException is supported on .NET and .NET Core runtimes only. This option can be set to instruct the instrumentation to record SqlExceptions as Activity -[events](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/exceptions.md). +[events](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/exceptions/exceptions-spans.md). The default value is `false` and can be changed by the code like below. @@ -236,7 +236,7 @@ using var tracerProvider = Sdk.CreateTracerProviderBuilder() ### Filter -> **Note** +> [!NOTE] > Filter is supported on .NET and .NET Core runtimes only. This option can be used to filter out activities based on the properties of the @@ -273,4 +273,4 @@ using var traceProvider = Sdk.CreateTracerProviderBuilder() * [OpenTelemetry Project](https://opentelemetry.io/) * [OpenTelemetry semantic conventions for database - calls](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md) + calls](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md) diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/SqlClientInstrumentation.cs b/src/OpenTelemetry.Instrumentation.SqlClient/SqlClientInstrumentation.cs index 380d70b20fb..9b4230a4606 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/SqlClientInstrumentation.cs +++ b/src/OpenTelemetry.Instrumentation.SqlClient/SqlClientInstrumentation.cs @@ -1,75 +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. -// +// SPDX-License-Identifier: Apache-2.0 +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif using OpenTelemetry.Instrumentation.SqlClient.Implementation; -namespace OpenTelemetry.Instrumentation.SqlClient -{ - /// - /// SqlClient instrumentation. - /// - internal sealed class SqlClientInstrumentation : IDisposable - { - internal const string SqlClientDiagnosticListenerName = "SqlClientDiagnosticListener"; +namespace OpenTelemetry.Instrumentation.SqlClient; +/// +/// SqlClient instrumentation. +/// +internal sealed class SqlClientInstrumentation : IDisposable +{ + internal const string SqlClientDiagnosticListenerName = "SqlClientDiagnosticListener"; +#if NET6_0_OR_GREATER + internal const string SqlClientTrimmingUnsupportedMessage = "Trimming is not yet supported with SqlClient instrumentation."; +#endif #if NETFRAMEWORK - private readonly SqlEventSourceListener sqlEventSourceListener; + private readonly SqlEventSourceListener sqlEventSourceListener; #else - private static readonly HashSet DiagnosticSourceEvents = new() - { - "System.Data.SqlClient.WriteCommandBefore", - "Microsoft.Data.SqlClient.WriteCommandBefore", - "System.Data.SqlClient.WriteCommandAfter", - "Microsoft.Data.SqlClient.WriteCommandAfter", - "System.Data.SqlClient.WriteCommandError", - "Microsoft.Data.SqlClient.WriteCommandError", - }; + private static readonly HashSet DiagnosticSourceEvents = new() + { + "System.Data.SqlClient.WriteCommandBefore", + "Microsoft.Data.SqlClient.WriteCommandBefore", + "System.Data.SqlClient.WriteCommandAfter", + "Microsoft.Data.SqlClient.WriteCommandAfter", + "System.Data.SqlClient.WriteCommandError", + "Microsoft.Data.SqlClient.WriteCommandError", + }; - private readonly Func isEnabled = (eventName, _, _) - => DiagnosticSourceEvents.Contains(eventName); + private readonly Func isEnabled = (eventName, _, _) + => DiagnosticSourceEvents.Contains(eventName); - private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; + private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; #endif - /// - /// Initializes a new instance of the class. - /// - /// Configuration options for sql instrumentation. - public SqlClientInstrumentation( - SqlClientInstrumentationOptions options = null) - { + /// + /// Initializes a new instance of the class. + /// + /// Configuration options for sql instrumentation. +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode(SqlClientTrimmingUnsupportedMessage)] +#endif + public SqlClientInstrumentation( + SqlClientTraceInstrumentationOptions options = null) + { #if NETFRAMEWORK - this.sqlEventSourceListener = new SqlEventSourceListener(options); + this.sqlEventSourceListener = new SqlEventSourceListener(options); #else - this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber( - name => new SqlClientDiagnosticListener(name, options), - listener => listener.Name == SqlClientDiagnosticListenerName, - this.isEnabled); - this.diagnosticSourceSubscriber.Subscribe(); + this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber( + name => new SqlClientDiagnosticListener(name, options), + listener => listener.Name == SqlClientDiagnosticListenerName, + this.isEnabled, + SqlClientInstrumentationEventSource.Log.UnknownErrorProcessingEvent); + this.diagnosticSourceSubscriber.Subscribe(); #endif - } + } - /// - public void Dispose() - { + /// + public void Dispose() + { #if NETFRAMEWORK - this.sqlEventSourceListener?.Dispose(); + this.sqlEventSourceListener?.Dispose(); #else - this.diagnosticSourceSubscriber?.Dispose(); + this.diagnosticSourceSubscriber?.Dispose(); #endif - } } } diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/SqlClientInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.SqlClient/SqlClientInstrumentationOptions.cs deleted file mode 100644 index fdb0e29eff4..00000000000 --- a/src/OpenTelemetry.Instrumentation.SqlClient/SqlClientInstrumentationOptions.cs +++ /dev/null @@ -1,318 +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.Collections.Concurrent; -using System.Data; -using System.Diagnostics; -using System.Text.RegularExpressions; -using OpenTelemetry.Trace; - -namespace OpenTelemetry.Instrumentation.SqlClient -{ - /// - /// Options for . - /// - /// - /// For help and examples see: . - /// - public class SqlClientInstrumentationOptions - { - /* - * Match... - * protocol[ ]:[ ]serverName - * serverName - * serverName[ ]\[ ]instanceName - * serverName[ ],[ ]port - * serverName[ ]\[ ]instanceName[ ],[ ]port - * - * [ ] can be any number of white-space, SQL allows it for some reason. - * - * Optional "protocol" can be "tcp", "lpc" (shared memory), or "np" (named pipes). See: - * https://docs.microsoft.com/troubleshoot/sql/connect/use-server-name-parameter-connection-string, and - * https://docs.microsoft.com/dotnet/api/system.data.sqlclient.sqlconnection.connectionstring?view=dotnet-plat-ext-5.0 - * - * In case of named pipes the Data Source string can take form of: - * np:serverName\instanceName, or - * np:\\serverName\pipe\pipeName, or - * np:\\serverName\pipe\MSSQL$instanceName\pipeName - in this case a separate regex (see NamedPipeRegex below) - * is used to extract instanceName - */ - private static readonly Regex DataSourceRegex = new("^(.*\\s*:\\s*\\\\{0,2})?(.*?)\\s*(?:[\\\\,]|$)\\s*(.*?)\\s*(?:,|$)\\s*(.*)$", RegexOptions.Compiled); - - /// - /// In a Data Source string like "np:\\serverName\pipe\MSSQL$instanceName\pipeName" match the - /// "pipe\MSSQL$instanceName" segment to extract instanceName if it is available. - /// - /// - /// - /// - private static readonly Regex NamedPipeRegex = new("pipe\\\\MSSQL\\$(.*?)\\\\", RegexOptions.Compiled); - - private static readonly ConcurrentDictionary ConnectionDetailCache = new(StringComparer.OrdinalIgnoreCase); - - /// - /// Gets or sets a value indicating whether or not the should add the names of commands as the tag. Default - /// value: . - /// - /// - /// SetDbStatementForStoredProcedure is only supported on .NET - /// and .NET Core runtimes. - /// - public bool SetDbStatementForStoredProcedure { get; set; } = true; - - /// - /// Gets or sets a value indicating whether or not the should add the text of commands as - /// the tag. - /// Default value: . - /// - /// - /// - /// WARNING: SetDbStatementForText will capture the raw - /// CommandText. Make sure your CommandText property never - /// contains any sensitive data. - /// - /// SetDbStatementForText is supported on all runtimes. - /// - /// On .NET and .NET Core SetDbStatementForText only applies to - /// SqlCommands with . - /// On .NET Framework SetDbStatementForText applies to all - /// SqlCommands regardless of . - /// - /// When using System.Data.SqlClient use - /// SetDbStatementForText to capture StoredProcedure command - /// names. - /// When using Microsoft.Data.SqlClient use - /// SetDbStatementForText to capture Text, StoredProcedure, and all - /// other command text. - /// - /// - /// - /// - public bool SetDbStatementForText { get; set; } - - /// - /// Gets or sets a value indicating whether or not the should parse the DataSource on a - /// SqlConnection into server name, instance name, and/or port - /// connection-level attribute tags. Default value: . - /// - /// - /// EnableConnectionLevelAttributes is supported on all - /// runtimes. - /// The default behavior is to set the SqlConnection DataSource as - /// the tag. If - /// enabled, SqlConnection DataSource will be parsed and the server name - /// will be sent as the or tag, the instance - /// name will be sent as the tag, and - /// the port will be sent as the tag if it is not - /// 1433 (the default port). - /// - public bool EnableConnectionLevelAttributes { get; set; } - - /// - /// Gets or sets an action to enrich an with the - /// raw SqlCommand object. - /// - /// - /// Enrich is only executed on .NET and .NET Core - /// runtimes. - /// The parameters passed to the enrich action are: - /// - /// The being enriched. - /// The name of the event. Currently only "OnCustom" is - /// used but more events may be added in the future. - /// The raw SqlCommand object from which additional - /// information can be extracted to enrich the . - /// - /// - public Action Enrich { get; set; } - - /// - /// Gets or sets a filter function that determines whether or not to - /// collect telemetry about a command. - /// - /// - /// Filter is only executed on .NET and .NET Core - /// runtimes. - /// Notes: - /// - /// The first parameter passed to the filter function is the raw - /// SqlCommand object for the command being executed. - /// The return value for the filter function is interpreted as: - /// - /// If filter returns , the command is - /// collected. - /// If filter returns or throws an - /// exception the command is NOT collected. - /// - /// - /// - public Func Filter { get; set; } - - /// - /// Gets or sets a value indicating whether the exception will be - /// recorded as or not. Default value: . - /// - /// - /// RecordException is only supported on .NET and .NET Core - /// runtimes. - /// For specification details see: . - /// - public bool RecordException { get; set; } - - internal static SqlConnectionDetails ParseDataSource(string dataSource) - { - Match match = DataSourceRegex.Match(dataSource); - - string serverHostName = match.Groups[2].Value; - string serverIpAddress = null; - - string instanceName; - - var uriHostNameType = Uri.CheckHostName(serverHostName); - if (uriHostNameType == UriHostNameType.IPv4 || uriHostNameType == UriHostNameType.IPv6) - { - serverIpAddress = serverHostName; - serverHostName = null; - } - - string maybeProtocol = match.Groups[1].Value; - bool isNamedPipe = maybeProtocol.Length > 0 && - maybeProtocol.StartsWith("np", StringComparison.OrdinalIgnoreCase); - - if (isNamedPipe) - { - string pipeName = match.Groups[3].Value; - if (pipeName.Length > 0) - { - var namedInstancePipeMatch = NamedPipeRegex.Match(pipeName); - if (namedInstancePipeMatch.Success) - { - instanceName = namedInstancePipeMatch.Groups[1].Value; - return new SqlConnectionDetails - { - ServerHostName = serverHostName, - ServerIpAddress = serverIpAddress, - InstanceName = instanceName, - Port = null, - }; - } - } - - return new SqlConnectionDetails - { - ServerHostName = serverHostName, - ServerIpAddress = serverIpAddress, - InstanceName = null, - Port = null, - }; - } - - string port; - if (match.Groups[4].Length > 0) - { - instanceName = match.Groups[3].Value; - port = match.Groups[4].Value; - if (port == "1433") - { - port = null; - } - } - else if (int.TryParse(match.Groups[3].Value, out int parsedPort)) - { - port = parsedPort == 1433 ? null : match.Groups[3].Value; - instanceName = null; - } - else - { - instanceName = match.Groups[3].Value; - - if (string.IsNullOrEmpty(instanceName)) - { - instanceName = null; - } - - port = null; - } - - return new SqlConnectionDetails - { - ServerHostName = serverHostName, - ServerIpAddress = serverIpAddress, - InstanceName = instanceName, - Port = port, - }; - } - - internal void AddConnectionLevelDetailsToActivity(string dataSource, Activity sqlActivity) - { - if (!this.EnableConnectionLevelAttributes) - { - sqlActivity.SetTag(SemanticConventions.AttributePeerService, dataSource); - } - else - { - if (!ConnectionDetailCache.TryGetValue(dataSource, out SqlConnectionDetails connectionDetails)) - { - connectionDetails = ParseDataSource(dataSource); - ConnectionDetailCache.TryAdd(dataSource, connectionDetails); - } - - if (!string.IsNullOrEmpty(connectionDetails.ServerHostName)) - { - sqlActivity.SetTag(SemanticConventions.AttributeNetPeerName, connectionDetails.ServerHostName); - } - else - { - sqlActivity.SetTag(SemanticConventions.AttributeNetPeerIp, connectionDetails.ServerIpAddress); - } - - if (!string.IsNullOrEmpty(connectionDetails.InstanceName)) - { - sqlActivity.SetTag(SemanticConventions.AttributeDbMsSqlInstanceName, connectionDetails.InstanceName); - } - - if (!string.IsNullOrEmpty(connectionDetails.Port)) - { - sqlActivity.SetTag(SemanticConventions.AttributeNetPeerPort, connectionDetails.Port); - } - } - } - - internal sealed class SqlConnectionDetails - { - public string ServerHostName { get; set; } - - public string ServerIpAddress { get; set; } - - public string InstanceName { get; set; } - - public string Port { get; set; } - } - } -} diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/SqlClientTraceInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.SqlClient/SqlClientTraceInstrumentationOptions.cs new file mode 100644 index 00000000000..3dec363163b --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.SqlClient/SqlClientTraceInstrumentationOptions.cs @@ -0,0 +1,302 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Concurrent; +using System.Data; +using System.Diagnostics; +using System.Text.RegularExpressions; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Instrumentation.SqlClient; + +/// +/// Options for . +/// +/// +/// For help and examples see: . +/// +public class SqlClientTraceInstrumentationOptions +{ + /* + * Match... + * protocol[ ]:[ ]serverName + * serverName + * serverName[ ]\[ ]instanceName + * serverName[ ],[ ]port + * serverName[ ]\[ ]instanceName[ ],[ ]port + * + * [ ] can be any number of white-space, SQL allows it for some reason. + * + * Optional "protocol" can be "tcp", "lpc" (shared memory), or "np" (named pipes). See: + * https://docs.microsoft.com/troubleshoot/sql/connect/use-server-name-parameter-connection-string, and + * https://docs.microsoft.com/dotnet/api/system.data.sqlclient.sqlconnection.connectionstring?view=dotnet-plat-ext-5.0 + * + * In case of named pipes the Data Source string can take form of: + * np:serverName\instanceName, or + * np:\\serverName\pipe\pipeName, or + * np:\\serverName\pipe\MSSQL$instanceName\pipeName - in this case a separate regex (see NamedPipeRegex below) + * is used to extract instanceName + */ + private static readonly Regex DataSourceRegex = new("^(.*\\s*:\\s*\\\\{0,2})?(.*?)\\s*(?:[\\\\,]|$)\\s*(.*?)\\s*(?:,|$)\\s*(.*)$", RegexOptions.Compiled); + + /// + /// In a Data Source string like "np:\\serverName\pipe\MSSQL$instanceName\pipeName" match the + /// "pipe\MSSQL$instanceName" segment to extract instanceName if it is available. + /// + /// + /// + /// + private static readonly Regex NamedPipeRegex = new("pipe\\\\MSSQL\\$(.*?)\\\\", RegexOptions.Compiled); + + private static readonly ConcurrentDictionary ConnectionDetailCache = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Gets or sets a value indicating whether or not the should add the names of commands as the tag. Default + /// value: . + /// + /// + /// SetDbStatementForStoredProcedure is only supported on .NET + /// and .NET Core runtimes. + /// + public bool SetDbStatementForStoredProcedure { get; set; } = true; + + /// + /// Gets or sets a value indicating whether or not the should add the text of commands as + /// the tag. + /// Default value: . + /// + /// + /// + /// WARNING: SetDbStatementForText will capture the raw + /// CommandText. Make sure your CommandText property never + /// contains any sensitive data. + /// + /// SetDbStatementForText is supported on all runtimes. + /// + /// On .NET and .NET Core SetDbStatementForText only applies to + /// SqlCommands with . + /// On .NET Framework SetDbStatementForText applies to all + /// SqlCommands regardless of . + /// + /// When using System.Data.SqlClient use + /// SetDbStatementForText to capture StoredProcedure command + /// names. + /// When using Microsoft.Data.SqlClient use + /// SetDbStatementForText to capture Text, StoredProcedure, and all + /// other command text. + /// + /// + /// + /// + public bool SetDbStatementForText { get; set; } + + /// + /// Gets or sets a value indicating whether or not the should parse the DataSource on a + /// SqlConnection into server name, instance name, and/or port + /// connection-level attribute tags. Default value: . + /// + /// + /// + /// EnableConnectionLevelAttributes is supported on all runtimes. + /// + /// + /// The default behavior is to set the SqlConnection DataSource as the tag. + /// If enabled, SqlConnection DataSource will be parsed and the server name will be sent as the + /// or tag, + /// the instance name will be sent as the tag, + /// and the port will be sent as the tag if it is not 1433 (the default port). + /// + /// + public bool EnableConnectionLevelAttributes { get; set; } + + /// + /// Gets or sets an action to enrich an with the + /// raw SqlCommand object. + /// + /// + /// Enrich is only executed on .NET and .NET Core + /// runtimes. + /// The parameters passed to the enrich action are: + /// + /// The being enriched. + /// The name of the event. Currently only "OnCustom" is + /// used but more events may be added in the future. + /// The raw SqlCommand object from which additional + /// information can be extracted to enrich the . + /// + /// + public Action Enrich { get; set; } + + /// + /// Gets or sets a filter function that determines whether or not to + /// collect telemetry about a command. + /// + /// + /// Filter is only executed on .NET and .NET Core + /// runtimes. + /// Notes: + /// + /// The first parameter passed to the filter function is the raw + /// SqlCommand object for the command being executed. + /// The return value for the filter function is interpreted as: + /// + /// If filter returns , the command is + /// collected. + /// If filter returns or throws an + /// exception the command is NOT collected. + /// + /// + /// + public Func Filter { get; set; } + + /// + /// Gets or sets a value indicating whether the exception will be + /// recorded as or not. Default value: . + /// + /// + /// RecordException is only supported on .NET and .NET Core + /// runtimes. + /// For specification details see: . + /// + public bool RecordException { get; set; } + + internal static SqlConnectionDetails ParseDataSource(string dataSource) + { + Match match = DataSourceRegex.Match(dataSource); + + string serverHostName = match.Groups[2].Value; + string serverIpAddress = null; + + string instanceName; + + var uriHostNameType = Uri.CheckHostName(serverHostName); + if (uriHostNameType == UriHostNameType.IPv4 || uriHostNameType == UriHostNameType.IPv6) + { + serverIpAddress = serverHostName; + serverHostName = null; + } + + string maybeProtocol = match.Groups[1].Value; + bool isNamedPipe = maybeProtocol.Length > 0 && + maybeProtocol.StartsWith("np", StringComparison.OrdinalIgnoreCase); + + if (isNamedPipe) + { + string pipeName = match.Groups[3].Value; + if (pipeName.Length > 0) + { + var namedInstancePipeMatch = NamedPipeRegex.Match(pipeName); + if (namedInstancePipeMatch.Success) + { + instanceName = namedInstancePipeMatch.Groups[1].Value; + return new SqlConnectionDetails + { + ServerHostName = serverHostName, + ServerIpAddress = serverIpAddress, + InstanceName = instanceName, + Port = null, + }; + } + } + + return new SqlConnectionDetails + { + ServerHostName = serverHostName, + ServerIpAddress = serverIpAddress, + InstanceName = null, + Port = null, + }; + } + + string port; + if (match.Groups[4].Length > 0) + { + instanceName = match.Groups[3].Value; + port = match.Groups[4].Value; + if (port == "1433") + { + port = null; + } + } + else if (int.TryParse(match.Groups[3].Value, out int parsedPort)) + { + port = parsedPort == 1433 ? null : match.Groups[3].Value; + instanceName = null; + } + else + { + instanceName = match.Groups[3].Value; + + if (string.IsNullOrEmpty(instanceName)) + { + instanceName = null; + } + + port = null; + } + + return new SqlConnectionDetails + { + ServerHostName = serverHostName, + ServerIpAddress = serverIpAddress, + InstanceName = instanceName, + Port = port, + }; + } + + internal void AddConnectionLevelDetailsToActivity(string dataSource, Activity sqlActivity) + { + if (!this.EnableConnectionLevelAttributes) + { + sqlActivity.SetTag(SemanticConventions.AttributePeerService, dataSource); + } + else + { + if (!ConnectionDetailCache.TryGetValue(dataSource, out SqlConnectionDetails connectionDetails)) + { + connectionDetails = ParseDataSource(dataSource); + ConnectionDetailCache.TryAdd(dataSource, connectionDetails); + } + + if (!string.IsNullOrEmpty(connectionDetails.InstanceName)) + { + sqlActivity.SetTag(SemanticConventions.AttributeDbMsSqlInstanceName, connectionDetails.InstanceName); + } + + if (!string.IsNullOrEmpty(connectionDetails.ServerHostName)) + { + sqlActivity.SetTag(SemanticConventions.AttributeServerAddress, connectionDetails.ServerHostName); + } + else + { + sqlActivity.SetTag(SemanticConventions.AttributeServerSocketAddress, connectionDetails.ServerIpAddress); + } + + if (!string.IsNullOrEmpty(connectionDetails.Port)) + { + // TODO: Should we continue to emit this if the default port (1433) is being used? + sqlActivity.SetTag(SemanticConventions.AttributeServerPort, connectionDetails.Port); + } + } + } + + internal sealed class SqlConnectionDetails + { + public string ServerHostName { get; set; } + + public string ServerIpAddress { get; set; } + + public string InstanceName { get; set; } + + public string Port { get; set; } + } +} diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.SqlClient/TracerProviderBuilderExtensions.cs index 5435dd8c1c1..9634759bccf 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.SqlClient/TracerProviderBuilderExtensions.cs @@ -1,82 +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. -// +// SPDX-License-Identifier: Apache-2.0 +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using OpenTelemetry.Instrumentation.SqlClient; using OpenTelemetry.Instrumentation.SqlClient.Implementation; using OpenTelemetry.Internal; -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +/// +/// Extension methods to simplify registering of dependency instrumentation. +/// +public static class TracerProviderBuilderExtensions { /// - /// Extension methods to simplify registering of dependency instrumentation. + /// Enables SqlClient instrumentation. /// - public static class TracerProviderBuilderExtensions - { - /// - /// Enables SqlClient instrumentation. - /// - /// being configured. - /// The instance of to chain the calls. - public static TracerProviderBuilder AddSqlClientInstrumentation(this TracerProviderBuilder builder) - => AddSqlClientInstrumentation(builder, name: null, configureSqlClientInstrumentationOptions: null); + /// being configured. + /// The instance of to chain the calls. +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode(SqlClientInstrumentation.SqlClientTrimmingUnsupportedMessage)] +#endif + public static TracerProviderBuilder AddSqlClientInstrumentation(this TracerProviderBuilder builder) + => AddSqlClientInstrumentation(builder, name: null, configureSqlClientTraceInstrumentationOptions: null); - /// - /// Enables SqlClient instrumentation. - /// - /// being configured. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static TracerProviderBuilder AddSqlClientInstrumentation( - this TracerProviderBuilder builder, - Action configureSqlClientInstrumentationOptions) - => AddSqlClientInstrumentation(builder, name: null, configureSqlClientInstrumentationOptions); + /// + /// Enables SqlClient instrumentation. + /// + /// being configured. + /// Callback action for configuring . + /// The instance of to chain the calls. +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode(SqlClientInstrumentation.SqlClientTrimmingUnsupportedMessage)] +#endif + public static TracerProviderBuilder AddSqlClientInstrumentation( + this TracerProviderBuilder builder, + Action configureSqlClientTraceInstrumentationOptions) + => AddSqlClientInstrumentation(builder, name: null, configureSqlClientTraceInstrumentationOptions); - /// - /// Enables SqlClient instrumentation. - /// - /// being configured. - /// Name which is used when retrieving options. - /// Callback action for configuring . - /// The instance of to chain the calls. - public static TracerProviderBuilder AddSqlClientInstrumentation( - this TracerProviderBuilder builder, - string name, - Action configureSqlClientInstrumentationOptions) - { - Guard.ThrowIfNull(builder); + /// + /// Enables SqlClient instrumentation. + /// + /// being configured. + /// Name which is used when retrieving options. + /// Callback action for configuring . + /// The instance of to chain the calls. +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode(SqlClientInstrumentation.SqlClientTrimmingUnsupportedMessage)] +#endif - name ??= Options.DefaultName; + public static TracerProviderBuilder AddSqlClientInstrumentation( + this TracerProviderBuilder builder, + string name, + Action configureSqlClientTraceInstrumentationOptions) + { + Guard.ThrowIfNull(builder); + + name ??= Options.DefaultName; - if (configureSqlClientInstrumentationOptions != null) - { - builder.ConfigureServices(services => services.Configure(name, configureSqlClientInstrumentationOptions)); - } + if (configureSqlClientTraceInstrumentationOptions != null) + { + builder.ConfigureServices(services => services.Configure(name, configureSqlClientTraceInstrumentationOptions)); + } - builder.AddInstrumentation(sp => - { - var sqlOptions = sp.GetRequiredService>().Get(name); + builder.AddInstrumentation(sp => + { + var sqlOptions = sp.GetRequiredService>().Get(name); - return new SqlClientInstrumentation(sqlOptions); - }); + return new SqlClientInstrumentation(sqlOptions); + }); - builder.AddSource(SqlActivitySourceHelper.ActivitySourceName); + builder.AddSource(SqlActivitySourceHelper.ActivitySourceName); - return builder; - } + return builder; } } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/net6.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.SemanticConventions/.publicApi/PublicAPI.Shipped.txt similarity index 100% rename from src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/net6.0/PublicAPI.Shipped.txt rename to src/OpenTelemetry.SemanticConventions/.publicApi/PublicAPI.Shipped.txt diff --git a/src/OpenTelemetry.SemanticConventions/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry.SemanticConventions/.publicApi/PublicAPI.Unshipped.txt similarity index 100% rename from src/OpenTelemetry.SemanticConventions/.publicApi/net462/PublicAPI.Unshipped.txt rename to src/OpenTelemetry.SemanticConventions/.publicApi/PublicAPI.Unshipped.txt diff --git a/src/OpenTelemetry.SemanticConventions/.publicApi/net462/PublicAPI.Shipped.txt b/src/OpenTelemetry.SemanticConventions/.publicApi/net462/PublicAPI.Shipped.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/OpenTelemetry.SemanticConventions/.publicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.SemanticConventions/.publicApi/netstandard2.0/PublicAPI.Shipped.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/OpenTelemetry.SemanticConventions/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.SemanticConventions/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt deleted file mode 100644 index 17864093bcd..00000000000 --- a/src/OpenTelemetry.SemanticConventions/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,526 +0,0 @@ -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeAwsEcsClusterArn = "aws.ecs.cluster.arn" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeAwsEcsContainerArn = "aws.ecs.container.arn" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeAwsEcsLaunchtype = "aws.ecs.launchtype" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeAwsEcsTaskArn = "aws.ecs.task.arn" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeAwsEcsTaskFamily = "aws.ecs.task.family" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeAwsEcsTaskRevision = "aws.ecs.task.revision" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeAwsEksClusterArn = "aws.eks.cluster.arn" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeAwsLogGroupArns = "aws.log.group.arns" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeAwsLogGroupNames = "aws.log.group.names" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeAwsLogStreamArns = "aws.log.stream.arns" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeAwsLogStreamNames = "aws.log.stream.names" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeBrowserBrands = "browser.brands" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeBrowserPlatform = "browser.platform" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeBrowserUserAgent = "browser.user_agent" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeCloudAccountId = "cloud.account.id" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeCloudAvailabilityZone = "cloud.availability_zone" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeCloudPlatform = "cloud.platform" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeCloudProvider = "cloud.provider" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeCloudRegion = "cloud.region" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeContainerId = "container.id" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeContainerImageName = "container.image.name" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeContainerImageTag = "container.image.tag" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeContainerName = "container.name" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeContainerRuntime = "container.runtime" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeDeploymentEnvironment = "deployment.environment" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeDeviceId = "device.id" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeDeviceManufacturer = "device.manufacturer" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeDeviceModelIdentifier = "device.model.identifier" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeDeviceModelName = "device.model.name" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeFaasId = "faas.id" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeFaasInstance = "faas.instance" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeFaasMaxMemory = "faas.max_memory" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeFaasName = "faas.name" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeFaasVersion = "faas.version" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeHostArch = "host.arch" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeHostId = "host.id" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeHostImageId = "host.image.id" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeHostImageName = "host.image.name" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeHostImageVersion = "host.image.version" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeHostName = "host.name" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeHostType = "host.type" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeK8sClusterName = "k8s.cluster.name" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeK8sContainerName = "k8s.container.name" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeK8sContainerRestartCount = "k8s.container.restart_count" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeK8sCronjobName = "k8s.cronjob.name" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeK8sCronjobUid = "k8s.cronjob.uid" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeK8sDaemonsetName = "k8s.daemonset.name" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeK8sDaemonsetUid = "k8s.daemonset.uid" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeK8sDeploymentName = "k8s.deployment.name" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeK8sDeploymentUid = "k8s.deployment.uid" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeK8sJobName = "k8s.job.name" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeK8sJobUid = "k8s.job.uid" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeK8sNamespaceName = "k8s.namespace.name" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeK8sNodeName = "k8s.node.name" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeK8sNodeUid = "k8s.node.uid" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeK8sPodName = "k8s.pod.name" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeK8sPodUid = "k8s.pod.uid" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeK8sReplicasetName = "k8s.replicaset.name" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeK8sReplicasetUid = "k8s.replicaset.uid" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeK8sStatefulsetName = "k8s.statefulset.name" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeK8sStatefulsetUid = "k8s.statefulset.uid" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeOsDescription = "os.description" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeOsName = "os.name" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeOsType = "os.type" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeOsVersion = "os.version" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeProcessCommand = "process.command" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeProcessCommandArgs = "process.command_args" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeProcessCommandLine = "process.command_line" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeProcessExecutableName = "process.executable.name" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeProcessExecutablePath = "process.executable.path" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeProcessOwner = "process.owner" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeProcessParentPid = "process.parent_pid" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeProcessPid = "process.pid" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeProcessRuntimeDescription = "process.runtime.description" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeProcessRuntimeName = "process.runtime.name" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeProcessRuntimeVersion = "process.runtime.version" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeServiceInstanceId = "service.instance.id" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeServiceName = "service.name" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeServiceNamespace = "service.namespace" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeServiceVersion = "service.version" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeTelemetryAutoVersion = "telemetry.auto.version" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeTelemetrySdkLanguage = "telemetry.sdk.language" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeTelemetrySdkName = "telemetry.sdk.name" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeTelemetrySdkVersion = "telemetry.sdk.version" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeWebengineDescription = "webengine.description" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeWebengineName = "webengine.name" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AttributeWebengineVersion = "webengine.version" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AwsEcsLaunchtypeValues.Ec2 = "ec2" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.AwsEcsLaunchtypeValues.Fargate = "fargate" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudPlatformValues.AlibabaCloudEcs = "alibaba_cloud_ecs" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudPlatformValues.AlibabaCloudFc = "alibaba_cloud_fc" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudPlatformValues.AwsAppRunner = "aws_app_runner" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudPlatformValues.AwsEc2 = "aws_ec2" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudPlatformValues.AwsEcs = "aws_ecs" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudPlatformValues.AwsEks = "aws_eks" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudPlatformValues.AwsElasticBeanstalk = "aws_elastic_beanstalk" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudPlatformValues.AwsLambda = "aws_lambda" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudPlatformValues.AzureAks = "azure_aks" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudPlatformValues.AzureAppService = "azure_app_service" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudPlatformValues.AzureContainerInstances = "azure_container_instances" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudPlatformValues.AzureFunctions = "azure_functions" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudPlatformValues.AzureVm = "azure_vm" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudPlatformValues.GcpAppEngine = "gcp_app_engine" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudPlatformValues.GcpCloudFunctions = "gcp_cloud_functions" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudPlatformValues.GcpCloudRun = "gcp_cloud_run" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudPlatformValues.GcpComputeEngine = "gcp_compute_engine" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudPlatformValues.GcpKubernetesEngine = "gcp_kubernetes_engine" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudPlatformValues.TencentCloudCvm = "tencent_cloud_cvm" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudPlatformValues.TencentCloudEks = "tencent_cloud_eks" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudPlatformValues.TencentCloudScf = "tencent_cloud_scf" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudProviderValues.AlibabaCloud = "alibaba_cloud" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudProviderValues.Aws = "aws" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudProviderValues.Azure = "azure" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudProviderValues.Gcp = "gcp" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.CloudProviderValues.TencentCloud = "tencent_cloud" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.HostArchValues.Amd64 = "amd64" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.HostArchValues.Arm32 = "arm32" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.HostArchValues.Arm64 = "arm64" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.HostArchValues.Ia64 = "ia64" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.HostArchValues.Ppc32 = "ppc32" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.HostArchValues.Ppc64 = "ppc64" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.HostArchValues.S390x = "s390x" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.HostArchValues.X86 = "x86" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.OsTypeValues.Aix = "aix" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.OsTypeValues.Darwin = "darwin" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.OsTypeValues.Dragonflybsd = "dragonflybsd" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.OsTypeValues.Freebsd = "freebsd" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.OsTypeValues.Hpux = "hpux" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.OsTypeValues.Linux = "linux" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.OsTypeValues.Netbsd = "netbsd" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.OsTypeValues.Openbsd = "openbsd" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.OsTypeValues.Solaris = "solaris" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.OsTypeValues.Windows = "windows" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.OsTypeValues.ZOs = "z_os" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.TelemetrySdkLanguageValues.Cpp = "cpp" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.TelemetrySdkLanguageValues.Dotnet = "dotnet" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.TelemetrySdkLanguageValues.Erlang = "erlang" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.TelemetrySdkLanguageValues.Go = "go" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.TelemetrySdkLanguageValues.Java = "java" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.TelemetrySdkLanguageValues.Nodejs = "nodejs" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.TelemetrySdkLanguageValues.Php = "php" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.TelemetrySdkLanguageValues.Python = "python" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.TelemetrySdkLanguageValues.Ruby = "ruby" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.TelemetrySdkLanguageValues.Swift = "swift" -> string -const OpenTelemetry.Resources.ResourceSemanticConventions.TelemetrySdkLanguageValues.Webjs = "webjs" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeAwsDynamodbAttributeDefinitions = "aws.dynamodb.attribute_definitions" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeAwsDynamodbAttributesToGet = "aws.dynamodb.attributes_to_get" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeAwsDynamodbConsistentRead = "aws.dynamodb.consistent_read" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeAwsDynamodbConsumedCapacity = "aws.dynamodb.consumed_capacity" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeAwsDynamodbCount = "aws.dynamodb.count" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeAwsDynamodbExclusiveStartTable = "aws.dynamodb.exclusive_start_table" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeAwsDynamodbGlobalSecondaryIndexes = "aws.dynamodb.global_secondary_indexes" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeAwsDynamodbGlobalSecondaryIndexUpdates = "aws.dynamodb.global_secondary_index_updates" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeAwsDynamodbIndexName = "aws.dynamodb.index_name" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeAwsDynamodbItemCollectionMetrics = "aws.dynamodb.item_collection_metrics" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeAwsDynamodbLimit = "aws.dynamodb.limit" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeAwsDynamodbLocalSecondaryIndexes = "aws.dynamodb.local_secondary_indexes" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeAwsDynamodbProjection = "aws.dynamodb.projection" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeAwsDynamodbProvisionedReadCapacity = "aws.dynamodb.provisioned_read_capacity" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeAwsDynamodbProvisionedWriteCapacity = "aws.dynamodb.provisioned_write_capacity" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeAwsDynamodbScanForward = "aws.dynamodb.scan_forward" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeAwsDynamodbScannedCount = "aws.dynamodb.scanned_count" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeAwsDynamodbSegment = "aws.dynamodb.segment" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeAwsDynamodbSelect = "aws.dynamodb.select" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeAwsDynamodbTableCount = "aws.dynamodb.table_count" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeAwsDynamodbTableNames = "aws.dynamodb.table_names" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeAwsDynamodbTotalSegments = "aws.dynamodb.total_segments" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeAwsLambdaInvokedArn = "aws.lambda.invoked_arn" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeCloudeventsEventId = "cloudevents.event_id" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeCloudeventsEventSource = "cloudevents.event_source" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeCloudeventsEventSpecVersion = "cloudevents.event_spec_version" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeCloudeventsEventSubject = "cloudevents.event_subject" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeCloudeventsEventType = "cloudevents.event_type" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeCodeFilepath = "code.filepath" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeCodeFunction = "code.function" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeCodeLineno = "code.lineno" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeCodeNamespace = "code.namespace" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeDbCassandraConsistencyLevel = "db.cassandra.consistency_level" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeDbCassandraCoordinatorDc = "db.cassandra.coordinator.dc" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeDbCassandraCoordinatorId = "db.cassandra.coordinator.id" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeDbCassandraIdempotence = "db.cassandra.idempotence" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeDbCassandraPageSize = "db.cassandra.page_size" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeDbCassandraSpeculativeExecutionCount = "db.cassandra.speculative_execution_count" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeDbCassandraTable = "db.cassandra.table" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeDbConnectionString = "db.connection_string" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeDbInstance = "db.instance" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeDbJdbcDriverClassname = "db.jdbc.driver_classname" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeDbMongodbCollection = "db.mongodb.collection" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeDbMssqlInstanceName = "db.mssql.instance_name" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeDbName = "db.name" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeDbOperation = "db.operation" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeDbRedisDatabaseIndex = "db.redis.database_index" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeDbSqlTable = "db.sql.table" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeDbStatement = "db.statement" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeDbSystem = "db.system" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeDbUser = "db.user" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeEnduserId = "enduser.id" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeEnduserRole = "enduser.role" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeEnduserScope = "enduser.scope" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeExceptionEscaped = "exception.escaped" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeExceptionMessage = "exception.message" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeExceptionStacktrace = "exception.stacktrace" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeExceptionType = "exception.type" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeFaasColdstart = "faas.coldstart" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeFaasCron = "faas.cron" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeFaasDocumentCollection = "faas.document.collection" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeFaasDocumentName = "faas.document.name" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeFaasDocumentOperation = "faas.document.operation" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeFaasDocumentTime = "faas.document.time" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeFaasExecution = "faas.execution" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeFaasInvokedName = "faas.invoked_name" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeFaasInvokedProvider = "faas.invoked_provider" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeFaasInvokedRegion = "faas.invoked_region" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeFaasTime = "faas.time" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeFaasTrigger = "faas.trigger" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeGraphqlDocument = "graphql.document" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeGraphqlOperationName = "graphql.operation.name" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeGraphqlOperationType = "graphql.operation.type" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeHttpClientIp = "http.client_ip" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeHttpFlavor = "http.flavor" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeHttpMethod = "http.method" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeHttpRequestContentLength = "http.request_content_length" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeHttpResponseContentLength = "http.response_content_length" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeHttpRetryCount = "http.retry_count" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeHttpRoute = "http.route" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeHttpScheme = "http.scheme" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeHttpStatusCode = "http.status_code" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeHttpTarget = "http.target" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeHttpUrl = "http.url" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeHttpUserAgent = "http.user_agent" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessageCompressedSize = "message.compressed_size" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessageId = "message.id" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessageType = "message.type" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessageUncompressedSize = "message.uncompressed_size" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingConsumerId = "messaging.consumer_id" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingConversationId = "messaging.conversation_id" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingDestination = "messaging.destination" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingDestinationKind = "messaging.destination_kind" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingKafkaClientId = "messaging.kafka.client_id" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingKafkaConsumerGroup = "messaging.kafka.consumer_group" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingKafkaMessageKey = "messaging.kafka.message_key" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingKafkaPartition = "messaging.kafka.partition" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingKafkaTombstone = "messaging.kafka.tombstone" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingMessageId = "messaging.message_id" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingMessagePayloadCompressedSizeBytes = "messaging.message_payload_compressed_size_bytes" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingMessagePayloadSizeBytes = "messaging.message_payload_size_bytes" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingOperation = "messaging.operation" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingProtocol = "messaging.protocol" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingProtocolVersion = "messaging.protocol_version" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingRabbitmqRoutingKey = "messaging.rabbitmq.routing_key" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingRocketmqClientGroup = "messaging.rocketmq.client_group" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingRocketmqClientId = "messaging.rocketmq.client_id" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingRocketmqConsumptionModel = "messaging.rocketmq.consumption_model" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingRocketmqMessageKeys = "messaging.rocketmq.message_keys" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingRocketmqMessageTag = "messaging.rocketmq.message_tag" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingRocketmqMessageType = "messaging.rocketmq.message_type" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingRocketmqNamespace = "messaging.rocketmq.namespace" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingSystem = "messaging.system" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingTempDestination = "messaging.temp_destination" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeMessagingUrl = "messaging.url" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeNetAppProtocolName = "net.app.protocol.name" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeNetAppProtocolVersion = "net.app.protocol.version" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeNetHostCarrierIcc = "net.host.carrier.icc" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeNetHostCarrierMcc = "net.host.carrier.mcc" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeNetHostCarrierMnc = "net.host.carrier.mnc" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeNetHostCarrierName = "net.host.carrier.name" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeNetHostConnectionSubtype = "net.host.connection.subtype" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeNetHostConnectionType = "net.host.connection.type" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeNetHostName = "net.host.name" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeNetHostPort = "net.host.port" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeNetPeerName = "net.peer.name" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeNetPeerPort = "net.peer.port" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeNetSockFamily = "net.sock.family" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeNetSockHostAddr = "net.sock.host.addr" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeNetSockHostPort = "net.sock.host.port" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeNetSockPeerAddr = "net.sock.peer.addr" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeNetSockPeerName = "net.sock.peer.name" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeNetSockPeerPort = "net.sock.peer.port" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeNetTransport = "net.transport" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeOpentracingRefType = "opentracing.ref_type" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributePeerService = "peer.service" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeRpcGrpcStatusCode = "rpc.grpc.status_code" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeRpcJsonrpcErrorCode = "rpc.jsonrpc.error_code" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeRpcJsonrpcErrorMessage = "rpc.jsonrpc.error_message" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeRpcJsonrpcRequestId = "rpc.jsonrpc.request_id" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeRpcJsonrpcVersion = "rpc.jsonrpc.version" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeRpcMethod = "rpc.method" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeRpcService = "rpc.service" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeRpcSystem = "rpc.system" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeThreadId = "thread.id" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.AttributeThreadName = "thread.name" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbCassandraConsistencyLevelValues.All = "all" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbCassandraConsistencyLevelValues.Any = "any" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbCassandraConsistencyLevelValues.EachQuorum = "each_quorum" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbCassandraConsistencyLevelValues.LocalOne = "local_one" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbCassandraConsistencyLevelValues.LocalQuorum = "local_quorum" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbCassandraConsistencyLevelValues.LocalSerial = "local_serial" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbCassandraConsistencyLevelValues.One = "one" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbCassandraConsistencyLevelValues.Quorum = "quorum" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbCassandraConsistencyLevelValues.Serial = "serial" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbCassandraConsistencyLevelValues.Three = "three" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbCassandraConsistencyLevelValues.Two = "two" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Adabas = "adabas" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Cache = "cache" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Cassandra = "cassandra" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Cloudscape = "cloudscape" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Cockroachdb = "cockroachdb" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Coldfusion = "coldfusion" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Cosmosdb = "cosmosdb" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Couchbase = "couchbase" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Couchdb = "couchdb" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Db2 = "db2" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Derby = "derby" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Dynamodb = "dynamodb" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Edb = "edb" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Elasticsearch = "elasticsearch" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Filemaker = "filemaker" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Firebird = "firebird" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Firstsql = "firstsql" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Geode = "geode" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.H2 = "h2" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Hanadb = "hanadb" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Hbase = "hbase" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Hive = "hive" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Hsqldb = "hsqldb" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Informix = "informix" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Ingres = "ingres" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Instantdb = "instantdb" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Interbase = "interbase" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Mariadb = "mariadb" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Maxdb = "maxdb" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Memcached = "memcached" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Mongodb = "mongodb" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Mssql = "mssql" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Mysql = "mysql" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Neo4j = "neo4j" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Netezza = "netezza" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Opensearch = "opensearch" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Oracle = "oracle" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.OtherSql = "other_sql" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Pervasive = "pervasive" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Pointbase = "pointbase" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Postgresql = "postgresql" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Progress = "progress" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Redis = "redis" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Redshift = "redshift" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Sqlite = "sqlite" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Sybase = "sybase" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Teradata = "teradata" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues.Vertica = "vertica" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.FaasDocumentOperationValues.Delete = "delete" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.FaasDocumentOperationValues.Edit = "edit" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.FaasDocumentOperationValues.Insert = "insert" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.FaasInvokedProviderValues.AlibabaCloud = "alibaba_cloud" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.FaasInvokedProviderValues.Aws = "aws" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.FaasInvokedProviderValues.Azure = "azure" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.FaasInvokedProviderValues.Gcp = "gcp" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.FaasInvokedProviderValues.TencentCloud = "tencent_cloud" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.FaasTriggerValues.Datasource = "datasource" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.FaasTriggerValues.Http = "http" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.FaasTriggerValues.Other = "other" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.FaasTriggerValues.Pubsub = "pubsub" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.FaasTriggerValues.Timer = "timer" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.GraphqlOperationTypeValues.Mutation = "mutation" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.GraphqlOperationTypeValues.Query = "query" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.GraphqlOperationTypeValues.Subscription = "subscription" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.HttpFlavorValues.Http10 = "1.0" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.HttpFlavorValues.Http11 = "1.1" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.HttpFlavorValues.Http20 = "2.0" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.HttpFlavorValues.Http30 = "3.0" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.HttpFlavorValues.Quic = "QUIC" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.HttpFlavorValues.Spdy = "SPDY" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.MessageTypeValues.Received = "RECEIVED" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.MessageTypeValues.Sent = "SENT" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.MessagingDestinationKindValues.Queue = "queue" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.MessagingDestinationKindValues.Topic = "topic" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.MessagingOperationValues.Process = "process" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.MessagingOperationValues.Receive = "receive" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.MessagingRocketmqConsumptionModelValues.Broadcasting = "broadcasting" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.MessagingRocketmqConsumptionModelValues.Clustering = "clustering" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.MessagingRocketmqMessageTypeValues.Delay = "delay" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.MessagingRocketmqMessageTypeValues.Fifo = "fifo" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.MessagingRocketmqMessageTypeValues.Normal = "normal" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.MessagingRocketmqMessageTypeValues.Transaction = "transaction" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionSubtypeValues.Cdma = "cdma" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionSubtypeValues.Cdma20001xrtt = "cdma2000_1xrtt" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionSubtypeValues.Edge = "edge" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionSubtypeValues.Ehrpd = "ehrpd" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionSubtypeValues.Evdo0 = "evdo_0" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionSubtypeValues.EvdoA = "evdo_a" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionSubtypeValues.EvdoB = "evdo_b" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionSubtypeValues.Gprs = "gprs" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionSubtypeValues.Gsm = "gsm" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionSubtypeValues.Hsdpa = "hsdpa" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionSubtypeValues.Hspa = "hspa" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionSubtypeValues.Hspap = "hspap" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionSubtypeValues.Hsupa = "hsupa" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionSubtypeValues.Iden = "iden" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionSubtypeValues.Iwlan = "iwlan" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionSubtypeValues.Lte = "lte" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionSubtypeValues.LteCa = "lte_ca" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionSubtypeValues.Nr = "nr" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionSubtypeValues.Nrnsa = "nrnsa" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionSubtypeValues.TdScdma = "td_scdma" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionSubtypeValues.Umts = "umts" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionTypeValues.Cell = "cell" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionTypeValues.Unavailable = "unavailable" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionTypeValues.Unknown = "unknown" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionTypeValues.Wifi = "wifi" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionTypeValues.Wired = "wired" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetSockFamilyValues.Inet = "inet" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetSockFamilyValues.Inet6 = "inet6" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetSockFamilyValues.Unix = "unix" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetTransportValues.Inproc = "inproc" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetTransportValues.IpTcp = "ip_tcp" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetTransportValues.IpUdp = "ip_udp" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetTransportValues.Other = "other" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.NetTransportValues.Pipe = "pipe" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.OpentracingRefTypeValues.ChildOf = "child_of" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.OpentracingRefTypeValues.FollowsFrom = "follows_from" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.RpcGrpcStatusCodeValues.Aborted = 10 -> int -const OpenTelemetry.Trace.TraceSemanticConventions.RpcGrpcStatusCodeValues.AlreadyExists = 6 -> int -const OpenTelemetry.Trace.TraceSemanticConventions.RpcGrpcStatusCodeValues.Cancelled = 1 -> int -const OpenTelemetry.Trace.TraceSemanticConventions.RpcGrpcStatusCodeValues.DataLoss = 15 -> int -const OpenTelemetry.Trace.TraceSemanticConventions.RpcGrpcStatusCodeValues.DeadlineExceeded = 4 -> int -const OpenTelemetry.Trace.TraceSemanticConventions.RpcGrpcStatusCodeValues.FailedPrecondition = 9 -> int -const OpenTelemetry.Trace.TraceSemanticConventions.RpcGrpcStatusCodeValues.Internal = 13 -> int -const OpenTelemetry.Trace.TraceSemanticConventions.RpcGrpcStatusCodeValues.InvalidArgument = 3 -> int -const OpenTelemetry.Trace.TraceSemanticConventions.RpcGrpcStatusCodeValues.NotFound = 5 -> int -const OpenTelemetry.Trace.TraceSemanticConventions.RpcGrpcStatusCodeValues.Ok = 0 -> int -const OpenTelemetry.Trace.TraceSemanticConventions.RpcGrpcStatusCodeValues.OutOfRange = 11 -> int -const OpenTelemetry.Trace.TraceSemanticConventions.RpcGrpcStatusCodeValues.PermissionDenied = 7 -> int -const OpenTelemetry.Trace.TraceSemanticConventions.RpcGrpcStatusCodeValues.ResourceExhausted = 8 -> int -const OpenTelemetry.Trace.TraceSemanticConventions.RpcGrpcStatusCodeValues.Unauthenticated = 16 -> int -const OpenTelemetry.Trace.TraceSemanticConventions.RpcGrpcStatusCodeValues.Unavailable = 14 -> int -const OpenTelemetry.Trace.TraceSemanticConventions.RpcGrpcStatusCodeValues.Unimplemented = 12 -> int -const OpenTelemetry.Trace.TraceSemanticConventions.RpcGrpcStatusCodeValues.Unknown = 2 -> int -const OpenTelemetry.Trace.TraceSemanticConventions.RpcSystemValues.ApacheDubbo = "apache_dubbo" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.RpcSystemValues.DotnetWcf = "dotnet_wcf" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.RpcSystemValues.Grpc = "grpc" -> string -const OpenTelemetry.Trace.TraceSemanticConventions.RpcSystemValues.JavaRmi = "java_rmi" -> string -OpenTelemetry.Resources.ResourceSemanticConventions -OpenTelemetry.Resources.ResourceSemanticConventions.AwsEcsLaunchtypeValues -OpenTelemetry.Resources.ResourceSemanticConventions.CloudPlatformValues -OpenTelemetry.Resources.ResourceSemanticConventions.CloudProviderValues -OpenTelemetry.Resources.ResourceSemanticConventions.HostArchValues -OpenTelemetry.Resources.ResourceSemanticConventions.OsTypeValues -OpenTelemetry.Resources.ResourceSemanticConventions.TelemetrySdkLanguageValues -OpenTelemetry.Trace.TraceSemanticConventions -OpenTelemetry.Trace.TraceSemanticConventions.DbCassandraConsistencyLevelValues -OpenTelemetry.Trace.TraceSemanticConventions.DbSystemValues -OpenTelemetry.Trace.TraceSemanticConventions.FaasDocumentOperationValues -OpenTelemetry.Trace.TraceSemanticConventions.FaasInvokedProviderValues -OpenTelemetry.Trace.TraceSemanticConventions.FaasTriggerValues -OpenTelemetry.Trace.TraceSemanticConventions.GraphqlOperationTypeValues -OpenTelemetry.Trace.TraceSemanticConventions.HttpFlavorValues -OpenTelemetry.Trace.TraceSemanticConventions.MessageTypeValues -OpenTelemetry.Trace.TraceSemanticConventions.MessagingDestinationKindValues -OpenTelemetry.Trace.TraceSemanticConventions.MessagingOperationValues -OpenTelemetry.Trace.TraceSemanticConventions.MessagingRocketmqConsumptionModelValues -OpenTelemetry.Trace.TraceSemanticConventions.MessagingRocketmqMessageTypeValues -OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionSubtypeValues -OpenTelemetry.Trace.TraceSemanticConventions.NetHostConnectionTypeValues -OpenTelemetry.Trace.TraceSemanticConventions.NetSockFamilyValues -OpenTelemetry.Trace.TraceSemanticConventions.NetTransportValues -OpenTelemetry.Trace.TraceSemanticConventions.OpentracingRefTypeValues -OpenTelemetry.Trace.TraceSemanticConventions.RpcGrpcStatusCodeValues -OpenTelemetry.Trace.TraceSemanticConventions.RpcSystemValues -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixAwsEcs -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixAwsEks -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixAwsLog -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixBrowser -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixCloud -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixContainer -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixDeployment -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixDevice -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixFaasResource -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixHost -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixK8sCluster -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixK8sContainer -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixK8sCronjob -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixK8sDaemonset -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixK8sDeployment -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixK8sJob -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixK8sNamespace -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixK8sNode -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixK8sPod -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixK8sReplicaset -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixK8sStatefulset -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixOs -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixProcess -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixProcessRuntime -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixService -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixTelemetry -> string -static readonly OpenTelemetry.Resources.ResourceSemanticConventions.PrefixWebengineResource -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.EventException -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.EventRpcMessage -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixAws -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixAwsLambda -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixCloudevents -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixCode -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixDb -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixDbCassandra -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixDbMongodb -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixDbMssql -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixDbRedis -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixDbSql -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixDynamodbShared -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixException -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixFaasSpan -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixFaasSpanDatasource -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixGraphql -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixHttp -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixIdentity -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixMessaging -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixMessagingKafka -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixMessagingRabbitmq -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixMessagingRocketmq -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixNetwork -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixOpentracing -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixPeer -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixRpc -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixRpcGrpc -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixRpcJsonrpc -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixRpcMessage -> string -static readonly OpenTelemetry.Trace.TraceSemanticConventions.PrefixThread -> string \ No newline at end of file diff --git a/src/OpenTelemetry.SemanticConventions/OpenTelemetry.SemanticConventions.csproj b/src/OpenTelemetry.SemanticConventions/OpenTelemetry.SemanticConventions.csproj index 392f6d156c0..493987b395a 100644 --- a/src/OpenTelemetry.SemanticConventions/OpenTelemetry.SemanticConventions.csproj +++ b/src/OpenTelemetry.SemanticConventions/OpenTelemetry.SemanticConventions.csproj @@ -1,7 +1,7 @@ - + - netstandard2.0;net462 + $(TargetFrameworksForLibraries) OpenTelemetry Semantic Conventions $(PackageTags);semantic-conventions diff --git a/src/OpenTelemetry.SemanticConventions/Resource/ResourceSemanticConventions.cs b/src/OpenTelemetry.SemanticConventions/Resource/ResourceSemanticConventions.cs index 1da782284c0..6409f23823c 100644 --- a/src/OpenTelemetry.SemanticConventions/Resource/ResourceSemanticConventions.cs +++ b/src/OpenTelemetry.SemanticConventions/Resource/ResourceSemanticConventions.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 // This file has been auto generated from buildscripts/semantic-conventions/templates/SemanticConventions.cs.j2 @@ -22,7 +9,7 @@ namespace OpenTelemetry.Resources { /// /// Constants for semantic attribute names outlined by the OpenTelemetry specifications. - /// . + /// . /// /// /// Schema and specification version: https://opentelemetry.io/schemas/v1.13.0. diff --git a/src/OpenTelemetry.SemanticConventions/Trace/TraceSemanticConventions.cs b/src/OpenTelemetry.SemanticConventions/Trace/TraceSemanticConventions.cs index 10fe6322f62..a9b253faa24 100644 --- a/src/OpenTelemetry.SemanticConventions/Trace/TraceSemanticConventions.cs +++ b/src/OpenTelemetry.SemanticConventions/Trace/TraceSemanticConventions.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 // This file has been auto generated from buildscripts/semantic-conventions/templates/SemanticConventions.cs.j2 @@ -22,7 +9,7 @@ namespace OpenTelemetry.Trace { /// /// Constants for semantic attribute names outlined by the OpenTelemetry specifications. - /// . + /// . /// /// /// Schema and specification version: https://opentelemetry.io/schemas/v1.13.0. diff --git a/src/OpenTelemetry.SemanticConventions/scripts/semantic-conventions/templates/SemanticConventions.cs.j2 b/src/OpenTelemetry.SemanticConventions/scripts/semantic-conventions/templates/SemanticConventions.cs.j2 index 057c417379e..d64fb46e03d 100644 --- a/src/OpenTelemetry.SemanticConventions/scripts/semantic-conventions/templates/SemanticConventions.cs.j2 +++ b/src/OpenTelemetry.SemanticConventions/scripts/semantic-conventions/templates/SemanticConventions.cs.j2 @@ -41,9 +41,9 @@ namespace {{pkg | trim}} /// /// Constants for semantic attribute names outlined by the OpenTelemetry specifications. {% if class == "TraceSemanticConventions" %} - /// . + /// . {% elif class == "ResourceSemanticConventions" %} - /// . + /// . {% endif %} /// /// diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/net7.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Shims.OpenTracing/.publicApi/PublicAPI.Shipped.txt similarity index 100% rename from src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/net7.0/PublicAPI.Shipped.txt rename to src/OpenTelemetry.Shims.OpenTracing/.publicApi/PublicAPI.Shipped.txt diff --git a/src/OpenTelemetry.Shims.OpenTracing/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Shims.OpenTracing/.publicApi/PublicAPI.Unshipped.txt similarity index 75% rename from src/OpenTelemetry.Shims.OpenTracing/.publicApi/net462/PublicAPI.Unshipped.txt rename to src/OpenTelemetry.Shims.OpenTracing/.publicApi/PublicAPI.Unshipped.txt index a9a1b3717e4..90f671f8a12 100644 --- a/src/OpenTelemetry.Shims.OpenTracing/.publicApi/net462/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Shims.OpenTracing/.publicApi/PublicAPI.Unshipped.txt @@ -4,4 +4,5 @@ OpenTelemetry.Shims.OpenTracing.TracerShim.BuildSpan(string operationName) -> Op OpenTelemetry.Shims.OpenTracing.TracerShim.Extract(OpenTracing.Propagation.IFormat format, TCarrier carrier) -> OpenTracing.ISpanContext OpenTelemetry.Shims.OpenTracing.TracerShim.Inject(OpenTracing.ISpanContext spanContext, OpenTracing.Propagation.IFormat format, TCarrier carrier) -> void OpenTelemetry.Shims.OpenTracing.TracerShim.ScopeManager.get -> OpenTracing.IScopeManager -OpenTelemetry.Shims.OpenTracing.TracerShim.TracerShim(OpenTelemetry.Trace.Tracer tracer, OpenTelemetry.Context.Propagation.TextMapPropagator textFormat) -> void \ No newline at end of file +OpenTelemetry.Shims.OpenTracing.TracerShim.TracerShim(OpenTelemetry.Trace.TracerProvider tracerProvider) -> void +OpenTelemetry.Shims.OpenTracing.TracerShim.TracerShim(OpenTelemetry.Trace.TracerProvider tracerProvider, OpenTelemetry.Context.Propagation.TextMapPropagator textFormat) -> void diff --git a/src/OpenTelemetry.Shims.OpenTracing/.publicApi/net462/PublicAPI.Shipped.txt b/src/OpenTelemetry.Shims.OpenTracing/.publicApi/net462/PublicAPI.Shipped.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/OpenTelemetry.Shims.OpenTracing/.publicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Shims.OpenTracing/.publicApi/netstandard2.0/PublicAPI.Shipped.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/OpenTelemetry.Shims.OpenTracing/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Shims.OpenTracing/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt deleted file mode 100644 index f6459c7461e..00000000000 --- a/src/OpenTelemetry.Shims.OpenTracing/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,7 +0,0 @@ -OpenTelemetry.Shims.OpenTracing.TracerShim -OpenTelemetry.Shims.OpenTracing.TracerShim.ActiveSpan.get -> OpenTracing.ISpan -OpenTelemetry.Shims.OpenTracing.TracerShim.BuildSpan(string operationName) -> OpenTracing.ISpanBuilder -OpenTelemetry.Shims.OpenTracing.TracerShim.Extract(OpenTracing.Propagation.IFormat format, TCarrier carrier) -> OpenTracing.ISpanContext -OpenTelemetry.Shims.OpenTracing.TracerShim.Inject(OpenTracing.ISpanContext spanContext, OpenTracing.Propagation.IFormat format, TCarrier carrier) -> void -OpenTelemetry.Shims.OpenTracing.TracerShim.ScopeManager.get -> OpenTracing.IScopeManager -OpenTelemetry.Shims.OpenTracing.TracerShim.TracerShim(OpenTelemetry.Trace.Tracer tracer, OpenTelemetry.Context.Propagation.TextMapPropagator textFormat) -> void diff --git a/src/OpenTelemetry.Shims.OpenTracing/AssemblyInfo.cs b/src/OpenTelemetry.Shims.OpenTracing/AssemblyInfo.cs index 8c8fdc47b3e..ae3704ebf27 100644 --- a/src/OpenTelemetry.Shims.OpenTracing/AssemblyInfo.cs +++ b/src/OpenTelemetry.Shims.OpenTracing/AssemblyInfo.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Runtime.CompilerServices; diff --git a/src/OpenTelemetry.Shims.OpenTracing/CHANGELOG.md b/src/OpenTelemetry.Shims.OpenTracing/CHANGELOG.md index 1529346f3c8..577ddc62a97 100644 --- a/src/OpenTelemetry.Shims.OpenTracing/CHANGELOG.md +++ b/src/OpenTelemetry.Shims.OpenTracing/CHANGELOG.md @@ -2,6 +2,27 @@ ## Unreleased +## 1.7.0-beta.1 + +Released 2023-Dec-08 + +* Remove obsolete `TracerShim(Tracer, TextMapPropagator)` constructor. + Use `TracerShim(TracerProvider)` + or `TracerShim(TracerProvider, TextMapPropagator)` constructors. + ([#4862](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4862)) + +## 1.6.0-beta.1 + +Released 2023-Sep-05 + +* Fix: Do not raise `ArgumentException` if `Activity` behind the shim span + has an invalid context. + ([#2787](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2787)) +* Obsolete `TracerShim(Tracer, TextMapPropagator)` constructor. + Provide `TracerShim(TracerProvider)` + and `TracerShim(TracerProvider, TextMapPropagator)` constructors. + ([#4812](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4812)) + ## 1.5.0-beta.1 Released 2023-Jun-05 diff --git a/src/OpenTelemetry.Shims.OpenTracing/OpenTelemetry.Shims.OpenTracing.csproj b/src/OpenTelemetry.Shims.OpenTracing/OpenTelemetry.Shims.OpenTracing.csproj index 13ec27ceb45..35295220a1f 100644 --- a/src/OpenTelemetry.Shims.OpenTracing/OpenTelemetry.Shims.OpenTracing.csproj +++ b/src/OpenTelemetry.Shims.OpenTracing/OpenTelemetry.Shims.OpenTracing.csproj @@ -1,7 +1,6 @@ - - netstandard2.0;net462 + $(TargetFrameworksForLibraries) OpenTracing shim for OpenTelemetry .NET $(PackageTags);distributed-tracing;OpenTracing true @@ -23,7 +22,7 @@ - + diff --git a/src/OpenTelemetry.Shims.OpenTracing/ScopeManagerShim.cs b/src/OpenTelemetry.Shims.OpenTracing/ScopeManagerShim.cs index 2984d3e7e38..f9464d966a1 100644 --- a/src/OpenTelemetry.Shims.OpenTracing/ScopeManagerShim.cs +++ b/src/OpenTelemetry.Shims.OpenTracing/ScopeManagerShim.cs @@ -1,114 +1,89 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Runtime.CompilerServices; using OpenTelemetry.Internal; using OpenTelemetry.Trace; using OpenTracing; -namespace OpenTelemetry.Shims.OpenTracing -{ - internal sealed class ScopeManagerShim : IScopeManager - { - private static readonly ConditionalWeakTable SpanScopeTable = new(); +namespace OpenTelemetry.Shims.OpenTracing; - private readonly Tracer tracer; +internal sealed class ScopeManagerShim : IScopeManager +{ + private static readonly ConditionalWeakTable SpanScopeTable = new(); #if DEBUG - private int spanScopeTableCount; -#endif - - public ScopeManagerShim(Tracer tracer) - { - Guard.ThrowIfNull(tracer); - - this.tracer = tracer; - } + private int spanScopeTableCount; -#if DEBUG - public int SpanScopeTableCount => this.spanScopeTableCount; + public int SpanScopeTableCount => this.spanScopeTableCount; #endif - /// - public IScope Active + /// + public IScope Active + { + get { - get + var currentSpan = Tracer.CurrentSpan; + if (currentSpan == null || !currentSpan.Context.IsValid) { - var currentSpan = Tracer.CurrentSpan; - if (currentSpan == null || !currentSpan.Context.IsValid) - { - return null; - } - - if (SpanScopeTable.TryGetValue(currentSpan, out var openTracingScope)) - { - return openTracingScope; - } + return null; + } - return new ScopeInstrumentation(currentSpan); + if (SpanScopeTable.TryGetValue(currentSpan, out var openTracingScope)) + { + return openTracingScope; } + + return new ScopeInstrumentation(currentSpan); } + } - /// - public IScope Activate(ISpan span, bool finishSpanOnDispose) - { - var shim = Guard.ThrowIfNotOfType(span); + /// + public IScope Activate(ISpan span, bool finishSpanOnDispose) + { + var shim = Guard.ThrowIfNotOfType(span); - var scope = Tracer.WithSpan(shim.Span); + var scope = Tracer.WithSpan(shim.Span); - var instrumentation = new ScopeInstrumentation( - shim.Span, - () => - { - var removed = SpanScopeTable.Remove(shim.Span); + var instrumentation = new ScopeInstrumentation( + shim.Span, + () => + { + var removed = SpanScopeTable.Remove(shim.Span); #if DEBUG - if (removed) - { - Interlocked.Decrement(ref this.spanScopeTableCount); - } + if (removed) + { + Interlocked.Decrement(ref this.spanScopeTableCount); + } #endif - scope.Dispose(); - }); + scope.Dispose(); + }); - SpanScopeTable.Add(shim.Span, instrumentation); + SpanScopeTable.Add(shim.Span, instrumentation); #if DEBUG - Interlocked.Increment(ref this.spanScopeTableCount); + Interlocked.Increment(ref this.spanScopeTableCount); #endif - return instrumentation; - } + return instrumentation; + } - private sealed class ScopeInstrumentation : IScope - { - private readonly Action disposeAction; + private sealed class ScopeInstrumentation : IScope + { + private readonly Action disposeAction; - public ScopeInstrumentation(TelemetrySpan span, Action disposeAction = null) - { - this.Span = new SpanShim(span); - this.disposeAction = disposeAction; - } + public ScopeInstrumentation(TelemetrySpan span, Action disposeAction = null) + { + this.Span = new SpanShim(span); + this.disposeAction = disposeAction; + } - /// - public ISpan Span { get; } + /// + public ISpan Span { get; } - /// - public void Dispose() - { - this.disposeAction?.Invoke(); - } + /// + public void Dispose() + { + this.disposeAction?.Invoke(); } } } diff --git a/src/OpenTelemetry.Shims.OpenTracing/SpanBuilderShim.cs b/src/OpenTelemetry.Shims.OpenTracing/SpanBuilderShim.cs index 0c42afc6186..0359762ac3e 100644 --- a/src/OpenTelemetry.Shims.OpenTracing/SpanBuilderShim.cs +++ b/src/OpenTelemetry.Shims.OpenTracing/SpanBuilderShim.cs @@ -1,323 +1,309 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Internal; using OpenTelemetry.Trace; using OpenTracing; -namespace OpenTelemetry.Shims.OpenTracing +namespace OpenTelemetry.Shims.OpenTracing; + +/// +/// Adapts OpenTracing ISpanBuilder to an underlying OpenTelemetry ISpanBuilder. +/// +/// Instances of this class are not thread-safe. +/// +internal sealed class SpanBuilderShim : ISpanBuilder { /// - /// Adapts OpenTracing ISpanBuilder to an underlying OpenTelemetry ISpanBuilder. + /// The tracer. /// - /// Instances of this class are not thread-safe. - /// - internal sealed class SpanBuilderShim : ISpanBuilder - { - /// - /// The tracer. - /// - private readonly Tracer tracer; + private readonly Tracer tracer; - /// - /// The span name. - /// - private readonly string spanName; + /// + /// The span name. + /// + private readonly string spanName; - /// - /// The OpenTelemetry links. These correspond loosely to OpenTracing references. - /// - private readonly List links = new(); + /// + /// The OpenTelemetry links. These correspond loosely to OpenTracing references. + /// + private readonly List links = new(); - /// - /// The OpenTelemetry attributes. These correspond to OpenTracing Tags. - /// - private readonly List> attributes = new(); + /// + /// The OpenTelemetry attributes. These correspond to OpenTracing Tags. + /// + private readonly List> attributes = new(); - /// - /// The parent as an TelemetrySpan, if any. - /// - private TelemetrySpan parentSpan; + /// + /// The parent as an TelemetrySpan, if any. + /// + private TelemetrySpan parentSpan; - /// - /// The parent as an SpanContext, if any. - /// - private SpanContext parentSpanContext; + /// + /// The parent as an SpanContext, if any. + /// + private SpanContext parentSpanContext; - /// - /// The explicit start time, if any. - /// - private DateTimeOffset? explicitStartTime; + /// + /// The explicit start time, if any. + /// + private DateTimeOffset? explicitStartTime; - private bool ignoreActiveSpan; + private bool ignoreActiveSpan; - private SpanKind spanKind; + private SpanKind spanKind; - private bool error; + private bool error; - public SpanBuilderShim(Tracer tracer, string spanName) - { - Guard.ThrowIfNull(tracer); - Guard.ThrowIfNull(spanName); + public SpanBuilderShim(Tracer tracer, string spanName) + { + Guard.ThrowIfNull(tracer); + Guard.ThrowIfNull(spanName); - this.tracer = tracer; - this.spanName = spanName; - this.ScopeManager = new ScopeManagerShim(this.tracer); - } + this.tracer = tracer; + this.spanName = spanName; + this.ScopeManager = new ScopeManagerShim(); + } - private IScopeManager ScopeManager { get; } + private IScopeManager ScopeManager { get; } - private bool ParentSet => this.parentSpan != null || this.parentSpanContext.IsValid; + private bool ParentSet => this.parentSpan != null || this.parentSpanContext.IsValid; - /// - public ISpanBuilder AsChildOf(ISpanContext parent) + /// + public ISpanBuilder AsChildOf(ISpanContext parent) + { + if (parent == null) { - if (parent == null) - { - return this; - } - - return this.AddReference(References.ChildOf, parent); + return this; } - /// - public ISpanBuilder AsChildOf(ISpan parent) - { - if (parent == null) - { - return this; - } - - if (!this.ParentSet) - { - this.parentSpan = GetOpenTelemetrySpan(parent); - return this; - } + return this.AddReference(References.ChildOf, parent); + } - return this.AsChildOf(parent.Context); + /// + public ISpanBuilder AsChildOf(ISpan parent) + { + if (parent == null) + { + return this; } - /// - public ISpanBuilder AddReference(string referenceType, ISpanContext referencedContext) + if (!this.ParentSet) { - if (referencedContext == null) - { - return this; - } - - Guard.ThrowIfNull(referenceType); + this.parentSpan = GetOpenTelemetrySpan(parent); + return this; + } - // TODO There is no relation between OpenTracing.References (referenceType) and OpenTelemetry Link - var actualContext = GetOpenTelemetrySpanContext(referencedContext); - if (!this.ParentSet) - { - this.parentSpanContext = actualContext; - return this; - } - else - { - this.links.Add(new Link(actualContext)); - } + return this.AsChildOf(parent.Context); + } + /// + public ISpanBuilder AddReference(string referenceType, ISpanContext referencedContext) + { + if (referencedContext == null) + { return this; } - /// - public ISpanBuilder IgnoreActiveSpan() + Guard.ThrowIfNull(referenceType); + + // TODO There is no relation between OpenTracing.References (referenceType) and OpenTelemetry Link + var actualContext = GetOpenTelemetrySpanContext(referencedContext); + if (!this.ParentSet) { - this.ignoreActiveSpan = true; + this.parentSpanContext = actualContext; return this; } - - /// - public ISpan Start() + else { - TelemetrySpan span = null; - - // If specified, this takes precedence. - if (this.ignoreActiveSpan) - { - span = this.tracer.StartRootSpan(this.spanName, this.spanKind, default, this.links, this.explicitStartTime ?? default); - } - else if (this.parentSpan != null) - { - span = this.tracer.StartSpan(this.spanName, this.spanKind, this.parentSpan, default, this.links, this.explicitStartTime ?? default); - } - else if (this.parentSpanContext.IsValid) - { - span = this.tracer.StartSpan(this.spanName, this.spanKind, this.parentSpanContext, default, this.links, this.explicitStartTime ?? default); - } + this.links.Add(new Link(actualContext)); + } - if (span == null) - { - span = this.tracer.StartSpan(this.spanName, this.spanKind, default(SpanContext), default, null, this.explicitStartTime ?? default); - } + return this; + } - foreach (var kvp in this.attributes) - { - span.SetAttribute(kvp.Key, kvp.Value.ToString()); - } + /// + public ISpanBuilder IgnoreActiveSpan() + { + this.ignoreActiveSpan = true; + return this; + } - if (this.error) - { - span.SetStatus(Status.Error); - } + /// + public ISpan Start() + { + TelemetrySpan span = null; - return new SpanShim(span); + // If specified, this takes precedence. + if (this.ignoreActiveSpan) + { + span = this.tracer.StartRootSpan(this.spanName, this.spanKind, default, this.links, this.explicitStartTime ?? default); } - - /// - public IScope StartActive() => this.StartActive(true); - - /// - public IScope StartActive(bool finishSpanOnDispose) + else if (this.parentSpan != null) + { + span = this.tracer.StartSpan(this.spanName, this.spanKind, this.parentSpan, default, this.links, this.explicitStartTime ?? default); + } + else if (this.parentSpanContext.IsValid) { - var span = this.Start(); - return this.ScopeManager.Activate(span, finishSpanOnDispose); + span = this.tracer.StartSpan(this.spanName, this.spanKind, this.parentSpanContext, default, this.links, this.explicitStartTime ?? default); } - /// - public ISpanBuilder WithStartTimestamp(DateTimeOffset timestamp) + if (span == null) { - this.explicitStartTime = timestamp; - return this; + span = this.tracer.StartSpan(this.spanName, this.spanKind, default(SpanContext), default, null, this.explicitStartTime ?? default); } - /// - public ISpanBuilder WithTag(string key, string value) + foreach (var kvp in this.attributes) { - // see https://opentracing.io/specification/conventions/ for special key handling. - if (global::OpenTracing.Tag.Tags.SpanKind.Key.Equals(key, StringComparison.Ordinal)) - { - this.spanKind = value switch - { - global::OpenTracing.Tag.Tags.SpanKindClient => SpanKind.Client, - global::OpenTracing.Tag.Tags.SpanKindServer => SpanKind.Server, - global::OpenTracing.Tag.Tags.SpanKindProducer => SpanKind.Producer, - global::OpenTracing.Tag.Tags.SpanKindConsumer => SpanKind.Consumer, - _ => SpanKind.Internal, - }; - } - else if (global::OpenTracing.Tag.Tags.Error.Key.Equals(key, StringComparison.Ordinal) && bool.TryParse(value, out var booleanValue)) - { - this.error = booleanValue; - } - else - { - // Keys must be non-null. - // Null values => string.Empty. - if (key != null) - { - this.attributes.Add(new KeyValuePair(key, value ?? string.Empty)); - } - } + span.SetAttribute(kvp.Key, kvp.Value.ToString()); + } - return this; + if (this.error) + { + span.SetStatus(Status.Error); } - /// - public ISpanBuilder WithTag(string key, bool value) + return new SpanShim(span); + } + + /// + public IScope StartActive() => this.StartActive(true); + + /// + public IScope StartActive(bool finishSpanOnDispose) + { + var span = this.Start(); + return this.ScopeManager.Activate(span, finishSpanOnDispose); + } + + /// + public ISpanBuilder WithStartTimestamp(DateTimeOffset timestamp) + { + this.explicitStartTime = timestamp; + return this; + } + + /// + public ISpanBuilder WithTag(string key, string value) + { + // see https://opentracing.io/specification/conventions/ for special key handling. + if (global::OpenTracing.Tag.Tags.SpanKind.Key.Equals(key, StringComparison.Ordinal)) { - if (global::OpenTracing.Tag.Tags.Error.Key.Equals(key, StringComparison.Ordinal)) + this.spanKind = value switch { - this.error = value; - } - else + global::OpenTracing.Tag.Tags.SpanKindClient => SpanKind.Client, + global::OpenTracing.Tag.Tags.SpanKindServer => SpanKind.Server, + global::OpenTracing.Tag.Tags.SpanKindProducer => SpanKind.Producer, + global::OpenTracing.Tag.Tags.SpanKindConsumer => SpanKind.Consumer, + _ => SpanKind.Internal, + }; + } + else if (global::OpenTracing.Tag.Tags.Error.Key.Equals(key, StringComparison.Ordinal) && bool.TryParse(value, out var booleanValue)) + { + this.error = booleanValue; + } + else + { + // Keys must be non-null. + // Null values => string.Empty. + if (key != null) { - this.attributes.Add(new KeyValuePair(key, value)); + this.attributes.Add(new KeyValuePair(key, value ?? string.Empty)); } - - return this; } - /// - public ISpanBuilder WithTag(string key, int value) + return this; + } + + /// + public ISpanBuilder WithTag(string key, bool value) + { + if (global::OpenTracing.Tag.Tags.Error.Key.Equals(key, StringComparison.Ordinal)) { - this.attributes.Add(new KeyValuePair(key, value)); - return this; + this.error = value; } - - /// - public ISpanBuilder WithTag(string key, double value) + else { this.attributes.Add(new KeyValuePair(key, value)); - return this; } - /// - public ISpanBuilder WithTag(global::OpenTracing.Tag.BooleanTag tag, bool value) - { - Guard.ThrowIfNull(tag?.Key); + return this; + } - return this.WithTag(tag.Key, value); - } + /// + public ISpanBuilder WithTag(string key, int value) + { + this.attributes.Add(new KeyValuePair(key, value)); + return this; + } - /// - public ISpanBuilder WithTag(global::OpenTracing.Tag.IntOrStringTag tag, string value) - { - Guard.ThrowIfNull(tag?.Key); + /// + public ISpanBuilder WithTag(string key, double value) + { + this.attributes.Add(new KeyValuePair(key, value)); + return this; + } - if (int.TryParse(value, out var result)) - { - return this.WithTag(tag.Key, result); - } + /// + public ISpanBuilder WithTag(global::OpenTracing.Tag.BooleanTag tag, bool value) + { + Guard.ThrowIfNull(tag?.Key); - return this.WithTag(tag.Key, value); - } + return this.WithTag(tag.Key, value); + } - /// - public ISpanBuilder WithTag(global::OpenTracing.Tag.IntTag tag, int value) - { - Guard.ThrowIfNull(tag?.Key); + /// + public ISpanBuilder WithTag(global::OpenTracing.Tag.IntOrStringTag tag, string value) + { + Guard.ThrowIfNull(tag?.Key); - return this.WithTag(tag.Key, value); + if (int.TryParse(value, out var result)) + { + return this.WithTag(tag.Key, result); } - /// - public ISpanBuilder WithTag(global::OpenTracing.Tag.StringTag tag, string value) - { - Guard.ThrowIfNull(tag?.Key); + return this.WithTag(tag.Key, value); + } - return this.WithTag(tag.Key, value); - } + /// + public ISpanBuilder WithTag(global::OpenTracing.Tag.IntTag tag, int value) + { + Guard.ThrowIfNull(tag?.Key); - /// - /// Gets an implementation of OpenTelemetry TelemetrySpan from the OpenTracing ISpan. - /// - /// The span. - /// an implementation of OpenTelemetry TelemetrySpan. - /// span is not a valid SpanShim object. - private static TelemetrySpan GetOpenTelemetrySpan(ISpan span) - { - var shim = Guard.ThrowIfNotOfType(span); + return this.WithTag(tag.Key, value); + } - return shim.Span; - } + /// + public ISpanBuilder WithTag(global::OpenTracing.Tag.StringTag tag, string value) + { + Guard.ThrowIfNull(tag?.Key); - /// - /// Gets the OpenTelemetry SpanContext. - /// - /// The span context. - /// the OpenTelemetry SpanContext. - /// context is not a valid SpanContextShim object. - private static SpanContext GetOpenTelemetrySpanContext(ISpanContext spanContext) - { - var shim = Guard.ThrowIfNotOfType(spanContext); + return this.WithTag(tag.Key, value); + } - return shim.SpanContext; - } + /// + /// Gets an implementation of OpenTelemetry TelemetrySpan from the OpenTracing ISpan. + /// + /// The span. + /// an implementation of OpenTelemetry TelemetrySpan. + /// span is not a valid SpanShim object. + private static TelemetrySpan GetOpenTelemetrySpan(ISpan span) + { + var shim = Guard.ThrowIfNotOfType(span); + + return shim.Span; + } + + /// + /// Gets the OpenTelemetry SpanContext. + /// + /// The span context. + /// the OpenTelemetry SpanContext. + /// context is not a valid SpanContextShim object. + private static SpanContext GetOpenTelemetrySpanContext(ISpanContext spanContext) + { + var shim = Guard.ThrowIfNotOfType(spanContext); + + return shim.SpanContext; } } diff --git a/src/OpenTelemetry.Shims.OpenTracing/SpanContextShim.cs b/src/OpenTelemetry.Shims.OpenTracing/SpanContextShim.cs index 4b3d979cddd..c222b49c326 100644 --- a/src/OpenTelemetry.Shims.OpenTracing/SpanContextShim.cs +++ b/src/OpenTelemetry.Shims.OpenTracing/SpanContextShim.cs @@ -1,44 +1,25 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTracing; -namespace OpenTelemetry.Shims.OpenTracing +namespace OpenTelemetry.Shims.OpenTracing; + +internal sealed class SpanContextShim : ISpanContext { - internal sealed class SpanContextShim : ISpanContext + public SpanContextShim(in Trace.SpanContext spanContext) { - public SpanContextShim(in Trace.SpanContext spanContext) - { - if (!spanContext.IsValid) - { - throw new ArgumentException($"Invalid '{nameof(Trace.SpanContext)}'", nameof(spanContext)); - } - - this.SpanContext = spanContext; - } + this.SpanContext = spanContext; + } - public Trace.SpanContext SpanContext { get; private set; } + public Trace.SpanContext SpanContext { get; private set; } - /// - public string TraceId => this.SpanContext.TraceId.ToString(); + /// + public string TraceId => this.SpanContext.TraceId.ToString(); - /// - public string SpanId => this.SpanContext.SpanId.ToString(); + /// + public string SpanId => this.SpanContext.SpanId.ToString(); - public IEnumerable> GetBaggageItems() - => Baggage.GetBaggage(); - } + public IEnumerable> GetBaggageItems() + => Baggage.GetBaggage(); } diff --git a/src/OpenTelemetry.Shims.OpenTracing/SpanShim.cs b/src/OpenTelemetry.Shims.OpenTracing/SpanShim.cs index a50d5ff0892..2ab6fee4fe8 100644 --- a/src/OpenTelemetry.Shims.OpenTracing/SpanShim.cs +++ b/src/OpenTelemetry.Shims.OpenTracing/SpanShim.cs @@ -1,295 +1,277 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Internal; using OpenTelemetry.Trace; using OpenTracing; -namespace OpenTelemetry.Shims.OpenTracing +namespace OpenTelemetry.Shims.OpenTracing; + +internal sealed class SpanShim : ISpan { - internal sealed class SpanShim : ISpan + /// + /// The default event name if not specified. + /// + public const string DefaultEventName = "log"; + + private static readonly IReadOnlyCollection OpenTelemetrySupportedAttributeValueTypes = new List { - /// - /// The default event name if not specified. - /// - public const string DefaultEventName = "log"; + typeof(string), + typeof(bool), + typeof(byte), + typeof(short), + typeof(int), + typeof(long), + typeof(float), + typeof(double), + }; + + private readonly SpanContextShim spanContextShim; + + public SpanShim(TelemetrySpan span) + { + Guard.ThrowIfNull(span); - private static readonly IReadOnlyCollection OpenTelemetrySupportedAttributeValueTypes = new List - { - typeof(string), - typeof(bool), - typeof(byte), - typeof(short), - typeof(int), - typeof(long), - typeof(float), - typeof(double), - }; - - private readonly SpanContextShim spanContextShim; - - public SpanShim(TelemetrySpan span) - { - Guard.ThrowIfNull(span); + this.Span = span; + this.spanContextShim = new SpanContextShim(this.Span.Context); + } - if (!span.Context.IsValid) - { - throw new ArgumentException($"Invalid '{nameof(SpanContext)}'", nameof(span.Context)); - } + /// + public ISpanContext Context => this.spanContextShim; - this.Span = span; - this.spanContextShim = new SpanContextShim(this.Span.Context); - } + public TelemetrySpan Span { get; private set; } + + /// + public void Finish() + { + this.Span.End(); + } + + /// + public void Finish(DateTimeOffset finishTimestamp) + { + this.Span.End(finishTimestamp); + } - public ISpanContext Context => this.spanContextShim; + /// + public string GetBaggageItem(string key) + => Baggage.GetBaggage(key); + + /// + public ISpan Log(DateTimeOffset timestamp, IEnumerable> fields) + { + Guard.ThrowIfNull(fields); - public TelemetrySpan Span { get; private set; } + var payload = ConvertToEventPayload(fields); + var eventName = payload.Item1; - /// - public void Finish() + var spanAttributes = new SpanAttributes(); + foreach (var field in payload.Item2) { - this.Span.End(); + switch (field.Value) + { + case long value: + spanAttributes.Add(field.Key, value); + break; + case long[] value: + spanAttributes.Add(field.Key, value); + break; + case bool value: + spanAttributes.Add(field.Key, value); + break; + case bool[] value: + spanAttributes.Add(field.Key, value); + break; + case double value: + spanAttributes.Add(field.Key, value); + break; + case double[] value: + spanAttributes.Add(field.Key, value); + break; + case string value: + spanAttributes.Add(field.Key, value); + break; + case string[] value: + spanAttributes.Add(field.Key, value); + break; + + default: + break; + } } - /// - public void Finish(DateTimeOffset finishTimestamp) + if (timestamp == DateTimeOffset.MinValue) { - this.Span.End(finishTimestamp); + this.Span.AddEvent(eventName, spanAttributes); + } + else + { + this.Span.AddEvent(eventName, timestamp, spanAttributes); } - /// - public string GetBaggageItem(string key) - => Baggage.GetBaggage(key); + return this; + } - /// - public ISpan Log(DateTimeOffset timestamp, IEnumerable> fields) - { - Guard.ThrowIfNull(fields); + /// + public ISpan Log(IEnumerable> fields) + { + return this.Log(DateTimeOffset.MinValue, fields); + } - var payload = ConvertToEventPayload(fields); - var eventName = payload.Item1; + /// + public ISpan Log(string @event) + { + Guard.ThrowIfNull(@event); - var spanAttributes = new SpanAttributes(); - foreach (var field in payload.Item2) - { - switch (field.Value) - { - case long value: - spanAttributes.Add(field.Key, value); - break; - case long[] value: - spanAttributes.Add(field.Key, value); - break; - case bool value: - spanAttributes.Add(field.Key, value); - break; - case bool[] value: - spanAttributes.Add(field.Key, value); - break; - case double value: - spanAttributes.Add(field.Key, value); - break; - case double[] value: - spanAttributes.Add(field.Key, value); - break; - case string value: - spanAttributes.Add(field.Key, value); - break; - case string[] value: - spanAttributes.Add(field.Key, value); - break; - - default: - break; - } - } + this.Span.AddEvent(@event); + return this; + } - if (timestamp == DateTimeOffset.MinValue) - { - this.Span.AddEvent(eventName, spanAttributes); - } - else - { - this.Span.AddEvent(eventName, timestamp, spanAttributes); - } + /// + public ISpan Log(DateTimeOffset timestamp, string @event) + { + Guard.ThrowIfNull(@event); - return this; - } + this.Span.AddEvent(@event, timestamp); + return this; + } - /// - public ISpan Log(IEnumerable> fields) - { - return this.Log(DateTimeOffset.MinValue, fields); - } + /// + public ISpan SetBaggageItem(string key, string value) + { + Baggage.SetBaggage(key, value); + return this; + } - /// - public ISpan Log(string @event) - { - Guard.ThrowIfNull(@event); + /// + public ISpan SetOperationName(string operationName) + { + Guard.ThrowIfNull(operationName); - this.Span.AddEvent(@event); - return this; - } + this.Span.UpdateName(operationName); + return this; + } - /// - public ISpan Log(DateTimeOffset timestamp, string @event) - { - Guard.ThrowIfNull(@event); + /// + public ISpan SetTag(string key, string value) + { + Guard.ThrowIfNull(key); - this.Span.AddEvent(@event, timestamp); - return this; - } + this.Span.SetAttribute(key, value); + return this; + } - /// - public ISpan SetBaggageItem(string key, string value) - { - Baggage.SetBaggage(key, value); - return this; - } + /// + public ISpan SetTag(string key, bool value) + { + Guard.ThrowIfNull(key); - /// - public ISpan SetOperationName(string operationName) + // Special case the OpenTracing Error Tag + // see https://opentracing.io/specification/conventions/ + if (global::OpenTracing.Tag.Tags.Error.Key.Equals(key, StringComparison.Ordinal)) { - Guard.ThrowIfNull(operationName); - - this.Span.UpdateName(operationName); - return this; + this.Span.SetStatus(value ? Status.Error : Status.Ok); } - - /// - public ISpan SetTag(string key, string value) + else { - Guard.ThrowIfNull(key); - this.Span.SetAttribute(key, value); - return this; } - /// - public ISpan SetTag(string key, bool value) - { - Guard.ThrowIfNull(key); - - // Special case the OpenTracing Error Tag - // see https://opentracing.io/specification/conventions/ - if (global::OpenTracing.Tag.Tags.Error.Key.Equals(key, StringComparison.Ordinal)) - { - this.Span.SetStatus(value ? Status.Error : Status.Ok); - } - else - { - this.Span.SetAttribute(key, value); - } + return this; + } - return this; - } + /// + public ISpan SetTag(string key, int value) + { + Guard.ThrowIfNull(key); - /// - public ISpan SetTag(string key, int value) - { - Guard.ThrowIfNull(key); + this.Span.SetAttribute(key, value); + return this; + } - this.Span.SetAttribute(key, value); - return this; - } + /// + public ISpan SetTag(string key, double value) + { + Guard.ThrowIfNull(key); - /// - public ISpan SetTag(string key, double value) - { - Guard.ThrowIfNull(key); + this.Span.SetAttribute(key, value); + return this; + } - this.Span.SetAttribute(key, value); - return this; - } + /// + public ISpan SetTag(global::OpenTracing.Tag.BooleanTag tag, bool value) + { + return this.SetTag(tag?.Key, value); + } - /// - public ISpan SetTag(global::OpenTracing.Tag.BooleanTag tag, bool value) + /// + public ISpan SetTag(global::OpenTracing.Tag.IntOrStringTag tag, string value) + { + if (int.TryParse(value, out var result)) { - return this.SetTag(tag?.Key, value); + return this.SetTag(tag?.Key, result); } - /// - public ISpan SetTag(global::OpenTracing.Tag.IntOrStringTag tag, string value) - { - if (int.TryParse(value, out var result)) - { - return this.SetTag(tag?.Key, result); - } + return this.SetTag(tag?.Key, value); + } - return this.SetTag(tag?.Key, value); - } + /// + public ISpan SetTag(global::OpenTracing.Tag.IntTag tag, int value) + { + return this.SetTag(tag?.Key, value); + } - /// - public ISpan SetTag(global::OpenTracing.Tag.IntTag tag, int value) - { - return this.SetTag(tag?.Key, value); - } + /// + public ISpan SetTag(global::OpenTracing.Tag.StringTag tag, string value) + { + return this.SetTag(tag?.Key, value); + } - /// - public ISpan SetTag(global::OpenTracing.Tag.StringTag tag, string value) - { - return this.SetTag(tag?.Key, value); - } + /// + /// Constructs an OpenTelemetry event payload from an OpenTracing Log key/value map. + /// + /// The fields. + /// A 2-Tuple containing the event name and payload information. + private static Tuple> ConvertToEventPayload(IEnumerable> fields) + { + string eventName = null; + var attributes = new Dictionary(); - /// - /// Constructs an OpenTelemetry event payload from an OpenTracing Log key/value map. - /// - /// The fields. - /// A 2-Tuple containing the event name and payload information. - private static Tuple> ConvertToEventPayload(IEnumerable> fields) + foreach (var field in fields) { - string eventName = null; - var attributes = new Dictionary(); + // TODO verify null values are NOT allowed. + if (field.Value == null) + { + continue; + } + + // Duplicate keys must be ignored even though they appear to be allowed in OpenTracing. + if (attributes.ContainsKey(field.Key)) + { + continue; + } - foreach (var field in fields) + if (eventName == null && field.Key.Equals(LogFields.Event, StringComparison.Ordinal) && field.Value is string value) { - // TODO verify null values are NOT allowed. - if (field.Value == null) - { - continue; - } - - // Duplicate keys must be ignored even though they appear to be allowed in OpenTracing. - if (attributes.ContainsKey(field.Key)) - { - continue; - } - - if (eventName == null && field.Key.Equals(LogFields.Event, StringComparison.Ordinal) && field.Value is string value) - { - // This is meant to be the event name - eventName = value; - - // We don't want to add the event name as a separate attribute - continue; - } - - // Supported types are added directly, all other types are converted to strings. - if (OpenTelemetrySupportedAttributeValueTypes.Contains(field.Value.GetType())) - { - attributes.Add(field.Key, field.Value); - } - else - { - // TODO should we completely ignore unsupported types? - attributes.Add(field.Key, field.Value.ToString()); - } + // This is meant to be the event name + eventName = value; + + // We don't want to add the event name as a separate attribute + continue; } - return new Tuple>(eventName ?? DefaultEventName, attributes); + // Supported types are added directly, all other types are converted to strings. + if (OpenTelemetrySupportedAttributeValueTypes.Contains(field.Value.GetType())) + { + attributes.Add(field.Key, field.Value); + } + else + { + // TODO should we completely ignore unsupported types? + attributes.Add(field.Key, field.Value.ToString()); + } } + + return new Tuple>(eventName ?? DefaultEventName, attributes); } } diff --git a/src/OpenTelemetry.Shims.OpenTracing/TracerShim.cs b/src/OpenTelemetry.Shims.OpenTracing/TracerShim.cs index f2a3c476cc4..1c16ac476fd 100644 --- a/src/OpenTelemetry.Shims.OpenTracing/TracerShim.cs +++ b/src/OpenTelemetry.Shims.OpenTracing/TracerShim.cs @@ -1,108 +1,109 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Context.Propagation; using OpenTelemetry.Internal; using OpenTracing.Propagation; -namespace OpenTelemetry.Shims.OpenTracing +namespace OpenTelemetry.Shims.OpenTracing; + +public class TracerShim : global::OpenTracing.ITracer { - public class TracerShim : global::OpenTracing.ITracer + private readonly Trace.Tracer tracer; + private readonly TextMapPropagator definedPropagator; + + public TracerShim(Trace.TracerProvider tracerProvider) + : this(tracerProvider, null) { - private readonly Trace.Tracer tracer; - private readonly TextMapPropagator propagator; + } - public TracerShim(Trace.Tracer tracer, TextMapPropagator textFormat) - { - Guard.ThrowIfNull(tracer); - Guard.ThrowIfNull(textFormat); + public TracerShim(Trace.TracerProvider tracerProvider, TextMapPropagator textFormat) + { + Guard.ThrowIfNull(tracerProvider); - this.tracer = tracer; - this.propagator = textFormat; - this.ScopeManager = new ScopeManagerShim(this.tracer); - } + var assemblyName = typeof(TracerShim).Assembly.GetName(); + var version = assemblyName.Version; + + this.tracer = tracerProvider.GetTracer("opentracing-shim", version?.ToString()); + this.definedPropagator = textFormat; + this.ScopeManager = new ScopeManagerShim(); + } - /// - public global::OpenTracing.IScopeManager ScopeManager { get; private set; } + /// + public global::OpenTracing.IScopeManager ScopeManager { get; } - /// - public global::OpenTracing.ISpan ActiveSpan => this.ScopeManager.Active?.Span; + /// + public global::OpenTracing.ISpan ActiveSpan => this.ScopeManager.Active?.Span; - /// - public global::OpenTracing.ISpanBuilder BuildSpan(string operationName) + private TextMapPropagator Propagator + { + get { - return new SpanBuilderShim(this.tracer, operationName); + return this.definedPropagator ?? Propagators.DefaultTextMapPropagator; } + } - /// - public global::OpenTracing.ISpanContext Extract(IFormat format, TCarrier carrier) - { - Guard.ThrowIfNull(format); - Guard.ThrowIfNull(carrier); + /// + public global::OpenTracing.ISpanBuilder BuildSpan(string operationName) + { + return new SpanBuilderShim(this.tracer, operationName); + } + + /// + public global::OpenTracing.ISpanContext Extract(IFormat format, TCarrier carrier) + { + Guard.ThrowIfNull(format); + Guard.ThrowIfNull(carrier); - PropagationContext propagationContext = default; + PropagationContext propagationContext = default; - if ((format == BuiltinFormats.TextMap || format == BuiltinFormats.HttpHeaders) && carrier is ITextMap textMapCarrier) + if ((format == BuiltinFormats.TextMap || format == BuiltinFormats.HttpHeaders) && carrier is ITextMap textMapCarrier) + { + var carrierMap = new Dictionary>(); + + foreach (var entry in textMapCarrier) { - var carrierMap = new Dictionary>(); + carrierMap.Add(entry.Key, new[] { entry.Value }); + } - foreach (var entry in textMapCarrier) + static IEnumerable GetCarrierKeyValue(Dictionary> source, string key) + { + if (key == null || !source.TryGetValue(key, out var value)) { - carrierMap.Add(entry.Key, new[] { entry.Value }); + return null; } - static IEnumerable GetCarrierKeyValue(Dictionary> source, string key) - { - if (key == null || !source.TryGetValue(key, out var value)) - { - return null; - } + return value; + } - return value; - } + propagationContext = this.Propagator.Extract(propagationContext, carrierMap, GetCarrierKeyValue); + } - propagationContext = this.propagator.Extract(propagationContext, carrierMap, GetCarrierKeyValue); - } + // TODO: + // Not sure what to do here. Really, Baggage should be returned and not set until this ISpanContext is turned into a live Span. + // But that code doesn't seem to exist. + // Baggage.Current = propagationContext.Baggage; - // TODO: - // Not sure what to do here. Really, Baggage should be returned and not set until this ISpanContext is turned into a live Span. - // But that code doesn't seem to exist. - // Baggage.Current = propagationContext.Baggage; + return !propagationContext.ActivityContext.IsValid() ? null : new SpanContextShim(new Trace.SpanContext(propagationContext.ActivityContext)); + } - return !propagationContext.ActivityContext.IsValid() ? null : new SpanContextShim(new Trace.SpanContext(propagationContext.ActivityContext)); - } + /// + public void Inject( + global::OpenTracing.ISpanContext spanContext, + IFormat format, + TCarrier carrier) + { + Guard.ThrowIfNull(spanContext); + var shim = Guard.ThrowIfNotOfType(spanContext); + Guard.ThrowIfNull(format); + Guard.ThrowIfNull(carrier); - /// - public void Inject( - global::OpenTracing.ISpanContext spanContext, - IFormat format, - TCarrier carrier) + if ((format == BuiltinFormats.TextMap || format == BuiltinFormats.HttpHeaders) && carrier is ITextMap textMapCarrier) { - Guard.ThrowIfNull(spanContext); - var shim = Guard.ThrowIfNotOfType(spanContext); - Guard.ThrowIfNull(format); - Guard.ThrowIfNull(carrier); - - if ((format == BuiltinFormats.TextMap || format == BuiltinFormats.HttpHeaders) && carrier is ITextMap textMapCarrier) - { - this.propagator.Inject( - new PropagationContext(shim.SpanContext, Baggage.Current), - textMapCarrier, - (instrumentation, key, value) => instrumentation.Set(key, value)); - } + this.Propagator.Inject( + new PropagationContext(shim.SpanContext, Baggage.Current), + textMapCarrier, + (instrumentation, key, value) => instrumentation.Set(key, value)); } } } diff --git a/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..8c1a746f041 --- /dev/null +++ b/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt @@ -0,0 +1,57 @@ +OpenTelemetry.Logs.LoggerProviderBuilderExtensions +OpenTelemetry.Logs.LoggerProviderExtensions +OpenTelemetry.Logs.LogRecord.Logger.get -> OpenTelemetry.Logs.Logger! +OpenTelemetry.Logs.LogRecord.Severity.get -> OpenTelemetry.Logs.LogRecordSeverity? +OpenTelemetry.Logs.LogRecord.Severity.set -> void +OpenTelemetry.Logs.LogRecord.SeverityText.get -> string? +OpenTelemetry.Logs.LogRecord.SeverityText.set -> void +OpenTelemetry.Metrics.Exemplar +OpenTelemetry.Metrics.Exemplar.DoubleValue.get -> double +OpenTelemetry.Metrics.Exemplar.Exemplar() -> void +OpenTelemetry.Metrics.Exemplar.FilteredTags.get -> OpenTelemetry.ReadOnlyFilteredTagCollection +OpenTelemetry.Metrics.Exemplar.LongValue.get -> long +OpenTelemetry.Metrics.Exemplar.SpanId.get -> System.Diagnostics.ActivitySpanId +OpenTelemetry.Metrics.Exemplar.Timestamp.get -> System.DateTimeOffset +OpenTelemetry.Metrics.Exemplar.TraceId.get -> System.Diagnostics.ActivityTraceId +OpenTelemetry.Metrics.ExemplarFilterType +OpenTelemetry.Metrics.ExemplarFilterType.AlwaysOff = 0 -> OpenTelemetry.Metrics.ExemplarFilterType +OpenTelemetry.Metrics.ExemplarFilterType.AlwaysOn = 1 -> OpenTelemetry.Metrics.ExemplarFilterType +OpenTelemetry.Metrics.ExemplarFilterType.TraceBased = 2 -> OpenTelemetry.Metrics.ExemplarFilterType +OpenTelemetry.Metrics.ExemplarMeasurement +OpenTelemetry.Metrics.ExemplarMeasurement.ExemplarMeasurement() -> void +OpenTelemetry.Metrics.ExemplarMeasurement.Tags.get -> System.ReadOnlySpan> +OpenTelemetry.Metrics.ExemplarMeasurement.Value.get -> T +OpenTelemetry.Metrics.MetricPoint.TryGetExemplars(out OpenTelemetry.Metrics.ReadOnlyExemplarCollection exemplars) -> bool +OpenTelemetry.Metrics.MetricStreamConfiguration.CardinalityLimit.get -> int? +OpenTelemetry.Metrics.MetricStreamConfiguration.CardinalityLimit.set -> void +OpenTelemetry.Metrics.ReadOnlyExemplarCollection +OpenTelemetry.Metrics.ReadOnlyExemplarCollection.Enumerator +OpenTelemetry.Metrics.ReadOnlyExemplarCollection.Enumerator.Current.get -> OpenTelemetry.Metrics.Exemplar +OpenTelemetry.Metrics.ReadOnlyExemplarCollection.Enumerator.Enumerator() -> void +OpenTelemetry.Metrics.ReadOnlyExemplarCollection.Enumerator.MoveNext() -> bool +OpenTelemetry.Metrics.ReadOnlyExemplarCollection.GetEnumerator() -> OpenTelemetry.Metrics.ReadOnlyExemplarCollection.Enumerator +OpenTelemetry.Metrics.ReadOnlyExemplarCollection.ReadOnlyExemplarCollection() -> void +OpenTelemetry.ReadOnlyFilteredTagCollection +OpenTelemetry.ReadOnlyFilteredTagCollection.Enumerator +OpenTelemetry.ReadOnlyFilteredTagCollection.Enumerator.Current.get -> System.Collections.Generic.KeyValuePair +OpenTelemetry.ReadOnlyFilteredTagCollection.Enumerator.Enumerator() -> void +OpenTelemetry.ReadOnlyFilteredTagCollection.Enumerator.MoveNext() -> bool +OpenTelemetry.ReadOnlyFilteredTagCollection.GetEnumerator() -> OpenTelemetry.ReadOnlyFilteredTagCollection.Enumerator +OpenTelemetry.ReadOnlyFilteredTagCollection.ReadOnlyFilteredTagCollection() -> void +static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Logs.LoggerProviderBuilder! +static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, System.Func!>! implementationFactory) -> OpenTelemetry.Logs.LoggerProviderBuilder! +static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder) -> OpenTelemetry.Logs.LoggerProviderBuilder! +static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.Build(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder) -> OpenTelemetry.Logs.LoggerProvider! +static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.ConfigureResource(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, System.Action! configure) -> OpenTelemetry.Logs.LoggerProviderBuilder! +static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.SetResourceBuilder(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, OpenTelemetry.Resources.ResourceBuilder! resourceBuilder) -> OpenTelemetry.Logs.LoggerProviderBuilder! +static OpenTelemetry.Logs.LoggerProviderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProvider! provider, OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Logs.LoggerProvider! +static OpenTelemetry.Logs.LoggerProviderExtensions.ForceFlush(this OpenTelemetry.Logs.LoggerProvider! provider, int timeoutMilliseconds = -1) -> bool +static OpenTelemetry.Logs.LoggerProviderExtensions.Shutdown(this OpenTelemetry.Logs.LoggerProvider! provider, int timeoutMilliseconds = -1) -> bool +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetExemplarFilter(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, OpenTelemetry.Metrics.ExemplarFilterType exemplarFilter = OpenTelemetry.Metrics.ExemplarFilterType.TraceBased) -> OpenTelemetry.Metrics.MeterProviderBuilder! +static OpenTelemetry.OpenTelemetryBuilderSdkExtensions.WithLogging(this OpenTelemetry.IOpenTelemetryBuilder! builder) -> OpenTelemetry.IOpenTelemetryBuilder! +static OpenTelemetry.OpenTelemetryBuilderSdkExtensions.WithLogging(this OpenTelemetry.IOpenTelemetryBuilder! builder, System.Action! configure) -> OpenTelemetry.IOpenTelemetryBuilder! +static OpenTelemetry.OpenTelemetryBuilderSdkExtensions.WithLogging(this OpenTelemetry.IOpenTelemetryBuilder! builder, System.Action? configureBuilder, System.Action? configureOptions) -> OpenTelemetry.IOpenTelemetryBuilder! +static OpenTelemetry.Sdk.CreateLoggerProviderBuilder() -> OpenTelemetry.Logs.LoggerProviderBuilder! +static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.UseOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder) -> Microsoft.Extensions.Logging.ILoggingBuilder! +static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.UseOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, System.Action! configure) -> Microsoft.Extensions.Logging.ILoggingBuilder! +static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.UseOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, System.Action? configureBuilder, System.Action? configureOptions) -> Microsoft.Extensions.Logging.ILoggingBuilder! diff --git a/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Shipped.txt b/src/OpenTelemetry/.publicApi/Stable/PublicAPI.Shipped.txt similarity index 91% rename from src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Shipped.txt rename to src/OpenTelemetry/.publicApi/Stable/PublicAPI.Shipped.txt index 493d28769f2..ae4fe170436 100644 --- a/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Shipped.txt +++ b/src/OpenTelemetry/.publicApi/Stable/PublicAPI.Shipped.txt @@ -1,28 +1,5 @@ #nullable enable ~OpenTelemetry.Batch.GetEnumerator() -> OpenTelemetry.Batch.Enumerator -~OpenTelemetry.Metrics.BaseExportingMetricReader.BaseExportingMetricReader(OpenTelemetry.BaseExporter exporter) -> void -~OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration.Boundaries.get -> double[] -~OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration.Boundaries.set -> void -~OpenTelemetry.Metrics.IPullMetricExporter.Collect.get -> System.Func -~OpenTelemetry.Metrics.IPullMetricExporter.Collect.set -> void -~OpenTelemetry.Metrics.Metric.Description.get -> string -~OpenTelemetry.Metrics.Metric.MeterName.get -> string -~OpenTelemetry.Metrics.Metric.MeterVersion.get -> string -~OpenTelemetry.Metrics.Metric.Name.get -> string -~OpenTelemetry.Metrics.Metric.Unit.get -> string -~OpenTelemetry.Metrics.MetricStreamConfiguration.Description.get -> string -~OpenTelemetry.Metrics.MetricStreamConfiguration.Description.set -> void -~OpenTelemetry.Metrics.MetricStreamConfiguration.Name.get -> string -~OpenTelemetry.Metrics.MetricStreamConfiguration.Name.set -> void -~OpenTelemetry.Metrics.MetricStreamConfiguration.TagKeys.get -> string[] -~OpenTelemetry.Metrics.MetricStreamConfiguration.TagKeys.set -> void -~OpenTelemetry.Metrics.PeriodicExportingMetricReader.PeriodicExportingMetricReader(OpenTelemetry.BaseExporter exporter, int exportIntervalMilliseconds = 60000, int exportTimeoutMilliseconds = 30000) -> void -~override OpenTelemetry.Metrics.MeterProviderBuilderBase.AddInstrumentation(System.Func! instrumentationFactory) -> OpenTelemetry.Metrics.MeterProviderBuilder! -~override OpenTelemetry.Trace.TracerProviderBuilderBase.AddInstrumentation(System.Func! instrumentationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder! -~readonly OpenTelemetry.Metrics.BaseExportingMetricReader.exporter -> OpenTelemetry.BaseExporter -~static OpenTelemetry.Metrics.MeterProviderExtensions.ForceFlush(this OpenTelemetry.Metrics.MeterProvider provider, int timeoutMilliseconds = -1) -> bool -~static OpenTelemetry.Metrics.MeterProviderExtensions.Shutdown(this OpenTelemetry.Metrics.MeterProvider provider, int timeoutMilliseconds = -1) -> bool -~static OpenTelemetry.Metrics.MetricStreamConfiguration.Drop.get -> OpenTelemetry.Metrics.MetricStreamConfiguration abstract OpenTelemetry.BaseExporter.Export(in OpenTelemetry.Batch batch) -> OpenTelemetry.ExportResult abstract OpenTelemetry.BaseExportProcessor.OnExport(T! data) -> void abstract OpenTelemetry.Trace.Sampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult @@ -77,6 +54,8 @@ OpenTelemetry.ExportProcessorType.Simple = 0 -> OpenTelemetry.ExportProcessorTyp OpenTelemetry.ExportResult OpenTelemetry.ExportResult.Failure = 1 -> OpenTelemetry.ExportResult OpenTelemetry.ExportResult.Success = 0 -> OpenTelemetry.ExportResult +OpenTelemetry.Logs.BatchExportLogRecordProcessorOptions +OpenTelemetry.Logs.BatchExportLogRecordProcessorOptions.BatchExportLogRecordProcessorOptions() -> void OpenTelemetry.Logs.LogRecord OpenTelemetry.Logs.LogRecord.Attributes.get -> System.Collections.Generic.IReadOnlyList>? OpenTelemetry.Logs.LogRecord.Attributes.set -> void @@ -107,6 +86,12 @@ OpenTelemetry.Logs.LogRecord.TraceId.get -> System.Diagnostics.ActivityTraceId OpenTelemetry.Logs.LogRecord.TraceId.set -> void OpenTelemetry.Logs.LogRecord.TraceState.get -> string? OpenTelemetry.Logs.LogRecord.TraceState.set -> void +OpenTelemetry.Logs.LogRecordExportProcessorOptions +OpenTelemetry.Logs.LogRecordExportProcessorOptions.BatchExportProcessorOptions.get -> OpenTelemetry.Logs.BatchExportLogRecordProcessorOptions! +OpenTelemetry.Logs.LogRecordExportProcessorOptions.BatchExportProcessorOptions.set -> void +OpenTelemetry.Logs.LogRecordExportProcessorOptions.ExportProcessorType.get -> OpenTelemetry.ExportProcessorType +OpenTelemetry.Logs.LogRecordExportProcessorOptions.ExportProcessorType.set -> void +OpenTelemetry.Logs.LogRecordExportProcessorOptions.LogRecordExportProcessorOptions() -> void OpenTelemetry.Logs.LogRecordScope OpenTelemetry.Logs.LogRecordScope.Enumerator OpenTelemetry.Logs.LogRecordScope.Enumerator.Current.get -> System.Collections.Generic.KeyValuePair @@ -120,6 +105,7 @@ OpenTelemetry.Logs.LogRecordScope.LogRecordScope() -> void OpenTelemetry.Logs.LogRecordScope.Scope.get -> object? OpenTelemetry.Logs.OpenTelemetryLoggerOptions OpenTelemetry.Logs.OpenTelemetryLoggerOptions.AddProcessor(OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! +OpenTelemetry.Logs.OpenTelemetryLoggerOptions.AddProcessor(System.Func!>! implementationFactory) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! OpenTelemetry.Logs.OpenTelemetryLoggerOptions.IncludeFormattedMessage.get -> bool OpenTelemetry.Logs.OpenTelemetryLoggerOptions.IncludeFormattedMessage.set -> void OpenTelemetry.Logs.OpenTelemetryLoggerOptions.IncludeScopes.get -> bool @@ -141,8 +127,11 @@ OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxScale.set OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxSize.get -> int OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxSize.set -> void OpenTelemetry.Metrics.BaseExportingMetricReader +OpenTelemetry.Metrics.BaseExportingMetricReader.BaseExportingMetricReader(OpenTelemetry.BaseExporter! exporter) -> void OpenTelemetry.Metrics.BaseExportingMetricReader.SupportedExportModes.get -> OpenTelemetry.Metrics.ExportModes OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration +OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration.Boundaries.get -> double[]? +OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration.Boundaries.set -> void OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration.ExplicitBucketHistogramConfiguration() -> void OpenTelemetry.Metrics.ExponentialHistogramBuckets OpenTelemetry.Metrics.ExponentialHistogramBuckets.Enumerator @@ -176,15 +165,23 @@ OpenTelemetry.Metrics.HistogramConfiguration.HistogramConfiguration() -> void OpenTelemetry.Metrics.HistogramConfiguration.RecordMinMax.get -> bool OpenTelemetry.Metrics.HistogramConfiguration.RecordMinMax.set -> void OpenTelemetry.Metrics.IPullMetricExporter +OpenTelemetry.Metrics.IPullMetricExporter.Collect.get -> System.Func? +OpenTelemetry.Metrics.IPullMetricExporter.Collect.set -> void OpenTelemetry.Metrics.MeterProviderBuilderBase OpenTelemetry.Metrics.MeterProviderBuilderBase.Build() -> OpenTelemetry.Metrics.MeterProvider! OpenTelemetry.Metrics.MeterProviderBuilderBase.MeterProviderBuilderBase() -> void OpenTelemetry.Metrics.MeterProviderBuilderExtensions OpenTelemetry.Metrics.MeterProviderExtensions OpenTelemetry.Metrics.Metric +OpenTelemetry.Metrics.Metric.Description.get -> string! OpenTelemetry.Metrics.Metric.GetMetricPoints() -> OpenTelemetry.Metrics.MetricPointsAccessor +OpenTelemetry.Metrics.Metric.MeterName.get -> string! +OpenTelemetry.Metrics.Metric.MeterTags.get -> System.Collections.Generic.IEnumerable>? +OpenTelemetry.Metrics.Metric.MeterVersion.get -> string! OpenTelemetry.Metrics.Metric.MetricType.get -> OpenTelemetry.Metrics.MetricType +OpenTelemetry.Metrics.Metric.Name.get -> string! OpenTelemetry.Metrics.Metric.Temporality.get -> OpenTelemetry.Metrics.AggregationTemporality +OpenTelemetry.Metrics.Metric.Unit.get -> string! OpenTelemetry.Metrics.MetricPoint OpenTelemetry.Metrics.MetricPoint.EndTime.get -> System.DateTimeOffset OpenTelemetry.Metrics.MetricPoint.GetExponentialHistogramData() -> OpenTelemetry.Metrics.ExponentialHistogramData! @@ -223,7 +220,13 @@ OpenTelemetry.Metrics.MetricReaderTemporalityPreference OpenTelemetry.Metrics.MetricReaderTemporalityPreference.Cumulative = 1 -> OpenTelemetry.Metrics.MetricReaderTemporalityPreference OpenTelemetry.Metrics.MetricReaderTemporalityPreference.Delta = 2 -> OpenTelemetry.Metrics.MetricReaderTemporalityPreference OpenTelemetry.Metrics.MetricStreamConfiguration +OpenTelemetry.Metrics.MetricStreamConfiguration.Description.get -> string? +OpenTelemetry.Metrics.MetricStreamConfiguration.Description.set -> void OpenTelemetry.Metrics.MetricStreamConfiguration.MetricStreamConfiguration() -> void +OpenTelemetry.Metrics.MetricStreamConfiguration.Name.get -> string? +OpenTelemetry.Metrics.MetricStreamConfiguration.Name.set -> void +OpenTelemetry.Metrics.MetricStreamConfiguration.TagKeys.get -> string![]? +OpenTelemetry.Metrics.MetricStreamConfiguration.TagKeys.set -> void OpenTelemetry.Metrics.MetricType OpenTelemetry.Metrics.MetricType.DoubleGauge = 45 -> OpenTelemetry.Metrics.MetricType OpenTelemetry.Metrics.MetricType.DoubleSum = 29 -> OpenTelemetry.Metrics.MetricType @@ -235,6 +238,7 @@ OpenTelemetry.Metrics.MetricType.LongSum = 26 -> OpenTelemetry.Metrics.MetricTyp OpenTelemetry.Metrics.MetricType.LongSumNonMonotonic = 138 -> OpenTelemetry.Metrics.MetricType OpenTelemetry.Metrics.MetricTypeExtensions OpenTelemetry.Metrics.PeriodicExportingMetricReader +OpenTelemetry.Metrics.PeriodicExportingMetricReader.PeriodicExportingMetricReader(OpenTelemetry.BaseExporter! exporter, int exportIntervalMilliseconds = 60000, int exportTimeoutMilliseconds = 30000) -> void OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds.get -> int? OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds.set -> void @@ -245,7 +249,7 @@ OpenTelemetry.ProviderExtensions OpenTelemetry.ReadOnlyTagCollection OpenTelemetry.ReadOnlyTagCollection.Count.get -> int OpenTelemetry.ReadOnlyTagCollection.Enumerator -OpenTelemetry.ReadOnlyTagCollection.Enumerator.Current.get -> System.Collections.Generic.KeyValuePair +OpenTelemetry.ReadOnlyTagCollection.Enumerator.Current.get -> System.Collections.Generic.KeyValuePair OpenTelemetry.ReadOnlyTagCollection.Enumerator.Enumerator() -> void OpenTelemetry.ReadOnlyTagCollection.Enumerator.MoveNext() -> bool OpenTelemetry.ReadOnlyTagCollection.GetEnumerator() -> OpenTelemetry.ReadOnlyTagCollection.Enumerator @@ -311,7 +315,7 @@ OpenTelemetry.Trace.SamplingResult.TraceStateString.get -> string? OpenTelemetry.Trace.TraceIdRatioBasedSampler OpenTelemetry.Trace.TraceIdRatioBasedSampler.TraceIdRatioBasedSampler(double probability) -> void OpenTelemetry.Trace.TracerProviderBuilderBase -OpenTelemetry.Trace.TracerProviderBuilderBase.AddInstrumentation(string! instrumentationName, string! instrumentationVersion, System.Func! instrumentationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder! +OpenTelemetry.Trace.TracerProviderBuilderBase.AddInstrumentation(string! instrumentationName, string! instrumentationVersion, System.Func! instrumentationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder! OpenTelemetry.Trace.TracerProviderBuilderBase.Build() -> OpenTelemetry.Trace.TracerProvider! OpenTelemetry.Trace.TracerProviderBuilderBase.TracerProviderBuilderBase() -> void OpenTelemetry.Trace.TracerProviderBuilderExtensions @@ -337,6 +341,7 @@ override OpenTelemetry.Logs.OpenTelemetryLoggerProvider.Dispose(bool disposing) override OpenTelemetry.Metrics.BaseExportingMetricReader.Dispose(bool disposing) -> void override OpenTelemetry.Metrics.BaseExportingMetricReader.OnCollect(int timeoutMilliseconds) -> bool override OpenTelemetry.Metrics.BaseExportingMetricReader.OnShutdown(int timeoutMilliseconds) -> bool +override OpenTelemetry.Metrics.MeterProviderBuilderBase.AddInstrumentation(System.Func! instrumentationFactory) -> OpenTelemetry.Metrics.MeterProviderBuilder! override OpenTelemetry.Metrics.MeterProviderBuilderBase.AddMeter(params string![]! names) -> OpenTelemetry.Metrics.MeterProviderBuilder! override OpenTelemetry.Metrics.PeriodicExportingMetricReader.Dispose(bool disposing) -> void override OpenTelemetry.Metrics.PeriodicExportingMetricReader.OnShutdown(int timeoutMilliseconds) -> bool @@ -348,10 +353,12 @@ override OpenTelemetry.Trace.ParentBasedSampler.ShouldSample(in OpenTelemetry.Tr override OpenTelemetry.Trace.SamplingResult.Equals(object? obj) -> bool override OpenTelemetry.Trace.SamplingResult.GetHashCode() -> int override OpenTelemetry.Trace.TraceIdRatioBasedSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult +override OpenTelemetry.Trace.TracerProviderBuilderBase.AddInstrumentation(System.Func! instrumentationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder! override OpenTelemetry.Trace.TracerProviderBuilderBase.AddLegacySource(string! operationName) -> OpenTelemetry.Trace.TracerProviderBuilder! override OpenTelemetry.Trace.TracerProviderBuilderBase.AddSource(params string![]! names) -> OpenTelemetry.Trace.TracerProviderBuilder! override sealed OpenTelemetry.BaseExportProcessor.OnStart(T! data) -> void readonly OpenTelemetry.BaseExportProcessor.exporter -> OpenTelemetry.BaseExporter! +readonly OpenTelemetry.Metrics.BaseExportingMetricReader.exporter -> OpenTelemetry.BaseExporter! static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.AddOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder) -> Microsoft.Extensions.Logging.ILoggingBuilder! static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.AddOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, System.Action? configure) -> Microsoft.Extensions.Logging.ILoggingBuilder! static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddReader(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, OpenTelemetry.Metrics.MetricReader! reader) -> OpenTelemetry.Metrics.MeterProviderBuilder! @@ -360,11 +367,14 @@ static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddReader(this Op static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddView(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, string! instrumentName, OpenTelemetry.Metrics.MetricStreamConfiguration! metricStreamConfiguration) -> OpenTelemetry.Metrics.MeterProviderBuilder! static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddView(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, string! instrumentName, string! name) -> OpenTelemetry.Metrics.MeterProviderBuilder! static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddView(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, System.Func! viewConfig) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.Build(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder) -> OpenTelemetry.Metrics.MeterProvider? +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.Build(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder) -> OpenTelemetry.Metrics.MeterProvider! static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.ConfigureResource(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, System.Action! configure) -> OpenTelemetry.Metrics.MeterProviderBuilder! static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetMaxMetricPointsPerMetricStream(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, int maxMetricPointsPerMetricStream) -> OpenTelemetry.Metrics.MeterProviderBuilder! static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetMaxMetricStreams(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, int maxMetricStreams) -> OpenTelemetry.Metrics.MeterProviderBuilder! static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetResourceBuilder(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, OpenTelemetry.Resources.ResourceBuilder! resourceBuilder) -> OpenTelemetry.Metrics.MeterProviderBuilder! +static OpenTelemetry.Metrics.MeterProviderExtensions.ForceFlush(this OpenTelemetry.Metrics.MeterProvider! provider, int timeoutMilliseconds = -1) -> bool +static OpenTelemetry.Metrics.MeterProviderExtensions.Shutdown(this OpenTelemetry.Metrics.MeterProvider! provider, int timeoutMilliseconds = -1) -> bool +static OpenTelemetry.Metrics.MetricStreamConfiguration.Drop.get -> OpenTelemetry.Metrics.MetricStreamConfiguration! static OpenTelemetry.Metrics.MetricTypeExtensions.IsDouble(this OpenTelemetry.Metrics.MetricType self) -> bool static OpenTelemetry.Metrics.MetricTypeExtensions.IsGauge(this OpenTelemetry.Metrics.MetricType self) -> bool static OpenTelemetry.Metrics.MetricTypeExtensions.IsHistogram(this OpenTelemetry.Metrics.MetricType self) -> bool @@ -390,7 +400,7 @@ static OpenTelemetry.Trace.SamplingResult.operator ==(OpenTelemetry.Trace.Sampli static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Trace.TracerProviderBuilder! static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, System.Func!>! implementationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder! static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.Build(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder) -> OpenTelemetry.Trace.TracerProvider? +static OpenTelemetry.Trace.TracerProviderBuilderExtensions.Build(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder) -> OpenTelemetry.Trace.TracerProvider! static OpenTelemetry.Trace.TracerProviderBuilderExtensions.ConfigureResource(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, System.Action! configure) -> OpenTelemetry.Trace.TracerProviderBuilder! static OpenTelemetry.Trace.TracerProviderBuilderExtensions.SetErrorStatusOnException(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, bool enabled = true) -> OpenTelemetry.Trace.TracerProviderBuilder! static OpenTelemetry.Trace.TracerProviderBuilderExtensions.SetResourceBuilder(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, OpenTelemetry.Resources.ResourceBuilder! resourceBuilder) -> OpenTelemetry.Trace.TracerProviderBuilder! diff --git a/src/OpenTelemetry/.publicApi/Stable/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/Stable/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..d58a0903257 --- /dev/null +++ b/src/OpenTelemetry/.publicApi/Stable/PublicAPI.Unshipped.txt @@ -0,0 +1,6 @@ +OpenTelemetry.OpenTelemetryBuilderSdkExtensions +static OpenTelemetry.OpenTelemetryBuilderSdkExtensions.ConfigureResource(this OpenTelemetry.IOpenTelemetryBuilder! builder, System.Action! configure) -> OpenTelemetry.IOpenTelemetryBuilder! +static OpenTelemetry.OpenTelemetryBuilderSdkExtensions.WithMetrics(this OpenTelemetry.IOpenTelemetryBuilder! builder) -> OpenTelemetry.IOpenTelemetryBuilder! +static OpenTelemetry.OpenTelemetryBuilderSdkExtensions.WithMetrics(this OpenTelemetry.IOpenTelemetryBuilder! builder, System.Action! configure) -> OpenTelemetry.IOpenTelemetryBuilder! +static OpenTelemetry.OpenTelemetryBuilderSdkExtensions.WithTracing(this OpenTelemetry.IOpenTelemetryBuilder! builder) -> OpenTelemetry.IOpenTelemetryBuilder! +static OpenTelemetry.OpenTelemetryBuilderSdkExtensions.WithTracing(this OpenTelemetry.IOpenTelemetryBuilder! builder, System.Action! configure) -> OpenTelemetry.IOpenTelemetryBuilder! diff --git a/src/OpenTelemetry/.publicApi/net462/PublicAPI.Shipped.txt b/src/OpenTelemetry/.publicApi/net462/PublicAPI.Shipped.txt deleted file mode 100644 index 493d28769f2..00000000000 --- a/src/OpenTelemetry/.publicApi/net462/PublicAPI.Shipped.txt +++ /dev/null @@ -1,413 +0,0 @@ -#nullable enable -~OpenTelemetry.Batch.GetEnumerator() -> OpenTelemetry.Batch.Enumerator -~OpenTelemetry.Metrics.BaseExportingMetricReader.BaseExportingMetricReader(OpenTelemetry.BaseExporter exporter) -> void -~OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration.Boundaries.get -> double[] -~OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration.Boundaries.set -> void -~OpenTelemetry.Metrics.IPullMetricExporter.Collect.get -> System.Func -~OpenTelemetry.Metrics.IPullMetricExporter.Collect.set -> void -~OpenTelemetry.Metrics.Metric.Description.get -> string -~OpenTelemetry.Metrics.Metric.MeterName.get -> string -~OpenTelemetry.Metrics.Metric.MeterVersion.get -> string -~OpenTelemetry.Metrics.Metric.Name.get -> string -~OpenTelemetry.Metrics.Metric.Unit.get -> string -~OpenTelemetry.Metrics.MetricStreamConfiguration.Description.get -> string -~OpenTelemetry.Metrics.MetricStreamConfiguration.Description.set -> void -~OpenTelemetry.Metrics.MetricStreamConfiguration.Name.get -> string -~OpenTelemetry.Metrics.MetricStreamConfiguration.Name.set -> void -~OpenTelemetry.Metrics.MetricStreamConfiguration.TagKeys.get -> string[] -~OpenTelemetry.Metrics.MetricStreamConfiguration.TagKeys.set -> void -~OpenTelemetry.Metrics.PeriodicExportingMetricReader.PeriodicExportingMetricReader(OpenTelemetry.BaseExporter exporter, int exportIntervalMilliseconds = 60000, int exportTimeoutMilliseconds = 30000) -> void -~override OpenTelemetry.Metrics.MeterProviderBuilderBase.AddInstrumentation(System.Func! instrumentationFactory) -> OpenTelemetry.Metrics.MeterProviderBuilder! -~override OpenTelemetry.Trace.TracerProviderBuilderBase.AddInstrumentation(System.Func! instrumentationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder! -~readonly OpenTelemetry.Metrics.BaseExportingMetricReader.exporter -> OpenTelemetry.BaseExporter -~static OpenTelemetry.Metrics.MeterProviderExtensions.ForceFlush(this OpenTelemetry.Metrics.MeterProvider provider, int timeoutMilliseconds = -1) -> bool -~static OpenTelemetry.Metrics.MeterProviderExtensions.Shutdown(this OpenTelemetry.Metrics.MeterProvider provider, int timeoutMilliseconds = -1) -> bool -~static OpenTelemetry.Metrics.MetricStreamConfiguration.Drop.get -> OpenTelemetry.Metrics.MetricStreamConfiguration -abstract OpenTelemetry.BaseExporter.Export(in OpenTelemetry.Batch batch) -> OpenTelemetry.ExportResult -abstract OpenTelemetry.BaseExportProcessor.OnExport(T! data) -> void -abstract OpenTelemetry.Trace.Sampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions -OpenTelemetry.BaseExporter -OpenTelemetry.BaseExporter.BaseExporter() -> void -OpenTelemetry.BaseExporter.Dispose() -> void -OpenTelemetry.BaseExporter.ForceFlush(int timeoutMilliseconds = -1) -> bool -OpenTelemetry.BaseExporter.ParentProvider.get -> OpenTelemetry.BaseProvider? -OpenTelemetry.BaseExporter.Shutdown(int timeoutMilliseconds = -1) -> bool -OpenTelemetry.BaseExportProcessor -OpenTelemetry.BaseExportProcessor.BaseExportProcessor(OpenTelemetry.BaseExporter! exporter) -> void -OpenTelemetry.BaseProcessor -OpenTelemetry.BaseProcessor.BaseProcessor() -> void -OpenTelemetry.BaseProcessor.Dispose() -> void -OpenTelemetry.BaseProcessor.ForceFlush(int timeoutMilliseconds = -1) -> bool -OpenTelemetry.BaseProcessor.ParentProvider.get -> OpenTelemetry.BaseProvider? -OpenTelemetry.BaseProcessor.Shutdown(int timeoutMilliseconds = -1) -> bool -OpenTelemetry.Batch -OpenTelemetry.Batch.Batch() -> void -OpenTelemetry.Batch.Batch(T![]! items, int count) -> void -OpenTelemetry.Batch.Count.get -> long -OpenTelemetry.Batch.Dispose() -> void -OpenTelemetry.Batch.Enumerator -OpenTelemetry.Batch.Enumerator.Current.get -> T! -OpenTelemetry.Batch.Enumerator.Dispose() -> void -OpenTelemetry.Batch.Enumerator.Enumerator() -> void -OpenTelemetry.Batch.Enumerator.MoveNext() -> bool -OpenTelemetry.Batch.Enumerator.Reset() -> void -OpenTelemetry.BatchActivityExportProcessor -OpenTelemetry.BatchActivityExportProcessor.BatchActivityExportProcessor(OpenTelemetry.BaseExporter! exporter, int maxQueueSize = 2048, int scheduledDelayMilliseconds = 5000, int exporterTimeoutMilliseconds = 30000, int maxExportBatchSize = 512) -> void -OpenTelemetry.BatchExportProcessor -OpenTelemetry.BatchExportProcessor.BatchExportProcessor(OpenTelemetry.BaseExporter! exporter, int maxQueueSize = 2048, int scheduledDelayMilliseconds = 5000, int exporterTimeoutMilliseconds = 30000, int maxExportBatchSize = 512) -> void -OpenTelemetry.BatchExportProcessorOptions -OpenTelemetry.BatchExportProcessorOptions.BatchExportProcessorOptions() -> void -OpenTelemetry.BatchExportProcessorOptions.ExporterTimeoutMilliseconds.get -> int -OpenTelemetry.BatchExportProcessorOptions.ExporterTimeoutMilliseconds.set -> void -OpenTelemetry.BatchExportProcessorOptions.MaxExportBatchSize.get -> int -OpenTelemetry.BatchExportProcessorOptions.MaxExportBatchSize.set -> void -OpenTelemetry.BatchExportProcessorOptions.MaxQueueSize.get -> int -OpenTelemetry.BatchExportProcessorOptions.MaxQueueSize.set -> void -OpenTelemetry.BatchExportProcessorOptions.ScheduledDelayMilliseconds.get -> int -OpenTelemetry.BatchExportProcessorOptions.ScheduledDelayMilliseconds.set -> void -OpenTelemetry.BatchLogRecordExportProcessor -OpenTelemetry.BatchLogRecordExportProcessor.BatchLogRecordExportProcessor(OpenTelemetry.BaseExporter! exporter, int maxQueueSize = 2048, int scheduledDelayMilliseconds = 5000, int exporterTimeoutMilliseconds = 30000, int maxExportBatchSize = 512) -> void -OpenTelemetry.CompositeProcessor -OpenTelemetry.CompositeProcessor.AddProcessor(OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.CompositeProcessor! -OpenTelemetry.CompositeProcessor.CompositeProcessor(System.Collections.Generic.IEnumerable!>! processors) -> void -OpenTelemetry.ExportProcessorType -OpenTelemetry.ExportProcessorType.Batch = 1 -> OpenTelemetry.ExportProcessorType -OpenTelemetry.ExportProcessorType.Simple = 0 -> OpenTelemetry.ExportProcessorType -OpenTelemetry.ExportResult -OpenTelemetry.ExportResult.Failure = 1 -> OpenTelemetry.ExportResult -OpenTelemetry.ExportResult.Success = 0 -> OpenTelemetry.ExportResult -OpenTelemetry.Logs.LogRecord -OpenTelemetry.Logs.LogRecord.Attributes.get -> System.Collections.Generic.IReadOnlyList>? -OpenTelemetry.Logs.LogRecord.Attributes.set -> void -OpenTelemetry.Logs.LogRecord.Body.get -> string? -OpenTelemetry.Logs.LogRecord.Body.set -> void -OpenTelemetry.Logs.LogRecord.CategoryName.get -> string? -OpenTelemetry.Logs.LogRecord.CategoryName.set -> void -OpenTelemetry.Logs.LogRecord.EventId.get -> Microsoft.Extensions.Logging.EventId -OpenTelemetry.Logs.LogRecord.EventId.set -> void -OpenTelemetry.Logs.LogRecord.Exception.get -> System.Exception? -OpenTelemetry.Logs.LogRecord.Exception.set -> void -OpenTelemetry.Logs.LogRecord.ForEachScope(System.Action! callback, TState state) -> void -OpenTelemetry.Logs.LogRecord.FormattedMessage.get -> string? -OpenTelemetry.Logs.LogRecord.FormattedMessage.set -> void -OpenTelemetry.Logs.LogRecord.LogLevel.get -> Microsoft.Extensions.Logging.LogLevel -OpenTelemetry.Logs.LogRecord.LogLevel.set -> void -OpenTelemetry.Logs.LogRecord.SpanId.get -> System.Diagnostics.ActivitySpanId -OpenTelemetry.Logs.LogRecord.SpanId.set -> void -OpenTelemetry.Logs.LogRecord.State.get -> object? -OpenTelemetry.Logs.LogRecord.State.set -> void -OpenTelemetry.Logs.LogRecord.StateValues.get -> System.Collections.Generic.IReadOnlyList>? -OpenTelemetry.Logs.LogRecord.StateValues.set -> void -OpenTelemetry.Logs.LogRecord.Timestamp.get -> System.DateTime -OpenTelemetry.Logs.LogRecord.Timestamp.set -> void -OpenTelemetry.Logs.LogRecord.TraceFlags.get -> System.Diagnostics.ActivityTraceFlags -OpenTelemetry.Logs.LogRecord.TraceFlags.set -> void -OpenTelemetry.Logs.LogRecord.TraceId.get -> System.Diagnostics.ActivityTraceId -OpenTelemetry.Logs.LogRecord.TraceId.set -> void -OpenTelemetry.Logs.LogRecord.TraceState.get -> string? -OpenTelemetry.Logs.LogRecord.TraceState.set -> void -OpenTelemetry.Logs.LogRecordScope -OpenTelemetry.Logs.LogRecordScope.Enumerator -OpenTelemetry.Logs.LogRecordScope.Enumerator.Current.get -> System.Collections.Generic.KeyValuePair -OpenTelemetry.Logs.LogRecordScope.Enumerator.Dispose() -> void -OpenTelemetry.Logs.LogRecordScope.Enumerator.Enumerator() -> void -OpenTelemetry.Logs.LogRecordScope.Enumerator.Enumerator(object? scope) -> void -OpenTelemetry.Logs.LogRecordScope.Enumerator.MoveNext() -> bool -OpenTelemetry.Logs.LogRecordScope.Enumerator.Reset() -> void -OpenTelemetry.Logs.LogRecordScope.GetEnumerator() -> OpenTelemetry.Logs.LogRecordScope.Enumerator -OpenTelemetry.Logs.LogRecordScope.LogRecordScope() -> void -OpenTelemetry.Logs.LogRecordScope.Scope.get -> object? -OpenTelemetry.Logs.OpenTelemetryLoggerOptions -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.AddProcessor(OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.IncludeFormattedMessage.get -> bool -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.IncludeFormattedMessage.set -> void -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.IncludeScopes.get -> bool -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.IncludeScopes.set -> void -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.OpenTelemetryLoggerOptions() -> void -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.ParseStateValues.get -> bool -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.ParseStateValues.set -> void -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.SetResourceBuilder(OpenTelemetry.Resources.ResourceBuilder! resourceBuilder) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! -OpenTelemetry.Logs.OpenTelemetryLoggerProvider -OpenTelemetry.Logs.OpenTelemetryLoggerProvider.CreateLogger(string! categoryName) -> Microsoft.Extensions.Logging.ILogger! -OpenTelemetry.Logs.OpenTelemetryLoggerProvider.OpenTelemetryLoggerProvider(Microsoft.Extensions.Options.IOptionsMonitor! options) -> void -OpenTelemetry.Metrics.AggregationTemporality -OpenTelemetry.Metrics.AggregationTemporality.Cumulative = 1 -> OpenTelemetry.Metrics.AggregationTemporality -OpenTelemetry.Metrics.AggregationTemporality.Delta = 2 -> OpenTelemetry.Metrics.AggregationTemporality -OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration -OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.Base2ExponentialBucketHistogramConfiguration() -> void -OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxScale.get -> int -OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxScale.set -> void -OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxSize.get -> int -OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxSize.set -> void -OpenTelemetry.Metrics.BaseExportingMetricReader -OpenTelemetry.Metrics.BaseExportingMetricReader.SupportedExportModes.get -> OpenTelemetry.Metrics.ExportModes -OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration -OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration.ExplicitBucketHistogramConfiguration() -> void -OpenTelemetry.Metrics.ExponentialHistogramBuckets -OpenTelemetry.Metrics.ExponentialHistogramBuckets.Enumerator -OpenTelemetry.Metrics.ExponentialHistogramBuckets.Enumerator.Current.get -> long -OpenTelemetry.Metrics.ExponentialHistogramBuckets.Enumerator.Enumerator() -> void -OpenTelemetry.Metrics.ExponentialHistogramBuckets.Enumerator.MoveNext() -> bool -OpenTelemetry.Metrics.ExponentialHistogramBuckets.GetEnumerator() -> OpenTelemetry.Metrics.ExponentialHistogramBuckets.Enumerator -OpenTelemetry.Metrics.ExponentialHistogramBuckets.Offset.get -> int -OpenTelemetry.Metrics.ExponentialHistogramData -OpenTelemetry.Metrics.ExponentialHistogramData.PositiveBuckets.get -> OpenTelemetry.Metrics.ExponentialHistogramBuckets! -OpenTelemetry.Metrics.ExponentialHistogramData.Scale.get -> int -OpenTelemetry.Metrics.ExponentialHistogramData.ZeroCount.get -> long -OpenTelemetry.Metrics.ExportModes -OpenTelemetry.Metrics.ExportModes.Pull = 2 -> OpenTelemetry.Metrics.ExportModes -OpenTelemetry.Metrics.ExportModes.Push = 1 -> OpenTelemetry.Metrics.ExportModes -OpenTelemetry.Metrics.ExportModesAttribute -OpenTelemetry.Metrics.ExportModesAttribute.ExportModesAttribute(OpenTelemetry.Metrics.ExportModes supported) -> void -OpenTelemetry.Metrics.ExportModesAttribute.Supported.get -> OpenTelemetry.Metrics.ExportModes -OpenTelemetry.Metrics.HistogramBucket -OpenTelemetry.Metrics.HistogramBucket.BucketCount.get -> long -OpenTelemetry.Metrics.HistogramBucket.ExplicitBound.get -> double -OpenTelemetry.Metrics.HistogramBucket.HistogramBucket() -> void -OpenTelemetry.Metrics.HistogramBuckets -OpenTelemetry.Metrics.HistogramBuckets.Enumerator -OpenTelemetry.Metrics.HistogramBuckets.Enumerator.Current.get -> OpenTelemetry.Metrics.HistogramBucket -OpenTelemetry.Metrics.HistogramBuckets.Enumerator.Enumerator() -> void -OpenTelemetry.Metrics.HistogramBuckets.Enumerator.MoveNext() -> bool -OpenTelemetry.Metrics.HistogramBuckets.GetEnumerator() -> OpenTelemetry.Metrics.HistogramBuckets.Enumerator -OpenTelemetry.Metrics.HistogramConfiguration -OpenTelemetry.Metrics.HistogramConfiguration.HistogramConfiguration() -> void -OpenTelemetry.Metrics.HistogramConfiguration.RecordMinMax.get -> bool -OpenTelemetry.Metrics.HistogramConfiguration.RecordMinMax.set -> void -OpenTelemetry.Metrics.IPullMetricExporter -OpenTelemetry.Metrics.MeterProviderBuilderBase -OpenTelemetry.Metrics.MeterProviderBuilderBase.Build() -> OpenTelemetry.Metrics.MeterProvider! -OpenTelemetry.Metrics.MeterProviderBuilderBase.MeterProviderBuilderBase() -> void -OpenTelemetry.Metrics.MeterProviderBuilderExtensions -OpenTelemetry.Metrics.MeterProviderExtensions -OpenTelemetry.Metrics.Metric -OpenTelemetry.Metrics.Metric.GetMetricPoints() -> OpenTelemetry.Metrics.MetricPointsAccessor -OpenTelemetry.Metrics.Metric.MetricType.get -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.Metric.Temporality.get -> OpenTelemetry.Metrics.AggregationTemporality -OpenTelemetry.Metrics.MetricPoint -OpenTelemetry.Metrics.MetricPoint.EndTime.get -> System.DateTimeOffset -OpenTelemetry.Metrics.MetricPoint.GetExponentialHistogramData() -> OpenTelemetry.Metrics.ExponentialHistogramData! -OpenTelemetry.Metrics.MetricPoint.GetGaugeLastValueDouble() -> double -OpenTelemetry.Metrics.MetricPoint.GetGaugeLastValueLong() -> long -OpenTelemetry.Metrics.MetricPoint.GetHistogramBuckets() -> OpenTelemetry.Metrics.HistogramBuckets! -OpenTelemetry.Metrics.MetricPoint.GetHistogramCount() -> long -OpenTelemetry.Metrics.MetricPoint.GetHistogramSum() -> double -OpenTelemetry.Metrics.MetricPoint.GetSumDouble() -> double -OpenTelemetry.Metrics.MetricPoint.GetSumLong() -> long -OpenTelemetry.Metrics.MetricPoint.MetricPoint() -> void -OpenTelemetry.Metrics.MetricPoint.StartTime.get -> System.DateTimeOffset -OpenTelemetry.Metrics.MetricPoint.Tags.get -> OpenTelemetry.ReadOnlyTagCollection -OpenTelemetry.Metrics.MetricPoint.TryGetHistogramMinMaxValues(out double min, out double max) -> bool -OpenTelemetry.Metrics.MetricPointsAccessor -OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator -OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator.Current.get -> OpenTelemetry.Metrics.MetricPoint -OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator.Enumerator() -> void -OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator.MoveNext() -> bool -OpenTelemetry.Metrics.MetricPointsAccessor.GetEnumerator() -> OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator -OpenTelemetry.Metrics.MetricPointsAccessor.MetricPointsAccessor() -> void -OpenTelemetry.Metrics.MetricReader -OpenTelemetry.Metrics.MetricReader.Collect(int timeoutMilliseconds = -1) -> bool -OpenTelemetry.Metrics.MetricReader.Dispose() -> void -OpenTelemetry.Metrics.MetricReader.MetricReader() -> void -OpenTelemetry.Metrics.MetricReader.Shutdown(int timeoutMilliseconds = -1) -> bool -OpenTelemetry.Metrics.MetricReader.TemporalityPreference.get -> OpenTelemetry.Metrics.MetricReaderTemporalityPreference -OpenTelemetry.Metrics.MetricReader.TemporalityPreference.set -> void -OpenTelemetry.Metrics.MetricReaderOptions -OpenTelemetry.Metrics.MetricReaderOptions.MetricReaderOptions() -> void -OpenTelemetry.Metrics.MetricReaderOptions.PeriodicExportingMetricReaderOptions.get -> OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions! -OpenTelemetry.Metrics.MetricReaderOptions.PeriodicExportingMetricReaderOptions.set -> void -OpenTelemetry.Metrics.MetricReaderOptions.TemporalityPreference.get -> OpenTelemetry.Metrics.MetricReaderTemporalityPreference -OpenTelemetry.Metrics.MetricReaderOptions.TemporalityPreference.set -> void -OpenTelemetry.Metrics.MetricReaderTemporalityPreference -OpenTelemetry.Metrics.MetricReaderTemporalityPreference.Cumulative = 1 -> OpenTelemetry.Metrics.MetricReaderTemporalityPreference -OpenTelemetry.Metrics.MetricReaderTemporalityPreference.Delta = 2 -> OpenTelemetry.Metrics.MetricReaderTemporalityPreference -OpenTelemetry.Metrics.MetricStreamConfiguration -OpenTelemetry.Metrics.MetricStreamConfiguration.MetricStreamConfiguration() -> void -OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricType.DoubleGauge = 45 -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricType.DoubleSum = 29 -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricType.DoubleSumNonMonotonic = 141 -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricType.ExponentialHistogram = 80 -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricType.Histogram = 64 -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricType.LongGauge = 42 -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricType.LongSum = 26 -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricType.LongSumNonMonotonic = 138 -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricTypeExtensions -OpenTelemetry.Metrics.PeriodicExportingMetricReader -OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions -OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds.get -> int? -OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds.set -> void -OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions.ExportTimeoutMilliseconds.get -> int? -OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions.ExportTimeoutMilliseconds.set -> void -OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions.PeriodicExportingMetricReaderOptions() -> void -OpenTelemetry.ProviderExtensions -OpenTelemetry.ReadOnlyTagCollection -OpenTelemetry.ReadOnlyTagCollection.Count.get -> int -OpenTelemetry.ReadOnlyTagCollection.Enumerator -OpenTelemetry.ReadOnlyTagCollection.Enumerator.Current.get -> System.Collections.Generic.KeyValuePair -OpenTelemetry.ReadOnlyTagCollection.Enumerator.Enumerator() -> void -OpenTelemetry.ReadOnlyTagCollection.Enumerator.MoveNext() -> bool -OpenTelemetry.ReadOnlyTagCollection.GetEnumerator() -> OpenTelemetry.ReadOnlyTagCollection.Enumerator -OpenTelemetry.ReadOnlyTagCollection.ReadOnlyTagCollection() -> void -OpenTelemetry.Resources.IResourceDetector -OpenTelemetry.Resources.IResourceDetector.Detect() -> OpenTelemetry.Resources.Resource! -OpenTelemetry.Resources.Resource -OpenTelemetry.Resources.Resource.Attributes.get -> System.Collections.Generic.IEnumerable>! -OpenTelemetry.Resources.Resource.Merge(OpenTelemetry.Resources.Resource! other) -> OpenTelemetry.Resources.Resource! -OpenTelemetry.Resources.Resource.Resource(System.Collections.Generic.IEnumerable>! attributes) -> void -OpenTelemetry.Resources.ResourceBuilder -OpenTelemetry.Resources.ResourceBuilder.AddDetector(OpenTelemetry.Resources.IResourceDetector! resourceDetector) -> OpenTelemetry.Resources.ResourceBuilder! -OpenTelemetry.Resources.ResourceBuilder.AddDetector(System.Func! resourceDetectorFactory) -> OpenTelemetry.Resources.ResourceBuilder! -OpenTelemetry.Resources.ResourceBuilder.Build() -> OpenTelemetry.Resources.Resource! -OpenTelemetry.Resources.ResourceBuilder.Clear() -> OpenTelemetry.Resources.ResourceBuilder! -OpenTelemetry.Resources.ResourceBuilderExtensions -OpenTelemetry.Sdk -OpenTelemetry.SimpleActivityExportProcessor -OpenTelemetry.SimpleActivityExportProcessor.SimpleActivityExportProcessor(OpenTelemetry.BaseExporter! exporter) -> void -OpenTelemetry.SimpleExportProcessor -OpenTelemetry.SimpleExportProcessor.SimpleExportProcessor(OpenTelemetry.BaseExporter! exporter) -> void -OpenTelemetry.SimpleLogRecordExportProcessor -OpenTelemetry.SimpleLogRecordExportProcessor.SimpleLogRecordExportProcessor(OpenTelemetry.BaseExporter! exporter) -> void -OpenTelemetry.SuppressInstrumentationScope -OpenTelemetry.SuppressInstrumentationScope.Dispose() -> void -OpenTelemetry.Trace.AlwaysOffSampler -OpenTelemetry.Trace.AlwaysOffSampler.AlwaysOffSampler() -> void -OpenTelemetry.Trace.AlwaysOnSampler -OpenTelemetry.Trace.AlwaysOnSampler.AlwaysOnSampler() -> void -OpenTelemetry.Trace.BatchExportActivityProcessorOptions -OpenTelemetry.Trace.BatchExportActivityProcessorOptions.BatchExportActivityProcessorOptions() -> void -OpenTelemetry.Trace.ParentBasedSampler -OpenTelemetry.Trace.ParentBasedSampler.ParentBasedSampler(OpenTelemetry.Trace.Sampler! rootSampler) -> void -OpenTelemetry.Trace.ParentBasedSampler.ParentBasedSampler(OpenTelemetry.Trace.Sampler! rootSampler, OpenTelemetry.Trace.Sampler? remoteParentSampled = null, OpenTelemetry.Trace.Sampler? remoteParentNotSampled = null, OpenTelemetry.Trace.Sampler? localParentSampled = null, OpenTelemetry.Trace.Sampler? localParentNotSampled = null) -> void -OpenTelemetry.Trace.Sampler -OpenTelemetry.Trace.Sampler.Description.get -> string! -OpenTelemetry.Trace.Sampler.Description.set -> void -OpenTelemetry.Trace.Sampler.Sampler() -> void -OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingDecision.Drop = 0 -> OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingDecision.RecordAndSample = 2 -> OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingDecision.RecordOnly = 1 -> OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingParameters -OpenTelemetry.Trace.SamplingParameters.Kind.get -> System.Diagnostics.ActivityKind -OpenTelemetry.Trace.SamplingParameters.Links.get -> System.Collections.Generic.IEnumerable? -OpenTelemetry.Trace.SamplingParameters.Name.get -> string! -OpenTelemetry.Trace.SamplingParameters.ParentContext.get -> System.Diagnostics.ActivityContext -OpenTelemetry.Trace.SamplingParameters.SamplingParameters() -> void -OpenTelemetry.Trace.SamplingParameters.SamplingParameters(System.Diagnostics.ActivityContext parentContext, System.Diagnostics.ActivityTraceId traceId, string! name, System.Diagnostics.ActivityKind kind, System.Collections.Generic.IEnumerable>? tags = null, System.Collections.Generic.IEnumerable? links = null) -> void -OpenTelemetry.Trace.SamplingParameters.Tags.get -> System.Collections.Generic.IEnumerable>? -OpenTelemetry.Trace.SamplingParameters.TraceId.get -> System.Diagnostics.ActivityTraceId -OpenTelemetry.Trace.SamplingResult -OpenTelemetry.Trace.SamplingResult.Attributes.get -> System.Collections.Generic.IEnumerable>! -OpenTelemetry.Trace.SamplingResult.Decision.get -> OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingResult.Equals(OpenTelemetry.Trace.SamplingResult other) -> bool -OpenTelemetry.Trace.SamplingResult.SamplingResult() -> void -OpenTelemetry.Trace.SamplingResult.SamplingResult(bool isSampled) -> void -OpenTelemetry.Trace.SamplingResult.SamplingResult(OpenTelemetry.Trace.SamplingDecision decision) -> void -OpenTelemetry.Trace.SamplingResult.SamplingResult(OpenTelemetry.Trace.SamplingDecision decision, string! traceStateString) -> void -OpenTelemetry.Trace.SamplingResult.SamplingResult(OpenTelemetry.Trace.SamplingDecision decision, System.Collections.Generic.IEnumerable>! attributes) -> void -OpenTelemetry.Trace.SamplingResult.SamplingResult(OpenTelemetry.Trace.SamplingDecision decision, System.Collections.Generic.IEnumerable>? attributes, string? traceStateString) -> void -OpenTelemetry.Trace.SamplingResult.TraceStateString.get -> string? -OpenTelemetry.Trace.TraceIdRatioBasedSampler -OpenTelemetry.Trace.TraceIdRatioBasedSampler.TraceIdRatioBasedSampler(double probability) -> void -OpenTelemetry.Trace.TracerProviderBuilderBase -OpenTelemetry.Trace.TracerProviderBuilderBase.AddInstrumentation(string! instrumentationName, string! instrumentationVersion, System.Func! instrumentationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder! -OpenTelemetry.Trace.TracerProviderBuilderBase.Build() -> OpenTelemetry.Trace.TracerProvider! -OpenTelemetry.Trace.TracerProviderBuilderBase.TracerProviderBuilderBase() -> void -OpenTelemetry.Trace.TracerProviderBuilderExtensions -OpenTelemetry.Trace.TracerProviderExtensions -override OpenTelemetry.BaseExportProcessor.Dispose(bool disposing) -> void -override OpenTelemetry.BaseExportProcessor.OnEnd(T! data) -> void -override OpenTelemetry.BaseExportProcessor.OnForceFlush(int timeoutMilliseconds) -> bool -override OpenTelemetry.BaseExportProcessor.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.BaseExportProcessor.ToString() -> string! -override OpenTelemetry.BaseProcessor.ToString() -> string! -override OpenTelemetry.BatchActivityExportProcessor.OnEnd(System.Diagnostics.Activity! data) -> void -override OpenTelemetry.BatchExportProcessor.Dispose(bool disposing) -> void -override OpenTelemetry.BatchExportProcessor.OnExport(T! data) -> void -override OpenTelemetry.BatchExportProcessor.OnForceFlush(int timeoutMilliseconds) -> bool -override OpenTelemetry.BatchExportProcessor.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.BatchLogRecordExportProcessor.OnEnd(OpenTelemetry.Logs.LogRecord! data) -> void -override OpenTelemetry.CompositeProcessor.Dispose(bool disposing) -> void -override OpenTelemetry.CompositeProcessor.OnEnd(T data) -> void -override OpenTelemetry.CompositeProcessor.OnForceFlush(int timeoutMilliseconds) -> bool -override OpenTelemetry.CompositeProcessor.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.CompositeProcessor.OnStart(T data) -> void -override OpenTelemetry.Logs.OpenTelemetryLoggerProvider.Dispose(bool disposing) -> void -override OpenTelemetry.Metrics.BaseExportingMetricReader.Dispose(bool disposing) -> void -override OpenTelemetry.Metrics.BaseExportingMetricReader.OnCollect(int timeoutMilliseconds) -> bool -override OpenTelemetry.Metrics.BaseExportingMetricReader.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.Metrics.MeterProviderBuilderBase.AddMeter(params string![]! names) -> OpenTelemetry.Metrics.MeterProviderBuilder! -override OpenTelemetry.Metrics.PeriodicExportingMetricReader.Dispose(bool disposing) -> void -override OpenTelemetry.Metrics.PeriodicExportingMetricReader.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.SimpleActivityExportProcessor.OnEnd(System.Diagnostics.Activity! data) -> void -override OpenTelemetry.SimpleExportProcessor.OnExport(T! data) -> void -override OpenTelemetry.Trace.AlwaysOffSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -override OpenTelemetry.Trace.AlwaysOnSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -override OpenTelemetry.Trace.ParentBasedSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -override OpenTelemetry.Trace.SamplingResult.Equals(object? obj) -> bool -override OpenTelemetry.Trace.SamplingResult.GetHashCode() -> int -override OpenTelemetry.Trace.TraceIdRatioBasedSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -override OpenTelemetry.Trace.TracerProviderBuilderBase.AddLegacySource(string! operationName) -> OpenTelemetry.Trace.TracerProviderBuilder! -override OpenTelemetry.Trace.TracerProviderBuilderBase.AddSource(params string![]! names) -> OpenTelemetry.Trace.TracerProviderBuilder! -override sealed OpenTelemetry.BaseExportProcessor.OnStart(T! data) -> void -readonly OpenTelemetry.BaseExportProcessor.exporter -> OpenTelemetry.BaseExporter! -static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.AddOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder) -> Microsoft.Extensions.Logging.ILoggingBuilder! -static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.AddOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, System.Action? configure) -> Microsoft.Extensions.Logging.ILoggingBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddReader(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, OpenTelemetry.Metrics.MetricReader! reader) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddReader(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, System.Func! implementationFactory) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddReader(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddView(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, string! instrumentName, OpenTelemetry.Metrics.MetricStreamConfiguration! metricStreamConfiguration) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddView(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, string! instrumentName, string! name) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddView(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, System.Func! viewConfig) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.Build(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder) -> OpenTelemetry.Metrics.MeterProvider? -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.ConfigureResource(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, System.Action! configure) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetMaxMetricPointsPerMetricStream(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, int maxMetricPointsPerMetricStream) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetMaxMetricStreams(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, int maxMetricStreams) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetResourceBuilder(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, OpenTelemetry.Resources.ResourceBuilder! resourceBuilder) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MetricTypeExtensions.IsDouble(this OpenTelemetry.Metrics.MetricType self) -> bool -static OpenTelemetry.Metrics.MetricTypeExtensions.IsGauge(this OpenTelemetry.Metrics.MetricType self) -> bool -static OpenTelemetry.Metrics.MetricTypeExtensions.IsHistogram(this OpenTelemetry.Metrics.MetricType self) -> bool -static OpenTelemetry.Metrics.MetricTypeExtensions.IsLong(this OpenTelemetry.Metrics.MetricType self) -> bool -static OpenTelemetry.Metrics.MetricTypeExtensions.IsSum(this OpenTelemetry.Metrics.MetricType self) -> bool -static OpenTelemetry.ProviderExtensions.GetDefaultResource(this OpenTelemetry.BaseProvider! baseProvider) -> OpenTelemetry.Resources.Resource! -static OpenTelemetry.ProviderExtensions.GetResource(this OpenTelemetry.BaseProvider! baseProvider) -> OpenTelemetry.Resources.Resource! -static OpenTelemetry.Resources.Resource.Empty.get -> OpenTelemetry.Resources.Resource! -static OpenTelemetry.Resources.ResourceBuilder.CreateDefault() -> OpenTelemetry.Resources.ResourceBuilder! -static OpenTelemetry.Resources.ResourceBuilder.CreateEmpty() -> OpenTelemetry.Resources.ResourceBuilder! -static OpenTelemetry.Resources.ResourceBuilderExtensions.AddAttributes(this OpenTelemetry.Resources.ResourceBuilder! resourceBuilder, System.Collections.Generic.IEnumerable>! attributes) -> OpenTelemetry.Resources.ResourceBuilder! -static OpenTelemetry.Resources.ResourceBuilderExtensions.AddEnvironmentVariableDetector(this OpenTelemetry.Resources.ResourceBuilder! resourceBuilder) -> OpenTelemetry.Resources.ResourceBuilder! -static OpenTelemetry.Resources.ResourceBuilderExtensions.AddService(this OpenTelemetry.Resources.ResourceBuilder! resourceBuilder, string! serviceName, string? serviceNamespace = null, string? serviceVersion = null, bool autoGenerateServiceInstanceId = true, string? serviceInstanceId = null) -> OpenTelemetry.Resources.ResourceBuilder! -static OpenTelemetry.Resources.ResourceBuilderExtensions.AddTelemetrySdk(this OpenTelemetry.Resources.ResourceBuilder! resourceBuilder) -> OpenTelemetry.Resources.ResourceBuilder! -static OpenTelemetry.Sdk.CreateMeterProviderBuilder() -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Sdk.CreateTracerProviderBuilder() -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Sdk.SetDefaultTextMapPropagator(OpenTelemetry.Context.Propagation.TextMapPropagator! textMapPropagator) -> void -static OpenTelemetry.Sdk.SuppressInstrumentation.get -> bool -static OpenTelemetry.SuppressInstrumentationScope.Begin(bool value = true) -> System.IDisposable! -static OpenTelemetry.SuppressInstrumentationScope.Enter() -> int -static OpenTelemetry.Trace.SamplingResult.operator !=(OpenTelemetry.Trace.SamplingResult decision1, OpenTelemetry.Trace.SamplingResult decision2) -> bool -static OpenTelemetry.Trace.SamplingResult.operator ==(OpenTelemetry.Trace.SamplingResult decision1, OpenTelemetry.Trace.SamplingResult decision2) -> bool -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, System.Func!>! implementationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.Build(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder) -> OpenTelemetry.Trace.TracerProvider? -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.ConfigureResource(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, System.Action! configure) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.SetErrorStatusOnException(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, bool enabled = true) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.SetResourceBuilder(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, OpenTelemetry.Resources.ResourceBuilder! resourceBuilder) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.SetSampler(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, OpenTelemetry.Trace.Sampler! sampler) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.SetSampler(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, System.Func! implementationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.SetSampler(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderExtensions.AddProcessor(this OpenTelemetry.Trace.TracerProvider! provider, OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Trace.TracerProvider! -static OpenTelemetry.Trace.TracerProviderExtensions.ForceFlush(this OpenTelemetry.Trace.TracerProvider! provider, int timeoutMilliseconds = -1) -> bool -static OpenTelemetry.Trace.TracerProviderExtensions.Shutdown(this OpenTelemetry.Trace.TracerProvider! provider, int timeoutMilliseconds = -1) -> bool -virtual OpenTelemetry.BaseExporter.Dispose(bool disposing) -> void -virtual OpenTelemetry.BaseExporter.OnForceFlush(int timeoutMilliseconds) -> bool -virtual OpenTelemetry.BaseExporter.OnShutdown(int timeoutMilliseconds) -> bool -virtual OpenTelemetry.BaseProcessor.Dispose(bool disposing) -> void -virtual OpenTelemetry.BaseProcessor.OnEnd(T data) -> void -virtual OpenTelemetry.BaseProcessor.OnForceFlush(int timeoutMilliseconds) -> bool -virtual OpenTelemetry.BaseProcessor.OnShutdown(int timeoutMilliseconds) -> bool -virtual OpenTelemetry.BaseProcessor.OnStart(T data) -> void -virtual OpenTelemetry.Metrics.MetricReader.Dispose(bool disposing) -> void -virtual OpenTelemetry.Metrics.MetricReader.OnCollect(int timeoutMilliseconds) -> bool -virtual OpenTelemetry.Metrics.MetricReader.OnShutdown(int timeoutMilliseconds) -> bool diff --git a/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt deleted file mode 100644 index e75f84d65db..00000000000 --- a/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,50 +0,0 @@ -OpenTelemetry.Logs.BatchExportLogRecordProcessorOptions -OpenTelemetry.Logs.BatchExportLogRecordProcessorOptions.BatchExportLogRecordProcessorOptions() -> void -OpenTelemetry.Logs.LoggerProviderBuilderExtensions -OpenTelemetry.Logs.LoggerProviderExtensions -OpenTelemetry.Logs.LogRecord.Logger.get -> OpenTelemetry.Logs.Logger? -OpenTelemetry.Logs.LogRecord.Severity.get -> OpenTelemetry.Logs.LogRecordSeverity? -OpenTelemetry.Logs.LogRecord.Severity.set -> void -OpenTelemetry.Logs.LogRecord.SeverityText.get -> string? -OpenTelemetry.Logs.LogRecord.SeverityText.set -> void -OpenTelemetry.Logs.LogRecordExportProcessorOptions -OpenTelemetry.Logs.LogRecordExportProcessorOptions.BatchExportProcessorOptions.get -> OpenTelemetry.Logs.BatchExportLogRecordProcessorOptions! -OpenTelemetry.Logs.LogRecordExportProcessorOptions.BatchExportProcessorOptions.set -> void -OpenTelemetry.Logs.LogRecordExportProcessorOptions.ExportProcessorType.get -> OpenTelemetry.ExportProcessorType -OpenTelemetry.Logs.LogRecordExportProcessorOptions.ExportProcessorType.set -> void -OpenTelemetry.Logs.LogRecordExportProcessorOptions.LogRecordExportProcessorOptions() -> void -OpenTelemetry.Metrics.AlwaysOffExemplarFilter -OpenTelemetry.Metrics.AlwaysOffExemplarFilter.AlwaysOffExemplarFilter() -> void -OpenTelemetry.Metrics.AlwaysOnExemplarFilter -OpenTelemetry.Metrics.AlwaysOnExemplarFilter.AlwaysOnExemplarFilter() -> void -OpenTelemetry.Metrics.Exemplar -OpenTelemetry.Metrics.Exemplar.DoubleValue.get -> double -OpenTelemetry.Metrics.Exemplar.Exemplar() -> void -OpenTelemetry.Metrics.Exemplar.SpanId.get -> System.Diagnostics.ActivitySpanId? -OpenTelemetry.Metrics.Exemplar.Timestamp.get -> System.DateTimeOffset -OpenTelemetry.Metrics.Exemplar.TraceId.get -> System.Diagnostics.ActivityTraceId? -OpenTelemetry.Metrics.ExemplarFilter -OpenTelemetry.Metrics.ExemplarFilter.ExemplarFilter() -> void -OpenTelemetry.Metrics.MetricPoint.GetExemplars() -> OpenTelemetry.Metrics.Exemplar[]! -OpenTelemetry.Metrics.TraceBasedExemplarFilter -OpenTelemetry.Metrics.TraceBasedExemplarFilter.TraceBasedExemplarFilter() -> void -static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, System.Func!>! implementationFactory) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.Build(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder) -> OpenTelemetry.Logs.LoggerProvider! -static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.ConfigureResource(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, System.Action! configure) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.SetResourceBuilder(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, OpenTelemetry.Resources.ResourceBuilder! resourceBuilder) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.LoggerProviderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProvider! provider, OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Logs.LoggerProvider! -static OpenTelemetry.Logs.LoggerProviderExtensions.ForceFlush(this OpenTelemetry.Logs.LoggerProvider! provider, int timeoutMilliseconds = -1) -> bool -static OpenTelemetry.Logs.LoggerProviderExtensions.Shutdown(this OpenTelemetry.Logs.LoggerProvider! provider, int timeoutMilliseconds = -1) -> bool -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetExemplarFilter(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, OpenTelemetry.Metrics.ExemplarFilter! exemplarFilter) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Sdk.CreateLoggerProviderBuilder() -> OpenTelemetry.Logs.LoggerProviderBuilder! -~abstract OpenTelemetry.Metrics.ExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool -~abstract OpenTelemetry.Metrics.ExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool -~OpenTelemetry.Metrics.Exemplar.FilteredTags.get -> System.Collections.Generic.List> -~override OpenTelemetry.Metrics.AlwaysOffExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool -~override OpenTelemetry.Metrics.AlwaysOffExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool -~override OpenTelemetry.Metrics.AlwaysOnExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool -~override OpenTelemetry.Metrics.AlwaysOnExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool -~override OpenTelemetry.Metrics.TraceBasedExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool -~override OpenTelemetry.Metrics.TraceBasedExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool diff --git a/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt deleted file mode 100644 index e75f84d65db..00000000000 --- a/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,50 +0,0 @@ -OpenTelemetry.Logs.BatchExportLogRecordProcessorOptions -OpenTelemetry.Logs.BatchExportLogRecordProcessorOptions.BatchExportLogRecordProcessorOptions() -> void -OpenTelemetry.Logs.LoggerProviderBuilderExtensions -OpenTelemetry.Logs.LoggerProviderExtensions -OpenTelemetry.Logs.LogRecord.Logger.get -> OpenTelemetry.Logs.Logger? -OpenTelemetry.Logs.LogRecord.Severity.get -> OpenTelemetry.Logs.LogRecordSeverity? -OpenTelemetry.Logs.LogRecord.Severity.set -> void -OpenTelemetry.Logs.LogRecord.SeverityText.get -> string? -OpenTelemetry.Logs.LogRecord.SeverityText.set -> void -OpenTelemetry.Logs.LogRecordExportProcessorOptions -OpenTelemetry.Logs.LogRecordExportProcessorOptions.BatchExportProcessorOptions.get -> OpenTelemetry.Logs.BatchExportLogRecordProcessorOptions! -OpenTelemetry.Logs.LogRecordExportProcessorOptions.BatchExportProcessorOptions.set -> void -OpenTelemetry.Logs.LogRecordExportProcessorOptions.ExportProcessorType.get -> OpenTelemetry.ExportProcessorType -OpenTelemetry.Logs.LogRecordExportProcessorOptions.ExportProcessorType.set -> void -OpenTelemetry.Logs.LogRecordExportProcessorOptions.LogRecordExportProcessorOptions() -> void -OpenTelemetry.Metrics.AlwaysOffExemplarFilter -OpenTelemetry.Metrics.AlwaysOffExemplarFilter.AlwaysOffExemplarFilter() -> void -OpenTelemetry.Metrics.AlwaysOnExemplarFilter -OpenTelemetry.Metrics.AlwaysOnExemplarFilter.AlwaysOnExemplarFilter() -> void -OpenTelemetry.Metrics.Exemplar -OpenTelemetry.Metrics.Exemplar.DoubleValue.get -> double -OpenTelemetry.Metrics.Exemplar.Exemplar() -> void -OpenTelemetry.Metrics.Exemplar.SpanId.get -> System.Diagnostics.ActivitySpanId? -OpenTelemetry.Metrics.Exemplar.Timestamp.get -> System.DateTimeOffset -OpenTelemetry.Metrics.Exemplar.TraceId.get -> System.Diagnostics.ActivityTraceId? -OpenTelemetry.Metrics.ExemplarFilter -OpenTelemetry.Metrics.ExemplarFilter.ExemplarFilter() -> void -OpenTelemetry.Metrics.MetricPoint.GetExemplars() -> OpenTelemetry.Metrics.Exemplar[]! -OpenTelemetry.Metrics.TraceBasedExemplarFilter -OpenTelemetry.Metrics.TraceBasedExemplarFilter.TraceBasedExemplarFilter() -> void -static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, System.Func!>! implementationFactory) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.Build(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder) -> OpenTelemetry.Logs.LoggerProvider! -static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.ConfigureResource(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, System.Action! configure) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.SetResourceBuilder(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, OpenTelemetry.Resources.ResourceBuilder! resourceBuilder) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.LoggerProviderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProvider! provider, OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Logs.LoggerProvider! -static OpenTelemetry.Logs.LoggerProviderExtensions.ForceFlush(this OpenTelemetry.Logs.LoggerProvider! provider, int timeoutMilliseconds = -1) -> bool -static OpenTelemetry.Logs.LoggerProviderExtensions.Shutdown(this OpenTelemetry.Logs.LoggerProvider! provider, int timeoutMilliseconds = -1) -> bool -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetExemplarFilter(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, OpenTelemetry.Metrics.ExemplarFilter! exemplarFilter) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Sdk.CreateLoggerProviderBuilder() -> OpenTelemetry.Logs.LoggerProviderBuilder! -~abstract OpenTelemetry.Metrics.ExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool -~abstract OpenTelemetry.Metrics.ExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool -~OpenTelemetry.Metrics.Exemplar.FilteredTags.get -> System.Collections.Generic.List> -~override OpenTelemetry.Metrics.AlwaysOffExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool -~override OpenTelemetry.Metrics.AlwaysOffExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool -~override OpenTelemetry.Metrics.AlwaysOnExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool -~override OpenTelemetry.Metrics.AlwaysOnExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool -~override OpenTelemetry.Metrics.TraceBasedExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool -~override OpenTelemetry.Metrics.TraceBasedExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool diff --git a/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Shipped.txt deleted file mode 100644 index 493d28769f2..00000000000 --- a/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Shipped.txt +++ /dev/null @@ -1,413 +0,0 @@ -#nullable enable -~OpenTelemetry.Batch.GetEnumerator() -> OpenTelemetry.Batch.Enumerator -~OpenTelemetry.Metrics.BaseExportingMetricReader.BaseExportingMetricReader(OpenTelemetry.BaseExporter exporter) -> void -~OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration.Boundaries.get -> double[] -~OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration.Boundaries.set -> void -~OpenTelemetry.Metrics.IPullMetricExporter.Collect.get -> System.Func -~OpenTelemetry.Metrics.IPullMetricExporter.Collect.set -> void -~OpenTelemetry.Metrics.Metric.Description.get -> string -~OpenTelemetry.Metrics.Metric.MeterName.get -> string -~OpenTelemetry.Metrics.Metric.MeterVersion.get -> string -~OpenTelemetry.Metrics.Metric.Name.get -> string -~OpenTelemetry.Metrics.Metric.Unit.get -> string -~OpenTelemetry.Metrics.MetricStreamConfiguration.Description.get -> string -~OpenTelemetry.Metrics.MetricStreamConfiguration.Description.set -> void -~OpenTelemetry.Metrics.MetricStreamConfiguration.Name.get -> string -~OpenTelemetry.Metrics.MetricStreamConfiguration.Name.set -> void -~OpenTelemetry.Metrics.MetricStreamConfiguration.TagKeys.get -> string[] -~OpenTelemetry.Metrics.MetricStreamConfiguration.TagKeys.set -> void -~OpenTelemetry.Metrics.PeriodicExportingMetricReader.PeriodicExportingMetricReader(OpenTelemetry.BaseExporter exporter, int exportIntervalMilliseconds = 60000, int exportTimeoutMilliseconds = 30000) -> void -~override OpenTelemetry.Metrics.MeterProviderBuilderBase.AddInstrumentation(System.Func! instrumentationFactory) -> OpenTelemetry.Metrics.MeterProviderBuilder! -~override OpenTelemetry.Trace.TracerProviderBuilderBase.AddInstrumentation(System.Func! instrumentationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder! -~readonly OpenTelemetry.Metrics.BaseExportingMetricReader.exporter -> OpenTelemetry.BaseExporter -~static OpenTelemetry.Metrics.MeterProviderExtensions.ForceFlush(this OpenTelemetry.Metrics.MeterProvider provider, int timeoutMilliseconds = -1) -> bool -~static OpenTelemetry.Metrics.MeterProviderExtensions.Shutdown(this OpenTelemetry.Metrics.MeterProvider provider, int timeoutMilliseconds = -1) -> bool -~static OpenTelemetry.Metrics.MetricStreamConfiguration.Drop.get -> OpenTelemetry.Metrics.MetricStreamConfiguration -abstract OpenTelemetry.BaseExporter.Export(in OpenTelemetry.Batch batch) -> OpenTelemetry.ExportResult -abstract OpenTelemetry.BaseExportProcessor.OnExport(T! data) -> void -abstract OpenTelemetry.Trace.Sampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions -OpenTelemetry.BaseExporter -OpenTelemetry.BaseExporter.BaseExporter() -> void -OpenTelemetry.BaseExporter.Dispose() -> void -OpenTelemetry.BaseExporter.ForceFlush(int timeoutMilliseconds = -1) -> bool -OpenTelemetry.BaseExporter.ParentProvider.get -> OpenTelemetry.BaseProvider? -OpenTelemetry.BaseExporter.Shutdown(int timeoutMilliseconds = -1) -> bool -OpenTelemetry.BaseExportProcessor -OpenTelemetry.BaseExportProcessor.BaseExportProcessor(OpenTelemetry.BaseExporter! exporter) -> void -OpenTelemetry.BaseProcessor -OpenTelemetry.BaseProcessor.BaseProcessor() -> void -OpenTelemetry.BaseProcessor.Dispose() -> void -OpenTelemetry.BaseProcessor.ForceFlush(int timeoutMilliseconds = -1) -> bool -OpenTelemetry.BaseProcessor.ParentProvider.get -> OpenTelemetry.BaseProvider? -OpenTelemetry.BaseProcessor.Shutdown(int timeoutMilliseconds = -1) -> bool -OpenTelemetry.Batch -OpenTelemetry.Batch.Batch() -> void -OpenTelemetry.Batch.Batch(T![]! items, int count) -> void -OpenTelemetry.Batch.Count.get -> long -OpenTelemetry.Batch.Dispose() -> void -OpenTelemetry.Batch.Enumerator -OpenTelemetry.Batch.Enumerator.Current.get -> T! -OpenTelemetry.Batch.Enumerator.Dispose() -> void -OpenTelemetry.Batch.Enumerator.Enumerator() -> void -OpenTelemetry.Batch.Enumerator.MoveNext() -> bool -OpenTelemetry.Batch.Enumerator.Reset() -> void -OpenTelemetry.BatchActivityExportProcessor -OpenTelemetry.BatchActivityExportProcessor.BatchActivityExportProcessor(OpenTelemetry.BaseExporter! exporter, int maxQueueSize = 2048, int scheduledDelayMilliseconds = 5000, int exporterTimeoutMilliseconds = 30000, int maxExportBatchSize = 512) -> void -OpenTelemetry.BatchExportProcessor -OpenTelemetry.BatchExportProcessor.BatchExportProcessor(OpenTelemetry.BaseExporter! exporter, int maxQueueSize = 2048, int scheduledDelayMilliseconds = 5000, int exporterTimeoutMilliseconds = 30000, int maxExportBatchSize = 512) -> void -OpenTelemetry.BatchExportProcessorOptions -OpenTelemetry.BatchExportProcessorOptions.BatchExportProcessorOptions() -> void -OpenTelemetry.BatchExportProcessorOptions.ExporterTimeoutMilliseconds.get -> int -OpenTelemetry.BatchExportProcessorOptions.ExporterTimeoutMilliseconds.set -> void -OpenTelemetry.BatchExportProcessorOptions.MaxExportBatchSize.get -> int -OpenTelemetry.BatchExportProcessorOptions.MaxExportBatchSize.set -> void -OpenTelemetry.BatchExportProcessorOptions.MaxQueueSize.get -> int -OpenTelemetry.BatchExportProcessorOptions.MaxQueueSize.set -> void -OpenTelemetry.BatchExportProcessorOptions.ScheduledDelayMilliseconds.get -> int -OpenTelemetry.BatchExportProcessorOptions.ScheduledDelayMilliseconds.set -> void -OpenTelemetry.BatchLogRecordExportProcessor -OpenTelemetry.BatchLogRecordExportProcessor.BatchLogRecordExportProcessor(OpenTelemetry.BaseExporter! exporter, int maxQueueSize = 2048, int scheduledDelayMilliseconds = 5000, int exporterTimeoutMilliseconds = 30000, int maxExportBatchSize = 512) -> void -OpenTelemetry.CompositeProcessor -OpenTelemetry.CompositeProcessor.AddProcessor(OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.CompositeProcessor! -OpenTelemetry.CompositeProcessor.CompositeProcessor(System.Collections.Generic.IEnumerable!>! processors) -> void -OpenTelemetry.ExportProcessorType -OpenTelemetry.ExportProcessorType.Batch = 1 -> OpenTelemetry.ExportProcessorType -OpenTelemetry.ExportProcessorType.Simple = 0 -> OpenTelemetry.ExportProcessorType -OpenTelemetry.ExportResult -OpenTelemetry.ExportResult.Failure = 1 -> OpenTelemetry.ExportResult -OpenTelemetry.ExportResult.Success = 0 -> OpenTelemetry.ExportResult -OpenTelemetry.Logs.LogRecord -OpenTelemetry.Logs.LogRecord.Attributes.get -> System.Collections.Generic.IReadOnlyList>? -OpenTelemetry.Logs.LogRecord.Attributes.set -> void -OpenTelemetry.Logs.LogRecord.Body.get -> string? -OpenTelemetry.Logs.LogRecord.Body.set -> void -OpenTelemetry.Logs.LogRecord.CategoryName.get -> string? -OpenTelemetry.Logs.LogRecord.CategoryName.set -> void -OpenTelemetry.Logs.LogRecord.EventId.get -> Microsoft.Extensions.Logging.EventId -OpenTelemetry.Logs.LogRecord.EventId.set -> void -OpenTelemetry.Logs.LogRecord.Exception.get -> System.Exception? -OpenTelemetry.Logs.LogRecord.Exception.set -> void -OpenTelemetry.Logs.LogRecord.ForEachScope(System.Action! callback, TState state) -> void -OpenTelemetry.Logs.LogRecord.FormattedMessage.get -> string? -OpenTelemetry.Logs.LogRecord.FormattedMessage.set -> void -OpenTelemetry.Logs.LogRecord.LogLevel.get -> Microsoft.Extensions.Logging.LogLevel -OpenTelemetry.Logs.LogRecord.LogLevel.set -> void -OpenTelemetry.Logs.LogRecord.SpanId.get -> System.Diagnostics.ActivitySpanId -OpenTelemetry.Logs.LogRecord.SpanId.set -> void -OpenTelemetry.Logs.LogRecord.State.get -> object? -OpenTelemetry.Logs.LogRecord.State.set -> void -OpenTelemetry.Logs.LogRecord.StateValues.get -> System.Collections.Generic.IReadOnlyList>? -OpenTelemetry.Logs.LogRecord.StateValues.set -> void -OpenTelemetry.Logs.LogRecord.Timestamp.get -> System.DateTime -OpenTelemetry.Logs.LogRecord.Timestamp.set -> void -OpenTelemetry.Logs.LogRecord.TraceFlags.get -> System.Diagnostics.ActivityTraceFlags -OpenTelemetry.Logs.LogRecord.TraceFlags.set -> void -OpenTelemetry.Logs.LogRecord.TraceId.get -> System.Diagnostics.ActivityTraceId -OpenTelemetry.Logs.LogRecord.TraceId.set -> void -OpenTelemetry.Logs.LogRecord.TraceState.get -> string? -OpenTelemetry.Logs.LogRecord.TraceState.set -> void -OpenTelemetry.Logs.LogRecordScope -OpenTelemetry.Logs.LogRecordScope.Enumerator -OpenTelemetry.Logs.LogRecordScope.Enumerator.Current.get -> System.Collections.Generic.KeyValuePair -OpenTelemetry.Logs.LogRecordScope.Enumerator.Dispose() -> void -OpenTelemetry.Logs.LogRecordScope.Enumerator.Enumerator() -> void -OpenTelemetry.Logs.LogRecordScope.Enumerator.Enumerator(object? scope) -> void -OpenTelemetry.Logs.LogRecordScope.Enumerator.MoveNext() -> bool -OpenTelemetry.Logs.LogRecordScope.Enumerator.Reset() -> void -OpenTelemetry.Logs.LogRecordScope.GetEnumerator() -> OpenTelemetry.Logs.LogRecordScope.Enumerator -OpenTelemetry.Logs.LogRecordScope.LogRecordScope() -> void -OpenTelemetry.Logs.LogRecordScope.Scope.get -> object? -OpenTelemetry.Logs.OpenTelemetryLoggerOptions -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.AddProcessor(OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.IncludeFormattedMessage.get -> bool -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.IncludeFormattedMessage.set -> void -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.IncludeScopes.get -> bool -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.IncludeScopes.set -> void -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.OpenTelemetryLoggerOptions() -> void -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.ParseStateValues.get -> bool -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.ParseStateValues.set -> void -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.SetResourceBuilder(OpenTelemetry.Resources.ResourceBuilder! resourceBuilder) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! -OpenTelemetry.Logs.OpenTelemetryLoggerProvider -OpenTelemetry.Logs.OpenTelemetryLoggerProvider.CreateLogger(string! categoryName) -> Microsoft.Extensions.Logging.ILogger! -OpenTelemetry.Logs.OpenTelemetryLoggerProvider.OpenTelemetryLoggerProvider(Microsoft.Extensions.Options.IOptionsMonitor! options) -> void -OpenTelemetry.Metrics.AggregationTemporality -OpenTelemetry.Metrics.AggregationTemporality.Cumulative = 1 -> OpenTelemetry.Metrics.AggregationTemporality -OpenTelemetry.Metrics.AggregationTemporality.Delta = 2 -> OpenTelemetry.Metrics.AggregationTemporality -OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration -OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.Base2ExponentialBucketHistogramConfiguration() -> void -OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxScale.get -> int -OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxScale.set -> void -OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxSize.get -> int -OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxSize.set -> void -OpenTelemetry.Metrics.BaseExportingMetricReader -OpenTelemetry.Metrics.BaseExportingMetricReader.SupportedExportModes.get -> OpenTelemetry.Metrics.ExportModes -OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration -OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration.ExplicitBucketHistogramConfiguration() -> void -OpenTelemetry.Metrics.ExponentialHistogramBuckets -OpenTelemetry.Metrics.ExponentialHistogramBuckets.Enumerator -OpenTelemetry.Metrics.ExponentialHistogramBuckets.Enumerator.Current.get -> long -OpenTelemetry.Metrics.ExponentialHistogramBuckets.Enumerator.Enumerator() -> void -OpenTelemetry.Metrics.ExponentialHistogramBuckets.Enumerator.MoveNext() -> bool -OpenTelemetry.Metrics.ExponentialHistogramBuckets.GetEnumerator() -> OpenTelemetry.Metrics.ExponentialHistogramBuckets.Enumerator -OpenTelemetry.Metrics.ExponentialHistogramBuckets.Offset.get -> int -OpenTelemetry.Metrics.ExponentialHistogramData -OpenTelemetry.Metrics.ExponentialHistogramData.PositiveBuckets.get -> OpenTelemetry.Metrics.ExponentialHistogramBuckets! -OpenTelemetry.Metrics.ExponentialHistogramData.Scale.get -> int -OpenTelemetry.Metrics.ExponentialHistogramData.ZeroCount.get -> long -OpenTelemetry.Metrics.ExportModes -OpenTelemetry.Metrics.ExportModes.Pull = 2 -> OpenTelemetry.Metrics.ExportModes -OpenTelemetry.Metrics.ExportModes.Push = 1 -> OpenTelemetry.Metrics.ExportModes -OpenTelemetry.Metrics.ExportModesAttribute -OpenTelemetry.Metrics.ExportModesAttribute.ExportModesAttribute(OpenTelemetry.Metrics.ExportModes supported) -> void -OpenTelemetry.Metrics.ExportModesAttribute.Supported.get -> OpenTelemetry.Metrics.ExportModes -OpenTelemetry.Metrics.HistogramBucket -OpenTelemetry.Metrics.HistogramBucket.BucketCount.get -> long -OpenTelemetry.Metrics.HistogramBucket.ExplicitBound.get -> double -OpenTelemetry.Metrics.HistogramBucket.HistogramBucket() -> void -OpenTelemetry.Metrics.HistogramBuckets -OpenTelemetry.Metrics.HistogramBuckets.Enumerator -OpenTelemetry.Metrics.HistogramBuckets.Enumerator.Current.get -> OpenTelemetry.Metrics.HistogramBucket -OpenTelemetry.Metrics.HistogramBuckets.Enumerator.Enumerator() -> void -OpenTelemetry.Metrics.HistogramBuckets.Enumerator.MoveNext() -> bool -OpenTelemetry.Metrics.HistogramBuckets.GetEnumerator() -> OpenTelemetry.Metrics.HistogramBuckets.Enumerator -OpenTelemetry.Metrics.HistogramConfiguration -OpenTelemetry.Metrics.HistogramConfiguration.HistogramConfiguration() -> void -OpenTelemetry.Metrics.HistogramConfiguration.RecordMinMax.get -> bool -OpenTelemetry.Metrics.HistogramConfiguration.RecordMinMax.set -> void -OpenTelemetry.Metrics.IPullMetricExporter -OpenTelemetry.Metrics.MeterProviderBuilderBase -OpenTelemetry.Metrics.MeterProviderBuilderBase.Build() -> OpenTelemetry.Metrics.MeterProvider! -OpenTelemetry.Metrics.MeterProviderBuilderBase.MeterProviderBuilderBase() -> void -OpenTelemetry.Metrics.MeterProviderBuilderExtensions -OpenTelemetry.Metrics.MeterProviderExtensions -OpenTelemetry.Metrics.Metric -OpenTelemetry.Metrics.Metric.GetMetricPoints() -> OpenTelemetry.Metrics.MetricPointsAccessor -OpenTelemetry.Metrics.Metric.MetricType.get -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.Metric.Temporality.get -> OpenTelemetry.Metrics.AggregationTemporality -OpenTelemetry.Metrics.MetricPoint -OpenTelemetry.Metrics.MetricPoint.EndTime.get -> System.DateTimeOffset -OpenTelemetry.Metrics.MetricPoint.GetExponentialHistogramData() -> OpenTelemetry.Metrics.ExponentialHistogramData! -OpenTelemetry.Metrics.MetricPoint.GetGaugeLastValueDouble() -> double -OpenTelemetry.Metrics.MetricPoint.GetGaugeLastValueLong() -> long -OpenTelemetry.Metrics.MetricPoint.GetHistogramBuckets() -> OpenTelemetry.Metrics.HistogramBuckets! -OpenTelemetry.Metrics.MetricPoint.GetHistogramCount() -> long -OpenTelemetry.Metrics.MetricPoint.GetHistogramSum() -> double -OpenTelemetry.Metrics.MetricPoint.GetSumDouble() -> double -OpenTelemetry.Metrics.MetricPoint.GetSumLong() -> long -OpenTelemetry.Metrics.MetricPoint.MetricPoint() -> void -OpenTelemetry.Metrics.MetricPoint.StartTime.get -> System.DateTimeOffset -OpenTelemetry.Metrics.MetricPoint.Tags.get -> OpenTelemetry.ReadOnlyTagCollection -OpenTelemetry.Metrics.MetricPoint.TryGetHistogramMinMaxValues(out double min, out double max) -> bool -OpenTelemetry.Metrics.MetricPointsAccessor -OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator -OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator.Current.get -> OpenTelemetry.Metrics.MetricPoint -OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator.Enumerator() -> void -OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator.MoveNext() -> bool -OpenTelemetry.Metrics.MetricPointsAccessor.GetEnumerator() -> OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator -OpenTelemetry.Metrics.MetricPointsAccessor.MetricPointsAccessor() -> void -OpenTelemetry.Metrics.MetricReader -OpenTelemetry.Metrics.MetricReader.Collect(int timeoutMilliseconds = -1) -> bool -OpenTelemetry.Metrics.MetricReader.Dispose() -> void -OpenTelemetry.Metrics.MetricReader.MetricReader() -> void -OpenTelemetry.Metrics.MetricReader.Shutdown(int timeoutMilliseconds = -1) -> bool -OpenTelemetry.Metrics.MetricReader.TemporalityPreference.get -> OpenTelemetry.Metrics.MetricReaderTemporalityPreference -OpenTelemetry.Metrics.MetricReader.TemporalityPreference.set -> void -OpenTelemetry.Metrics.MetricReaderOptions -OpenTelemetry.Metrics.MetricReaderOptions.MetricReaderOptions() -> void -OpenTelemetry.Metrics.MetricReaderOptions.PeriodicExportingMetricReaderOptions.get -> OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions! -OpenTelemetry.Metrics.MetricReaderOptions.PeriodicExportingMetricReaderOptions.set -> void -OpenTelemetry.Metrics.MetricReaderOptions.TemporalityPreference.get -> OpenTelemetry.Metrics.MetricReaderTemporalityPreference -OpenTelemetry.Metrics.MetricReaderOptions.TemporalityPreference.set -> void -OpenTelemetry.Metrics.MetricReaderTemporalityPreference -OpenTelemetry.Metrics.MetricReaderTemporalityPreference.Cumulative = 1 -> OpenTelemetry.Metrics.MetricReaderTemporalityPreference -OpenTelemetry.Metrics.MetricReaderTemporalityPreference.Delta = 2 -> OpenTelemetry.Metrics.MetricReaderTemporalityPreference -OpenTelemetry.Metrics.MetricStreamConfiguration -OpenTelemetry.Metrics.MetricStreamConfiguration.MetricStreamConfiguration() -> void -OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricType.DoubleGauge = 45 -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricType.DoubleSum = 29 -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricType.DoubleSumNonMonotonic = 141 -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricType.ExponentialHistogram = 80 -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricType.Histogram = 64 -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricType.LongGauge = 42 -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricType.LongSum = 26 -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricType.LongSumNonMonotonic = 138 -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricTypeExtensions -OpenTelemetry.Metrics.PeriodicExportingMetricReader -OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions -OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds.get -> int? -OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds.set -> void -OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions.ExportTimeoutMilliseconds.get -> int? -OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions.ExportTimeoutMilliseconds.set -> void -OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions.PeriodicExportingMetricReaderOptions() -> void -OpenTelemetry.ProviderExtensions -OpenTelemetry.ReadOnlyTagCollection -OpenTelemetry.ReadOnlyTagCollection.Count.get -> int -OpenTelemetry.ReadOnlyTagCollection.Enumerator -OpenTelemetry.ReadOnlyTagCollection.Enumerator.Current.get -> System.Collections.Generic.KeyValuePair -OpenTelemetry.ReadOnlyTagCollection.Enumerator.Enumerator() -> void -OpenTelemetry.ReadOnlyTagCollection.Enumerator.MoveNext() -> bool -OpenTelemetry.ReadOnlyTagCollection.GetEnumerator() -> OpenTelemetry.ReadOnlyTagCollection.Enumerator -OpenTelemetry.ReadOnlyTagCollection.ReadOnlyTagCollection() -> void -OpenTelemetry.Resources.IResourceDetector -OpenTelemetry.Resources.IResourceDetector.Detect() -> OpenTelemetry.Resources.Resource! -OpenTelemetry.Resources.Resource -OpenTelemetry.Resources.Resource.Attributes.get -> System.Collections.Generic.IEnumerable>! -OpenTelemetry.Resources.Resource.Merge(OpenTelemetry.Resources.Resource! other) -> OpenTelemetry.Resources.Resource! -OpenTelemetry.Resources.Resource.Resource(System.Collections.Generic.IEnumerable>! attributes) -> void -OpenTelemetry.Resources.ResourceBuilder -OpenTelemetry.Resources.ResourceBuilder.AddDetector(OpenTelemetry.Resources.IResourceDetector! resourceDetector) -> OpenTelemetry.Resources.ResourceBuilder! -OpenTelemetry.Resources.ResourceBuilder.AddDetector(System.Func! resourceDetectorFactory) -> OpenTelemetry.Resources.ResourceBuilder! -OpenTelemetry.Resources.ResourceBuilder.Build() -> OpenTelemetry.Resources.Resource! -OpenTelemetry.Resources.ResourceBuilder.Clear() -> OpenTelemetry.Resources.ResourceBuilder! -OpenTelemetry.Resources.ResourceBuilderExtensions -OpenTelemetry.Sdk -OpenTelemetry.SimpleActivityExportProcessor -OpenTelemetry.SimpleActivityExportProcessor.SimpleActivityExportProcessor(OpenTelemetry.BaseExporter! exporter) -> void -OpenTelemetry.SimpleExportProcessor -OpenTelemetry.SimpleExportProcessor.SimpleExportProcessor(OpenTelemetry.BaseExporter! exporter) -> void -OpenTelemetry.SimpleLogRecordExportProcessor -OpenTelemetry.SimpleLogRecordExportProcessor.SimpleLogRecordExportProcessor(OpenTelemetry.BaseExporter! exporter) -> void -OpenTelemetry.SuppressInstrumentationScope -OpenTelemetry.SuppressInstrumentationScope.Dispose() -> void -OpenTelemetry.Trace.AlwaysOffSampler -OpenTelemetry.Trace.AlwaysOffSampler.AlwaysOffSampler() -> void -OpenTelemetry.Trace.AlwaysOnSampler -OpenTelemetry.Trace.AlwaysOnSampler.AlwaysOnSampler() -> void -OpenTelemetry.Trace.BatchExportActivityProcessorOptions -OpenTelemetry.Trace.BatchExportActivityProcessorOptions.BatchExportActivityProcessorOptions() -> void -OpenTelemetry.Trace.ParentBasedSampler -OpenTelemetry.Trace.ParentBasedSampler.ParentBasedSampler(OpenTelemetry.Trace.Sampler! rootSampler) -> void -OpenTelemetry.Trace.ParentBasedSampler.ParentBasedSampler(OpenTelemetry.Trace.Sampler! rootSampler, OpenTelemetry.Trace.Sampler? remoteParentSampled = null, OpenTelemetry.Trace.Sampler? remoteParentNotSampled = null, OpenTelemetry.Trace.Sampler? localParentSampled = null, OpenTelemetry.Trace.Sampler? localParentNotSampled = null) -> void -OpenTelemetry.Trace.Sampler -OpenTelemetry.Trace.Sampler.Description.get -> string! -OpenTelemetry.Trace.Sampler.Description.set -> void -OpenTelemetry.Trace.Sampler.Sampler() -> void -OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingDecision.Drop = 0 -> OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingDecision.RecordAndSample = 2 -> OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingDecision.RecordOnly = 1 -> OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingParameters -OpenTelemetry.Trace.SamplingParameters.Kind.get -> System.Diagnostics.ActivityKind -OpenTelemetry.Trace.SamplingParameters.Links.get -> System.Collections.Generic.IEnumerable? -OpenTelemetry.Trace.SamplingParameters.Name.get -> string! -OpenTelemetry.Trace.SamplingParameters.ParentContext.get -> System.Diagnostics.ActivityContext -OpenTelemetry.Trace.SamplingParameters.SamplingParameters() -> void -OpenTelemetry.Trace.SamplingParameters.SamplingParameters(System.Diagnostics.ActivityContext parentContext, System.Diagnostics.ActivityTraceId traceId, string! name, System.Diagnostics.ActivityKind kind, System.Collections.Generic.IEnumerable>? tags = null, System.Collections.Generic.IEnumerable? links = null) -> void -OpenTelemetry.Trace.SamplingParameters.Tags.get -> System.Collections.Generic.IEnumerable>? -OpenTelemetry.Trace.SamplingParameters.TraceId.get -> System.Diagnostics.ActivityTraceId -OpenTelemetry.Trace.SamplingResult -OpenTelemetry.Trace.SamplingResult.Attributes.get -> System.Collections.Generic.IEnumerable>! -OpenTelemetry.Trace.SamplingResult.Decision.get -> OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingResult.Equals(OpenTelemetry.Trace.SamplingResult other) -> bool -OpenTelemetry.Trace.SamplingResult.SamplingResult() -> void -OpenTelemetry.Trace.SamplingResult.SamplingResult(bool isSampled) -> void -OpenTelemetry.Trace.SamplingResult.SamplingResult(OpenTelemetry.Trace.SamplingDecision decision) -> void -OpenTelemetry.Trace.SamplingResult.SamplingResult(OpenTelemetry.Trace.SamplingDecision decision, string! traceStateString) -> void -OpenTelemetry.Trace.SamplingResult.SamplingResult(OpenTelemetry.Trace.SamplingDecision decision, System.Collections.Generic.IEnumerable>! attributes) -> void -OpenTelemetry.Trace.SamplingResult.SamplingResult(OpenTelemetry.Trace.SamplingDecision decision, System.Collections.Generic.IEnumerable>? attributes, string? traceStateString) -> void -OpenTelemetry.Trace.SamplingResult.TraceStateString.get -> string? -OpenTelemetry.Trace.TraceIdRatioBasedSampler -OpenTelemetry.Trace.TraceIdRatioBasedSampler.TraceIdRatioBasedSampler(double probability) -> void -OpenTelemetry.Trace.TracerProviderBuilderBase -OpenTelemetry.Trace.TracerProviderBuilderBase.AddInstrumentation(string! instrumentationName, string! instrumentationVersion, System.Func! instrumentationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder! -OpenTelemetry.Trace.TracerProviderBuilderBase.Build() -> OpenTelemetry.Trace.TracerProvider! -OpenTelemetry.Trace.TracerProviderBuilderBase.TracerProviderBuilderBase() -> void -OpenTelemetry.Trace.TracerProviderBuilderExtensions -OpenTelemetry.Trace.TracerProviderExtensions -override OpenTelemetry.BaseExportProcessor.Dispose(bool disposing) -> void -override OpenTelemetry.BaseExportProcessor.OnEnd(T! data) -> void -override OpenTelemetry.BaseExportProcessor.OnForceFlush(int timeoutMilliseconds) -> bool -override OpenTelemetry.BaseExportProcessor.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.BaseExportProcessor.ToString() -> string! -override OpenTelemetry.BaseProcessor.ToString() -> string! -override OpenTelemetry.BatchActivityExportProcessor.OnEnd(System.Diagnostics.Activity! data) -> void -override OpenTelemetry.BatchExportProcessor.Dispose(bool disposing) -> void -override OpenTelemetry.BatchExportProcessor.OnExport(T! data) -> void -override OpenTelemetry.BatchExportProcessor.OnForceFlush(int timeoutMilliseconds) -> bool -override OpenTelemetry.BatchExportProcessor.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.BatchLogRecordExportProcessor.OnEnd(OpenTelemetry.Logs.LogRecord! data) -> void -override OpenTelemetry.CompositeProcessor.Dispose(bool disposing) -> void -override OpenTelemetry.CompositeProcessor.OnEnd(T data) -> void -override OpenTelemetry.CompositeProcessor.OnForceFlush(int timeoutMilliseconds) -> bool -override OpenTelemetry.CompositeProcessor.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.CompositeProcessor.OnStart(T data) -> void -override OpenTelemetry.Logs.OpenTelemetryLoggerProvider.Dispose(bool disposing) -> void -override OpenTelemetry.Metrics.BaseExportingMetricReader.Dispose(bool disposing) -> void -override OpenTelemetry.Metrics.BaseExportingMetricReader.OnCollect(int timeoutMilliseconds) -> bool -override OpenTelemetry.Metrics.BaseExportingMetricReader.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.Metrics.MeterProviderBuilderBase.AddMeter(params string![]! names) -> OpenTelemetry.Metrics.MeterProviderBuilder! -override OpenTelemetry.Metrics.PeriodicExportingMetricReader.Dispose(bool disposing) -> void -override OpenTelemetry.Metrics.PeriodicExportingMetricReader.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.SimpleActivityExportProcessor.OnEnd(System.Diagnostics.Activity! data) -> void -override OpenTelemetry.SimpleExportProcessor.OnExport(T! data) -> void -override OpenTelemetry.Trace.AlwaysOffSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -override OpenTelemetry.Trace.AlwaysOnSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -override OpenTelemetry.Trace.ParentBasedSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -override OpenTelemetry.Trace.SamplingResult.Equals(object? obj) -> bool -override OpenTelemetry.Trace.SamplingResult.GetHashCode() -> int -override OpenTelemetry.Trace.TraceIdRatioBasedSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -override OpenTelemetry.Trace.TracerProviderBuilderBase.AddLegacySource(string! operationName) -> OpenTelemetry.Trace.TracerProviderBuilder! -override OpenTelemetry.Trace.TracerProviderBuilderBase.AddSource(params string![]! names) -> OpenTelemetry.Trace.TracerProviderBuilder! -override sealed OpenTelemetry.BaseExportProcessor.OnStart(T! data) -> void -readonly OpenTelemetry.BaseExportProcessor.exporter -> OpenTelemetry.BaseExporter! -static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.AddOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder) -> Microsoft.Extensions.Logging.ILoggingBuilder! -static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.AddOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, System.Action? configure) -> Microsoft.Extensions.Logging.ILoggingBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddReader(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, OpenTelemetry.Metrics.MetricReader! reader) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddReader(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, System.Func! implementationFactory) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddReader(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddView(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, string! instrumentName, OpenTelemetry.Metrics.MetricStreamConfiguration! metricStreamConfiguration) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddView(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, string! instrumentName, string! name) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddView(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, System.Func! viewConfig) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.Build(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder) -> OpenTelemetry.Metrics.MeterProvider? -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.ConfigureResource(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, System.Action! configure) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetMaxMetricPointsPerMetricStream(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, int maxMetricPointsPerMetricStream) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetMaxMetricStreams(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, int maxMetricStreams) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetResourceBuilder(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, OpenTelemetry.Resources.ResourceBuilder! resourceBuilder) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MetricTypeExtensions.IsDouble(this OpenTelemetry.Metrics.MetricType self) -> bool -static OpenTelemetry.Metrics.MetricTypeExtensions.IsGauge(this OpenTelemetry.Metrics.MetricType self) -> bool -static OpenTelemetry.Metrics.MetricTypeExtensions.IsHistogram(this OpenTelemetry.Metrics.MetricType self) -> bool -static OpenTelemetry.Metrics.MetricTypeExtensions.IsLong(this OpenTelemetry.Metrics.MetricType self) -> bool -static OpenTelemetry.Metrics.MetricTypeExtensions.IsSum(this OpenTelemetry.Metrics.MetricType self) -> bool -static OpenTelemetry.ProviderExtensions.GetDefaultResource(this OpenTelemetry.BaseProvider! baseProvider) -> OpenTelemetry.Resources.Resource! -static OpenTelemetry.ProviderExtensions.GetResource(this OpenTelemetry.BaseProvider! baseProvider) -> OpenTelemetry.Resources.Resource! -static OpenTelemetry.Resources.Resource.Empty.get -> OpenTelemetry.Resources.Resource! -static OpenTelemetry.Resources.ResourceBuilder.CreateDefault() -> OpenTelemetry.Resources.ResourceBuilder! -static OpenTelemetry.Resources.ResourceBuilder.CreateEmpty() -> OpenTelemetry.Resources.ResourceBuilder! -static OpenTelemetry.Resources.ResourceBuilderExtensions.AddAttributes(this OpenTelemetry.Resources.ResourceBuilder! resourceBuilder, System.Collections.Generic.IEnumerable>! attributes) -> OpenTelemetry.Resources.ResourceBuilder! -static OpenTelemetry.Resources.ResourceBuilderExtensions.AddEnvironmentVariableDetector(this OpenTelemetry.Resources.ResourceBuilder! resourceBuilder) -> OpenTelemetry.Resources.ResourceBuilder! -static OpenTelemetry.Resources.ResourceBuilderExtensions.AddService(this OpenTelemetry.Resources.ResourceBuilder! resourceBuilder, string! serviceName, string? serviceNamespace = null, string? serviceVersion = null, bool autoGenerateServiceInstanceId = true, string? serviceInstanceId = null) -> OpenTelemetry.Resources.ResourceBuilder! -static OpenTelemetry.Resources.ResourceBuilderExtensions.AddTelemetrySdk(this OpenTelemetry.Resources.ResourceBuilder! resourceBuilder) -> OpenTelemetry.Resources.ResourceBuilder! -static OpenTelemetry.Sdk.CreateMeterProviderBuilder() -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Sdk.CreateTracerProviderBuilder() -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Sdk.SetDefaultTextMapPropagator(OpenTelemetry.Context.Propagation.TextMapPropagator! textMapPropagator) -> void -static OpenTelemetry.Sdk.SuppressInstrumentation.get -> bool -static OpenTelemetry.SuppressInstrumentationScope.Begin(bool value = true) -> System.IDisposable! -static OpenTelemetry.SuppressInstrumentationScope.Enter() -> int -static OpenTelemetry.Trace.SamplingResult.operator !=(OpenTelemetry.Trace.SamplingResult decision1, OpenTelemetry.Trace.SamplingResult decision2) -> bool -static OpenTelemetry.Trace.SamplingResult.operator ==(OpenTelemetry.Trace.SamplingResult decision1, OpenTelemetry.Trace.SamplingResult decision2) -> bool -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, System.Func!>! implementationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.Build(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder) -> OpenTelemetry.Trace.TracerProvider? -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.ConfigureResource(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, System.Action! configure) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.SetErrorStatusOnException(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, bool enabled = true) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.SetResourceBuilder(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, OpenTelemetry.Resources.ResourceBuilder! resourceBuilder) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.SetSampler(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, OpenTelemetry.Trace.Sampler! sampler) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.SetSampler(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, System.Func! implementationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.SetSampler(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderExtensions.AddProcessor(this OpenTelemetry.Trace.TracerProvider! provider, OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Trace.TracerProvider! -static OpenTelemetry.Trace.TracerProviderExtensions.ForceFlush(this OpenTelemetry.Trace.TracerProvider! provider, int timeoutMilliseconds = -1) -> bool -static OpenTelemetry.Trace.TracerProviderExtensions.Shutdown(this OpenTelemetry.Trace.TracerProvider! provider, int timeoutMilliseconds = -1) -> bool -virtual OpenTelemetry.BaseExporter.Dispose(bool disposing) -> void -virtual OpenTelemetry.BaseExporter.OnForceFlush(int timeoutMilliseconds) -> bool -virtual OpenTelemetry.BaseExporter.OnShutdown(int timeoutMilliseconds) -> bool -virtual OpenTelemetry.BaseProcessor.Dispose(bool disposing) -> void -virtual OpenTelemetry.BaseProcessor.OnEnd(T data) -> void -virtual OpenTelemetry.BaseProcessor.OnForceFlush(int timeoutMilliseconds) -> bool -virtual OpenTelemetry.BaseProcessor.OnShutdown(int timeoutMilliseconds) -> bool -virtual OpenTelemetry.BaseProcessor.OnStart(T data) -> void -virtual OpenTelemetry.Metrics.MetricReader.Dispose(bool disposing) -> void -virtual OpenTelemetry.Metrics.MetricReader.OnCollect(int timeoutMilliseconds) -> bool -virtual OpenTelemetry.Metrics.MetricReader.OnShutdown(int timeoutMilliseconds) -> bool diff --git a/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt deleted file mode 100644 index e75f84d65db..00000000000 --- a/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,50 +0,0 @@ -OpenTelemetry.Logs.BatchExportLogRecordProcessorOptions -OpenTelemetry.Logs.BatchExportLogRecordProcessorOptions.BatchExportLogRecordProcessorOptions() -> void -OpenTelemetry.Logs.LoggerProviderBuilderExtensions -OpenTelemetry.Logs.LoggerProviderExtensions -OpenTelemetry.Logs.LogRecord.Logger.get -> OpenTelemetry.Logs.Logger? -OpenTelemetry.Logs.LogRecord.Severity.get -> OpenTelemetry.Logs.LogRecordSeverity? -OpenTelemetry.Logs.LogRecord.Severity.set -> void -OpenTelemetry.Logs.LogRecord.SeverityText.get -> string? -OpenTelemetry.Logs.LogRecord.SeverityText.set -> void -OpenTelemetry.Logs.LogRecordExportProcessorOptions -OpenTelemetry.Logs.LogRecordExportProcessorOptions.BatchExportProcessorOptions.get -> OpenTelemetry.Logs.BatchExportLogRecordProcessorOptions! -OpenTelemetry.Logs.LogRecordExportProcessorOptions.BatchExportProcessorOptions.set -> void -OpenTelemetry.Logs.LogRecordExportProcessorOptions.ExportProcessorType.get -> OpenTelemetry.ExportProcessorType -OpenTelemetry.Logs.LogRecordExportProcessorOptions.ExportProcessorType.set -> void -OpenTelemetry.Logs.LogRecordExportProcessorOptions.LogRecordExportProcessorOptions() -> void -OpenTelemetry.Metrics.AlwaysOffExemplarFilter -OpenTelemetry.Metrics.AlwaysOffExemplarFilter.AlwaysOffExemplarFilter() -> void -OpenTelemetry.Metrics.AlwaysOnExemplarFilter -OpenTelemetry.Metrics.AlwaysOnExemplarFilter.AlwaysOnExemplarFilter() -> void -OpenTelemetry.Metrics.Exemplar -OpenTelemetry.Metrics.Exemplar.DoubleValue.get -> double -OpenTelemetry.Metrics.Exemplar.Exemplar() -> void -OpenTelemetry.Metrics.Exemplar.SpanId.get -> System.Diagnostics.ActivitySpanId? -OpenTelemetry.Metrics.Exemplar.Timestamp.get -> System.DateTimeOffset -OpenTelemetry.Metrics.Exemplar.TraceId.get -> System.Diagnostics.ActivityTraceId? -OpenTelemetry.Metrics.ExemplarFilter -OpenTelemetry.Metrics.ExemplarFilter.ExemplarFilter() -> void -OpenTelemetry.Metrics.MetricPoint.GetExemplars() -> OpenTelemetry.Metrics.Exemplar[]! -OpenTelemetry.Metrics.TraceBasedExemplarFilter -OpenTelemetry.Metrics.TraceBasedExemplarFilter.TraceBasedExemplarFilter() -> void -static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, System.Func!>! implementationFactory) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.Build(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder) -> OpenTelemetry.Logs.LoggerProvider! -static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.ConfigureResource(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, System.Action! configure) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.SetResourceBuilder(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, OpenTelemetry.Resources.ResourceBuilder! resourceBuilder) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.LoggerProviderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProvider! provider, OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Logs.LoggerProvider! -static OpenTelemetry.Logs.LoggerProviderExtensions.ForceFlush(this OpenTelemetry.Logs.LoggerProvider! provider, int timeoutMilliseconds = -1) -> bool -static OpenTelemetry.Logs.LoggerProviderExtensions.Shutdown(this OpenTelemetry.Logs.LoggerProvider! provider, int timeoutMilliseconds = -1) -> bool -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetExemplarFilter(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, OpenTelemetry.Metrics.ExemplarFilter! exemplarFilter) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Sdk.CreateLoggerProviderBuilder() -> OpenTelemetry.Logs.LoggerProviderBuilder! -~abstract OpenTelemetry.Metrics.ExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool -~abstract OpenTelemetry.Metrics.ExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool -~OpenTelemetry.Metrics.Exemplar.FilteredTags.get -> System.Collections.Generic.List> -~override OpenTelemetry.Metrics.AlwaysOffExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool -~override OpenTelemetry.Metrics.AlwaysOffExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool -~override OpenTelemetry.Metrics.AlwaysOnExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool -~override OpenTelemetry.Metrics.AlwaysOnExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool -~override OpenTelemetry.Metrics.TraceBasedExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool -~override OpenTelemetry.Metrics.TraceBasedExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool diff --git a/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Shipped.txt b/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Shipped.txt deleted file mode 100644 index 493d28769f2..00000000000 --- a/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Shipped.txt +++ /dev/null @@ -1,413 +0,0 @@ -#nullable enable -~OpenTelemetry.Batch.GetEnumerator() -> OpenTelemetry.Batch.Enumerator -~OpenTelemetry.Metrics.BaseExportingMetricReader.BaseExportingMetricReader(OpenTelemetry.BaseExporter exporter) -> void -~OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration.Boundaries.get -> double[] -~OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration.Boundaries.set -> void -~OpenTelemetry.Metrics.IPullMetricExporter.Collect.get -> System.Func -~OpenTelemetry.Metrics.IPullMetricExporter.Collect.set -> void -~OpenTelemetry.Metrics.Metric.Description.get -> string -~OpenTelemetry.Metrics.Metric.MeterName.get -> string -~OpenTelemetry.Metrics.Metric.MeterVersion.get -> string -~OpenTelemetry.Metrics.Metric.Name.get -> string -~OpenTelemetry.Metrics.Metric.Unit.get -> string -~OpenTelemetry.Metrics.MetricStreamConfiguration.Description.get -> string -~OpenTelemetry.Metrics.MetricStreamConfiguration.Description.set -> void -~OpenTelemetry.Metrics.MetricStreamConfiguration.Name.get -> string -~OpenTelemetry.Metrics.MetricStreamConfiguration.Name.set -> void -~OpenTelemetry.Metrics.MetricStreamConfiguration.TagKeys.get -> string[] -~OpenTelemetry.Metrics.MetricStreamConfiguration.TagKeys.set -> void -~OpenTelemetry.Metrics.PeriodicExportingMetricReader.PeriodicExportingMetricReader(OpenTelemetry.BaseExporter exporter, int exportIntervalMilliseconds = 60000, int exportTimeoutMilliseconds = 30000) -> void -~override OpenTelemetry.Metrics.MeterProviderBuilderBase.AddInstrumentation(System.Func! instrumentationFactory) -> OpenTelemetry.Metrics.MeterProviderBuilder! -~override OpenTelemetry.Trace.TracerProviderBuilderBase.AddInstrumentation(System.Func! instrumentationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder! -~readonly OpenTelemetry.Metrics.BaseExportingMetricReader.exporter -> OpenTelemetry.BaseExporter -~static OpenTelemetry.Metrics.MeterProviderExtensions.ForceFlush(this OpenTelemetry.Metrics.MeterProvider provider, int timeoutMilliseconds = -1) -> bool -~static OpenTelemetry.Metrics.MeterProviderExtensions.Shutdown(this OpenTelemetry.Metrics.MeterProvider provider, int timeoutMilliseconds = -1) -> bool -~static OpenTelemetry.Metrics.MetricStreamConfiguration.Drop.get -> OpenTelemetry.Metrics.MetricStreamConfiguration -abstract OpenTelemetry.BaseExporter.Export(in OpenTelemetry.Batch batch) -> OpenTelemetry.ExportResult -abstract OpenTelemetry.BaseExportProcessor.OnExport(T! data) -> void -abstract OpenTelemetry.Trace.Sampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions -OpenTelemetry.BaseExporter -OpenTelemetry.BaseExporter.BaseExporter() -> void -OpenTelemetry.BaseExporter.Dispose() -> void -OpenTelemetry.BaseExporter.ForceFlush(int timeoutMilliseconds = -1) -> bool -OpenTelemetry.BaseExporter.ParentProvider.get -> OpenTelemetry.BaseProvider? -OpenTelemetry.BaseExporter.Shutdown(int timeoutMilliseconds = -1) -> bool -OpenTelemetry.BaseExportProcessor -OpenTelemetry.BaseExportProcessor.BaseExportProcessor(OpenTelemetry.BaseExporter! exporter) -> void -OpenTelemetry.BaseProcessor -OpenTelemetry.BaseProcessor.BaseProcessor() -> void -OpenTelemetry.BaseProcessor.Dispose() -> void -OpenTelemetry.BaseProcessor.ForceFlush(int timeoutMilliseconds = -1) -> bool -OpenTelemetry.BaseProcessor.ParentProvider.get -> OpenTelemetry.BaseProvider? -OpenTelemetry.BaseProcessor.Shutdown(int timeoutMilliseconds = -1) -> bool -OpenTelemetry.Batch -OpenTelemetry.Batch.Batch() -> void -OpenTelemetry.Batch.Batch(T![]! items, int count) -> void -OpenTelemetry.Batch.Count.get -> long -OpenTelemetry.Batch.Dispose() -> void -OpenTelemetry.Batch.Enumerator -OpenTelemetry.Batch.Enumerator.Current.get -> T! -OpenTelemetry.Batch.Enumerator.Dispose() -> void -OpenTelemetry.Batch.Enumerator.Enumerator() -> void -OpenTelemetry.Batch.Enumerator.MoveNext() -> bool -OpenTelemetry.Batch.Enumerator.Reset() -> void -OpenTelemetry.BatchActivityExportProcessor -OpenTelemetry.BatchActivityExportProcessor.BatchActivityExportProcessor(OpenTelemetry.BaseExporter! exporter, int maxQueueSize = 2048, int scheduledDelayMilliseconds = 5000, int exporterTimeoutMilliseconds = 30000, int maxExportBatchSize = 512) -> void -OpenTelemetry.BatchExportProcessor -OpenTelemetry.BatchExportProcessor.BatchExportProcessor(OpenTelemetry.BaseExporter! exporter, int maxQueueSize = 2048, int scheduledDelayMilliseconds = 5000, int exporterTimeoutMilliseconds = 30000, int maxExportBatchSize = 512) -> void -OpenTelemetry.BatchExportProcessorOptions -OpenTelemetry.BatchExportProcessorOptions.BatchExportProcessorOptions() -> void -OpenTelemetry.BatchExportProcessorOptions.ExporterTimeoutMilliseconds.get -> int -OpenTelemetry.BatchExportProcessorOptions.ExporterTimeoutMilliseconds.set -> void -OpenTelemetry.BatchExportProcessorOptions.MaxExportBatchSize.get -> int -OpenTelemetry.BatchExportProcessorOptions.MaxExportBatchSize.set -> void -OpenTelemetry.BatchExportProcessorOptions.MaxQueueSize.get -> int -OpenTelemetry.BatchExportProcessorOptions.MaxQueueSize.set -> void -OpenTelemetry.BatchExportProcessorOptions.ScheduledDelayMilliseconds.get -> int -OpenTelemetry.BatchExportProcessorOptions.ScheduledDelayMilliseconds.set -> void -OpenTelemetry.BatchLogRecordExportProcessor -OpenTelemetry.BatchLogRecordExportProcessor.BatchLogRecordExportProcessor(OpenTelemetry.BaseExporter! exporter, int maxQueueSize = 2048, int scheduledDelayMilliseconds = 5000, int exporterTimeoutMilliseconds = 30000, int maxExportBatchSize = 512) -> void -OpenTelemetry.CompositeProcessor -OpenTelemetry.CompositeProcessor.AddProcessor(OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.CompositeProcessor! -OpenTelemetry.CompositeProcessor.CompositeProcessor(System.Collections.Generic.IEnumerable!>! processors) -> void -OpenTelemetry.ExportProcessorType -OpenTelemetry.ExportProcessorType.Batch = 1 -> OpenTelemetry.ExportProcessorType -OpenTelemetry.ExportProcessorType.Simple = 0 -> OpenTelemetry.ExportProcessorType -OpenTelemetry.ExportResult -OpenTelemetry.ExportResult.Failure = 1 -> OpenTelemetry.ExportResult -OpenTelemetry.ExportResult.Success = 0 -> OpenTelemetry.ExportResult -OpenTelemetry.Logs.LogRecord -OpenTelemetry.Logs.LogRecord.Attributes.get -> System.Collections.Generic.IReadOnlyList>? -OpenTelemetry.Logs.LogRecord.Attributes.set -> void -OpenTelemetry.Logs.LogRecord.Body.get -> string? -OpenTelemetry.Logs.LogRecord.Body.set -> void -OpenTelemetry.Logs.LogRecord.CategoryName.get -> string? -OpenTelemetry.Logs.LogRecord.CategoryName.set -> void -OpenTelemetry.Logs.LogRecord.EventId.get -> Microsoft.Extensions.Logging.EventId -OpenTelemetry.Logs.LogRecord.EventId.set -> void -OpenTelemetry.Logs.LogRecord.Exception.get -> System.Exception? -OpenTelemetry.Logs.LogRecord.Exception.set -> void -OpenTelemetry.Logs.LogRecord.ForEachScope(System.Action! callback, TState state) -> void -OpenTelemetry.Logs.LogRecord.FormattedMessage.get -> string? -OpenTelemetry.Logs.LogRecord.FormattedMessage.set -> void -OpenTelemetry.Logs.LogRecord.LogLevel.get -> Microsoft.Extensions.Logging.LogLevel -OpenTelemetry.Logs.LogRecord.LogLevel.set -> void -OpenTelemetry.Logs.LogRecord.SpanId.get -> System.Diagnostics.ActivitySpanId -OpenTelemetry.Logs.LogRecord.SpanId.set -> void -OpenTelemetry.Logs.LogRecord.State.get -> object? -OpenTelemetry.Logs.LogRecord.State.set -> void -OpenTelemetry.Logs.LogRecord.StateValues.get -> System.Collections.Generic.IReadOnlyList>? -OpenTelemetry.Logs.LogRecord.StateValues.set -> void -OpenTelemetry.Logs.LogRecord.Timestamp.get -> System.DateTime -OpenTelemetry.Logs.LogRecord.Timestamp.set -> void -OpenTelemetry.Logs.LogRecord.TraceFlags.get -> System.Diagnostics.ActivityTraceFlags -OpenTelemetry.Logs.LogRecord.TraceFlags.set -> void -OpenTelemetry.Logs.LogRecord.TraceId.get -> System.Diagnostics.ActivityTraceId -OpenTelemetry.Logs.LogRecord.TraceId.set -> void -OpenTelemetry.Logs.LogRecord.TraceState.get -> string? -OpenTelemetry.Logs.LogRecord.TraceState.set -> void -OpenTelemetry.Logs.LogRecordScope -OpenTelemetry.Logs.LogRecordScope.Enumerator -OpenTelemetry.Logs.LogRecordScope.Enumerator.Current.get -> System.Collections.Generic.KeyValuePair -OpenTelemetry.Logs.LogRecordScope.Enumerator.Dispose() -> void -OpenTelemetry.Logs.LogRecordScope.Enumerator.Enumerator() -> void -OpenTelemetry.Logs.LogRecordScope.Enumerator.Enumerator(object? scope) -> void -OpenTelemetry.Logs.LogRecordScope.Enumerator.MoveNext() -> bool -OpenTelemetry.Logs.LogRecordScope.Enumerator.Reset() -> void -OpenTelemetry.Logs.LogRecordScope.GetEnumerator() -> OpenTelemetry.Logs.LogRecordScope.Enumerator -OpenTelemetry.Logs.LogRecordScope.LogRecordScope() -> void -OpenTelemetry.Logs.LogRecordScope.Scope.get -> object? -OpenTelemetry.Logs.OpenTelemetryLoggerOptions -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.AddProcessor(OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.IncludeFormattedMessage.get -> bool -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.IncludeFormattedMessage.set -> void -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.IncludeScopes.get -> bool -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.IncludeScopes.set -> void -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.OpenTelemetryLoggerOptions() -> void -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.ParseStateValues.get -> bool -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.ParseStateValues.set -> void -OpenTelemetry.Logs.OpenTelemetryLoggerOptions.SetResourceBuilder(OpenTelemetry.Resources.ResourceBuilder! resourceBuilder) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! -OpenTelemetry.Logs.OpenTelemetryLoggerProvider -OpenTelemetry.Logs.OpenTelemetryLoggerProvider.CreateLogger(string! categoryName) -> Microsoft.Extensions.Logging.ILogger! -OpenTelemetry.Logs.OpenTelemetryLoggerProvider.OpenTelemetryLoggerProvider(Microsoft.Extensions.Options.IOptionsMonitor! options) -> void -OpenTelemetry.Metrics.AggregationTemporality -OpenTelemetry.Metrics.AggregationTemporality.Cumulative = 1 -> OpenTelemetry.Metrics.AggregationTemporality -OpenTelemetry.Metrics.AggregationTemporality.Delta = 2 -> OpenTelemetry.Metrics.AggregationTemporality -OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration -OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.Base2ExponentialBucketHistogramConfiguration() -> void -OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxScale.get -> int -OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxScale.set -> void -OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxSize.get -> int -OpenTelemetry.Metrics.Base2ExponentialBucketHistogramConfiguration.MaxSize.set -> void -OpenTelemetry.Metrics.BaseExportingMetricReader -OpenTelemetry.Metrics.BaseExportingMetricReader.SupportedExportModes.get -> OpenTelemetry.Metrics.ExportModes -OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration -OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration.ExplicitBucketHistogramConfiguration() -> void -OpenTelemetry.Metrics.ExponentialHistogramBuckets -OpenTelemetry.Metrics.ExponentialHistogramBuckets.Enumerator -OpenTelemetry.Metrics.ExponentialHistogramBuckets.Enumerator.Current.get -> long -OpenTelemetry.Metrics.ExponentialHistogramBuckets.Enumerator.Enumerator() -> void -OpenTelemetry.Metrics.ExponentialHistogramBuckets.Enumerator.MoveNext() -> bool -OpenTelemetry.Metrics.ExponentialHistogramBuckets.GetEnumerator() -> OpenTelemetry.Metrics.ExponentialHistogramBuckets.Enumerator -OpenTelemetry.Metrics.ExponentialHistogramBuckets.Offset.get -> int -OpenTelemetry.Metrics.ExponentialHistogramData -OpenTelemetry.Metrics.ExponentialHistogramData.PositiveBuckets.get -> OpenTelemetry.Metrics.ExponentialHistogramBuckets! -OpenTelemetry.Metrics.ExponentialHistogramData.Scale.get -> int -OpenTelemetry.Metrics.ExponentialHistogramData.ZeroCount.get -> long -OpenTelemetry.Metrics.ExportModes -OpenTelemetry.Metrics.ExportModes.Pull = 2 -> OpenTelemetry.Metrics.ExportModes -OpenTelemetry.Metrics.ExportModes.Push = 1 -> OpenTelemetry.Metrics.ExportModes -OpenTelemetry.Metrics.ExportModesAttribute -OpenTelemetry.Metrics.ExportModesAttribute.ExportModesAttribute(OpenTelemetry.Metrics.ExportModes supported) -> void -OpenTelemetry.Metrics.ExportModesAttribute.Supported.get -> OpenTelemetry.Metrics.ExportModes -OpenTelemetry.Metrics.HistogramBucket -OpenTelemetry.Metrics.HistogramBucket.BucketCount.get -> long -OpenTelemetry.Metrics.HistogramBucket.ExplicitBound.get -> double -OpenTelemetry.Metrics.HistogramBucket.HistogramBucket() -> void -OpenTelemetry.Metrics.HistogramBuckets -OpenTelemetry.Metrics.HistogramBuckets.Enumerator -OpenTelemetry.Metrics.HistogramBuckets.Enumerator.Current.get -> OpenTelemetry.Metrics.HistogramBucket -OpenTelemetry.Metrics.HistogramBuckets.Enumerator.Enumerator() -> void -OpenTelemetry.Metrics.HistogramBuckets.Enumerator.MoveNext() -> bool -OpenTelemetry.Metrics.HistogramBuckets.GetEnumerator() -> OpenTelemetry.Metrics.HistogramBuckets.Enumerator -OpenTelemetry.Metrics.HistogramConfiguration -OpenTelemetry.Metrics.HistogramConfiguration.HistogramConfiguration() -> void -OpenTelemetry.Metrics.HistogramConfiguration.RecordMinMax.get -> bool -OpenTelemetry.Metrics.HistogramConfiguration.RecordMinMax.set -> void -OpenTelemetry.Metrics.IPullMetricExporter -OpenTelemetry.Metrics.MeterProviderBuilderBase -OpenTelemetry.Metrics.MeterProviderBuilderBase.Build() -> OpenTelemetry.Metrics.MeterProvider! -OpenTelemetry.Metrics.MeterProviderBuilderBase.MeterProviderBuilderBase() -> void -OpenTelemetry.Metrics.MeterProviderBuilderExtensions -OpenTelemetry.Metrics.MeterProviderExtensions -OpenTelemetry.Metrics.Metric -OpenTelemetry.Metrics.Metric.GetMetricPoints() -> OpenTelemetry.Metrics.MetricPointsAccessor -OpenTelemetry.Metrics.Metric.MetricType.get -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.Metric.Temporality.get -> OpenTelemetry.Metrics.AggregationTemporality -OpenTelemetry.Metrics.MetricPoint -OpenTelemetry.Metrics.MetricPoint.EndTime.get -> System.DateTimeOffset -OpenTelemetry.Metrics.MetricPoint.GetExponentialHistogramData() -> OpenTelemetry.Metrics.ExponentialHistogramData! -OpenTelemetry.Metrics.MetricPoint.GetGaugeLastValueDouble() -> double -OpenTelemetry.Metrics.MetricPoint.GetGaugeLastValueLong() -> long -OpenTelemetry.Metrics.MetricPoint.GetHistogramBuckets() -> OpenTelemetry.Metrics.HistogramBuckets! -OpenTelemetry.Metrics.MetricPoint.GetHistogramCount() -> long -OpenTelemetry.Metrics.MetricPoint.GetHistogramSum() -> double -OpenTelemetry.Metrics.MetricPoint.GetSumDouble() -> double -OpenTelemetry.Metrics.MetricPoint.GetSumLong() -> long -OpenTelemetry.Metrics.MetricPoint.MetricPoint() -> void -OpenTelemetry.Metrics.MetricPoint.StartTime.get -> System.DateTimeOffset -OpenTelemetry.Metrics.MetricPoint.Tags.get -> OpenTelemetry.ReadOnlyTagCollection -OpenTelemetry.Metrics.MetricPoint.TryGetHistogramMinMaxValues(out double min, out double max) -> bool -OpenTelemetry.Metrics.MetricPointsAccessor -OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator -OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator.Current.get -> OpenTelemetry.Metrics.MetricPoint -OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator.Enumerator() -> void -OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator.MoveNext() -> bool -OpenTelemetry.Metrics.MetricPointsAccessor.GetEnumerator() -> OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator -OpenTelemetry.Metrics.MetricPointsAccessor.MetricPointsAccessor() -> void -OpenTelemetry.Metrics.MetricReader -OpenTelemetry.Metrics.MetricReader.Collect(int timeoutMilliseconds = -1) -> bool -OpenTelemetry.Metrics.MetricReader.Dispose() -> void -OpenTelemetry.Metrics.MetricReader.MetricReader() -> void -OpenTelemetry.Metrics.MetricReader.Shutdown(int timeoutMilliseconds = -1) -> bool -OpenTelemetry.Metrics.MetricReader.TemporalityPreference.get -> OpenTelemetry.Metrics.MetricReaderTemporalityPreference -OpenTelemetry.Metrics.MetricReader.TemporalityPreference.set -> void -OpenTelemetry.Metrics.MetricReaderOptions -OpenTelemetry.Metrics.MetricReaderOptions.MetricReaderOptions() -> void -OpenTelemetry.Metrics.MetricReaderOptions.PeriodicExportingMetricReaderOptions.get -> OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions! -OpenTelemetry.Metrics.MetricReaderOptions.PeriodicExportingMetricReaderOptions.set -> void -OpenTelemetry.Metrics.MetricReaderOptions.TemporalityPreference.get -> OpenTelemetry.Metrics.MetricReaderTemporalityPreference -OpenTelemetry.Metrics.MetricReaderOptions.TemporalityPreference.set -> void -OpenTelemetry.Metrics.MetricReaderTemporalityPreference -OpenTelemetry.Metrics.MetricReaderTemporalityPreference.Cumulative = 1 -> OpenTelemetry.Metrics.MetricReaderTemporalityPreference -OpenTelemetry.Metrics.MetricReaderTemporalityPreference.Delta = 2 -> OpenTelemetry.Metrics.MetricReaderTemporalityPreference -OpenTelemetry.Metrics.MetricStreamConfiguration -OpenTelemetry.Metrics.MetricStreamConfiguration.MetricStreamConfiguration() -> void -OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricType.DoubleGauge = 45 -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricType.DoubleSum = 29 -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricType.DoubleSumNonMonotonic = 141 -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricType.ExponentialHistogram = 80 -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricType.Histogram = 64 -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricType.LongGauge = 42 -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricType.LongSum = 26 -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricType.LongSumNonMonotonic = 138 -> OpenTelemetry.Metrics.MetricType -OpenTelemetry.Metrics.MetricTypeExtensions -OpenTelemetry.Metrics.PeriodicExportingMetricReader -OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions -OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds.get -> int? -OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds.set -> void -OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions.ExportTimeoutMilliseconds.get -> int? -OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions.ExportTimeoutMilliseconds.set -> void -OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions.PeriodicExportingMetricReaderOptions() -> void -OpenTelemetry.ProviderExtensions -OpenTelemetry.ReadOnlyTagCollection -OpenTelemetry.ReadOnlyTagCollection.Count.get -> int -OpenTelemetry.ReadOnlyTagCollection.Enumerator -OpenTelemetry.ReadOnlyTagCollection.Enumerator.Current.get -> System.Collections.Generic.KeyValuePair -OpenTelemetry.ReadOnlyTagCollection.Enumerator.Enumerator() -> void -OpenTelemetry.ReadOnlyTagCollection.Enumerator.MoveNext() -> bool -OpenTelemetry.ReadOnlyTagCollection.GetEnumerator() -> OpenTelemetry.ReadOnlyTagCollection.Enumerator -OpenTelemetry.ReadOnlyTagCollection.ReadOnlyTagCollection() -> void -OpenTelemetry.Resources.IResourceDetector -OpenTelemetry.Resources.IResourceDetector.Detect() -> OpenTelemetry.Resources.Resource! -OpenTelemetry.Resources.Resource -OpenTelemetry.Resources.Resource.Attributes.get -> System.Collections.Generic.IEnumerable>! -OpenTelemetry.Resources.Resource.Merge(OpenTelemetry.Resources.Resource! other) -> OpenTelemetry.Resources.Resource! -OpenTelemetry.Resources.Resource.Resource(System.Collections.Generic.IEnumerable>! attributes) -> void -OpenTelemetry.Resources.ResourceBuilder -OpenTelemetry.Resources.ResourceBuilder.AddDetector(OpenTelemetry.Resources.IResourceDetector! resourceDetector) -> OpenTelemetry.Resources.ResourceBuilder! -OpenTelemetry.Resources.ResourceBuilder.AddDetector(System.Func! resourceDetectorFactory) -> OpenTelemetry.Resources.ResourceBuilder! -OpenTelemetry.Resources.ResourceBuilder.Build() -> OpenTelemetry.Resources.Resource! -OpenTelemetry.Resources.ResourceBuilder.Clear() -> OpenTelemetry.Resources.ResourceBuilder! -OpenTelemetry.Resources.ResourceBuilderExtensions -OpenTelemetry.Sdk -OpenTelemetry.SimpleActivityExportProcessor -OpenTelemetry.SimpleActivityExportProcessor.SimpleActivityExportProcessor(OpenTelemetry.BaseExporter! exporter) -> void -OpenTelemetry.SimpleExportProcessor -OpenTelemetry.SimpleExportProcessor.SimpleExportProcessor(OpenTelemetry.BaseExporter! exporter) -> void -OpenTelemetry.SimpleLogRecordExportProcessor -OpenTelemetry.SimpleLogRecordExportProcessor.SimpleLogRecordExportProcessor(OpenTelemetry.BaseExporter! exporter) -> void -OpenTelemetry.SuppressInstrumentationScope -OpenTelemetry.SuppressInstrumentationScope.Dispose() -> void -OpenTelemetry.Trace.AlwaysOffSampler -OpenTelemetry.Trace.AlwaysOffSampler.AlwaysOffSampler() -> void -OpenTelemetry.Trace.AlwaysOnSampler -OpenTelemetry.Trace.AlwaysOnSampler.AlwaysOnSampler() -> void -OpenTelemetry.Trace.BatchExportActivityProcessorOptions -OpenTelemetry.Trace.BatchExportActivityProcessorOptions.BatchExportActivityProcessorOptions() -> void -OpenTelemetry.Trace.ParentBasedSampler -OpenTelemetry.Trace.ParentBasedSampler.ParentBasedSampler(OpenTelemetry.Trace.Sampler! rootSampler) -> void -OpenTelemetry.Trace.ParentBasedSampler.ParentBasedSampler(OpenTelemetry.Trace.Sampler! rootSampler, OpenTelemetry.Trace.Sampler? remoteParentSampled = null, OpenTelemetry.Trace.Sampler? remoteParentNotSampled = null, OpenTelemetry.Trace.Sampler? localParentSampled = null, OpenTelemetry.Trace.Sampler? localParentNotSampled = null) -> void -OpenTelemetry.Trace.Sampler -OpenTelemetry.Trace.Sampler.Description.get -> string! -OpenTelemetry.Trace.Sampler.Description.set -> void -OpenTelemetry.Trace.Sampler.Sampler() -> void -OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingDecision.Drop = 0 -> OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingDecision.RecordAndSample = 2 -> OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingDecision.RecordOnly = 1 -> OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingParameters -OpenTelemetry.Trace.SamplingParameters.Kind.get -> System.Diagnostics.ActivityKind -OpenTelemetry.Trace.SamplingParameters.Links.get -> System.Collections.Generic.IEnumerable? -OpenTelemetry.Trace.SamplingParameters.Name.get -> string! -OpenTelemetry.Trace.SamplingParameters.ParentContext.get -> System.Diagnostics.ActivityContext -OpenTelemetry.Trace.SamplingParameters.SamplingParameters() -> void -OpenTelemetry.Trace.SamplingParameters.SamplingParameters(System.Diagnostics.ActivityContext parentContext, System.Diagnostics.ActivityTraceId traceId, string! name, System.Diagnostics.ActivityKind kind, System.Collections.Generic.IEnumerable>? tags = null, System.Collections.Generic.IEnumerable? links = null) -> void -OpenTelemetry.Trace.SamplingParameters.Tags.get -> System.Collections.Generic.IEnumerable>? -OpenTelemetry.Trace.SamplingParameters.TraceId.get -> System.Diagnostics.ActivityTraceId -OpenTelemetry.Trace.SamplingResult -OpenTelemetry.Trace.SamplingResult.Attributes.get -> System.Collections.Generic.IEnumerable>! -OpenTelemetry.Trace.SamplingResult.Decision.get -> OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingResult.Equals(OpenTelemetry.Trace.SamplingResult other) -> bool -OpenTelemetry.Trace.SamplingResult.SamplingResult() -> void -OpenTelemetry.Trace.SamplingResult.SamplingResult(bool isSampled) -> void -OpenTelemetry.Trace.SamplingResult.SamplingResult(OpenTelemetry.Trace.SamplingDecision decision) -> void -OpenTelemetry.Trace.SamplingResult.SamplingResult(OpenTelemetry.Trace.SamplingDecision decision, string! traceStateString) -> void -OpenTelemetry.Trace.SamplingResult.SamplingResult(OpenTelemetry.Trace.SamplingDecision decision, System.Collections.Generic.IEnumerable>! attributes) -> void -OpenTelemetry.Trace.SamplingResult.SamplingResult(OpenTelemetry.Trace.SamplingDecision decision, System.Collections.Generic.IEnumerable>? attributes, string? traceStateString) -> void -OpenTelemetry.Trace.SamplingResult.TraceStateString.get -> string? -OpenTelemetry.Trace.TraceIdRatioBasedSampler -OpenTelemetry.Trace.TraceIdRatioBasedSampler.TraceIdRatioBasedSampler(double probability) -> void -OpenTelemetry.Trace.TracerProviderBuilderBase -OpenTelemetry.Trace.TracerProviderBuilderBase.AddInstrumentation(string! instrumentationName, string! instrumentationVersion, System.Func! instrumentationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder! -OpenTelemetry.Trace.TracerProviderBuilderBase.Build() -> OpenTelemetry.Trace.TracerProvider! -OpenTelemetry.Trace.TracerProviderBuilderBase.TracerProviderBuilderBase() -> void -OpenTelemetry.Trace.TracerProviderBuilderExtensions -OpenTelemetry.Trace.TracerProviderExtensions -override OpenTelemetry.BaseExportProcessor.Dispose(bool disposing) -> void -override OpenTelemetry.BaseExportProcessor.OnEnd(T! data) -> void -override OpenTelemetry.BaseExportProcessor.OnForceFlush(int timeoutMilliseconds) -> bool -override OpenTelemetry.BaseExportProcessor.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.BaseExportProcessor.ToString() -> string! -override OpenTelemetry.BaseProcessor.ToString() -> string! -override OpenTelemetry.BatchActivityExportProcessor.OnEnd(System.Diagnostics.Activity! data) -> void -override OpenTelemetry.BatchExportProcessor.Dispose(bool disposing) -> void -override OpenTelemetry.BatchExportProcessor.OnExport(T! data) -> void -override OpenTelemetry.BatchExportProcessor.OnForceFlush(int timeoutMilliseconds) -> bool -override OpenTelemetry.BatchExportProcessor.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.BatchLogRecordExportProcessor.OnEnd(OpenTelemetry.Logs.LogRecord! data) -> void -override OpenTelemetry.CompositeProcessor.Dispose(bool disposing) -> void -override OpenTelemetry.CompositeProcessor.OnEnd(T data) -> void -override OpenTelemetry.CompositeProcessor.OnForceFlush(int timeoutMilliseconds) -> bool -override OpenTelemetry.CompositeProcessor.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.CompositeProcessor.OnStart(T data) -> void -override OpenTelemetry.Logs.OpenTelemetryLoggerProvider.Dispose(bool disposing) -> void -override OpenTelemetry.Metrics.BaseExportingMetricReader.Dispose(bool disposing) -> void -override OpenTelemetry.Metrics.BaseExportingMetricReader.OnCollect(int timeoutMilliseconds) -> bool -override OpenTelemetry.Metrics.BaseExportingMetricReader.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.Metrics.MeterProviderBuilderBase.AddMeter(params string![]! names) -> OpenTelemetry.Metrics.MeterProviderBuilder! -override OpenTelemetry.Metrics.PeriodicExportingMetricReader.Dispose(bool disposing) -> void -override OpenTelemetry.Metrics.PeriodicExportingMetricReader.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.SimpleActivityExportProcessor.OnEnd(System.Diagnostics.Activity! data) -> void -override OpenTelemetry.SimpleExportProcessor.OnExport(T! data) -> void -override OpenTelemetry.Trace.AlwaysOffSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -override OpenTelemetry.Trace.AlwaysOnSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -override OpenTelemetry.Trace.ParentBasedSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -override OpenTelemetry.Trace.SamplingResult.Equals(object? obj) -> bool -override OpenTelemetry.Trace.SamplingResult.GetHashCode() -> int -override OpenTelemetry.Trace.TraceIdRatioBasedSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -override OpenTelemetry.Trace.TracerProviderBuilderBase.AddLegacySource(string! operationName) -> OpenTelemetry.Trace.TracerProviderBuilder! -override OpenTelemetry.Trace.TracerProviderBuilderBase.AddSource(params string![]! names) -> OpenTelemetry.Trace.TracerProviderBuilder! -override sealed OpenTelemetry.BaseExportProcessor.OnStart(T! data) -> void -readonly OpenTelemetry.BaseExportProcessor.exporter -> OpenTelemetry.BaseExporter! -static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.AddOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder) -> Microsoft.Extensions.Logging.ILoggingBuilder! -static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.AddOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, System.Action? configure) -> Microsoft.Extensions.Logging.ILoggingBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddReader(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, OpenTelemetry.Metrics.MetricReader! reader) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddReader(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, System.Func! implementationFactory) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddReader(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddView(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, string! instrumentName, OpenTelemetry.Metrics.MetricStreamConfiguration! metricStreamConfiguration) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddView(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, string! instrumentName, string! name) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddView(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, System.Func! viewConfig) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.Build(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder) -> OpenTelemetry.Metrics.MeterProvider? -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.ConfigureResource(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, System.Action! configure) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetMaxMetricPointsPerMetricStream(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, int maxMetricPointsPerMetricStream) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetMaxMetricStreams(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, int maxMetricStreams) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetResourceBuilder(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, OpenTelemetry.Resources.ResourceBuilder! resourceBuilder) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MetricTypeExtensions.IsDouble(this OpenTelemetry.Metrics.MetricType self) -> bool -static OpenTelemetry.Metrics.MetricTypeExtensions.IsGauge(this OpenTelemetry.Metrics.MetricType self) -> bool -static OpenTelemetry.Metrics.MetricTypeExtensions.IsHistogram(this OpenTelemetry.Metrics.MetricType self) -> bool -static OpenTelemetry.Metrics.MetricTypeExtensions.IsLong(this OpenTelemetry.Metrics.MetricType self) -> bool -static OpenTelemetry.Metrics.MetricTypeExtensions.IsSum(this OpenTelemetry.Metrics.MetricType self) -> bool -static OpenTelemetry.ProviderExtensions.GetDefaultResource(this OpenTelemetry.BaseProvider! baseProvider) -> OpenTelemetry.Resources.Resource! -static OpenTelemetry.ProviderExtensions.GetResource(this OpenTelemetry.BaseProvider! baseProvider) -> OpenTelemetry.Resources.Resource! -static OpenTelemetry.Resources.Resource.Empty.get -> OpenTelemetry.Resources.Resource! -static OpenTelemetry.Resources.ResourceBuilder.CreateDefault() -> OpenTelemetry.Resources.ResourceBuilder! -static OpenTelemetry.Resources.ResourceBuilder.CreateEmpty() -> OpenTelemetry.Resources.ResourceBuilder! -static OpenTelemetry.Resources.ResourceBuilderExtensions.AddAttributes(this OpenTelemetry.Resources.ResourceBuilder! resourceBuilder, System.Collections.Generic.IEnumerable>! attributes) -> OpenTelemetry.Resources.ResourceBuilder! -static OpenTelemetry.Resources.ResourceBuilderExtensions.AddEnvironmentVariableDetector(this OpenTelemetry.Resources.ResourceBuilder! resourceBuilder) -> OpenTelemetry.Resources.ResourceBuilder! -static OpenTelemetry.Resources.ResourceBuilderExtensions.AddService(this OpenTelemetry.Resources.ResourceBuilder! resourceBuilder, string! serviceName, string? serviceNamespace = null, string? serviceVersion = null, bool autoGenerateServiceInstanceId = true, string? serviceInstanceId = null) -> OpenTelemetry.Resources.ResourceBuilder! -static OpenTelemetry.Resources.ResourceBuilderExtensions.AddTelemetrySdk(this OpenTelemetry.Resources.ResourceBuilder! resourceBuilder) -> OpenTelemetry.Resources.ResourceBuilder! -static OpenTelemetry.Sdk.CreateMeterProviderBuilder() -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Sdk.CreateTracerProviderBuilder() -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Sdk.SetDefaultTextMapPropagator(OpenTelemetry.Context.Propagation.TextMapPropagator! textMapPropagator) -> void -static OpenTelemetry.Sdk.SuppressInstrumentation.get -> bool -static OpenTelemetry.SuppressInstrumentationScope.Begin(bool value = true) -> System.IDisposable! -static OpenTelemetry.SuppressInstrumentationScope.Enter() -> int -static OpenTelemetry.Trace.SamplingResult.operator !=(OpenTelemetry.Trace.SamplingResult decision1, OpenTelemetry.Trace.SamplingResult decision2) -> bool -static OpenTelemetry.Trace.SamplingResult.operator ==(OpenTelemetry.Trace.SamplingResult decision1, OpenTelemetry.Trace.SamplingResult decision2) -> bool -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, System.Func!>! implementationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.Build(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder) -> OpenTelemetry.Trace.TracerProvider? -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.ConfigureResource(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, System.Action! configure) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.SetErrorStatusOnException(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, bool enabled = true) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.SetResourceBuilder(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, OpenTelemetry.Resources.ResourceBuilder! resourceBuilder) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.SetSampler(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, OpenTelemetry.Trace.Sampler! sampler) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.SetSampler(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder, System.Func! implementationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.SetSampler(this OpenTelemetry.Trace.TracerProviderBuilder! tracerProviderBuilder) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderExtensions.AddProcessor(this OpenTelemetry.Trace.TracerProvider! provider, OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Trace.TracerProvider! -static OpenTelemetry.Trace.TracerProviderExtensions.ForceFlush(this OpenTelemetry.Trace.TracerProvider! provider, int timeoutMilliseconds = -1) -> bool -static OpenTelemetry.Trace.TracerProviderExtensions.Shutdown(this OpenTelemetry.Trace.TracerProvider! provider, int timeoutMilliseconds = -1) -> bool -virtual OpenTelemetry.BaseExporter.Dispose(bool disposing) -> void -virtual OpenTelemetry.BaseExporter.OnForceFlush(int timeoutMilliseconds) -> bool -virtual OpenTelemetry.BaseExporter.OnShutdown(int timeoutMilliseconds) -> bool -virtual OpenTelemetry.BaseProcessor.Dispose(bool disposing) -> void -virtual OpenTelemetry.BaseProcessor.OnEnd(T data) -> void -virtual OpenTelemetry.BaseProcessor.OnForceFlush(int timeoutMilliseconds) -> bool -virtual OpenTelemetry.BaseProcessor.OnShutdown(int timeoutMilliseconds) -> bool -virtual OpenTelemetry.BaseProcessor.OnStart(T data) -> void -virtual OpenTelemetry.Metrics.MetricReader.Dispose(bool disposing) -> void -virtual OpenTelemetry.Metrics.MetricReader.OnCollect(int timeoutMilliseconds) -> bool -virtual OpenTelemetry.Metrics.MetricReader.OnShutdown(int timeoutMilliseconds) -> bool diff --git a/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt deleted file mode 100644 index e75f84d65db..00000000000 --- a/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,50 +0,0 @@ -OpenTelemetry.Logs.BatchExportLogRecordProcessorOptions -OpenTelemetry.Logs.BatchExportLogRecordProcessorOptions.BatchExportLogRecordProcessorOptions() -> void -OpenTelemetry.Logs.LoggerProviderBuilderExtensions -OpenTelemetry.Logs.LoggerProviderExtensions -OpenTelemetry.Logs.LogRecord.Logger.get -> OpenTelemetry.Logs.Logger? -OpenTelemetry.Logs.LogRecord.Severity.get -> OpenTelemetry.Logs.LogRecordSeverity? -OpenTelemetry.Logs.LogRecord.Severity.set -> void -OpenTelemetry.Logs.LogRecord.SeverityText.get -> string? -OpenTelemetry.Logs.LogRecord.SeverityText.set -> void -OpenTelemetry.Logs.LogRecordExportProcessorOptions -OpenTelemetry.Logs.LogRecordExportProcessorOptions.BatchExportProcessorOptions.get -> OpenTelemetry.Logs.BatchExportLogRecordProcessorOptions! -OpenTelemetry.Logs.LogRecordExportProcessorOptions.BatchExportProcessorOptions.set -> void -OpenTelemetry.Logs.LogRecordExportProcessorOptions.ExportProcessorType.get -> OpenTelemetry.ExportProcessorType -OpenTelemetry.Logs.LogRecordExportProcessorOptions.ExportProcessorType.set -> void -OpenTelemetry.Logs.LogRecordExportProcessorOptions.LogRecordExportProcessorOptions() -> void -OpenTelemetry.Metrics.AlwaysOffExemplarFilter -OpenTelemetry.Metrics.AlwaysOffExemplarFilter.AlwaysOffExemplarFilter() -> void -OpenTelemetry.Metrics.AlwaysOnExemplarFilter -OpenTelemetry.Metrics.AlwaysOnExemplarFilter.AlwaysOnExemplarFilter() -> void -OpenTelemetry.Metrics.Exemplar -OpenTelemetry.Metrics.Exemplar.DoubleValue.get -> double -OpenTelemetry.Metrics.Exemplar.Exemplar() -> void -OpenTelemetry.Metrics.Exemplar.SpanId.get -> System.Diagnostics.ActivitySpanId? -OpenTelemetry.Metrics.Exemplar.Timestamp.get -> System.DateTimeOffset -OpenTelemetry.Metrics.Exemplar.TraceId.get -> System.Diagnostics.ActivityTraceId? -OpenTelemetry.Metrics.ExemplarFilter -OpenTelemetry.Metrics.ExemplarFilter.ExemplarFilter() -> void -OpenTelemetry.Metrics.MetricPoint.GetExemplars() -> OpenTelemetry.Metrics.Exemplar[]! -OpenTelemetry.Metrics.TraceBasedExemplarFilter -OpenTelemetry.Metrics.TraceBasedExemplarFilter.TraceBasedExemplarFilter() -> void -static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, System.Func!>! implementationFactory) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.Build(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder) -> OpenTelemetry.Logs.LoggerProvider! -static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.ConfigureResource(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, System.Action! configure) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.LoggerProviderBuilderExtensions.SetResourceBuilder(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, OpenTelemetry.Resources.ResourceBuilder! resourceBuilder) -> OpenTelemetry.Logs.LoggerProviderBuilder! -static OpenTelemetry.Logs.LoggerProviderExtensions.AddProcessor(this OpenTelemetry.Logs.LoggerProvider! provider, OpenTelemetry.BaseProcessor! processor) -> OpenTelemetry.Logs.LoggerProvider! -static OpenTelemetry.Logs.LoggerProviderExtensions.ForceFlush(this OpenTelemetry.Logs.LoggerProvider! provider, int timeoutMilliseconds = -1) -> bool -static OpenTelemetry.Logs.LoggerProviderExtensions.Shutdown(this OpenTelemetry.Logs.LoggerProvider! provider, int timeoutMilliseconds = -1) -> bool -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetExemplarFilter(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, OpenTelemetry.Metrics.ExemplarFilter! exemplarFilter) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Sdk.CreateLoggerProviderBuilder() -> OpenTelemetry.Logs.LoggerProviderBuilder! -~abstract OpenTelemetry.Metrics.ExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool -~abstract OpenTelemetry.Metrics.ExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool -~OpenTelemetry.Metrics.Exemplar.FilteredTags.get -> System.Collections.Generic.List> -~override OpenTelemetry.Metrics.AlwaysOffExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool -~override OpenTelemetry.Metrics.AlwaysOffExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool -~override OpenTelemetry.Metrics.AlwaysOnExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool -~override OpenTelemetry.Metrics.AlwaysOnExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool -~override OpenTelemetry.Metrics.TraceBasedExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool -~override OpenTelemetry.Metrics.TraceBasedExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool diff --git a/src/OpenTelemetry/AssemblyInfo.cs b/src/OpenTelemetry/AssemblyInfo.cs index 6b94f59eb69..90823131448 100644 --- a/src/OpenTelemetry/AssemblyInfo.cs +++ b/src/OpenTelemetry/AssemblyInfo.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Runtime.CompilerServices; @@ -23,5 +10,23 @@ [assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Prometheus.HttpListener.Tests" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("OpenTelemetry.Extensions.Hosting" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("OpenTelemetry.Extensions.Hosting.Tests" + AssemblyInfo.PublicKey)] -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2" + AssemblyInfo.MoqPublicKey)] [assembly: InternalsVisibleTo("Benchmarks" + AssemblyInfo.PublicKey)] + +#if !EXPOSE_EXPERIMENTAL_FEATURES +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Console" + AssemblyInfo.PublicKey)] +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol" + AssemblyInfo.PublicKey)] +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests" + AssemblyInfo.PublicKey)] +[assembly: InternalsVisibleTo("OpenTelemetry.Tests.Stress.Metrics" + AssemblyInfo.PublicKey)] +#endif + +#if SIGNED +file static class AssemblyInfo +{ + public const string PublicKey = ", PublicKey=002400000480000094000000060200000024000052534131000400000100010051C1562A090FB0C9F391012A32198B5E5D9A60E9B80FA2D7B434C9E5CCB7259BD606E66F9660676AFC6692B8CDC6793D190904551D2103B7B22FA636DCBB8208839785BA402EA08FC00C8F1500CCEF28BBF599AA64FFB1E1D5DC1BF3420A3777BADFE697856E9D52070A50C3EA5821C80BEF17CA3ACFFA28F89DD413F096F898"; +} +#else +file static class AssemblyInfo +{ + public const string PublicKey = ""; +} +#endif diff --git a/src/OpenTelemetry/BaseExportProcessor.cs b/src/OpenTelemetry/BaseExportProcessor.cs index ebe282c33c2..2e9fe7619c2 100644 --- a/src/OpenTelemetry/BaseExportProcessor.cs +++ b/src/OpenTelemetry/BaseExportProcessor.cs @@ -1,126 +1,126 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Internal; -namespace OpenTelemetry +namespace OpenTelemetry; + +/// +/// Type of Export Processor to be used. +/// +public enum ExportProcessorType { /// - /// Type of Export Processor to be used. + /// Use SimpleExportProcessor. + /// Refer to the + /// specification for more information. /// - public enum ExportProcessorType - { - /// - /// Use SimpleExportProcessor. - /// Refer to the - /// specification for more information. - /// - Simple, - - /// - /// Use BatchExportProcessor. - /// Refer to - /// specification for more information. - /// - Batch, - } + Simple, /// - /// Implements processor that exports telemetry objects. + /// Use BatchExportProcessor. + /// Refer to + /// specification for more information. /// - /// The type of telemetry object to be exported. - public abstract class BaseExportProcessor : BaseProcessor - where T : class - { - protected readonly BaseExporter exporter; - private readonly string friendlyTypeName; - private bool disposed; - - /// - /// Initializes a new instance of the class. - /// - /// Exporter instance. - protected BaseExportProcessor(BaseExporter exporter) - { - Guard.ThrowIfNull(exporter); + Batch, +} - this.friendlyTypeName = $"{this.GetType().Name}{{{exporter.GetType().Name}}}"; - this.exporter = exporter; - } +/// +/// Implements processor that exports telemetry objects. +/// +/// The type of telemetry object to be exported. +public abstract class BaseExportProcessor : BaseProcessor + where T : class +{ + /// + /// Gets the exporter used by the processor. + /// + protected readonly BaseExporter exporter; - internal BaseExporter Exporter => this.exporter; + private readonly string friendlyTypeName; + private bool disposed; - /// - public override string ToString() - => this.friendlyTypeName; + /// + /// Initializes a new instance of the class. + /// + /// Exporter instance. + protected BaseExportProcessor(BaseExporter exporter) + { + Guard.ThrowIfNull(exporter); - /// - public sealed override void OnStart(T data) - { - } + this.friendlyTypeName = $"{this.GetType().Name}{{{exporter.GetType().Name}}}"; + this.exporter = exporter; + } - public override void OnEnd(T data) - { - this.OnExport(data); - } + internal BaseExporter Exporter => this.exporter; - internal override void SetParentProvider(BaseProvider parentProvider) - { - base.SetParentProvider(parentProvider); + /// + public override string ToString() + => this.friendlyTypeName; - this.exporter.ParentProvider = parentProvider; - } + /// + public sealed override void OnStart(T data) + { + } - protected abstract void OnExport(T data); + /// + public override void OnEnd(T data) + { + this.OnExport(data); + } - /// - protected override bool OnForceFlush(int timeoutMilliseconds) - { - return this.exporter.ForceFlush(timeoutMilliseconds); - } + internal override void SetParentProvider(BaseProvider parentProvider) + { + base.SetParentProvider(parentProvider); - /// - protected override bool OnShutdown(int timeoutMilliseconds) - { - return this.exporter.Shutdown(timeoutMilliseconds); - } + this.exporter.ParentProvider = parentProvider; + } - /// - protected override void Dispose(bool disposing) + /// + /// Called synchronously when a telemetry object is exported. + /// + /// + /// The exported telemetry object. + /// + /// + /// This function is called synchronously on the thread which ended + /// the telemetry object. This function should be thread-safe, and + /// should not block indefinitely or throw exceptions. + /// + protected abstract void OnExport(T data); + + /// + protected override bool OnForceFlush(int timeoutMilliseconds) + { + return this.exporter.ForceFlush(timeoutMilliseconds); + } + + /// + protected override bool OnShutdown(int timeoutMilliseconds) + { + return this.exporter.Shutdown(timeoutMilliseconds); + } + + /// + protected override void Dispose(bool disposing) + { + if (!this.disposed) { - if (!this.disposed) + if (disposing) { - if (disposing) + try { - try - { - this.exporter.Dispose(); - } - catch (Exception ex) - { - OpenTelemetrySdkEventSource.Log.SpanProcessorException(nameof(this.Dispose), ex); - } + this.exporter.Dispose(); + } + catch (Exception ex) + { + OpenTelemetrySdkEventSource.Log.SpanProcessorException(nameof(this.Dispose), ex); } - - this.disposed = true; } - base.Dispose(disposing); + this.disposed = true; } + + base.Dispose(disposing); } } diff --git a/src/OpenTelemetry/BaseExporter.cs b/src/OpenTelemetry/BaseExporter.cs index 40c3b8b210b..9fdf396a38c 100644 --- a/src/OpenTelemetry/BaseExporter.cs +++ b/src/OpenTelemetry/BaseExporter.cs @@ -1,191 +1,175 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Internal; -namespace OpenTelemetry +namespace OpenTelemetry; + +/// +/// Enumeration used to define the result of an export operation. +/// +public enum ExportResult { /// - /// Enumeration used to define the result of an export operation. + /// Export succeeded. /// - public enum ExportResult - { - /// - /// Export succeeded. - /// - Success = 0, - - /// - /// Export failed. - /// - Failure = 1, - } + Success = 0, /// - /// Exporter base class. + /// Export failed. /// - /// The type of object to be exported. - public abstract class BaseExporter : IDisposable - where T : class + Failure = 1, +} + +/// +/// Exporter base class. +/// +/// The type of object to be exported. +public abstract class BaseExporter : IDisposable + where T : class +{ + private int shutdownCount; + + /// + /// Gets the parent . + /// + public BaseProvider? ParentProvider { get; internal set; } + + /// + /// Exports a batch of telemetry objects. + /// + /// Batch of telemetry objects to export. + /// Result of the export operation. + public abstract ExportResult Export(in Batch batch); + + /// + /// Flushes the exporter, blocks the current thread until flush + /// completed, shutdown signaled or timed out. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when flush succeeded; otherwise, false. + /// + /// + /// Thrown when the timeoutMilliseconds is smaller than -1. + /// + /// + /// This function guarantees thread-safety. + /// + public bool ForceFlush(int timeoutMilliseconds = Timeout.Infinite) { - private int shutdownCount; - - /// - /// Gets the parent . - /// - public BaseProvider? ParentProvider { get; internal set; } - - /// - /// Exports a batch of telemetry objects. - /// - /// Batch of telemetry objects to export. - /// Result of the export operation. - public abstract ExportResult Export(in Batch batch); - - /// - /// Flushes the exporter, blocks the current thread until flush - /// completed, shutdown signaled or timed out. - /// - /// - /// The number (non-negative) of milliseconds to wait, or - /// Timeout.Infinite to wait indefinitely. - /// - /// - /// Returns true when flush succeeded; otherwise, false. - /// - /// - /// Thrown when the timeoutMilliseconds is smaller than -1. - /// - /// - /// This function guarantees thread-safety. - /// - public bool ForceFlush(int timeoutMilliseconds = Timeout.Infinite) - { - Guard.ThrowIfInvalidTimeout(timeoutMilliseconds); - - try - { - return this.OnForceFlush(timeoutMilliseconds); - } - catch (Exception ex) - { - OpenTelemetrySdkEventSource.Log.SpanProcessorException(nameof(this.ForceFlush), ex); - return false; - } - } + Guard.ThrowIfInvalidTimeout(timeoutMilliseconds); - /// - /// Attempts to shutdown the exporter, blocks the current thread until - /// shutdown completed or timed out. - /// - /// - /// The number (non-negative) of milliseconds to wait, or - /// Timeout.Infinite to wait indefinitely. - /// - /// - /// Returns true when shutdown succeeded; otherwise, false. - /// - /// - /// Thrown when the timeoutMilliseconds is smaller than -1. - /// - /// - /// This function guarantees thread-safety. Only the first call will - /// win, subsequent calls will be no-op. - /// - public bool Shutdown(int timeoutMilliseconds = Timeout.Infinite) + try { - Guard.ThrowIfInvalidTimeout(timeoutMilliseconds); - - if (Interlocked.Increment(ref this.shutdownCount) > 1) - { - return false; // shutdown already called - } - - try - { - return this.OnShutdown(timeoutMilliseconds); - } - catch (Exception ex) - { - OpenTelemetrySdkEventSource.Log.SpanProcessorException(nameof(this.Shutdown), ex); - return false; - } + return this.OnForceFlush(timeoutMilliseconds); } - - /// - public void Dispose() + catch (Exception ex) { - this.Dispose(true); - GC.SuppressFinalize(this); + OpenTelemetrySdkEventSource.Log.SpanProcessorException(nameof(this.ForceFlush), ex); + return false; } + } + + /// + /// Attempts to shutdown the exporter, blocks the current thread until + /// shutdown completed or timed out. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when shutdown succeeded; otherwise, false. + /// + /// + /// Thrown when the timeoutMilliseconds is smaller than -1. + /// + /// + /// This function guarantees thread-safety. Only the first call will + /// win, subsequent calls will be no-op. + /// + public bool Shutdown(int timeoutMilliseconds = Timeout.Infinite) + { + Guard.ThrowIfInvalidTimeout(timeoutMilliseconds); - /// - /// Called by ForceFlush. This function should block the current - /// thread until flush completed, shutdown signaled or timed out. - /// - /// - /// The number (non-negative) of milliseconds to wait, or - /// Timeout.Infinite to wait indefinitely. - /// - /// - /// Returns true when flush succeeded; otherwise, false. - /// - /// - /// This function is called synchronously on the thread which called - /// ForceFlush. This function should be thread-safe, and should - /// not throw exceptions. - /// - protected virtual bool OnForceFlush(int timeoutMilliseconds) + if (Interlocked.Increment(ref this.shutdownCount) > 1) { - return true; + return false; // shutdown already called } - /// - /// Called by Shutdown. This function should block the current - /// thread until shutdown completed or timed out. - /// - /// - /// The number (non-negative) of milliseconds to wait, or - /// Timeout.Infinite to wait indefinitely. - /// - /// - /// Returns true when shutdown succeeded; otherwise, false. - /// - /// - /// This function is called synchronously on the thread which made the - /// first call to Shutdown. This function should not throw - /// exceptions. - /// - protected virtual bool OnShutdown(int timeoutMilliseconds) + try { - return true; + return this.OnShutdown(timeoutMilliseconds); } - - /// - /// Releases the unmanaged resources used by this class and optionally - /// releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) + catch (Exception ex) { + OpenTelemetrySdkEventSource.Log.SpanProcessorException(nameof(this.Shutdown), ex); + return false; } } + + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Called by ForceFlush. This function should block the current + /// thread until flush completed, shutdown signaled or timed out. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when flush succeeded; otherwise, false. + /// + /// + /// This function is called synchronously on the thread which called + /// ForceFlush. This function should be thread-safe, and should + /// not throw exceptions. + /// + protected virtual bool OnForceFlush(int timeoutMilliseconds) + { + return true; + } + + /// + /// Called by Shutdown. This function should block the current + /// thread until shutdown completed or timed out. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when shutdown succeeded; otherwise, false. + /// + /// + /// This function is called synchronously on the thread which made the + /// first call to Shutdown. This function should not throw + /// exceptions. + /// + protected virtual bool OnShutdown(int timeoutMilliseconds) + { + return true; + } + + /// + /// Releases the unmanaged resources used by this class and optionally + /// releases the managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + } } diff --git a/src/OpenTelemetry/BaseProcessor.cs b/src/OpenTelemetry/BaseProcessor.cs index 8020a141b50..88e1aca35e0 100644 --- a/src/OpenTelemetry/BaseProcessor.cs +++ b/src/OpenTelemetry/BaseProcessor.cs @@ -1,219 +1,203 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Internal; -namespace OpenTelemetry +namespace OpenTelemetry; + +/// +/// Base processor base class. +/// +/// The type of object to be processed. +public abstract class BaseProcessor : IDisposable { + private readonly string typeName; + private int shutdownCount; + /// - /// Base processor base class. + /// Initializes a new instance of the class. /// - /// The type of object to be processed. - public abstract class BaseProcessor : IDisposable + public BaseProcessor() { - private readonly string typeName; - private int shutdownCount; - - /// - /// Initializes a new instance of the class. - /// - public BaseProcessor() - { - this.typeName = this.GetType().Name; - } + this.typeName = this.GetType().Name; + } - /// - /// Gets the parent . - /// - public BaseProvider? ParentProvider { get; private set; } - - /// - /// Called synchronously when a telemetry object is started. - /// - /// - /// The started telemetry object. - /// - /// - /// This function is called synchronously on the thread which started - /// the telemetry object. This function should be thread-safe, and - /// should not block indefinitely or throw exceptions. - /// - public virtual void OnStart(T data) - { - } + /// + /// Gets the parent . + /// + public BaseProvider? ParentProvider { get; private set; } - /// - /// Called synchronously when a telemetry object is ended. - /// - /// - /// The ended telemetry object. - /// - /// - /// This function is called synchronously on the thread which ended - /// the telemetry object. This function should be thread-safe, and - /// should not block indefinitely or throw exceptions. - /// - public virtual void OnEnd(T data) - { - } + /// + /// Called synchronously when a telemetry object is started. + /// + /// + /// The started telemetry object. + /// + /// + /// This function is called synchronously on the thread which started + /// the telemetry object. This function should be thread-safe, and + /// should not block indefinitely or throw exceptions. + /// + public virtual void OnStart(T data) + { + } - /// - /// Flushes the processor, blocks the current thread until flush - /// completed, shutdown signaled or timed out. - /// - /// - /// The number (non-negative) of milliseconds to wait, or - /// Timeout.Infinite to wait indefinitely. - /// - /// - /// Returns true when flush succeeded; otherwise, false. - /// - /// - /// Thrown when the timeoutMilliseconds is smaller than -1. - /// - /// - /// This function guarantees thread-safety. - /// - public bool ForceFlush(int timeoutMilliseconds = Timeout.Infinite) - { - Guard.ThrowIfInvalidTimeout(timeoutMilliseconds); + /// + /// Called synchronously when a telemetry object is ended. + /// + /// + /// The ended telemetry object. + /// + /// + /// This function is called synchronously on the thread which ended + /// the telemetry object. This function should be thread-safe, and + /// should not block indefinitely or throw exceptions. + /// + public virtual void OnEnd(T data) + { + } - try - { - bool result = this.OnForceFlush(timeoutMilliseconds); + /// + /// Flushes the processor, blocks the current thread until flush + /// completed, shutdown signaled or timed out. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when flush succeeded; otherwise, false. + /// + /// + /// Thrown when the timeoutMilliseconds is smaller than -1. + /// + /// + /// This function guarantees thread-safety. + /// + public bool ForceFlush(int timeoutMilliseconds = Timeout.Infinite) + { + Guard.ThrowIfInvalidTimeout(timeoutMilliseconds); - OpenTelemetrySdkEventSource.Log.ProcessorForceFlushInvoked(this.typeName, result); + try + { + bool result = this.OnForceFlush(timeoutMilliseconds); - return result; - } - catch (Exception ex) - { - OpenTelemetrySdkEventSource.Log.SpanProcessorException(nameof(this.ForceFlush), ex); - return false; - } - } + OpenTelemetrySdkEventSource.Log.ProcessorForceFlushInvoked(this.typeName, result); - /// - /// Attempts to shutdown the processor, blocks the current thread until - /// shutdown completed or timed out. - /// - /// - /// The number (non-negative) of milliseconds to wait, or - /// Timeout.Infinite to wait indefinitely. - /// - /// - /// Returns true when shutdown succeeded; otherwise, false. - /// - /// - /// Thrown when the timeoutMilliseconds is smaller than -1. - /// - /// - /// This function guarantees thread-safety. Only the first call will - /// win, subsequent calls will be no-op. - /// - public bool Shutdown(int timeoutMilliseconds = Timeout.Infinite) - { - Guard.ThrowIfInvalidTimeout(timeoutMilliseconds); - - if (Interlocked.CompareExchange(ref this.shutdownCount, 1, 0) != 0) - { - return false; // shutdown already called - } - - try - { - return this.OnShutdown(timeoutMilliseconds); - } - catch (Exception ex) - { - OpenTelemetrySdkEventSource.Log.SpanProcessorException(nameof(this.Shutdown), ex); - return false; - } + return result; } - - /// - public void Dispose() + catch (Exception ex) { - this.Dispose(true); - GC.SuppressFinalize(this); + OpenTelemetrySdkEventSource.Log.SpanProcessorException(nameof(this.ForceFlush), ex); + return false; } + } - /// - public override string ToString() - => this.typeName; + /// + /// Attempts to shutdown the processor, blocks the current thread until + /// shutdown completed or timed out. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when shutdown succeeded; otherwise, false. + /// + /// + /// Thrown when the timeoutMilliseconds is smaller than -1. + /// + /// + /// This function guarantees thread-safety. Only the first call will + /// win, subsequent calls will be no-op. + /// + public bool Shutdown(int timeoutMilliseconds = Timeout.Infinite) + { + Guard.ThrowIfInvalidTimeout(timeoutMilliseconds); - internal virtual void SetParentProvider(BaseProvider parentProvider) + if (Interlocked.CompareExchange(ref this.shutdownCount, 1, 0) != 0) { - this.ParentProvider = parentProvider; + return false; // shutdown already called } - /// - /// Called by ForceFlush. This function should block the current - /// thread until flush completed, shutdown signaled or timed out. - /// - /// - /// The number (non-negative) of milliseconds to wait, or - /// Timeout.Infinite to wait indefinitely. - /// - /// - /// Returns true when flush succeeded; otherwise, false. - /// - /// - /// This function is called synchronously on the thread which called - /// ForceFlush. This function should be thread-safe, and should - /// not throw exceptions. - /// - protected virtual bool OnForceFlush(int timeoutMilliseconds) + try { - return true; + return this.OnShutdown(timeoutMilliseconds); } - - /// - /// Called by Shutdown. This function should block the current - /// thread until shutdown completed or timed out. - /// - /// - /// The number (non-negative) of milliseconds to wait, or - /// Timeout.Infinite to wait indefinitely. - /// - /// - /// Returns true when shutdown succeeded; otherwise, false. - /// - /// - /// This function is called synchronously on the thread which made the - /// first call to Shutdown. This function should not throw - /// exceptions. - /// - protected virtual bool OnShutdown(int timeoutMilliseconds) + catch (Exception ex) { - return true; + OpenTelemetrySdkEventSource.Log.SpanProcessorException(nameof(this.Shutdown), ex); + return false; } + } - /// - /// Releases the unmanaged resources used by this class and optionally - /// releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) - { - } + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + public override string ToString() + => this.typeName; + + internal virtual void SetParentProvider(BaseProvider parentProvider) + { + this.ParentProvider = parentProvider; + } + + /// + /// Called by ForceFlush. This function should block the current + /// thread until flush completed, shutdown signaled or timed out. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when flush succeeded; otherwise, false. + /// + /// + /// This function is called synchronously on the thread which called + /// ForceFlush. This function should be thread-safe, and should + /// not throw exceptions. + /// + protected virtual bool OnForceFlush(int timeoutMilliseconds) + { + return true; + } + + /// + /// Called by Shutdown. This function should block the current + /// thread until shutdown completed or timed out. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when shutdown succeeded; otherwise, false. + /// + /// + /// This function is called synchronously on the thread which made the + /// first call to Shutdown. This function should not throw + /// exceptions. + /// + protected virtual bool OnShutdown(int timeoutMilliseconds) + { + return true; + } + + /// + /// Releases the unmanaged resources used by this class and optionally + /// releases the managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { } } diff --git a/src/OpenTelemetry/Batch.cs b/src/OpenTelemetry/Batch.cs index ed2427bd9c3..b4ddd08cf17 100644 --- a/src/OpenTelemetry/Batch.cs +++ b/src/OpenTelemetry/Batch.cs @@ -1,20 +1,5 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Collections; using System.Diagnostics; @@ -22,229 +7,241 @@ using OpenTelemetry.Internal; using OpenTelemetry.Logs; -namespace OpenTelemetry +namespace OpenTelemetry; + +/// +/// Stores a batch of completed objects to be exported. +/// +/// The type of object in the . +public readonly struct Batch : IDisposable + where T : class { + private readonly T? item = null; + private readonly CircularBuffer? circularBuffer = null; + private readonly T[]? items = null; + private readonly long targetCount; + /// - /// Stores a batch of completed objects to be exported. + /// Initializes a new instance of the struct. /// - /// The type of object in the . - public readonly struct Batch : IDisposable - where T : class + /// The items to store in the batch. + /// The number of items in the batch. + public Batch(T[] items, int count) { - private readonly T? item = null; - private readonly CircularBuffer? circularBuffer = null; - private readonly T[]? items = null; - private readonly long targetCount; - - /// - /// Initializes a new instance of the struct. - /// - /// The items to store in the batch. - /// The number of items in the batch. - public Batch(T[] items, int count) - { - Guard.ThrowIfNull(items); - Guard.ThrowIfOutOfRange(count, min: 0, max: items.Length); + Guard.ThrowIfNull(items); + Guard.ThrowIfOutOfRange(count, min: 0, max: items.Length); - this.items = items; - this.Count = this.targetCount = count; - } + this.items = items; + this.Count = this.targetCount = count; + } - internal Batch(T item) - { - Debug.Assert(item != null, $"{nameof(item)} was null."); + internal Batch(T item) + { + Debug.Assert(item != null, $"{nameof(item)} was null."); - this.item = item; - this.Count = this.targetCount = 1; - } + this.item = item; + this.Count = this.targetCount = 1; + } - internal Batch(CircularBuffer circularBuffer, int maxSize) - { - Debug.Assert(maxSize > 0, $"{nameof(maxSize)} should be a positive number."); - Debug.Assert(circularBuffer != null, $"{nameof(circularBuffer)} was null."); + internal Batch(CircularBuffer circularBuffer, int maxSize) + { + Debug.Assert(maxSize > 0, $"{nameof(maxSize)} should be a positive number."); + Debug.Assert(circularBuffer != null, $"{nameof(circularBuffer)} was null."); - this.circularBuffer = circularBuffer; - this.Count = Math.Min(maxSize, circularBuffer!.Count); - this.targetCount = circularBuffer.RemovedCount + this.Count; - } + this.circularBuffer = circularBuffer; + this.Count = Math.Min(maxSize, circularBuffer!.Count); + this.targetCount = circularBuffer.RemovedCount + this.Count; + } - private delegate bool BatchEnumeratorMoveNextFunc(ref Enumerator enumerator); + private delegate bool BatchEnumeratorMoveNextFunc(ref Enumerator enumerator); - /// - /// Gets the count of items in the batch. - /// - public long Count { get; } + /// + /// Gets the count of items in the batch. + /// + public long Count { get; } - /// - public void Dispose() + /// + public void Dispose() + { + if (this.circularBuffer != null) { - if (this.circularBuffer != null) + // Drain anything left in the batch. + while (this.circularBuffer.RemovedCount < this.targetCount) { - // Drain anything left in the batch. - while (this.circularBuffer.RemovedCount < this.targetCount) + T item = this.circularBuffer.Read(); + if (typeof(T) == typeof(LogRecord)) { - T item = this.circularBuffer.Read(); - if (typeof(T) == typeof(LogRecord)) + var logRecord = (LogRecord)(object)item; + if (logRecord.Source == LogRecord.LogRecordSource.FromSharedPool) { - LogRecordSharedPool.Current.Return((LogRecord)(object)item); + LogRecordSharedPool.Current.Return(logRecord); } } } } + } - /// - /// Returns an enumerator that iterates through the . - /// - /// . - public Enumerator GetEnumerator() - { - return this.circularBuffer != null - ? new Enumerator(this.circularBuffer, this.targetCount) - : this.item != null - ? new Enumerator(this.item) - /* In the event someone uses default/new Batch() to create Batch we fallback to empty items mode. */ - : new Enumerator(this.items ?? Array.Empty(), this.targetCount); - } + /// + /// Returns an enumerator that iterates through the . + /// + /// . + public Enumerator GetEnumerator() + { + return this.circularBuffer != null + ? new Enumerator(this.circularBuffer, this.targetCount) + : this.item != null + ? new Enumerator(this.item) + /* In the event someone uses default/new Batch() to create Batch we fallback to empty items mode. */ + : new Enumerator(this.items ?? Array.Empty(), this.targetCount); + } - /// - /// Enumerates the elements of a . - /// - public struct Enumerator : IEnumerator + /// + /// Enumerates the elements of a . + /// + public struct Enumerator : IEnumerator + { + private static readonly BatchEnumeratorMoveNextFunc MoveNextSingleItem = (ref Enumerator enumerator) => { - private static readonly BatchEnumeratorMoveNextFunc MoveNextSingleItem = (ref Enumerator enumerator) => + if (enumerator.targetCount >= 0) { - if (enumerator.targetCount >= 0) - { - enumerator.current = null; - return false; - } + enumerator.current = null; + return false; + } - enumerator.targetCount++; - return true; - }; + enumerator.targetCount++; + return true; + }; - private static readonly BatchEnumeratorMoveNextFunc MoveNextCircularBuffer = (ref Enumerator enumerator) => - { - var circularBuffer = enumerator.circularBuffer; + private static readonly BatchEnumeratorMoveNextFunc MoveNextCircularBuffer = (ref Enumerator enumerator) => + { + var circularBuffer = enumerator.circularBuffer; - if (circularBuffer!.RemovedCount < enumerator.targetCount) - { - enumerator.current = circularBuffer.Read(); - return true; - } + if (circularBuffer!.RemovedCount < enumerator.targetCount) + { + enumerator.current = circularBuffer.Read(); + return true; + } - enumerator.current = null; - return false; - }; + enumerator.current = null; + return false; + }; - private static readonly BatchEnumeratorMoveNextFunc MoveNextCircularBufferLogRecord = (ref Enumerator enumerator) => + private static readonly BatchEnumeratorMoveNextFunc MoveNextCircularBufferLogRecord = (ref Enumerator enumerator) => + { + // Note: This type check here is to give the JIT a hint it can + // remove all of this code when T != LogRecord + if (typeof(T) == typeof(LogRecord)) { - // Note: This type check here is to give the JIT a hint it can - // remove all of this code when T != LogRecord - if (typeof(T) == typeof(LogRecord)) - { - var circularBuffer = enumerator.circularBuffer; - - var currentItem = enumerator.Current; + var circularBuffer = enumerator.circularBuffer; - if (currentItem != null) - { - LogRecordSharedPool.Current.Return((LogRecord)(object)currentItem); - } + var currentItem = enumerator.Current; - if (circularBuffer!.RemovedCount < enumerator.targetCount) + if (currentItem != null) + { + var logRecord = (LogRecord)(object)currentItem; + if (logRecord.Source == LogRecord.LogRecordSource.FromSharedPool) { - enumerator.current = circularBuffer.Read(); - return true; + LogRecordSharedPool.Current.Return(logRecord); } - - enumerator.current = null; } - return false; - }; - - private static readonly BatchEnumeratorMoveNextFunc MoveNextArray = (ref Enumerator enumerator) => - { - var items = enumerator.items; - - if (enumerator.itemIndex < enumerator.targetCount) + if (circularBuffer!.RemovedCount < enumerator.targetCount) { - enumerator.current = items![enumerator.itemIndex++]; + enumerator.current = circularBuffer.Read(); return true; } enumerator.current = null; - return false; - }; + } - private readonly CircularBuffer? circularBuffer; - private readonly T[]? items; - private readonly BatchEnumeratorMoveNextFunc moveNextFunc; - private long targetCount; - private int itemIndex; - [AllowNull] - private T current; + return false; + }; - internal Enumerator(T item) - { - this.current = item; - this.circularBuffer = null; - this.items = null; - this.targetCount = -1; - this.itemIndex = 0; - this.moveNextFunc = MoveNextSingleItem; - } + private static readonly BatchEnumeratorMoveNextFunc MoveNextArray = (ref Enumerator enumerator) => + { + var items = enumerator.items; - internal Enumerator(CircularBuffer circularBuffer, long targetCount) + if (enumerator.itemIndex < enumerator.targetCount) { - this.current = null; - this.items = null; - this.circularBuffer = circularBuffer; - this.targetCount = targetCount; - this.itemIndex = 0; - this.moveNextFunc = typeof(T) == typeof(LogRecord) ? MoveNextCircularBufferLogRecord : MoveNextCircularBuffer; + enumerator.current = items![enumerator.itemIndex++]; + return true; } - internal Enumerator(T[] items, long targetCount) - { - this.current = null; - this.circularBuffer = null; - this.items = items; - this.targetCount = targetCount; - this.itemIndex = 0; - this.moveNextFunc = MoveNextArray; - } + enumerator.current = null; + return false; + }; + + private readonly CircularBuffer? circularBuffer; + private readonly T[]? items; + private readonly BatchEnumeratorMoveNextFunc moveNextFunc; + private long targetCount; + private int itemIndex; + [AllowNull] + private T current; + + internal Enumerator(T item) + { + this.current = item; + this.circularBuffer = null; + this.items = null; + this.targetCount = -1; + this.itemIndex = 0; + this.moveNextFunc = MoveNextSingleItem; + } - /// - public readonly T Current => this.current; + internal Enumerator(CircularBuffer circularBuffer, long targetCount) + { + this.current = null; + this.items = null; + this.circularBuffer = circularBuffer; + this.targetCount = targetCount; + this.itemIndex = 0; + this.moveNextFunc = typeof(T) == typeof(LogRecord) ? MoveNextCircularBufferLogRecord : MoveNextCircularBuffer; + } - /// - readonly object? IEnumerator.Current => this.current; + internal Enumerator(T[] items, long targetCount) + { + this.current = null; + this.circularBuffer = null; + this.items = items; + this.targetCount = targetCount; + this.itemIndex = 0; + this.moveNextFunc = MoveNextArray; + } + + /// + public readonly T Current => this.current; - /// - public void Dispose() + /// + readonly object? IEnumerator.Current => this.current; + + /// + public void Dispose() + { + if (typeof(T) == typeof(LogRecord)) { - if (typeof(T) == typeof(LogRecord)) + var currentItem = this.current; + if (currentItem != null) { - var currentItem = this.current; - if (currentItem != null) + var logRecord = (LogRecord)(object)currentItem; + if (logRecord.Source == LogRecord.LogRecordSource.FromSharedPool) { - LogRecordSharedPool.Current.Return((LogRecord)(object)currentItem); - this.current = null; + LogRecordSharedPool.Current.Return(logRecord); } - } - } - /// - public bool MoveNext() - { - return this.moveNextFunc(ref this); + this.current = null; + } } + } - /// - public readonly void Reset() - => throw new NotSupportedException(); + /// + public bool MoveNext() + { + return this.moveNextFunc(ref this); } + + /// + public readonly void Reset() + => throw new NotSupportedException(); } } diff --git a/src/OpenTelemetry/BatchExportProcessor.cs b/src/OpenTelemetry/BatchExportProcessor.cs index 5b587dddcb2..8e1e2ed0e58 100644 --- a/src/OpenTelemetry/BatchExportProcessor.cs +++ b/src/OpenTelemetry/BatchExportProcessor.cs @@ -1,305 +1,289 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Runtime.CompilerServices; using OpenTelemetry.Internal; -namespace OpenTelemetry +namespace OpenTelemetry; + +/// +/// Implements processor that batches telemetry objects before calling exporter. +/// +/// The type of telemetry object to be exported. +public abstract class BatchExportProcessor : BaseExportProcessor + where T : class { + internal const int DefaultMaxQueueSize = 2048; + internal const int DefaultScheduledDelayMilliseconds = 5000; + internal const int DefaultExporterTimeoutMilliseconds = 30000; + internal const int DefaultMaxExportBatchSize = 512; + + internal readonly int MaxExportBatchSize; + + private readonly CircularBuffer circularBuffer; + private readonly int scheduledDelayMilliseconds; + private readonly int exporterTimeoutMilliseconds; + private readonly Thread exporterThread; + private readonly AutoResetEvent exportTrigger = new(false); + private readonly ManualResetEvent dataExportedNotification = new(false); + private readonly ManualResetEvent shutdownTrigger = new(false); + private long shutdownDrainTarget = long.MaxValue; + private long droppedCount; + private bool disposed; + /// - /// Implements processor that batches telemetry objects before calling exporter. + /// Initializes a new instance of the class. /// - /// The type of telemetry object to be exported. - public abstract class BatchExportProcessor : BaseExportProcessor - where T : class + /// Exporter instance. + /// The maximum queue size. After the size is reached data are dropped. The default value is 2048. + /// The delay interval in milliseconds between two consecutive exports. The default value is 5000. + /// How long the export can run before it is cancelled. The default value is 30000. + /// The maximum batch size of every export. It must be smaller or equal to maxQueueSize. The default value is 512. + protected BatchExportProcessor( + BaseExporter exporter, + int maxQueueSize = DefaultMaxQueueSize, + int scheduledDelayMilliseconds = DefaultScheduledDelayMilliseconds, + int exporterTimeoutMilliseconds = DefaultExporterTimeoutMilliseconds, + int maxExportBatchSize = DefaultMaxExportBatchSize) + : base(exporter) { - internal const int DefaultMaxQueueSize = 2048; - internal const int DefaultScheduledDelayMilliseconds = 5000; - internal const int DefaultExporterTimeoutMilliseconds = 30000; - internal const int DefaultMaxExportBatchSize = 512; - - internal readonly int MaxExportBatchSize; - - private readonly CircularBuffer circularBuffer; - private readonly int scheduledDelayMilliseconds; - private readonly int exporterTimeoutMilliseconds; - private readonly Thread exporterThread; - private readonly AutoResetEvent exportTrigger = new(false); - private readonly ManualResetEvent dataExportedNotification = new(false); - private readonly ManualResetEvent shutdownTrigger = new(false); - private long shutdownDrainTarget = long.MaxValue; - private long droppedCount; - private bool disposed; - - /// - /// Initializes a new instance of the class. - /// - /// Exporter instance. - /// The maximum queue size. After the size is reached data are dropped. The default value is 2048. - /// The delay interval in milliseconds between two consecutive exports. The default value is 5000. - /// How long the export can run before it is cancelled. The default value is 30000. - /// The maximum batch size of every export. It must be smaller or equal to maxQueueSize. The default value is 512. - protected BatchExportProcessor( - BaseExporter exporter, - int maxQueueSize = DefaultMaxQueueSize, - int scheduledDelayMilliseconds = DefaultScheduledDelayMilliseconds, - int exporterTimeoutMilliseconds = DefaultExporterTimeoutMilliseconds, - int maxExportBatchSize = DefaultMaxExportBatchSize) - : base(exporter) + Guard.ThrowIfOutOfRange(maxQueueSize, min: 1); + Guard.ThrowIfOutOfRange(maxExportBatchSize, min: 1, max: maxQueueSize, maxName: nameof(maxQueueSize)); + Guard.ThrowIfOutOfRange(scheduledDelayMilliseconds, min: 1); + Guard.ThrowIfOutOfRange(exporterTimeoutMilliseconds, min: 0); + + this.circularBuffer = new CircularBuffer(maxQueueSize); + this.scheduledDelayMilliseconds = scheduledDelayMilliseconds; + this.exporterTimeoutMilliseconds = exporterTimeoutMilliseconds; + this.MaxExportBatchSize = maxExportBatchSize; + this.exporterThread = new Thread(this.ExporterProc) { - Guard.ThrowIfOutOfRange(maxQueueSize, min: 1); - Guard.ThrowIfOutOfRange(maxExportBatchSize, min: 1, max: maxQueueSize, maxName: nameof(maxQueueSize)); - Guard.ThrowIfOutOfRange(scheduledDelayMilliseconds, min: 1); - Guard.ThrowIfOutOfRange(exporterTimeoutMilliseconds, min: 0); - - this.circularBuffer = new CircularBuffer(maxQueueSize); - this.scheduledDelayMilliseconds = scheduledDelayMilliseconds; - this.exporterTimeoutMilliseconds = exporterTimeoutMilliseconds; - this.MaxExportBatchSize = maxExportBatchSize; - this.exporterThread = new Thread(this.ExporterProc) - { - IsBackground = true, - Name = $"OpenTelemetry-{nameof(BatchExportProcessor)}-{exporter.GetType().Name}", - }; - this.exporterThread.Start(); - } + IsBackground = true, + Name = $"OpenTelemetry-{nameof(BatchExportProcessor)}-{exporter.GetType().Name}", + }; + this.exporterThread.Start(); + } - /// - /// Gets the number of telemetry objects dropped by the processor. - /// - internal long DroppedCount => Volatile.Read(ref this.droppedCount); + /// + /// Gets the number of telemetry objects dropped by the processor. + /// + internal long DroppedCount => Volatile.Read(ref this.droppedCount); - /// - /// Gets the number of telemetry objects received by the processor. - /// - internal long ReceivedCount => this.circularBuffer.AddedCount + this.DroppedCount; + /// + /// Gets the number of telemetry objects received by the processor. + /// + internal long ReceivedCount => this.circularBuffer.AddedCount + this.DroppedCount; - /// - /// Gets the number of telemetry objects processed by the underlying exporter. - /// - internal long ProcessedCount => this.circularBuffer.RemovedCount; + /// + /// Gets the number of telemetry objects processed by the underlying exporter. + /// + internal long ProcessedCount => this.circularBuffer.RemovedCount; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal bool TryExport(T data) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool TryExport(T data) + { + if (this.circularBuffer.TryAdd(data, maxSpinCount: 50000)) { - if (this.circularBuffer.TryAdd(data, maxSpinCount: 50000)) + if (this.circularBuffer.Count >= this.MaxExportBatchSize) { - if (this.circularBuffer.Count >= this.MaxExportBatchSize) + try + { + this.exportTrigger.Set(); + } + catch (ObjectDisposedException) { - try - { - this.exportTrigger.Set(); - } - catch (ObjectDisposedException) - { - } } - - return true; // enqueue succeeded } - // either the queue is full or exceeded the spin limit, drop the item on the floor - Interlocked.Increment(ref this.droppedCount); - - return false; + return true; // enqueue succeeded } - /// - protected override void OnExport(T data) + // either the queue is full or exceeded the spin limit, drop the item on the floor + Interlocked.Increment(ref this.droppedCount); + + return false; + } + + /// + protected override void OnExport(T data) + { + this.TryExport(data); + } + + /// + protected override bool OnForceFlush(int timeoutMilliseconds) + { + var tail = this.circularBuffer.RemovedCount; + var head = this.circularBuffer.AddedCount; + + if (head == tail) { - this.TryExport(data); + return true; // nothing to flush } - /// - protected override bool OnForceFlush(int timeoutMilliseconds) + try { - var tail = this.circularBuffer.RemovedCount; - var head = this.circularBuffer.AddedCount; - - if (head == tail) - { - return true; // nothing to flush - } - - try - { - this.exportTrigger.Set(); - } - catch (ObjectDisposedException) - { - return false; - } + this.exportTrigger.Set(); + } + catch (ObjectDisposedException) + { + return false; + } - if (timeoutMilliseconds == 0) - { - return false; - } + if (timeoutMilliseconds == 0) + { + return false; + } - var triggers = new WaitHandle[] { this.dataExportedNotification, this.shutdownTrigger }; + var triggers = new WaitHandle[] { this.dataExportedNotification, this.shutdownTrigger }; - var sw = timeoutMilliseconds == Timeout.Infinite - ? null - : Stopwatch.StartNew(); + var sw = timeoutMilliseconds == Timeout.Infinite + ? null + : Stopwatch.StartNew(); - // There is a chance that the export thread finished processing all the data from the queue, - // and signaled before we enter wait here, use polling to prevent being blocked indefinitely. - const int pollingMilliseconds = 1000; + // There is a chance that the export thread finished processing all the data from the queue, + // and signaled before we enter wait here, use polling to prevent being blocked indefinitely. + const int pollingMilliseconds = 1000; - while (true) + while (true) + { + if (sw == null) { - if (sw == null) + try { - try - { - WaitHandle.WaitAny(triggers, pollingMilliseconds); - } - catch (ObjectDisposedException) - { - return false; - } + WaitHandle.WaitAny(triggers, pollingMilliseconds); } - else + catch (ObjectDisposedException) { - var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; - - if (timeout <= 0) - { - return this.circularBuffer.RemovedCount >= head; - } - - try - { - WaitHandle.WaitAny(triggers, Math.Min((int)timeout, pollingMilliseconds)); - } - catch (ObjectDisposedException) - { - return false; - } + return false; } + } + else + { + var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; - if (this.circularBuffer.RemovedCount >= head) + if (timeout <= 0) { - return true; + return this.circularBuffer.RemovedCount >= head; } - if (Volatile.Read(ref this.shutdownDrainTarget) != long.MaxValue) + try + { + WaitHandle.WaitAny(triggers, Math.Min((int)timeout, pollingMilliseconds)); + } + catch (ObjectDisposedException) { return false; } } - } - - /// - protected override bool OnShutdown(int timeoutMilliseconds) - { - Volatile.Write(ref this.shutdownDrainTarget, this.circularBuffer.AddedCount); - try + if (this.circularBuffer.RemovedCount >= head) { - this.shutdownTrigger.Set(); + return true; } - catch (ObjectDisposedException) + + if (Volatile.Read(ref this.shutdownDrainTarget) != long.MaxValue) { return false; } + } + } - OpenTelemetrySdkEventSource.Log.DroppedExportProcessorItems(this.GetType().Name, this.exporter.GetType().Name, this.DroppedCount); + /// + protected override bool OnShutdown(int timeoutMilliseconds) + { + Volatile.Write(ref this.shutdownDrainTarget, this.circularBuffer.AddedCount); - if (timeoutMilliseconds == Timeout.Infinite) - { - this.exporterThread.Join(); - return this.exporter.Shutdown(); - } + try + { + this.shutdownTrigger.Set(); + } + catch (ObjectDisposedException) + { + return false; + } - if (timeoutMilliseconds == 0) - { - return this.exporter.Shutdown(0); - } + OpenTelemetrySdkEventSource.Log.DroppedExportProcessorItems(this.GetType().Name, this.exporter.GetType().Name, this.DroppedCount); - var sw = Stopwatch.StartNew(); - this.exporterThread.Join(timeoutMilliseconds); - var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; - return this.exporter.Shutdown((int)Math.Max(timeout, 0)); + if (timeoutMilliseconds == Timeout.Infinite) + { + this.exporterThread.Join(); + return this.exporter.Shutdown(); } - /// - protected override void Dispose(bool disposing) + if (timeoutMilliseconds == 0) { - if (!this.disposed) - { - if (disposing) - { - this.exportTrigger.Dispose(); - this.dataExportedNotification.Dispose(); - this.shutdownTrigger.Dispose(); - } + return this.exporter.Shutdown(0); + } + + var sw = Stopwatch.StartNew(); + this.exporterThread.Join(timeoutMilliseconds); + var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; + return this.exporter.Shutdown((int)Math.Max(timeout, 0)); + } - this.disposed = true; + /// + protected override void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing) + { + this.exportTrigger.Dispose(); + this.dataExportedNotification.Dispose(); + this.shutdownTrigger.Dispose(); } - base.Dispose(disposing); + this.disposed = true; } - private void ExporterProc() - { - var triggers = new WaitHandle[] { this.exportTrigger, this.shutdownTrigger }; + base.Dispose(disposing); + } + + private void ExporterProc() + { + var triggers = new WaitHandle[] { this.exportTrigger, this.shutdownTrigger }; - while (true) + while (true) + { + // only wait when the queue doesn't have enough items, otherwise keep busy and send data continuously + if (this.circularBuffer.Count < this.MaxExportBatchSize) { - // only wait when the queue doesn't have enough items, otherwise keep busy and send data continuously - if (this.circularBuffer.Count < this.MaxExportBatchSize) + try + { + WaitHandle.WaitAny(triggers, this.scheduledDelayMilliseconds); + } + catch (ObjectDisposedException) { - try - { - WaitHandle.WaitAny(triggers, this.scheduledDelayMilliseconds); - } - catch (ObjectDisposedException) - { - // the exporter is somehow disposed before the worker thread could finish its job - return; - } + // the exporter is somehow disposed before the worker thread could finish its job + return; } + } - if (this.circularBuffer.Count > 0) + if (this.circularBuffer.Count > 0) + { + using (var batch = new Batch(this.circularBuffer, this.MaxExportBatchSize)) { - using (var batch = new Batch(this.circularBuffer, this.MaxExportBatchSize)) - { - this.exporter.Export(batch); - } - - try - { - this.dataExportedNotification.Set(); - this.dataExportedNotification.Reset(); - } - catch (ObjectDisposedException) - { - // the exporter is somehow disposed before the worker thread could finish its job - return; - } + this.exporter.Export(batch); } - if (this.circularBuffer.RemovedCount >= Volatile.Read(ref this.shutdownDrainTarget)) + try { + this.dataExportedNotification.Set(); + this.dataExportedNotification.Reset(); + } + catch (ObjectDisposedException) + { + // the exporter is somehow disposed before the worker thread could finish its job return; } } + + if (this.circularBuffer.RemovedCount >= Volatile.Read(ref this.shutdownDrainTarget)) + { + return; + } } } } diff --git a/src/OpenTelemetry/BatchExportProcessorOptions.cs b/src/OpenTelemetry/BatchExportProcessorOptions.cs index 827d90cb5b8..67345e9715c 100644 --- a/src/OpenTelemetry/BatchExportProcessorOptions.cs +++ b/src/OpenTelemetry/BatchExportProcessorOptions.cs @@ -1,44 +1,32 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -#nullable enable +namespace OpenTelemetry; -namespace OpenTelemetry +/// +/// Contains batch export processor options. +/// +/// The type of telemetry object to be exported. +public class BatchExportProcessorOptions + where T : class { - public class BatchExportProcessorOptions - where T : class - { - /// - /// Gets or sets the maximum queue size. The queue drops the data if the maximum size is reached. The default value is 2048. - /// - public int MaxQueueSize { get; set; } = BatchExportProcessor.DefaultMaxQueueSize; + /// + /// Gets or sets the maximum queue size. The queue drops the data if the maximum size is reached. The default value is 2048. + /// + public int MaxQueueSize { get; set; } = BatchExportProcessor.DefaultMaxQueueSize; - /// - /// Gets or sets the delay interval (in milliseconds) between two consecutive exports. The default value is 5000. - /// - public int ScheduledDelayMilliseconds { get; set; } = BatchExportProcessor.DefaultScheduledDelayMilliseconds; + /// + /// Gets or sets the delay interval (in milliseconds) between two consecutive exports. The default value is 5000. + /// + public int ScheduledDelayMilliseconds { get; set; } = BatchExportProcessor.DefaultScheduledDelayMilliseconds; - /// - /// Gets or sets the timeout (in milliseconds) after which the export is cancelled. The default value is 30000. - /// - public int ExporterTimeoutMilliseconds { get; set; } = BatchExportProcessor.DefaultExporterTimeoutMilliseconds; + /// + /// Gets or sets the timeout (in milliseconds) after which the export is cancelled. The default value is 30000. + /// + public int ExporterTimeoutMilliseconds { get; set; } = BatchExportProcessor.DefaultExporterTimeoutMilliseconds; - /// - /// Gets or sets the maximum batch size of every export. It must be smaller or equal to MaxQueueLength. The default value is 512. - /// - public int MaxExportBatchSize { get; set; } = BatchExportProcessor.DefaultMaxExportBatchSize; - } + /// + /// Gets or sets the maximum batch size of every export. It must be smaller or equal to MaxQueueLength. The default value is 512. + /// + public int MaxExportBatchSize { get; set; } = BatchExportProcessor.DefaultMaxExportBatchSize; } diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 39fd0dc1b6f..b3e5a11e449 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -2,25 +2,251 @@ ## Unreleased -* Add back support for Exemplars. See [exemplars](../../docs/metrics/customizing-the-sdk/README.md#exemplars) - for instructions to enable exemplars. - ([#4553](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4553)) - -* Added [Logs Bridge - API](https://github.com/open-telemetry/opentelemetry-specification/blob/976432b74c565e8a84af3570e9b82cb95e1d844c/specification/logs/bridge-api.md) - implementation (`Sdk.CreateLoggerProviderBuilder`, etc.). - ([#4433](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4433)) +* Throw NotSupportedException when using `SetErrorStatusOnException` method for + Tracing in Mono Runtime and Native AOT environment because the dependent + `Marshal.GetExceptionPointers()` API is not supported on these platforms. + ([#5374](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5374)) + +* Fixed an issue where `LogRecord.Attributes` (or `LogRecord.StateValues` alias) + could become out of sync with `LogRecord.State` if either is set directly via + the public setters. This was done to further mitigate issues introduced in + 1.5.0 causing attributes added using custom processor(s) to be missing after + upgrading. For details see: + ([#5169](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5169)) + +* Fixed an issue where `SimpleExemplarReservoir` was not resetting internal + state for cumulative temporality. + ([#5230](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5230)) + +* Fixed an issue causing `LogRecord`s to be incorrectly reused when wrapping an + instance of `BatchLogRecordExportProcessor` inside another + `BaseProcessor` which leads to missing or incorrect data during + export. + ([#5255](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5255)) + +* **Experimental (pre-release builds only):** Added support for setting + `CardinalityLimit` (the maximum number of data points allowed for a metric) + when configuring a view (applies to individual metrics) and obsoleted + `MeterProviderBuilderExtensions.SetMaxMetricPointsPerMetricStream` (previously + applied to all metrics). The default cardinality limit for metrics remains at + `2000`. + ([#5312](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5312), + [#5328](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5328)) + +* Updated `LogRecord` to keep `CategoryName` and `Logger` in sync when using the + experimental Log Bridge API. + [#5317](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5317) + +* Added `OpenTelemetryBuilderSdkExtensions` class which contains extension + methods (`ConfigureResource`, `WithMetrics`, `WithTracing`, and experimental + `WithLogging`) for the `IOpenTelemetryBuilder` interface. + ([#5265](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5265)) + +* Added `Microsoft.Extensions.Diagnostics.Abstractions` dependency so that the + `IOpenTelemetryBuilder.WithMetrics` extension method can configure + [IMetricsListener](https://learn.microsoft.com/dotNet/api/microsoft.extensions.diagnostics.metrics.imetricslistener). + ([#5265](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5265)) + +* **Experimental (pre-release builds only):** The `Exemplar.FilteredTags` + property now returns a `ReadOnlyFilteredTagCollection` instance and the + `Exemplar.LongValue` property has been added. The `MetricPoint.GetExemplars` + method has been replaced by `MetricPoint.TryGetExemplars` which outputs a + `ReadOnlyExemplarCollection` instance. These are **breaking changes** for + metrics exporters which support exemplars. + ([#5386](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5386)) + +* **Experimental (pre-release builds only):** Added support for exemplars when + using Base2 Exponential Bucket Histogram Aggregation configured via the View + API. + ([#5396](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5396)) + +* **Experimental (pre-release builds only):** Removed the `ExemplarFilter`, + `AlwaysOffExemplarFilter`, `AlwaysOnExemplarFilter`, and + `TraceBasedExemplarFilter` APIs. The `MeterProviderBuilder.SetExemplarFilter` + extension method now accepts an `ExemplarFilterType` enumeration (which + contains definitions for the supported filter types `AlwaysOff`, `AlwaysOn`, + and `TraceBased`) instead of an `ExemplarFilter` instance. This was done in + response to changes made to the [OpenTelemetry Metrics SDK + Specification](https://github.com/open-telemetry/opentelemetry-specification/pull/3820). + ([#5404](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5404)) + +* **Experimental (pre-release builds only):** The `ExemplarFilter` used by SDK + `MeterProvider`s can now be controlled via the `OTEL_METRICS_EXEMPLAR_FILTER` + environment variable. The supported values are: `always_off`, `always_on`, and + `trace_based`. For details see: [OpenTelemetry Environment Variable + Specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md#exemplar). + ([#5412](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5412)) + +## 1.7.0 + +Released 2023-Dec-08 + +## 1.7.0-rc.1 + +Released 2023-Nov-29 + +* The `AddService` `ResourceBuilder` extension method will now generate the same + `service.instance.id` for the lifetime of a process when + `autoGenerateServiceInstanceId` is `true`. + ([#4988](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4988)) + +* Fixed a Metrics SDK bug which led to `ExemplarReservoir.Offer` always being + called regardless of whether or not the `ExemplarFilter` sampled the + measurement. + ([#5004](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5004)) + ([#5016](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5016)) + +* Update Metrics SDK to override the default histogram buckets for the following + metrics from ASP.NET Core and HttpClient runtime: + * `signalr.server.connection.duration` + * `kestrel.connection.duration` + * `http.client.connection.duration` + + These histogram metrics which have their `Unit` as `s` (second) will have + their default histogram buckets as `[ 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, + 5, 10, 30, 60, 120, 300 ]`. + ([#5008](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5008)) + ([#5021](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5021)) + +* Remove the bucket with value `0` for histogram buckets for all metrics from + ASP.NET Core and HttpClient. + ([#5021](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5021)) + +* Updated `Microsoft.Extensions.Logging.Configuration` package version to + `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)) + +* Added `AddProcessor` overload on `OpenTelemetryLoggerOptions` which exposes + the factory pattern `(Func> + implementationFactory)`. + ([#4916](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4916)) + +* Add support for Instrumentation Scope Attributes (i.e [Meter + Tags](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.meter.tags)), + fixing issue + [#4563](https://github.com/open-telemetry/opentelemetry-dotnet/issues/4563). + ([#5089](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5089)) + +* Added the `ILoggingBuilder.UseOpenTelemetry` experimental API extension for + registering OpenTelemetry `ILogger` integration using `LoggerProviderBuilder` + which supports the full DI (`IServiceCollection` \ `IServiceProvider`) API + surface (mirrors tracing & metrics). + ([#5072](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5072)) + +* Changed the `ILoggingBuilder` registration extensions (`AddOpenTelemetry` & + `UseOpenTelemetry`) to fire the optional `OpenTelemetryLoggerOptions` + configuration delegate AFTER the "Logging:OpenTelemetry" `IConfiguration` + section has been applied. + ([#5072](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5072)) + +## 1.7.0-alpha.1 + +Released 2023-Oct-16 + +* Update `AggregatorStore` to reclaim unused MetricPoints for Delta aggregation + temporality. + ([#4486](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4486)) + +* Fixed a bug where `TracerProviderBuilderBase` was not invoking the + `instrumentationFactory` delegate passed to the `protected` + `AddInstrumentation` method. + ([#4873](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4873)) + +* Allowed metric instrument names to contain `/` characters. + ([#4882](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4882)) + +* **Breaking Change** `[Tracer|Meter|Logger]ProviderBuilder.Build` extension + will now throw a `NotSupportedException` if invoked on a non-SDK builder type. + Previously it would return `null`. + ([#4885](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4885)) + +* Updated `Microsoft.Extensions.Logging` package version to + `8.0.0-rc.1.23419.4`. + ([#4920](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4920), + [#4933](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4933)) + +## 1.6.0 + +Released 2023-Sep-05 + +* Increased the character limit of the Meter instrument name from 63 to 255. + ([#4798](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4798)) + +* Update default size for `SimpleExemplarReservoir` to `1`. + ([#4803](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4803)) + +* Update Metrics SDK to override the default histogram buckets for a set of + well-known histogram metrics from ASP.NET Core and HttpClient runtime. These + histogram metrics which have their `Unit` as `s` (second) will have their + default histogram buckets as `[ 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 ]`. + ([#4820](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4820)) + +## 1.6.0-rc.1 + +Released 2023-Aug-21 + +* **Experimental Feature** Added an opt-in feature to aggregate any metric + measurements that were dropped due to reaching the [max MetricPoints + limit](https://github.com/open-telemetry/opentelemetry-dotnet/tree/core-1.6.0-alpha.1/docs/metrics/customizing-the-sdk). + When this feature is enabled, SDK would aggregate such measurements using a + reserved MetricPoint with a single tag with key as `otel.metric.overflow` and + value as `true`. The feature is turned-off by default. You can enable it by + setting the environment variable + `OTEL_DOTNET_EXPERIMENTAL_METRICS_EMIT_OVERFLOW_ATTRIBUTE` to `true` before + setting up the `MeterProvider`. + ([#4737](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4737)) + +## 1.6.0-alpha.1 + +Released 2023-Jul-12 + +* **Experimental (pre-release builds only):** + + * Note: See + [#4735](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4735) + for the introduction of experimental api support. -* Obsoleted `LogRecord.LogLevel` in favor of the `LogRecord.Severity` property - which matches the [OpenTelemetry Specification > Logs DataModel > Severity - definition](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#field-severitynumber). - ([#4433](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4433)) + * Add back support for Exemplars. See + [exemplars](../../docs/metrics/customizing-the-sdk/README.md#exemplars) for + instructions to enable exemplars. + ([#4553](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4553)) + + * Added [Logs Bridge + API](https://github.com/open-telemetry/opentelemetry-specification/blob/976432b74c565e8a84af3570e9b82cb95e1d844c/specification/logs/bridge-api.md) + implementation (`Sdk.CreateLoggerProviderBuilder`, etc.). + ([#4433](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4433)) + + * Obsoleted `LogRecord.LogLevel` in favor of the `LogRecord.Severity` property + which matches the [OpenTelemetry Specification > Logs DataModel > Severity + definition](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#field-severitynumber). + ([#4433](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4433)) -* Added `LogRecord.Logger` property to access the [OpenTelemetry Specification > - Instrumentation - Scope](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#instrumentation-scope) - provided during Logger creation. - ([#4433](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4433)) + * Added `LogRecord.Logger` property to access the [OpenTelemetry Specification + Instrumentation + Scope](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#instrumentation-scope) + provided during Logger creation. + ([#4433](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4433)) * Fix the issue of potentially running into the `ArgumentException`: `An instance of EventSource with Guid af2d5796-946b-50cb-5f76-166a609afcbb already @@ -742,7 +968,7 @@ Released 2020-Nov-17 `TracerProviderBuilder.SetResourceBuilder`. ([#1533](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1533)) * By default `TracerProvider` will set a `Resource` containing [Telemetry - SDK](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/resource/semantic_conventions#telemetry-sdk) + SDK](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md#telemetry-sdk) details ([#1533](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1533)): * `telemetry.sdk.name` = `opentelemetry` diff --git a/src/OpenTelemetry/CompositeProcessor.cs b/src/OpenTelemetry/CompositeProcessor.cs index 7973550364c..614e0dd3b13 100644 --- a/src/OpenTelemetry/CompositeProcessor.cs +++ b/src/OpenTelemetry/CompositeProcessor.cs @@ -1,183 +1,180 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Internal; -namespace OpenTelemetry +namespace OpenTelemetry; + +/// +/// Represents a chain of s. +/// +/// The type of object to be processed. +public class CompositeProcessor : BaseProcessor { - public class CompositeProcessor : BaseProcessor + internal readonly DoublyLinkedListNode Head; + private DoublyLinkedListNode tail; + private bool disposed; + + /// + /// Initializes a new instance of the class. + /// + /// Processors to add to the composite processor chain. + public CompositeProcessor(IEnumerable> processors) { - internal readonly DoublyLinkedListNode Head; - private DoublyLinkedListNode tail; - private bool disposed; + Guard.ThrowIfNull(processors); - public CompositeProcessor(IEnumerable> processors) + using var iter = processors.GetEnumerator(); + if (!iter.MoveNext()) { - Guard.ThrowIfNull(processors); - - using var iter = processors.GetEnumerator(); - if (!iter.MoveNext()) - { - throw new ArgumentException($"'{iter}' is null or empty", nameof(iter)); - } + throw new ArgumentException($"'{iter}' is null or empty", nameof(iter)); + } - this.Head = new DoublyLinkedListNode(iter.Current); - this.tail = this.Head; + this.Head = new DoublyLinkedListNode(iter.Current); + this.tail = this.Head; - while (iter.MoveNext()) - { - this.AddProcessor(iter.Current); - } + while (iter.MoveNext()) + { + this.AddProcessor(iter.Current); } + } + + /// + /// Adds a processor to the composite processor chain. + /// + /// . + /// The current instance to support call chaining. + public CompositeProcessor AddProcessor(BaseProcessor processor) + { + Guard.ThrowIfNull(processor); - public CompositeProcessor AddProcessor(BaseProcessor processor) + var node = new DoublyLinkedListNode(processor) { - Guard.ThrowIfNull(processor); + Previous = this.tail, + }; + this.tail.Next = node; + this.tail = node; - var node = new DoublyLinkedListNode(processor) - { - Previous = this.tail, - }; - this.tail.Next = node; - this.tail = node; + return this; + } - return this; + /// + public override void OnEnd(T data) + { + for (var cur = this.Head; cur != null; cur = cur.Next) + { + cur.Value.OnEnd(data); } + } - /// - public override void OnEnd(T data) + /// + public override void OnStart(T data) + { + for (var cur = this.Head; cur != null; cur = cur.Next) { - for (var cur = this.Head; cur != null; cur = cur.Next) - { - cur.Value.OnEnd(data); - } + cur.Value.OnStart(data); } + } - /// - public override void OnStart(T data) + internal override void SetParentProvider(BaseProvider parentProvider) + { + base.SetParentProvider(parentProvider); + + for (var cur = this.Head; cur != null; cur = cur.Next) { - for (var cur = this.Head; cur != null; cur = cur.Next) - { - cur.Value.OnStart(data); - } + cur.Value.SetParentProvider(parentProvider); } + } - internal override void SetParentProvider(BaseProvider parentProvider) - { - base.SetParentProvider(parentProvider); + /// + protected override bool OnForceFlush(int timeoutMilliseconds) + { + var result = true; + var sw = timeoutMilliseconds == Timeout.Infinite + ? null + : Stopwatch.StartNew(); - for (var cur = this.Head; cur != null; cur = cur.Next) + for (var cur = this.Head; cur != null; cur = cur.Next) + { + if (sw == null) { - cur.Value.SetParentProvider(parentProvider); + result = cur.Value.ForceFlush() && result; } - } - - /// - protected override bool OnForceFlush(int timeoutMilliseconds) - { - var result = true; - var sw = timeoutMilliseconds == Timeout.Infinite - ? null - : Stopwatch.StartNew(); - - for (var cur = this.Head; cur != null; cur = cur.Next) + else { - if (sw == null) - { - result = cur.Value.ForceFlush() && result; - } - else - { - var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; + var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; - // notify all the processors, even if we run overtime - result = cur.Value.ForceFlush((int)Math.Max(timeout, 0)) && result; - } + // notify all the processors, even if we run overtime + result = cur.Value.ForceFlush((int)Math.Max(timeout, 0)) && result; } - - return result; } - /// - protected override bool OnShutdown(int timeoutMilliseconds) - { - var result = true; - var sw = timeoutMilliseconds == Timeout.Infinite - ? null - : Stopwatch.StartNew(); + return result; + } - for (var cur = this.Head; cur != null; cur = cur.Next) - { - if (sw == null) - { - result = cur.Value.Shutdown() && result; - } - else - { - var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; + /// + protected override bool OnShutdown(int timeoutMilliseconds) + { + var result = true; + var sw = timeoutMilliseconds == Timeout.Infinite + ? null + : Stopwatch.StartNew(); - // notify all the processors, even if we run overtime - result = cur.Value.Shutdown((int)Math.Max(timeout, 0)) && result; - } + for (var cur = this.Head; cur != null; cur = cur.Next) + { + if (sw == null) + { + result = cur.Value.Shutdown() && result; } + else + { + var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; - return result; + // notify all the processors, even if we run overtime + result = cur.Value.Shutdown((int)Math.Max(timeout, 0)) && result; + } } - /// - protected override void Dispose(bool disposing) + return result; + } + + /// + protected override void Dispose(bool disposing) + { + if (!this.disposed) { - if (!this.disposed) + if (disposing) { - if (disposing) + for (var cur = this.Head; cur != null; cur = cur.Next) { - for (var cur = this.Head; cur != null; cur = cur.Next) + try { - try - { - cur.Value.Dispose(); - } - catch (Exception ex) - { - OpenTelemetrySdkEventSource.Log.SpanProcessorException(nameof(this.Dispose), ex); - } + cur.Value.Dispose(); + } + catch (Exception ex) + { + OpenTelemetrySdkEventSource.Log.SpanProcessorException(nameof(this.Dispose), ex); } } - - this.disposed = true; } - base.Dispose(disposing); + this.disposed = true; } - internal sealed class DoublyLinkedListNode - { - public readonly BaseProcessor Value; - - public DoublyLinkedListNode(BaseProcessor value) - { - this.Value = value; - } + base.Dispose(disposing); + } - public DoublyLinkedListNode? Previous { get; set; } + internal sealed class DoublyLinkedListNode + { + public readonly BaseProcessor Value; - public DoublyLinkedListNode? Next { get; set; } + public DoublyLinkedListNode(BaseProcessor value) + { + this.Value = value; } + + public DoublyLinkedListNode? Previous { get; set; } + + public DoublyLinkedListNode? Next { get; set; } } } diff --git a/src/OpenTelemetry/Internal/Builder/ProviderBuilderServiceCollectionExtensions.cs b/src/OpenTelemetry/Internal/Builder/ProviderBuilderServiceCollectionExtensions.cs index 3f9c379fd85..1666291bb52 100644 --- a/src/OpenTelemetry/Internal/Builder/ProviderBuilderServiceCollectionExtensions.cs +++ b/src/OpenTelemetry/Internal/Builder/ProviderBuilderServiceCollectionExtensions.cs @@ -1,20 +1,5 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using Microsoft.Extensions.Configuration; @@ -78,7 +63,7 @@ public static IServiceCollection AddOpenTelemetrySharedProviderBuilderServices(t // which sets default Propagators and default Activity Id format _ = Sdk.SuppressInstrumentation; - services.AddOptions(); + services!.AddOptions(); // Note: When using a host builder IConfiguration is automatically // registered and this registration will no-op. This only runs for diff --git a/src/OpenTelemetry/Internal/CircularBuffer.cs b/src/OpenTelemetry/Internal/CircularBuffer.cs index 2625289abcc..74c3c8a6c5d 100644 --- a/src/OpenTelemetry/Internal/CircularBuffer.cs +++ b/src/OpenTelemetry/Internal/CircularBuffer.cs @@ -1,179 +1,163 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Runtime.CompilerServices; -namespace OpenTelemetry.Internal +namespace OpenTelemetry.Internal; + +/// +/// Lock-free implementation of single-reader multi-writer circular buffer. +/// +/// The type of the underlying value. +internal sealed class CircularBuffer + where T : class { + private readonly T?[] trait; + private long head; + private long tail; + /// - /// Lock-free implementation of single-reader multi-writer circular buffer. + /// Initializes a new instance of the class. /// - /// The type of the underlying value. - internal sealed class CircularBuffer - where T : class + /// The capacity of the circular buffer, must be a positive integer. + public CircularBuffer(int capacity) { - private readonly T?[] trait; - private long head; - private long tail; - - /// - /// Initializes a new instance of the class. - /// - /// The capacity of the circular buffer, must be a positive integer. - public CircularBuffer(int capacity) - { - Guard.ThrowIfOutOfRange(capacity, min: 1); + Guard.ThrowIfOutOfRange(capacity, min: 1); - this.Capacity = capacity; - this.trait = new T[capacity]; - } + this.Capacity = capacity; + this.trait = new T[capacity]; + } - /// - /// Gets the capacity of the . - /// - public int Capacity { get; } + /// + /// Gets the capacity of the . + /// + public int Capacity { get; } - /// - /// Gets the number of items contained in the . - /// - public int Count + /// + /// Gets the number of items contained in the . + /// + public int Count + { + get { - get - { - var tailSnapshot = Volatile.Read(ref this.tail); - return (int)(Volatile.Read(ref this.head) - tailSnapshot); - } + var tailSnapshot = Volatile.Read(ref this.tail); + return (int)(Volatile.Read(ref this.head) - tailSnapshot); } + } - /// - /// Gets the number of items added to the . - /// - public long AddedCount => Volatile.Read(ref this.head); - - /// - /// Gets the number of items removed from the . - /// - public long RemovedCount => Volatile.Read(ref this.tail); - - /// - /// Adds the specified item to the buffer. - /// - /// The value to add. - /// - /// Returns true if the item was added to the buffer successfully; - /// false if the buffer is full. - /// - public bool Add(T value) - { - Guard.ThrowIfNull(value); + /// + /// Gets the number of items added to the . + /// + public long AddedCount => Volatile.Read(ref this.head); - while (true) - { - var tailSnapshot = Volatile.Read(ref this.tail); - var headSnapshot = Volatile.Read(ref this.head); + /// + /// Gets the number of items removed from the . + /// + public long RemovedCount => Volatile.Read(ref this.tail); - if (headSnapshot - tailSnapshot >= this.Capacity) - { - return false; // buffer is full - } + /// + /// Adds the specified item to the buffer. + /// + /// The value to add. + /// + /// Returns true if the item was added to the buffer successfully; + /// false if the buffer is full. + /// + public bool Add(T value) + { + Guard.ThrowIfNull(value); - if (Interlocked.CompareExchange(ref this.head, headSnapshot + 1, headSnapshot) != headSnapshot) - { - continue; - } + while (true) + { + var tailSnapshot = Volatile.Read(ref this.tail); + var headSnapshot = Volatile.Read(ref this.head); - Volatile.Write(ref this.trait[headSnapshot % this.Capacity], value); + if (headSnapshot - tailSnapshot >= this.Capacity) + { + return false; // buffer is full + } - return true; + if (Interlocked.CompareExchange(ref this.head, headSnapshot + 1, headSnapshot) != headSnapshot) + { + continue; } + + Volatile.Write(ref this.trait[headSnapshot % this.Capacity], value); + + return true; } + } - /// - /// Attempts to add the specified item to the buffer. - /// - /// The value to add. - /// The maximum allowed spin count, when set to a negative number or zero, will spin indefinitely. - /// - /// Returns true if the item was added to the buffer successfully; - /// false if the buffer is full or the spin count exceeded . - /// - public bool TryAdd(T value, int maxSpinCount) + /// + /// Attempts to add the specified item to the buffer. + /// + /// The value to add. + /// The maximum allowed spin count, when set to a negative number or zero, will spin indefinitely. + /// + /// Returns true if the item was added to the buffer successfully; + /// false if the buffer is full or the spin count exceeded . + /// + public bool TryAdd(T value, int maxSpinCount) + { + if (maxSpinCount <= 0) { - if (maxSpinCount <= 0) - { - return this.Add(value); - } + return this.Add(value); + } - Guard.ThrowIfNull(value); + Guard.ThrowIfNull(value); - var spinCountDown = maxSpinCount; + var spinCountDown = maxSpinCount; - while (true) + while (true) + { + var tailSnapshot = Volatile.Read(ref this.tail); + var headSnapshot = Volatile.Read(ref this.head); + + if (headSnapshot - tailSnapshot >= this.Capacity) { - var tailSnapshot = Volatile.Read(ref this.tail); - var headSnapshot = Volatile.Read(ref this.head); + return false; // buffer is full + } - if (headSnapshot - tailSnapshot >= this.Capacity) + if (Interlocked.CompareExchange(ref this.head, headSnapshot + 1, headSnapshot) != headSnapshot) + { + if (spinCountDown-- == 0) { - return false; // buffer is full + return false; // exceeded maximum spin count } - if (Interlocked.CompareExchange(ref this.head, headSnapshot + 1, headSnapshot) != headSnapshot) - { - if (spinCountDown-- == 0) - { - return false; // exceeded maximum spin count - } - - continue; - } + continue; + } - Volatile.Write(ref this.trait[headSnapshot % this.Capacity], value); + Volatile.Write(ref this.trait[headSnapshot % this.Capacity], value); - return true; - } + return true; } + } - /// - /// Reads an item from the . - /// - /// - /// This function is not reentrant-safe, only one reader is allowed at any given time. - /// Warning: There is no bounds check in this method. Do not call unless you have verified Count > 0. - /// - /// Item read. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T Read() + /// + /// Reads an item from the . + /// + /// + /// This function is not reentrant-safe, only one reader is allowed at any given time. + /// Warning: There is no bounds check in this method. Do not call unless you have verified Count > 0. + /// + /// Item read. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T Read() + { + var tail = Volatile.Read(ref this.tail); + var index = (int)(tail % this.Capacity); + while (true) { - var tail = Volatile.Read(ref this.tail); - var index = (int)(tail % this.Capacity); - while (true) + var previous = Interlocked.Exchange(ref this.trait[index], null); + if (previous == null) { - var previous = Interlocked.Exchange(ref this.trait[index], null); - if (previous == null) - { - // If we got here it means a writer isn't done. - continue; - } - - Volatile.Write(ref this.tail, tail + 1); - return previous; + // If we got here it means a writer isn't done. + continue; } + + Volatile.Write(ref this.tail, tail + 1); + return previous; } } } diff --git a/src/OpenTelemetry/Internal/InstrumentationScopeLogger.cs b/src/OpenTelemetry/Internal/InstrumentationScopeLogger.cs new file mode 100644 index 00000000000..2e38f7ff041 --- /dev/null +++ b/src/OpenTelemetry/Internal/InstrumentationScopeLogger.cs @@ -0,0 +1,29 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Concurrent; +using OpenTelemetry.Logs; + +namespace OpenTelemetry.Internal; + +internal sealed class InstrumentationScopeLogger : Logger +{ + private static readonly ConcurrentDictionary Cache = new(); + + private InstrumentationScopeLogger(string name) + : base(name) + { + } + + public static InstrumentationScopeLogger Default { get; } = new(string.Empty); + + public static InstrumentationScopeLogger GetInstrumentationScopeLoggerForName(string? name) + { + return string.IsNullOrWhiteSpace(name) + ? Default + : Cache.GetOrAdd(name!, static n => new(n)); + } + + public override void EmitLog(in LogRecordData data, in LogRecordAttributeList attributes) + => throw new NotSupportedException(); +} diff --git a/src/OpenTelemetry/Internal/InterlockedHelper.cs b/src/OpenTelemetry/Internal/InterlockedHelper.cs new file mode 100644 index 00000000000..98639d234a4 --- /dev/null +++ b/src/OpenTelemetry/Internal/InterlockedHelper.cs @@ -0,0 +1,46 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Runtime.CompilerServices; + +namespace OpenTelemetry.Internal; + +internal static class InterlockedHelper +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Add(ref double location, double value) + { + // Note: Not calling InterlockedHelper.Read here on purpose because it + // is too expensive for fast/happy-path. If the first attempt fails + // we'll end up in an Interlocked.CompareExchange loop anyway. + double currentValue = Volatile.Read(ref location); + + var returnedValue = Interlocked.CompareExchange(ref location, currentValue + value, currentValue); + if (returnedValue != currentValue) + { + AddRare(ref location, value, returnedValue); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Read(ref double location) + => Interlocked.CompareExchange(ref location, double.NaN, double.NaN); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void AddRare(ref double location, double value, double currentValue) + { + var sw = default(SpinWait); + while (true) + { + sw.SpinOnce(); + + var returnedValue = Interlocked.CompareExchange(ref location, currentValue + value, currentValue); + if (returnedValue == currentValue) + { + break; + } + + currentValue = returnedValue; + } + } +} diff --git a/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs b/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs index b7681339b6e..38765609b54 100644 --- a/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs +++ b/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs @@ -1,383 +1,397 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 + using System.Diagnostics; +#if NET6_0_OR_GREATER using System.Diagnostics.CodeAnalysis; +#endif using System.Diagnostics.Tracing; -namespace OpenTelemetry.Internal +namespace OpenTelemetry.Internal; + +/// +/// EventSource implementation for OpenTelemetry SDK implementation. +/// +[EventSource(Name = "OpenTelemetry-Sdk")] +internal sealed class OpenTelemetrySdkEventSource : EventSource { - /// - /// EventSource implementation for OpenTelemetry SDK implementation. - /// - [EventSource(Name = "OpenTelemetry-Sdk")] - internal sealed class OpenTelemetrySdkEventSource : EventSource - { - public static OpenTelemetrySdkEventSource Log = new(); + public static OpenTelemetrySdkEventSource Log = new(); #if DEBUG - public static OpenTelemetryEventListener Listener = new(); + public static OpenTelemetryEventListener Listener = new(); #endif - [NonEvent] - public void SpanProcessorException(string evnt, Exception ex) + [NonEvent] + public void SpanProcessorException(string evnt, Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.SpanProcessorException(evnt, ex.ToInvariantString()); - } + this.SpanProcessorException(evnt, ex.ToInvariantString()); } + } - [NonEvent] - public void MetricObserverCallbackException(Exception exception) + [NonEvent] + public void MetricObserverCallbackException(Exception exception) + { + if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) + if (exception is AggregateException aggregateException) { - if (exception is AggregateException aggregateException) + foreach (var ex in aggregateException.InnerExceptions) { - foreach (var ex in aggregateException.InnerExceptions) - { - this.ObservableInstrumentCallbackException(ex.ToInvariantString()); - } - } - else - { - this.ObservableInstrumentCallbackException(exception.ToInvariantString()); + this.ObservableInstrumentCallbackException(ex.ToInvariantString()); } } - } - - [NonEvent] - public void MetricReaderException(string methodName, Exception ex) - { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + else { - this.MetricReaderException(methodName, ex.ToInvariantString()); + this.ObservableInstrumentCallbackException(exception.ToInvariantString()); } } + } - [NonEvent] - public void ActivityStarted(Activity activity) + [NonEvent] + public void MetricReaderException(string methodName, Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Verbose, EventKeywords.All)) - { - // Accessing activity.Id here will cause the Id to be initialized - // before the sampler runs in case where the activity is created using legacy way - // i.e. new Activity("Operation name"). This will result in Id not reflecting the - // correct sampling flags - // https://github.com/dotnet/runtime/issues/61857 - var activityId = string.Concat("00-", activity.TraceId.ToHexString(), "-", activity.SpanId.ToHexString()); - activityId = string.Concat(activityId, activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded) ? "-01" : "-00"); - this.ActivityStarted(activity.DisplayName, activityId); - } + this.MetricReaderException(methodName, ex.ToInvariantString()); } + } - [NonEvent] - public void ActivityStopped(Activity activity) + [NonEvent] + public void ActivityStarted(Activity activity) + { + if (this.IsEnabled(EventLevel.Verbose, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Verbose, EventKeywords.All)) - { - this.ActivityStopped(activity.DisplayName, activity.Id); - } + // Accessing activity.Id here will cause the Id to be initialized + // before the sampler runs in case where the activity is created using legacy way + // i.e. new Activity("Operation name"). This will result in Id not reflecting the + // correct sampling flags + // https://github.com/dotnet/runtime/issues/61857 + var activityId = string.Concat("00-", activity.TraceId.ToHexString(), "-", activity.SpanId.ToHexString()); + activityId = string.Concat(activityId, activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded) ? "-01" : "-00"); + this.ActivityStarted(activity.DisplayName, activityId); } + } - [NonEvent] - public void SelfDiagnosticsFileCreateException(string logDirectory, Exception ex) + [NonEvent] + public void ActivityStopped(Activity activity) + { + if (this.IsEnabled(EventLevel.Verbose, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) - { - this.SelfDiagnosticsFileCreateException(logDirectory, ex.ToInvariantString()); - } + this.ActivityStopped(activity.DisplayName, activity.Id); } + } - [NonEvent] - public void TracerProviderException(string evnt, Exception ex) + [NonEvent] + public void SelfDiagnosticsFileCreateException(string logDirectory, Exception ex) + { + if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.TracerProviderException(evnt, ex.ToInvariantString()); - } + this.SelfDiagnosticsFileCreateException(logDirectory, ex.ToInvariantString()); } + } - [NonEvent] - public void MeterProviderException(string methodName, Exception ex) + [NonEvent] + public void TracerProviderException(string evnt, Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.MeterProviderException(methodName, ex.ToInvariantString()); - } + this.TracerProviderException(evnt, ex.ToInvariantString()); } + } - [NonEvent] - public void DroppedExportProcessorItems(string exportProcessorName, string exporterName, long droppedCount) + [NonEvent] + public void MeterProviderException(string methodName, Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - if (droppedCount > 0) - { - if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) - { - this.ExistsDroppedExportProcessorItems(exportProcessorName, exporterName, droppedCount); - } - } - else - { - if (this.IsEnabled(EventLevel.Informational, EventKeywords.All)) - { - this.NoDroppedExportProcessorItems(exportProcessorName, exporterName); - } - } + this.MeterProviderException(methodName, ex.ToInvariantString()); } + } - [NonEvent] - public void LoggerParseStateException(Exception exception) + [NonEvent] + public void DroppedExportProcessorItems(string exportProcessorName, string exporterName, long droppedCount) + { + if (droppedCount > 0) { if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) { - this.LoggerParseStateException(typeof(TState).FullName!, exception.ToInvariantString()); + this.ExistsDroppedExportProcessorItems(exportProcessorName, exporterName, droppedCount); } } - - [NonEvent] - public void LoggerProviderException(string methodName, Exception ex) + else { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + if (this.IsEnabled(EventLevel.Informational, EventKeywords.All)) { - this.LoggerProviderException(methodName, ex.ToInvariantString()); + this.NoDroppedExportProcessorItems(exportProcessorName, exporterName); } } + } - [NonEvent] - public void LoggerProcessStateSkipped() + [NonEvent] + public void LoggerParseStateException(Exception exception) + { + if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) { - if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) - { - this.LoggerProcessStateSkipped( - typeof(TState).FullName!, - "because it does not implement a supported interface (either IReadOnlyList> or IEnumerable>)"); - } + this.LoggerParseStateException(typeof(TState).FullName!, exception.ToInvariantString()); } + } - [Event(4, Message = "Unknown error in SpanProcessor event '{0}': '{1}'.", Level = EventLevel.Error)] - public void SpanProcessorException(string evnt, string ex) + [NonEvent] + public void LoggerProviderException(string methodName, Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - this.WriteEvent(4, evnt, ex); + this.LoggerProviderException(methodName, ex.ToInvariantString()); } + } - [Event(8, Message = "Calling method '{0}' with invalid argument '{1}', issue '{2}'.", Level = EventLevel.Warning)] - public void InvalidArgument(string methodName, string argumentName, string issue) + [NonEvent] + public void LoggerProcessStateSkipped() + { + if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) { - this.WriteEvent(8, methodName, argumentName, issue); + this.LoggerProcessStateSkipped( + typeof(TState).FullName!, + "because it does not implement a supported interface (either IReadOnlyList> or IEnumerable>)"); } + } - [Event(16, Message = "Exception occurred while invoking Observable instrument callback. Exception: '{0}'", Level = EventLevel.Warning)] - public void ObservableInstrumentCallbackException(string exception) - { - this.WriteEvent(16, exception); - } + [Event(4, Message = "Unknown error in SpanProcessor event '{0}': '{1}'.", Level = EventLevel.Error)] + public void SpanProcessorException(string evnt, string ex) + { + this.WriteEvent(4, evnt, ex); + } - [Event(24, Message = "Activity started. Name = '{0}', Id = '{1}'.", Level = EventLevel.Verbose)] - public void ActivityStarted(string name, string id) - { - this.WriteEvent(24, name, id); - } + [Event(8, Message = "Calling method '{0}' with invalid argument '{1}', issue '{2}'.", Level = EventLevel.Warning)] + public void InvalidArgument(string methodName, string argumentName, string issue) + { + this.WriteEvent(8, methodName, argumentName, issue); + } - [Event(25, Message = "Activity stopped. Name = '{0}', Id = '{1}'.", Level = EventLevel.Verbose)] - public void ActivityStopped(string name, string? id) - { - this.WriteEvent(25, name, id); - } + [Event(16, Message = "Exception occurred while invoking Observable instrument callback. Exception: '{0}'", Level = EventLevel.Warning)] + public void ObservableInstrumentCallbackException(string exception) + { + this.WriteEvent(16, exception); + } - [Event(26, Message = "Failed to create file. LogDirectory ='{0}', Id = '{1}'.", Level = EventLevel.Warning)] - public void SelfDiagnosticsFileCreateException(string logDirectory, string exception) - { - this.WriteEvent(26, logDirectory, exception); - } + [Event(24, Message = "Activity started. Name = '{0}', Id = '{1}'.", Level = EventLevel.Verbose)] + public void ActivityStarted(string name, string id) + { + this.WriteEvent(24, name, id); + } - [Event(28, Message = "Unknown error in TracerProvider '{0}': '{1}'.", Level = EventLevel.Error)] - public void TracerProviderException(string evnt, string ex) - { - this.WriteEvent(28, evnt, ex); - } + [Event(25, Message = "Activity stopped. Name = '{0}', Id = '{1}'.", Level = EventLevel.Verbose)] + public void ActivityStopped(string name, string? id) + { + this.WriteEvent(25, name, id); + } - [Event(31, Message = "'{0}' exporting to '{1}' dropped '0' items.", Level = EventLevel.Informational)] - public void NoDroppedExportProcessorItems(string exportProcessorName, string exporterName) - { - this.WriteEvent(31, exportProcessorName, exporterName); - } + [Event(26, Message = "Failed to create file. LogDirectory ='{0}', Id = '{1}'.", Level = EventLevel.Warning)] + public void SelfDiagnosticsFileCreateException(string logDirectory, string exception) + { + this.WriteEvent(26, logDirectory, exception); + } - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")] - [Event(32, Message = "'{0}' exporting to '{1}' dropped '{2}' item(s) due to buffer full.", Level = EventLevel.Warning)] - public void ExistsDroppedExportProcessorItems(string exportProcessorName, string exporterName, long droppedCount) - { - this.WriteEvent(32, exportProcessorName, exporterName, droppedCount); - } + [Event(28, Message = "Unknown error in TracerProvider '{0}': '{1}'.", Level = EventLevel.Error)] + public void TracerProviderException(string evnt, string ex) + { + this.WriteEvent(28, evnt, ex); + } - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")] - [Event(33, Message = "Measurements from Instrument '{0}', Meter '{1}' will be ignored. Reason: '{2}'. Suggested action: '{3}'", Level = EventLevel.Warning)] - public void MetricInstrumentIgnored(string instrumentName, string meterName, string reason, string fix) - { - this.WriteEvent(33, instrumentName, meterName, reason, fix); - } + [Event(31, Message = "'{0}' exporting to '{1}' dropped '0' items.", Level = EventLevel.Informational)] + public void NoDroppedExportProcessorItems(string exportProcessorName, string exporterName) + { + this.WriteEvent(31, exportProcessorName, exporterName); + } - [Event(34, Message = "Unknown error in MetricReader event '{0}': '{1}'.", Level = EventLevel.Error)] - public void MetricReaderException(string methodName, string ex) - { - this.WriteEvent(34, methodName, ex); - } +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")] +#endif + [Event(32, Message = "'{0}' exporting to '{1}' dropped '{2}' item(s) due to buffer full.", Level = EventLevel.Warning)] + public void ExistsDroppedExportProcessorItems(string exportProcessorName, string exporterName, long droppedCount) + { + this.WriteEvent(32, exportProcessorName, exporterName, droppedCount); + } - [Event(35, Message = "Unknown error in MeterProvider '{0}': '{1}'.", Level = EventLevel.Error)] - public void MeterProviderException(string methodName, string ex) - { - this.WriteEvent(35, methodName, ex); - } +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")] +#endif + [Event(33, Message = "Measurements from Instrument '{0}', Meter '{1}' will be ignored. Reason: '{2}'. Suggested action: '{3}'", Level = EventLevel.Warning)] + public void MetricInstrumentIgnored(string instrumentName, string meterName, string reason, string fix) + { + this.WriteEvent(33, instrumentName, meterName, reason, fix); + } - [Event(36, Message = "Measurement dropped from Instrument Name/Metric Stream Name '{0}'. Reason: '{1}'. Suggested action: '{2}'", Level = EventLevel.Warning)] - public void MeasurementDropped(string instrumentName, string reason, string fix) - { - this.WriteEvent(36, instrumentName, reason, fix); - } + [Event(34, Message = "Unknown error in MetricReader event '{0}': '{1}'.", Level = EventLevel.Error)] + public void MetricReaderException(string methodName, string ex) + { + this.WriteEvent(34, methodName, ex); + } - [Event(37, Message = "'{0}' Disposed.", Level = EventLevel.Informational)] - public void ProviderDisposed(string providerName) - { - this.WriteEvent(37, providerName); - } + [Event(35, Message = "Unknown error in MeterProvider '{0}': '{1}'.", Level = EventLevel.Error)] + public void MeterProviderException(string methodName, string ex) + { + this.WriteEvent(35, methodName, ex); + } - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")] - [Event(38, Message = "Duplicate Instrument '{0}', Meter '{1}' encountered. Reason: '{2}'. Suggested action: '{3}'", Level = EventLevel.Warning)] - public void DuplicateMetricInstrument(string instrumentName, string meterName, string reason, string fix) - { - this.WriteEvent(38, instrumentName, meterName, reason, fix); - } + [Event(36, Message = "Measurement dropped from Instrument Name/Metric Stream Name '{0}'. Reason: '{1}'. Suggested action: '{2}'", Level = EventLevel.Warning)] + public void MeasurementDropped(string instrumentName, string reason, string fix) + { + this.WriteEvent(36, instrumentName, reason, fix); + } - [Event(39, Message = "MeterProviderSdk event: '{0}'", Level = EventLevel.Verbose)] - public void MeterProviderSdkEvent(string message) - { - this.WriteEvent(39, message); - } + [Event(37, Message = "'{0}' Disposed.", Level = EventLevel.Informational)] + public void ProviderDisposed(string providerName) + { + this.WriteEvent(37, providerName); + } - [Event(40, Message = "MetricReader event: '{0}'", Level = EventLevel.Verbose)] - public void MetricReaderEvent(string message) - { - this.WriteEvent(40, message); - } +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")] +#endif + [Event(38, Message = "Duplicate Instrument '{0}', Meter '{1}' encountered. Reason: '{2}'. Suggested action: '{3}'", Level = EventLevel.Warning)] + public void DuplicateMetricInstrument(string instrumentName, string meterName, string reason, string fix) + { + this.WriteEvent(38, instrumentName, meterName, reason, fix); + } - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")] - [Event(41, Message = "View Configuration ignored for Instrument '{0}', Meter '{1}'. Reason: '{2}'. Measurements from the instrument will use default configuration for Aggregation. Suggested action: '{3}'", Level = EventLevel.Warning)] - public void MetricViewIgnored(string instrumentName, string meterName, string reason, string fix) - { - this.WriteEvent(41, instrumentName, meterName, reason, fix); - } + [Event(39, Message = "MeterProviderSdk event: '{0}'", Level = EventLevel.Verbose)] + public void MeterProviderSdkEvent(string message) + { + this.WriteEvent(39, message); + } - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")] - [Event(43, Message = "ForceFlush invoked for processor type '{0}' returned result '{1}'.", Level = EventLevel.Verbose)] - public void ProcessorForceFlushInvoked(string processorType, bool result) - { - this.WriteEvent(43, processorType, result); - } + [Event(40, Message = "MetricReader event: '{0}'", Level = EventLevel.Verbose)] + public void MetricReaderEvent(string message) + { + this.WriteEvent(40, message); + } - [Event(44, Message = "OpenTelemetryLoggerProvider event: '{0}'", Level = EventLevel.Verbose)] - public void OpenTelemetryLoggerProviderEvent(string message) - { - this.WriteEvent(44, message); - } +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")] +#endif + [Event(41, Message = "View Configuration ignored for Instrument '{0}', Meter '{1}'. Reason: '{2}'. Measurements from the instrument will use default configuration for Aggregation. Suggested action: '{3}'", Level = EventLevel.Warning)] + public void MetricViewIgnored(string instrumentName, string meterName, string reason, string fix) + { + this.WriteEvent(41, instrumentName, meterName, reason, fix); + } - [Event(45, Message = "ForceFlush invoked for OpenTelemetryLoggerProvider with timeoutMilliseconds = '{0}'.", Level = EventLevel.Verbose)] - public void OpenTelemetryLoggerProviderForceFlushInvoked(int timeoutMilliseconds) - { - this.WriteEvent(45, timeoutMilliseconds); - } +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")] +#endif + [Event(43, Message = "ForceFlush invoked for processor type '{0}' returned result '{1}'.", Level = EventLevel.Verbose)] + public void ProcessorForceFlushInvoked(string processorType, bool result) + { + this.WriteEvent(43, processorType, result); + } - [Event(46, Message = "TracerProviderSdk event: '{0}'", Level = EventLevel.Verbose)] - public void TracerProviderSdkEvent(string message) - { - this.WriteEvent(46, message); - } + [Event(44, Message = "OpenTelemetryLoggerProvider event: '{0}'", Level = EventLevel.Verbose)] + public void OpenTelemetryLoggerProviderEvent(string message) + { + this.WriteEvent(44, message); + } - [Event(47, Message = "{0} environment variable has an invalid value: '{1}'", Level = EventLevel.Warning)] - public void InvalidEnvironmentVariable(string key, string? value) - { - this.WriteEvent(47, key, value); - } + [Event(45, Message = "ForceFlush invoked for OpenTelemetryLoggerProvider with timeoutMilliseconds = '{0}'.", Level = EventLevel.Verbose)] + public void OpenTelemetryLoggerProviderForceFlushInvoked(int timeoutMilliseconds) + { + this.WriteEvent(45, timeoutMilliseconds); + } - [Event(48, Message = "Exception thrown parsing log state of type '{0}'. Exception: '{1}'", Level = EventLevel.Warning)] - public void LoggerParseStateException(string type, string error) - { - this.WriteEvent(48, type, error); - } + [Event(46, Message = "TracerProviderSdk event: '{0}'", Level = EventLevel.Verbose)] + public void TracerProviderSdkEvent(string message) + { + this.WriteEvent(46, message); + } - [Event(49, Message = "LoggerProviderSdk event: '{0}'", Level = EventLevel.Verbose)] - public void LoggerProviderSdkEvent(string message) - { - this.WriteEvent(49, message); - } + [Event(47, Message = "{0} environment variable has an invalid value: '{1}'", Level = EventLevel.Warning)] + public void InvalidEnvironmentVariable(string key, string? value) + { + this.WriteEvent(47, key, value); + } + + [Event(48, Message = "Exception thrown parsing log state of type '{0}'. Exception: '{1}'", Level = EventLevel.Warning)] + public void LoggerParseStateException(string type, string error) + { + this.WriteEvent(48, type, error); + } + + [Event(49, Message = "LoggerProviderSdk event: '{0}'", Level = EventLevel.Verbose)] + public void LoggerProviderSdkEvent(string message) + { + this.WriteEvent(49, message); + } + + [Event(50, Message = "Unknown error in LoggerProvider '{0}': '{1}'.", Level = EventLevel.Error)] + public void LoggerProviderException(string methodName, string ex) + { + this.WriteEvent(50, methodName, ex); + } + + [Event(51, Message = "Skipped processing log state of type '{0}' {1}.", Level = EventLevel.Warning)] + 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 + { + private readonly Dictionary eventSources = new(); - [Event(50, Message = "Unknown error in LoggerProvider '{0}': '{1}'.", Level = EventLevel.Error)] - public void LoggerProviderException(string methodName, string ex) + public override void Dispose() { - this.WriteEvent(50, methodName, ex); + foreach (var kvp in this.eventSources) + { + this.DisableEvents(kvp.Value); + } + + base.Dispose(); + GC.SuppressFinalize(this); } - [Event(51, Message = "Skipped processing log state of type '{0}' {1}.", Level = EventLevel.Warning)] - public void LoggerProcessStateSkipped(string type, string reason) + protected override void OnEventSourceCreated(EventSource eventSource) { - this.WriteEvent(51, type, reason); + if (eventSource.Name.StartsWith("OpenTelemetry", StringComparison.OrdinalIgnoreCase)) + { + this.eventSources.Add(eventSource.Name, eventSource); + this.EnableEvents(eventSource, EventLevel.Verbose, EventKeywords.All); + } + + base.OnEventSourceCreated(eventSource); } -#if DEBUG - public class OpenTelemetryEventListener : EventListener + protected override void OnEventWritten(EventWrittenEventArgs e) { - private readonly List eventSources = new(); - - public override void Dispose() + if (!this.eventSources.ContainsKey(e.EventSource.Name)) { - foreach (EventSource eventSource in this.eventSources) - { - this.DisableEvents(eventSource); - } - - base.Dispose(); - GC.SuppressFinalize(this); + return; } - protected override void OnEventSourceCreated(EventSource eventSource) + string? message; + if (e.Message != null && e.Payload != null && e.Payload.Count > 0) { - if (eventSource.Name.StartsWith("OpenTelemetry", StringComparison.OrdinalIgnoreCase)) - { - this.eventSources.Add(eventSource); - this.EnableEvents(eventSource, EventLevel.Verbose, EventKeywords.All); - } - - base.OnEventSourceCreated(eventSource); + message = string.Format(e.Message, e.Payload.ToArray()); } - - protected override void OnEventWritten(EventWrittenEventArgs e) + else { - string? message; - if (e.Message != null && e.Payload != null && e.Payload.Count > 0) - { - message = string.Format(e.Message, e.Payload.ToArray()); - } - else - { - message = e.Message; - } - - Debug.WriteLine($"{e.EventSource.Name} - Level: [{e.Level}], EventId: [{e.EventId}], EventName: [{e.EventName}], Message: [{message}]"); + message = e.Message; } + + Debug.WriteLine($"{e.EventSource.Name} - Level: [{e.Level}], EventId: [{e.EventId}], EventName: [{e.EventName}], Message: [{message}]"); } -#endif } +#endif } diff --git a/src/OpenTelemetry/Internal/PeerServiceResolver.cs b/src/OpenTelemetry/Internal/PeerServiceResolver.cs deleted file mode 100644 index 3227bb1e700..00000000000 --- a/src/OpenTelemetry/Internal/PeerServiceResolver.cs +++ /dev/null @@ -1,109 +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. -// - -#nullable enable - -using System.Runtime.CompilerServices; -using OpenTelemetry.Trace; - -namespace OpenTelemetry.Exporter -{ - internal static class PeerServiceResolver - { - private static readonly Dictionary PeerServiceKeyResolutionDictionary = new(StringComparer.OrdinalIgnoreCase) - { - [SemanticConventions.AttributePeerService] = 0, // priority 0 (highest). - ["peer.hostname"] = 1, - ["peer.address"] = 1, - [SemanticConventions.AttributeHttpHost] = 2, // peer.service for Http. - [SemanticConventions.AttributeDbInstance] = 2, // peer.service for Redis. - }; - - public interface IPeerServiceState - { - string? PeerService { get; set; } - - int? PeerServicePriority { get; set; } - - string? HostName { get; set; } - - string? IpAddress { get; set; } - - long Port { get; set; } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void InspectTag(ref T state, string key, string? value) - where T : struct, IPeerServiceState - { - if (PeerServiceKeyResolutionDictionary.TryGetValue(key, out int priority) - && (state.PeerService == null || priority < state.PeerServicePriority)) - { - state.PeerService = value; - state.PeerServicePriority = priority; - } - else if (key == SemanticConventions.AttributeNetPeerName) - { - state.HostName = value; - } - else if (key == SemanticConventions.AttributeNetPeerIp) - { - state.IpAddress = value; - } - else if (key == SemanticConventions.AttributeNetPeerPort && long.TryParse(value, out var port)) - { - state.Port = port; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void InspectTag(ref T state, string key, long value) - where T : struct, IPeerServiceState - { - if (key == SemanticConventions.AttributeNetPeerPort) - { - state.Port = value; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Resolve(ref T state, out string? peerServiceName, out bool addAsTag) - where T : struct, IPeerServiceState - { - peerServiceName = state.PeerService; - - // If priority = 0 that means peer.service was included in tags - addAsTag = state.PeerServicePriority != 0; - - if (addAsTag) - { - var hostNameOrIpAddress = state.HostName ?? state.IpAddress; - - // peer.service has not already been included, but net.peer.name/ip and optionally net.peer.port are present - if (hostNameOrIpAddress != null) - { - peerServiceName = state.Port == default - ? hostNameOrIpAddress - : $"{hostNameOrIpAddress}:{state.Port}"; - } - else if (state.PeerService != null) - { - peerServiceName = state.PeerService; - } - } - } - } -} diff --git a/src/OpenTelemetry/Internal/PooledList.cs b/src/OpenTelemetry/Internal/PooledList.cs deleted file mode 100644 index 8aad286d5e6..00000000000 --- a/src/OpenTelemetry/Internal/PooledList.cs +++ /dev/null @@ -1,151 +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.Buffers; -using System.Collections; - -namespace OpenTelemetry.Internal -{ - internal readonly struct PooledList : IEnumerable, ICollection - { - private static int lastAllocatedSize = 64; - - private readonly T[] buffer; - - private PooledList(T[] buffer, int count) - { - this.buffer = buffer; - this.Count = count; - } - - public int Count { get; } - - public bool IsEmpty => this.Count == 0; - - bool ICollection.IsSynchronized => false; - - object ICollection.SyncRoot => this; - - public ref T this[int index] - { - get => ref this.buffer[index]; - } - - public static PooledList Create() - { - return new PooledList(ArrayPool.Shared.Rent(lastAllocatedSize), 0); - } - - public static void Add(ref PooledList list, T item) - { - Guard.ThrowIfNull(list.buffer); - - var buffer = list.buffer; - - if (list.Count >= buffer.Length) - { - lastAllocatedSize = buffer.Length * 2; - var previousBuffer = buffer; - - buffer = ArrayPool.Shared.Rent(lastAllocatedSize); - - var span = previousBuffer.AsSpan(); - span.CopyTo(buffer); - ArrayPool.Shared.Return(previousBuffer); - } - - buffer[list.Count] = item; - list = new PooledList(buffer, list.Count + 1); - } - - public static void Clear(ref PooledList list) - { - list = new PooledList(list.buffer, 0); - } - - public void Return() - { - var buffer = this.buffer; - if (buffer != null) - { - ArrayPool.Shared.Return(buffer); - } - } - - void ICollection.CopyTo(Array array, int index) - { - Array.Copy(this.buffer, 0, array, index, this.Count); - } - - public Enumerator GetEnumerator() - { - return new Enumerator(in this); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return new Enumerator(in this); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return new Enumerator(in this); - } - - public struct Enumerator : IEnumerator, IEnumerator - { - private readonly T[] buffer; - private readonly int count; - private int index; - private T current; - - public Enumerator(in PooledList list) - { - this.buffer = list.buffer; - this.count = list.Count; - this.index = 0; - this.current = default; - } - - public T Current { get => this.current; } - - object IEnumerator.Current { get => this.Current; } - - public void Dispose() - { - } - - public bool MoveNext() - { - if (this.index < this.count) - { - this.current = this.buffer[this.index++]; - return true; - } - - this.index = this.count + 1; - this.current = default; - return false; - } - - void IEnumerator.Reset() - { - this.index = 0; - this.current = default; - } - } - } -} diff --git a/src/OpenTelemetry/Internal/ResourceSemanticConventions.cs b/src/OpenTelemetry/Internal/ResourceSemanticConventions.cs deleted file mode 100644 index e3e50430439..00000000000 --- a/src/OpenTelemetry/Internal/ResourceSemanticConventions.cs +++ /dev/null @@ -1,67 +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. -// - -#nullable enable - -namespace OpenTelemetry.Resources -{ - internal static class ResourceSemanticConventions - { - public const string AttributeServiceName = "service.name"; - public const string AttributeServiceNamespace = "service.namespace"; - public const string AttributeServiceInstance = "service.instance.id"; - public const string AttributeServiceVersion = "service.version"; - - public const string AttributeTelemetrySdkName = "telemetry.sdk.name"; - public const string AttributeTelemetrySdkLanguage = "telemetry.sdk.language"; - public const string AttributeTelemetrySdkVersion = "telemetry.sdk.version"; - - public const string AttributeContainerName = "container.name"; - public const string AttributeContainerImage = "container.image.name"; - public const string AttributeContainerTag = "container.image.tag"; - - public const string AttributeFaasName = "faas.name"; - public const string AttributeFaasId = "faas.id"; - public const string AttributeFaasVersion = "faas.version"; - public const string AttributeFaasInstance = "faas.instance"; - - public const string AttributeK8sCluster = "k8s.cluster.name"; - public const string AttributeK8sNamespace = "k8s.namespace.name"; - public const string AttributeK8sPod = "k8s.pod.name"; - public const string AttributeK8sDeployment = "k8s.deployment.name"; - - public const string AttributeHostHostname = "host.hostname"; - public const string AttributeHostId = "host.id"; - public const string AttributeHostName = "host.name"; - public const string AttributeHostType = "host.type"; - public const string AttributeHostImageName = "host.image.name"; - public const string AttributeHostImageId = "host.image.id"; - public const string AttributeHostImageVersion = "host.image.version"; - - public const string AttributeProcessId = "process.id"; - public const string AttributeProcessExecutableName = "process.executable.name"; - public const string AttributeProcessExecutablePath = "process.executable.path"; - public const string AttributeProcessCommand = "process.command"; - public const string AttributeProcessCommandLine = "process.command_line"; - public const string AttributeProcessUsername = "process.username"; - - public const string AttributeCloudProvider = "cloud.provider"; - public const string AttributeCloudAccount = "cloud.account.id"; - public const string AttributeCloudRegion = "cloud.region"; - public const string AttributeCloudZone = "cloud.zone"; - public const string AttributeComponent = "component"; - } -} diff --git a/src/OpenTelemetry/Internal/SelfDiagnostics.cs b/src/OpenTelemetry/Internal/SelfDiagnostics.cs index 77a25be743a..a3f946fa05e 100644 --- a/src/OpenTelemetry/Internal/SelfDiagnostics.cs +++ b/src/OpenTelemetry/Internal/SelfDiagnostics.cs @@ -1,69 +1,55 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Internal +namespace OpenTelemetry.Internal; + +/// +/// Self diagnostics class captures the EventSource events sent by OpenTelemetry +/// modules and writes them to local file for internal troubleshooting. +/// +internal sealed class SelfDiagnostics : IDisposable { /// - /// Self diagnostics class captures the EventSource events sent by OpenTelemetry - /// modules and writes them to local file for internal troubleshooting. + /// Long-living object that hold relevant resources. /// - internal sealed class SelfDiagnostics : IDisposable - { - /// - /// Long-living object that hold relevant resources. - /// - private static readonly SelfDiagnostics Instance = new(); - private readonly SelfDiagnosticsConfigRefresher configRefresher; + private static readonly SelfDiagnostics Instance = new(); + private readonly SelfDiagnosticsConfigRefresher configRefresher; - static SelfDiagnostics() + static SelfDiagnostics() + { + AppDomain.CurrentDomain.ProcessExit += (sender, eventArgs) => { - AppDomain.CurrentDomain.ProcessExit += (sender, eventArgs) => - { - Instance.Dispose(); - }; - } + Instance.Dispose(); + }; + } - private SelfDiagnostics() - { - this.configRefresher = new SelfDiagnosticsConfigRefresher(); - } + private SelfDiagnostics() + { + this.configRefresher = new SelfDiagnosticsConfigRefresher(); + } - /// - /// No member of SelfDiagnostics class is explicitly called when an EventSource class, say - /// OpenTelemetryApiEventSource, is invoked to send an event. - /// To trigger CLR to initialize static fields and static constructors of SelfDiagnostics, - /// call EnsureInitialized method before any EventSource event is sent. - /// - public static void EnsureInitialized() - { - } + /// + /// No member of SelfDiagnostics class is explicitly called when an EventSource class, say + /// OpenTelemetryApiEventSource, is invoked to send an event. + /// To trigger CLR to initialize static fields and static constructors of SelfDiagnostics, + /// call EnsureInitialized method before any EventSource event is sent. + /// + public static void EnsureInitialized() + { + } - /// - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } - private void Dispose(bool disposing) + private void Dispose(bool disposing) + { + if (disposing) { - if (disposing) - { - this.configRefresher.Dispose(); - } + this.configRefresher.Dispose(); } } } diff --git a/src/OpenTelemetry/Internal/SelfDiagnosticsConfigParser.cs b/src/OpenTelemetry/Internal/SelfDiagnosticsConfigParser.cs index d562ae5f403..3216c050f7c 100644 --- a/src/OpenTelemetry/Internal/SelfDiagnosticsConfigParser.cs +++ b/src/OpenTelemetry/Internal/SelfDiagnosticsConfigParser.cs @@ -1,136 +1,132 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Tracing; using System.Text; using System.Text.RegularExpressions; -namespace OpenTelemetry.Internal +namespace OpenTelemetry.Internal; + +internal sealed class SelfDiagnosticsConfigParser { - internal sealed class SelfDiagnosticsConfigParser - { - public const string ConfigFileName = "OTEL_DIAGNOSTICS.json"; - private const int FileSizeLowerLimit = 1024; // Lower limit for log file size in KB: 1MB - private const int FileSizeUpperLimit = 128 * 1024; // Upper limit for log file size in KB: 128MB + public const string ConfigFileName = "OTEL_DIAGNOSTICS.json"; + private const int FileSizeLowerLimit = 1024; // Lower limit for log file size in KB: 1MB + private const int FileSizeUpperLimit = 128 * 1024; // Upper limit for log file size in KB: 128MB - /// - /// ConfigBufferSize is the maximum bytes of config file that will be read. - /// - private const int ConfigBufferSize = 4 * 1024; + /// + /// ConfigBufferSize is the maximum bytes of config file that will be read. + /// + private const int ConfigBufferSize = 4 * 1024; - private static readonly Regex LogDirectoryRegex = new( - @"""LogDirectory""\s*:\s*""(?.*?)""", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex LogDirectoryRegex = new( + @"""LogDirectory""\s*:\s*""(?.*?)""", RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static readonly Regex FileSizeRegex = new( - @"""FileSize""\s*:\s*(?\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex FileSizeRegex = new( + @"""FileSize""\s*:\s*(?\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static readonly Regex LogLevelRegex = new( - @"""LogLevel""\s*:\s*""(?.*?)""", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex LogLevelRegex = new( + @"""LogLevel""\s*:\s*""(?.*?)""", RegexOptions.IgnoreCase | RegexOptions.Compiled); - // This class is called in SelfDiagnosticsConfigRefresher.UpdateMemoryMappedFileFromConfiguration - // in both main thread and the worker thread. - // In theory the variable won't be access at the same time because worker thread first Task.Delay for a few seconds. - private byte[] configBuffer; + // This class is called in SelfDiagnosticsConfigRefresher.UpdateMemoryMappedFileFromConfiguration + // in both main thread and the worker thread. + // In theory the variable won't be access at the same time because worker thread first Task.Delay for a few seconds. + private byte[]? configBuffer; - public bool TryGetConfiguration(out string logDirectory, out int fileSizeInKB, out EventLevel logLevel) + public bool TryGetConfiguration( + [NotNullWhen(true)] out string? logDirectory, + out int fileSizeInKB, + out EventLevel logLevel) + { + logDirectory = null; + fileSizeInKB = 0; + logLevel = EventLevel.LogAlways; + try { - logDirectory = null; - fileSizeInKB = 0; - logLevel = EventLevel.LogAlways; - try + var configFilePath = ConfigFileName; + + // First check using current working directory + if (!File.Exists(configFilePath)) { - var configFilePath = ConfigFileName; + configFilePath = Path.Combine(AppContext.BaseDirectory, ConfigFileName); - // First check using current working directory + // Second check using application base directory if (!File.Exists(configFilePath)) { - configFilePath = Path.Combine(AppContext.BaseDirectory, ConfigFileName); - - // Second check using application base directory - if (!File.Exists(configFilePath)) - { - return false; - } + return false; } + } - using FileStream file = File.Open(configFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); - var buffer = this.configBuffer; - if (buffer == null) - { - buffer = new byte[ConfigBufferSize]; // Fail silently if OOM - this.configBuffer = buffer; - } + using FileStream file = File.Open(configFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); - file.Read(buffer, 0, buffer.Length); - string configJson = Encoding.UTF8.GetString(buffer); - if (!TryParseLogDirectory(configJson, out logDirectory)) - { - return false; - } + var buffer = this.configBuffer; + if (buffer == null) + { + buffer = new byte[ConfigBufferSize]; // Fail silently if OOM + this.configBuffer = buffer; + } - if (!TryParseFileSize(configJson, out fileSizeInKB)) - { - return false; - } + file.Read(buffer, 0, buffer.Length); + string configJson = Encoding.UTF8.GetString(buffer); - if (fileSizeInKB < FileSizeLowerLimit) - { - fileSizeInKB = FileSizeLowerLimit; - } + if (!TryParseLogDirectory(configJson, out logDirectory)) + { + return false; + } - if (fileSizeInKB > FileSizeUpperLimit) - { - fileSizeInKB = FileSizeUpperLimit; - } + if (!TryParseFileSize(configJson, out fileSizeInKB)) + { + return false; + } - if (!TryParseLogLevel(configJson, out var logLevelString)) - { - return false; - } + if (fileSizeInKB < FileSizeLowerLimit) + { + fileSizeInKB = FileSizeLowerLimit; + } - logLevel = (EventLevel)Enum.Parse(typeof(EventLevel), logLevelString); - return true; + if (fileSizeInKB > FileSizeUpperLimit) + { + fileSizeInKB = FileSizeUpperLimit; } - catch (Exception) + + if (!TryParseLogLevel(configJson, out var logLevelString)) { - // do nothing on failure to open/read/parse config file + return false; } - return false; + return Enum.TryParse(logLevelString, out logLevel); } - - internal static bool TryParseLogDirectory(string configJson, out string logDirectory) + catch (Exception) { - var logDirectoryResult = LogDirectoryRegex.Match(configJson); - logDirectory = logDirectoryResult.Groups["LogDirectory"].Value; - return logDirectoryResult.Success && !string.IsNullOrWhiteSpace(logDirectory); + // do nothing on failure to open/read/parse config file + return false; } + } - internal static bool TryParseFileSize(string configJson, out int fileSizeInKB) - { - fileSizeInKB = 0; - var fileSizeResult = FileSizeRegex.Match(configJson); - return fileSizeResult.Success && int.TryParse(fileSizeResult.Groups["FileSize"].Value, out fileSizeInKB); - } + internal static bool TryParseLogDirectory( + string configJson, + [NotNullWhen(true)] + out string logDirectory) + { + var logDirectoryResult = LogDirectoryRegex.Match(configJson); + logDirectory = logDirectoryResult.Groups["LogDirectory"].Value; + return logDirectoryResult.Success && !string.IsNullOrWhiteSpace(logDirectory); + } - internal static bool TryParseLogLevel(string configJson, out string logLevel) - { - var logLevelResult = LogLevelRegex.Match(configJson); - logLevel = logLevelResult.Groups["LogLevel"].Value; - return logLevelResult.Success && !string.IsNullOrWhiteSpace(logLevel); - } + internal static bool TryParseFileSize(string configJson, out int fileSizeInKB) + { + fileSizeInKB = 0; + var fileSizeResult = FileSizeRegex.Match(configJson); + return fileSizeResult.Success && int.TryParse(fileSizeResult.Groups["FileSize"].Value, out fileSizeInKB); + } + + internal static bool TryParseLogLevel( + string configJson, + [NotNullWhen(true)] + out string? logLevel) + { + var logLevelResult = LogLevelRegex.Match(configJson); + logLevel = logLevelResult.Groups["LogLevel"].Value; + return logLevelResult.Success && !string.IsNullOrWhiteSpace(logLevel); } } diff --git a/src/OpenTelemetry/Internal/SelfDiagnosticsConfigRefresher.cs b/src/OpenTelemetry/Internal/SelfDiagnosticsConfigRefresher.cs index 5590a8faea2..3d14dc16e0b 100644 --- a/src/OpenTelemetry/Internal/SelfDiagnosticsConfigRefresher.cs +++ b/src/OpenTelemetry/Internal/SelfDiagnosticsConfigRefresher.cs @@ -1,286 +1,276 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Tracing; using System.IO.MemoryMappedFiles; -using System.Text; -namespace OpenTelemetry.Internal +namespace OpenTelemetry.Internal; + +/// +/// SelfDiagnosticsConfigRefresher class checks a location for a configuration file +/// and open a MemoryMappedFile of a configured size at the configured file path. +/// The class provides a stream object with proper write position if the configuration +/// file is present and valid. Otherwise, the stream object would be unavailable, +/// nothing will be logged to any file. +/// +internal class SelfDiagnosticsConfigRefresher : IDisposable { + public static readonly byte[] MessageOnNewFile = "If you are seeing this message, it means that the OpenTelemetry SDK has successfully created the log file used to write self-diagnostic logs. This file will be appended with logs as they appear. If you do not see any logs following this line, it means no logs of the configured LogLevel is occurring. You may change the LogLevel to show lower log levels, so that logs of lower severities will be shown.\n"u8.ToArray(); + + private const int ConfigurationUpdatePeriodMilliSeconds = 10000; + + private readonly CancellationTokenSource cancellationTokenSource; + private readonly Task worker; + private readonly SelfDiagnosticsConfigParser configParser; + /// - /// SelfDiagnosticsConfigRefresher class checks a location for a configuration file - /// and open a MemoryMappedFile of a configured size at the configured file path. - /// The class provides a stream object with proper write position if the configuration - /// file is present and valid. Otherwise, the stream object would be unavailable, - /// nothing will be logged to any file. + /// memoryMappedFileCache is a handle kept in thread-local storage as a cache to indicate whether the cached + /// viewStream is created from the current m_memoryMappedFile. /// - internal class SelfDiagnosticsConfigRefresher : IDisposable - { - public static readonly byte[] MessageOnNewFile = Encoding.UTF8.GetBytes("If you are seeing this message, it means that the OpenTelemetry SDK has successfully created the log file used to write self-diagnostic logs. This file will be appended with logs as they appear. If you do not see any logs following this line, it means no logs of the configured LogLevel is occurring. You may change the LogLevel to show lower log levels, so that logs of lower severities will be shown.\n"); + private readonly ThreadLocal memoryMappedFileCache = new(true); + private readonly ThreadLocal viewStream = new(true); + private bool disposedValue; - private const int ConfigurationUpdatePeriodMilliSeconds = 10000; + // Once the configuration file is valid, an eventListener object will be created. + private SelfDiagnosticsEventListener? eventListener; + private volatile FileStream? underlyingFileStreamForMemoryMappedFile; + private volatile MemoryMappedFile? memoryMappedFile; + private string? logDirectory; // Log directory for log files + private int logFileSize; // Log file size in bytes + private long logFilePosition; // The logger will write into the byte at this position + private EventLevel logEventLevel = (EventLevel)(-1); - private readonly CancellationTokenSource cancellationTokenSource; - private readonly Task worker; - private readonly SelfDiagnosticsConfigParser configParser; - - /// - /// memoryMappedFileCache is a handle kept in thread-local storage as a cache to indicate whether the cached - /// viewStream is created from the current m_memoryMappedFile. - /// - private readonly ThreadLocal memoryMappedFileCache = new(true); - private readonly ThreadLocal viewStream = new(true); - private bool disposedValue; + public SelfDiagnosticsConfigRefresher() + { + this.configParser = new SelfDiagnosticsConfigParser(); + this.UpdateMemoryMappedFileFromConfiguration(); + this.cancellationTokenSource = new CancellationTokenSource(); + this.worker = Task.Run(() => this.Worker(this.cancellationTokenSource.Token), this.cancellationTokenSource.Token); + } - // Once the configuration file is valid, an eventListener object will be created. - private SelfDiagnosticsEventListener eventListener; - private volatile FileStream underlyingFileStreamForMemoryMappedFile; - private volatile MemoryMappedFile memoryMappedFile; - private string logDirectory; // Log directory for log files - private int logFileSize; // Log file size in bytes - private long logFilePosition; // The logger will write into the byte at this position - private EventLevel logEventLevel = (EventLevel)(-1); + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } - public SelfDiagnosticsConfigRefresher() + /// + /// Try to get the log stream which is seeked to the position where the next line of log should be written. + /// + /// The number of bytes that need to be written. + /// When this method returns, contains the Stream object where `byteCount` of bytes can be written. + /// The number of bytes that is remaining until the end of the stream. + /// Whether the logger should log in the stream. + public virtual bool TryGetLogStream( + int byteCount, + [NotNullWhen(true)] + out Stream? stream, + out int availableByteCount) + { + if (this.memoryMappedFile == null) { - this.configParser = new SelfDiagnosticsConfigParser(); - this.UpdateMemoryMappedFileFromConfiguration(); - this.cancellationTokenSource = new CancellationTokenSource(); - this.worker = Task.Run(() => this.Worker(this.cancellationTokenSource.Token), this.cancellationTokenSource.Token); + stream = null; + availableByteCount = 0; + return false; } - /// - public void Dispose() + try { - this.Dispose(true); - GC.SuppressFinalize(this); - } + var cachedViewStream = this.viewStream.Value; - /// - /// Try to get the log stream which is seeked to the position where the next line of log should be written. - /// - /// The number of bytes that need to be written. - /// When this method returns, contains the Stream object where `byteCount` of bytes can be written. - /// The number of bytes that is remaining until the end of the stream. - /// Whether the logger should log in the stream. - public virtual bool TryGetLogStream(int byteCount, out Stream stream, out int availableByteCount) - { - if (this.memoryMappedFile == null) + // Each thread has its own MemoryMappedViewStream created from the only one MemoryMappedFile. + // Once worker thread updates the MemoryMappedFile, all the cached ViewStream objects become + // obsolete. + // Each thread creates a new MemoryMappedViewStream the next time it tries to retrieve it. + // Whether the MemoryMappedViewStream is obsolete is determined by comparing the current + // MemoryMappedFile object with the MemoryMappedFile object cached at the creation time of the + // MemoryMappedViewStream. + if (cachedViewStream == null || this.memoryMappedFileCache.Value != this.memoryMappedFile) { - stream = null; - availableByteCount = 0; - return false; + // Race condition: The code might reach here right after the worker thread sets memoryMappedFile + // to null in CloseLogFile(). + // In this case, let the NullReferenceException be caught and fail silently. + // By design, all events captured will be dropped during a configuration file refresh if + // the file changed, regardless whether the file is deleted or updated. + cachedViewStream = this.memoryMappedFile.CreateViewStream(); + this.viewStream.Value = cachedViewStream; + this.memoryMappedFileCache.Value = this.memoryMappedFile; } - try + long beginPosition, endPosition; + do { - var cachedViewStream = this.viewStream.Value; - - // Each thread has its own MemoryMappedViewStream created from the only one MemoryMappedFile. - // Once worker thread updates the MemoryMappedFile, all the cached ViewStream objects become - // obsolete. - // Each thread creates a new MemoryMappedViewStream the next time it tries to retrieve it. - // Whether the MemoryMappedViewStream is obsolete is determined by comparing the current - // MemoryMappedFile object with the MemoryMappedFile object cached at the creation time of the - // MemoryMappedViewStream. - if (cachedViewStream == null || this.memoryMappedFileCache.Value != this.memoryMappedFile) + beginPosition = this.logFilePosition; + endPosition = beginPosition + byteCount; + if (endPosition >= this.logFileSize) { - // Race condition: The code might reach here right after the worker thread sets memoryMappedFile - // to null in CloseLogFile(). - // In this case, let the NullReferenceException be caught and fail silently. - // By design, all events captured will be dropped during a configuration file refresh if - // the file changed, regardless whether the file is deleted or updated. - cachedViewStream = this.memoryMappedFile.CreateViewStream(); - this.viewStream.Value = cachedViewStream; - this.memoryMappedFileCache.Value = this.memoryMappedFile; + endPosition %= this.logFileSize; } - - long beginPosition, endPosition; - do - { - beginPosition = this.logFilePosition; - endPosition = beginPosition + byteCount; - if (endPosition >= this.logFileSize) - { - endPosition %= this.logFileSize; - } - } - while (beginPosition != Interlocked.CompareExchange(ref this.logFilePosition, endPosition, beginPosition)); - availableByteCount = (int)(this.logFileSize - beginPosition); - cachedViewStream.Seek(beginPosition, SeekOrigin.Begin); - stream = cachedViewStream; - return true; - } - catch (Exception) - { - stream = null; - availableByteCount = 0; - return false; } + while (beginPosition != Interlocked.CompareExchange(ref this.logFilePosition, endPosition, beginPosition)); + availableByteCount = (int)(this.logFileSize - beginPosition); + cachedViewStream.Seek(beginPosition, SeekOrigin.Begin); + stream = cachedViewStream; + return true; } + catch (Exception) + { + stream = null; + availableByteCount = 0; + return false; + } + } - private async Task Worker(CancellationToken cancellationToken) + private async Task Worker(CancellationToken cancellationToken) + { + await Task.Delay(ConfigurationUpdatePeriodMilliSeconds, cancellationToken).ConfigureAwait(false); + while (!cancellationToken.IsCancellationRequested) { + this.UpdateMemoryMappedFileFromConfiguration(); await Task.Delay(ConfigurationUpdatePeriodMilliSeconds, cancellationToken).ConfigureAwait(false); - while (!cancellationToken.IsCancellationRequested) - { - this.UpdateMemoryMappedFileFromConfiguration(); - await Task.Delay(ConfigurationUpdatePeriodMilliSeconds, cancellationToken).ConfigureAwait(false); - } } + } - private void UpdateMemoryMappedFileFromConfiguration() + private void UpdateMemoryMappedFileFromConfiguration() + { + if (this.configParser.TryGetConfiguration(out string? newLogDirectory, out int fileSizeInKB, out EventLevel newEventLevel)) { - if (this.configParser.TryGetConfiguration(out string newLogDirectory, out int fileSizeInKB, out EventLevel newEventLevel)) + int newFileSize = fileSizeInKB * 1024; + if (!newLogDirectory.Equals(this.logDirectory) || this.logFileSize != newFileSize) { - int newFileSize = fileSizeInKB * 1024; - if (!newLogDirectory.Equals(this.logDirectory) || this.logFileSize != newFileSize) - { - this.CloseLogFile(); - this.OpenLogFile(newLogDirectory, newFileSize); - } + this.CloseLogFile(); + this.OpenLogFile(newLogDirectory, newFileSize); + } - if (!newEventLevel.Equals(this.logEventLevel)) + if (!newEventLevel.Equals(this.logEventLevel)) + { + if (this.eventListener != null) { - if (this.eventListener != null) - { - this.eventListener.Dispose(); - } - - this.eventListener = new SelfDiagnosticsEventListener(newEventLevel, this); - this.logEventLevel = newEventLevel; + this.eventListener.Dispose(); } + + this.eventListener = new SelfDiagnosticsEventListener(newEventLevel, this); + this.logEventLevel = newEventLevel; } - else - { - this.CloseLogFile(); - } } + else + { + this.CloseLogFile(); + } + } - private void CloseLogFile() + private void CloseLogFile() + { + MemoryMappedFile? mmf = Interlocked.CompareExchange(ref this.memoryMappedFile, null, this.memoryMappedFile); + if (mmf != null) { - MemoryMappedFile mmf = Interlocked.CompareExchange(ref this.memoryMappedFile, null, this.memoryMappedFile); - if (mmf != null) + // Each thread has its own MemoryMappedViewStream created from the only one MemoryMappedFile. + // Once worker thread closes the MemoryMappedFile, all the ViewStream objects should be disposed + // properly. + foreach (MemoryMappedViewStream stream in this.viewStream.Values) { - // Each thread has its own MemoryMappedViewStream created from the only one MemoryMappedFile. - // Once worker thread closes the MemoryMappedFile, all the ViewStream objects should be disposed - // properly. - foreach (MemoryMappedViewStream stream in this.viewStream.Values) + if (stream != null) { - if (stream != null) - { - stream.Dispose(); - } + stream.Dispose(); } - - mmf.Dispose(); } - FileStream fs = Interlocked.CompareExchange( - ref this.underlyingFileStreamForMemoryMappedFile, - null, - this.underlyingFileStreamForMemoryMappedFile); - fs?.Dispose(); + mmf.Dispose(); } - private void OpenLogFile(string newLogDirectory, int newFileSize) + FileStream? fs = Interlocked.CompareExchange( + ref this.underlyingFileStreamForMemoryMappedFile, + null, + this.underlyingFileStreamForMemoryMappedFile); + fs?.Dispose(); + } + + private void OpenLogFile(string newLogDirectory, int newFileSize) + { + try { - try - { - Directory.CreateDirectory(newLogDirectory); - var fileName = Path.GetFileName(Process.GetCurrentProcess().MainModule.FileName) + "." - + Process.GetCurrentProcess().Id + ".log"; - var filePath = Path.Combine(newLogDirectory, fileName); + Directory.CreateDirectory(newLogDirectory); + var fileName = Path.GetFileName(Process.GetCurrentProcess().MainModule?.FileName ?? "OpenTelemetrySdk") + "." + + Process.GetCurrentProcess().Id + ".log"; + var filePath = Path.Combine(newLogDirectory, fileName); - // Because the API [MemoryMappedFile.CreateFromFile][1](the string version) behaves differently on - // .NET Framework and .NET Core, here I am using the [FileStream version][2] of it. - // Taking the last four parameter values from [.NET Framework] - // (https://referencesource.microsoft.com/#system.core/System/IO/MemoryMappedFiles/MemoryMappedFile.cs,148) - // and [.NET Core] - // (https://github.com/dotnet/runtime/blob/master/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.cs#L152) - // The parameter for FileAccess is different in type but the same in rules, both are Read and Write. - // The parameter for FileShare is different in values and in behavior. - // .NET Framework doesn't allow sharing but .NET Core allows reading by other programs. - // The last two parameters are the same values for both frameworks. - // [1]: https://docs.microsoft.com/dotnet/api/system.io.memorymappedfiles.memorymappedfile.createfromfile?view=net-5.0#System_IO_MemoryMappedFiles_MemoryMappedFile_CreateFromFile_System_String_System_IO_FileMode_System_String_System_Int64_ - // [2]: https://docs.microsoft.com/dotnet/api/system.io.memorymappedfiles.memorymappedfile.createfromfile?view=net-5.0#System_IO_MemoryMappedFiles_MemoryMappedFile_CreateFromFile_System_IO_FileStream_System_String_System_Int64_System_IO_MemoryMappedFiles_MemoryMappedFileAccess_System_IO_HandleInheritability_System_Boolean_ - this.underlyingFileStreamForMemoryMappedFile = - new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 0x1000, FileOptions.None); + // Because the API [MemoryMappedFile.CreateFromFile][1](the string version) behaves differently on + // .NET Framework and .NET Core, here I am using the [FileStream version][2] of it. + // Taking the last four parameter values from [.NET Framework] + // (https://referencesource.microsoft.com/#system.core/System/IO/MemoryMappedFiles/MemoryMappedFile.cs,148) + // and [.NET Core] + // (https://github.com/dotnet/runtime/blob/master/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.cs#L152) + // The parameter for FileAccess is different in type but the same in rules, both are Read and Write. + // The parameter for FileShare is different in values and in behavior. + // .NET Framework doesn't allow sharing but .NET Core allows reading by other programs. + // The last two parameters are the same values for both frameworks. + // [1]: https://docs.microsoft.com/dotnet/api/system.io.memorymappedfiles.memorymappedfile.createfromfile?view=net-5.0#System_IO_MemoryMappedFiles_MemoryMappedFile_CreateFromFile_System_String_System_IO_FileMode_System_String_System_Int64_ + // [2]: https://docs.microsoft.com/dotnet/api/system.io.memorymappedfiles.memorymappedfile.createfromfile?view=net-5.0#System_IO_MemoryMappedFiles_MemoryMappedFile_CreateFromFile_System_IO_FileStream_System_String_System_Int64_System_IO_MemoryMappedFiles_MemoryMappedFileAccess_System_IO_HandleInheritability_System_Boolean_ + this.underlyingFileStreamForMemoryMappedFile = + new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 0x1000, FileOptions.None); - // The parameter values for MemoryMappedFileSecurity, HandleInheritability and leaveOpen are the same - // values for .NET Framework and .NET Core: - // https://referencesource.microsoft.com/#system.core/System/IO/MemoryMappedFiles/MemoryMappedFile.cs,172 - // https://github.com/dotnet/runtime/blob/master/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.cs#L168-L179 - this.memoryMappedFile = MemoryMappedFile.CreateFromFile( - this.underlyingFileStreamForMemoryMappedFile, - null, - newFileSize, - MemoryMappedFileAccess.ReadWrite, - HandleInheritability.None, - false); - this.logDirectory = newLogDirectory; - this.logFileSize = newFileSize; - this.logFilePosition = MessageOnNewFile.Length; - using var stream = this.memoryMappedFile.CreateViewStream(); - stream.Write(MessageOnNewFile, 0, MessageOnNewFile.Length); - } - catch (Exception ex) - { - OpenTelemetrySdkEventSource.Log.SelfDiagnosticsFileCreateException(newLogDirectory, ex); - } + // The parameter values for MemoryMappedFileSecurity, HandleInheritability and leaveOpen are the same + // values for .NET Framework and .NET Core: + // https://referencesource.microsoft.com/#system.core/System/IO/MemoryMappedFiles/MemoryMappedFile.cs,172 + // https://github.com/dotnet/runtime/blob/master/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.cs#L168-L179 + this.memoryMappedFile = MemoryMappedFile.CreateFromFile( + this.underlyingFileStreamForMemoryMappedFile, + null, + newFileSize, + MemoryMappedFileAccess.ReadWrite, + HandleInheritability.None, + false); + this.logDirectory = newLogDirectory; + this.logFileSize = newFileSize; + this.logFilePosition = MessageOnNewFile.Length; + using var stream = this.memoryMappedFile.CreateViewStream(); + stream.Write(MessageOnNewFile, 0, MessageOnNewFile.Length); } + catch (Exception ex) + { + OpenTelemetrySdkEventSource.Log.SelfDiagnosticsFileCreateException(newLogDirectory, ex); + } + } - private void Dispose(bool disposing) + private void Dispose(bool disposing) + { + if (!this.disposedValue) { - if (!this.disposedValue) + if (disposing) { - if (disposing) + this.cancellationTokenSource.Cancel(false); + try { - this.cancellationTokenSource.Cancel(false); - try - { - this.worker.Wait(); - } - catch (AggregateException) - { - } - finally - { - this.cancellationTokenSource.Dispose(); - } - - // Dispose EventListener before files, because EventListener writes to files. - if (this.eventListener != null) - { - this.eventListener.Dispose(); - } - - // Ensure worker thread properly finishes. - // Or it might have created another MemoryMappedFile in that thread - // after the CloseLogFile() below is called. - this.CloseLogFile(); + this.worker.Wait(); + } + catch (AggregateException) + { + } + finally + { + this.cancellationTokenSource.Dispose(); + } - // Dispose ThreadLocal variables after the file handles are disposed. - this.viewStream.Dispose(); - this.memoryMappedFileCache.Dispose(); + // Dispose EventListener before files, because EventListener writes to files. + if (this.eventListener != null) + { + this.eventListener.Dispose(); } - this.disposedValue = true; + // Ensure worker thread properly finishes. + // Or it might have created another MemoryMappedFile in that thread + // after the CloseLogFile() below is called. + this.CloseLogFile(); + + // Dispose ThreadLocal variables after the file handles are disposed. + this.viewStream.Dispose(); + this.memoryMappedFileCache.Dispose(); } + + this.disposedValue = true; } } } diff --git a/src/OpenTelemetry/Internal/SelfDiagnosticsEventListener.cs b/src/OpenTelemetry/Internal/SelfDiagnosticsEventListener.cs index 7a089171bca..cdfe233fd66 100644 --- a/src/OpenTelemetry/Internal/SelfDiagnosticsEventListener.cs +++ b/src/OpenTelemetry/Internal/SelfDiagnosticsEventListener.cs @@ -1,339 +1,330 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Collections.ObjectModel; using System.Diagnostics.Tracing; using System.Text; -namespace OpenTelemetry.Internal +namespace OpenTelemetry.Internal; + +/// +/// SelfDiagnosticsEventListener class enables the events from OpenTelemetry event sources +/// and write the events to a local file in a circular way. +/// +internal sealed class SelfDiagnosticsEventListener : EventListener { - /// - /// SelfDiagnosticsEventListener class enables the events from OpenTelemetry event sources - /// and write the events to a local file in a circular way. - /// - internal sealed class SelfDiagnosticsEventListener : EventListener + // Buffer size of the log line. A UTF-16 encoded character in C# can take up to 4 bytes if encoded in UTF-8. + private const int BUFFERSIZE = 4 * 5120; + private const string EventSourceNamePrefix = "OpenTelemetry-"; + private readonly object lockObj = new(); + private readonly EventLevel logLevel; + private readonly SelfDiagnosticsConfigRefresher configRefresher; + private readonly ThreadLocal writeBuffer = new(() => null); + private readonly List? eventSourcesBeforeConstructor = new(); + + private bool disposedValue = false; + + public SelfDiagnosticsEventListener(EventLevel logLevel, SelfDiagnosticsConfigRefresher configRefresher) { - // Buffer size of the log line. A UTF-16 encoded character in C# can take up to 4 bytes if encoded in UTF-8. - private const int BUFFERSIZE = 4 * 5120; - private const string EventSourceNamePrefix = "OpenTelemetry-"; - private readonly object lockObj = new(); - private readonly EventLevel logLevel; - private readonly SelfDiagnosticsConfigRefresher configRefresher; - private readonly ThreadLocal writeBuffer = new(() => null); - private readonly List eventSourcesBeforeConstructor = new(); - - private bool disposedValue = false; - - public SelfDiagnosticsEventListener(EventLevel logLevel, SelfDiagnosticsConfigRefresher configRefresher) - { - Guard.ThrowIfNull(configRefresher); + Guard.ThrowIfNull(configRefresher); - this.logLevel = logLevel; - this.configRefresher = configRefresher; + this.logLevel = logLevel; + this.configRefresher = configRefresher; - List eventSources; - lock (this.lockObj) - { - eventSources = this.eventSourcesBeforeConstructor; - this.eventSourcesBeforeConstructor = null; - } - - foreach (var eventSource in eventSources) - { - this.EnableEvents(eventSource, this.logLevel, EventKeywords.All); - } + List eventSources; + lock (this.lockObj) + { + eventSources = this.eventSourcesBeforeConstructor; + this.eventSourcesBeforeConstructor = null; } - /// - public override void Dispose() + foreach (var eventSource in eventSources) { - this.Dispose(true); - base.Dispose(); - GC.SuppressFinalize(this); + this.EnableEvents(eventSource, this.logLevel, EventKeywords.All); } + } - /// - /// Encode a string into the designated position in a buffer of bytes, which will be written as log. - /// If isParameter is true, wrap "{}" around the string. - /// The buffer should not be filled to full, leaving at least one byte empty space to fill a '\n' later. - /// If the buffer cannot hold all characters, truncate the string and replace extra content with "...". - /// The buffer is not guaranteed to be filled until the last byte due to variable encoding length of UTF-8, - /// in order to prioritize speed over space. - /// - /// The string to be encoded. - /// Whether the string is a parameter. If true, "{}" will be wrapped around the string. - /// The byte array to contain the resulting sequence of bytes. - /// The position at which to start writing the resulting sequence of bytes. - /// The position of the buffer after the last byte of the resulting sequence. - internal static int EncodeInBuffer(string str, bool isParameter, byte[] buffer, int position) - { - if (string.IsNullOrEmpty(str)) - { - return position; - } + /// + public override void Dispose() + { + this.Dispose(true); + base.Dispose(); + GC.SuppressFinalize(this); + } - int charCount = str.Length; - int ellipses = isParameter ? "{...}\n".Length : "...\n".Length; + /// + /// Encode a string into the designated position in a buffer of bytes, which will be written as log. + /// If isParameter is true, wrap "{}" around the string. + /// The buffer should not be filled to full, leaving at least one byte empty space to fill a '\n' later. + /// If the buffer cannot hold all characters, truncate the string and replace extra content with "...". + /// The buffer is not guaranteed to be filled until the last byte due to variable encoding length of UTF-8, + /// in order to prioritize speed over space. + /// + /// The string to be encoded. + /// Whether the string is a parameter. If true, "{}" will be wrapped around the string. + /// The byte array to contain the resulting sequence of bytes. + /// The position at which to start writing the resulting sequence of bytes. + /// The position of the buffer after the last byte of the resulting sequence. + internal static int EncodeInBuffer(string? str, bool isParameter, byte[] buffer, int position) + { + if (string.IsNullOrEmpty(str)) + { + return position; + } - // Ensure there is space for "{...}\n" or "...\n". - if (buffer.Length - position - ellipses < 0) - { - return position; - } + int charCount = str!.Length; + int ellipses = isParameter ? "{...}\n".Length : "...\n".Length; - int estimateOfCharacters = (buffer.Length - position - ellipses) / 2; + // Ensure there is space for "{...}\n" or "...\n". + if (buffer.Length - position - ellipses < 0) + { + return position; + } - // Ensure the UTF-16 encoded string can fit in buffer UTF-8 encoding. - // And leave space for "{...}\n" or "...\n". - if (charCount > estimateOfCharacters) - { - charCount = estimateOfCharacters; - } + int estimateOfCharacters = (buffer.Length - position - ellipses) / 2; - if (isParameter) - { - buffer[position++] = (byte)'{'; - } + // Ensure the UTF-16 encoded string can fit in buffer UTF-8 encoding. + // And leave space for "{...}\n" or "...\n". + if (charCount > estimateOfCharacters) + { + charCount = estimateOfCharacters; + } - position += Encoding.UTF8.GetBytes(str, 0, charCount, buffer, position); - if (charCount != str.Length) - { - buffer[position++] = (byte)'.'; - buffer[position++] = (byte)'.'; - buffer[position++] = (byte)'.'; - } + if (isParameter) + { + buffer[position++] = (byte)'{'; + } - if (isParameter) - { - buffer[position++] = (byte)'}'; - } + position += Encoding.UTF8.GetBytes(str, 0, charCount, buffer, position); + if (charCount != str.Length) + { + buffer[position++] = (byte)'.'; + buffer[position++] = (byte)'.'; + buffer[position++] = (byte)'.'; + } - return position; + if (isParameter) + { + buffer[position++] = (byte)'}'; } - internal void WriteEvent(string eventMessage, ReadOnlyCollection payload) + return position; + } + + internal void WriteEvent(string? eventMessage, ReadOnlyCollection? payload) + { + try { - try + var buffer = this.writeBuffer.Value; + if (buffer == null) { - var buffer = this.writeBuffer.Value; - if (buffer == null) - { - buffer = new byte[BUFFERSIZE]; - this.writeBuffer.Value = buffer; - } - - var pos = this.DateTimeGetBytes(DateTime.UtcNow, buffer, 0); - buffer[pos++] = (byte)':'; - pos = EncodeInBuffer(eventMessage, false, buffer, pos); - if (payload != null) - { - // Not using foreach because it can cause allocations - for (int i = 0; i < payload.Count; ++i) - { - object obj = payload[i]; - if (obj != null) - { - pos = EncodeInBuffer(obj.ToString(), true, buffer, pos); - } - else - { - pos = EncodeInBuffer("null", true, buffer, pos); - } - } - } + buffer = new byte[BUFFERSIZE]; + this.writeBuffer.Value = buffer; + } - buffer[pos++] = (byte)'\n'; - int byteCount = pos - 0; - if (this.configRefresher.TryGetLogStream(byteCount, out Stream stream, out int availableByteCount)) + var pos = this.DateTimeGetBytes(DateTime.UtcNow, buffer, 0); + buffer[pos++] = (byte)':'; + pos = EncodeInBuffer(eventMessage, false, buffer, pos); + if (payload != null) + { + // Not using foreach because it can cause allocations + for (int i = 0; i < payload.Count; ++i) { - if (availableByteCount >= byteCount) + object? obj = payload[i]; + if (obj != null) { - stream.Write(buffer, 0, byteCount); + pos = EncodeInBuffer(obj.ToString() ?? "null", true, buffer, pos); } else { - stream.Write(buffer, 0, availableByteCount); - stream.Seek(0, SeekOrigin.Begin); - stream.Write(buffer, availableByteCount, byteCount - availableByteCount); + pos = EncodeInBuffer("null", true, buffer, pos); } } } - catch (Exception) + + buffer[pos++] = (byte)'\n'; + int byteCount = pos - 0; + if (this.configRefresher.TryGetLogStream(byteCount, out Stream? stream, out int availableByteCount)) { - // Fail to allocate memory for buffer, or - // A concurrent condition: memory mapped file is disposed in other thread after TryGetLogStream() finishes. - // In this case, silently fail. + if (availableByteCount >= byteCount) + { + stream.Write(buffer, 0, byteCount); + } + else + { + stream.Write(buffer, 0, availableByteCount); + stream.Seek(0, SeekOrigin.Begin); + stream.Write(buffer, availableByteCount, byteCount - availableByteCount); + } } } - - /// - /// Write the datetime formatted string into bytes byte-array starting at byteIndex position. - /// - /// [DateTimeKind.Utc] - /// format: yyyy - MM - dd T HH : mm : ss . fffffff Z (i.e. 2020-12-09T10:20:50.4659412Z). - /// - /// - /// [DateTimeKind.Local] - /// format: yyyy - MM - dd T HH : mm : ss . fffffff +|- HH : mm (i.e. 2020-12-09T10:20:50.4659412-08:00). - /// - /// - /// [DateTimeKind.Unspecified] - /// format: yyyy - MM - dd T HH : mm : ss . fffffff (i.e. 2020-12-09T10:20:50.4659412). - /// - /// - /// - /// The bytes array must be large enough to write 27-33 charaters from the byteIndex starting position. - /// - /// DateTime. - /// Array of bytes to write. - /// Starting index into bytes array. - /// The number of bytes written. - internal int DateTimeGetBytes(DateTime datetime, byte[] bytes, int byteIndex) + catch (Exception) { - int num; - int pos = byteIndex; + // Fail to allocate memory for buffer, or + // A concurrent condition: memory mapped file is disposed in other thread after TryGetLogStream() finishes. + // In this case, silently fail. + } + } - num = datetime.Year; - bytes[pos++] = (byte)('0' + ((num / 1000) % 10)); - bytes[pos++] = (byte)('0' + ((num / 100) % 10)); - bytes[pos++] = (byte)('0' + ((num / 10) % 10)); - bytes[pos++] = (byte)('0' + (num % 10)); + /// + /// Write the datetime formatted string into bytes byte-array starting at byteIndex position. + /// + /// [DateTimeKind.Utc] + /// format: yyyy - MM - dd T HH : mm : ss . fffffff Z (i.e. 2020-12-09T10:20:50.4659412Z). + /// + /// + /// [DateTimeKind.Local] + /// format: yyyy - MM - dd T HH : mm : ss . fffffff +|- HH : mm (i.e. 2020-12-09T10:20:50.4659412-08:00). + /// + /// + /// [DateTimeKind.Unspecified] + /// format: yyyy - MM - dd T HH : mm : ss . fffffff (i.e. 2020-12-09T10:20:50.4659412). + /// + /// + /// + /// The bytes array must be large enough to write 27-33 charaters from the byteIndex starting position. + /// + /// DateTime. + /// Array of bytes to write. + /// Starting index into bytes array. + /// The number of bytes written. + internal int DateTimeGetBytes(DateTime datetime, byte[] bytes, int byteIndex) + { + int num; + int pos = byteIndex; - bytes[pos++] = (byte)'-'; + num = datetime.Year; + bytes[pos++] = (byte)('0' + ((num / 1000) % 10)); + bytes[pos++] = (byte)('0' + ((num / 100) % 10)); + bytes[pos++] = (byte)('0' + ((num / 10) % 10)); + bytes[pos++] = (byte)('0' + (num % 10)); - num = datetime.Month; - bytes[pos++] = (byte)('0' + ((num / 10) % 10)); - bytes[pos++] = (byte)('0' + (num % 10)); + bytes[pos++] = (byte)'-'; - bytes[pos++] = (byte)'-'; + num = datetime.Month; + bytes[pos++] = (byte)('0' + ((num / 10) % 10)); + bytes[pos++] = (byte)('0' + (num % 10)); - num = datetime.Day; - bytes[pos++] = (byte)('0' + ((num / 10) % 10)); - bytes[pos++] = (byte)('0' + (num % 10)); + bytes[pos++] = (byte)'-'; - bytes[pos++] = (byte)'T'; + num = datetime.Day; + bytes[pos++] = (byte)('0' + ((num / 10) % 10)); + bytes[pos++] = (byte)('0' + (num % 10)); - num = datetime.Hour; - bytes[pos++] = (byte)('0' + ((num / 10) % 10)); - bytes[pos++] = (byte)('0' + (num % 10)); + bytes[pos++] = (byte)'T'; - bytes[pos++] = (byte)':'; + num = datetime.Hour; + bytes[pos++] = (byte)('0' + ((num / 10) % 10)); + bytes[pos++] = (byte)('0' + (num % 10)); - num = datetime.Minute; - bytes[pos++] = (byte)('0' + ((num / 10) % 10)); - bytes[pos++] = (byte)('0' + (num % 10)); + bytes[pos++] = (byte)':'; - bytes[pos++] = (byte)':'; + num = datetime.Minute; + bytes[pos++] = (byte)('0' + ((num / 10) % 10)); + bytes[pos++] = (byte)('0' + (num % 10)); - num = datetime.Second; - bytes[pos++] = (byte)('0' + ((num / 10) % 10)); - bytes[pos++] = (byte)('0' + (num % 10)); + bytes[pos++] = (byte)':'; - bytes[pos++] = (byte)'.'; + num = datetime.Second; + bytes[pos++] = (byte)('0' + ((num / 10) % 10)); + bytes[pos++] = (byte)('0' + (num % 10)); - num = (int)(Math.Round(datetime.TimeOfDay.TotalMilliseconds * 10000) % 10000000); - bytes[pos++] = (byte)('0' + ((num / 1000000) % 10)); - bytes[pos++] = (byte)('0' + ((num / 100000) % 10)); - bytes[pos++] = (byte)('0' + ((num / 10000) % 10)); - bytes[pos++] = (byte)('0' + ((num / 1000) % 10)); - bytes[pos++] = (byte)('0' + ((num / 100) % 10)); - bytes[pos++] = (byte)('0' + ((num / 10) % 10)); - bytes[pos++] = (byte)('0' + (num % 10)); + bytes[pos++] = (byte)'.'; - switch (datetime.Kind) - { - case DateTimeKind.Utc: - bytes[pos++] = (byte)'Z'; - break; + num = (int)(Math.Round(datetime.TimeOfDay.TotalMilliseconds * 10000) % 10000000); + bytes[pos++] = (byte)('0' + ((num / 1000000) % 10)); + bytes[pos++] = (byte)('0' + ((num / 100000) % 10)); + bytes[pos++] = (byte)('0' + ((num / 10000) % 10)); + bytes[pos++] = (byte)('0' + ((num / 1000) % 10)); + bytes[pos++] = (byte)('0' + ((num / 100) % 10)); + bytes[pos++] = (byte)('0' + ((num / 10) % 10)); + bytes[pos++] = (byte)('0' + (num % 10)); - case DateTimeKind.Local: - TimeSpan ts = TimeZoneInfo.Local.GetUtcOffset(datetime); + switch (datetime.Kind) + { + case DateTimeKind.Utc: + bytes[pos++] = (byte)'Z'; + break; - bytes[pos++] = (byte)(ts.Hours >= 0 ? '+' : '-'); + case DateTimeKind.Local: + TimeSpan ts = TimeZoneInfo.Local.GetUtcOffset(datetime); - num = Math.Abs(ts.Hours); - bytes[pos++] = (byte)('0' + ((num / 10) % 10)); - bytes[pos++] = (byte)('0' + (num % 10)); + bytes[pos++] = (byte)(ts.Hours >= 0 ? '+' : '-'); - bytes[pos++] = (byte)':'; + num = Math.Abs(ts.Hours); + bytes[pos++] = (byte)('0' + ((num / 10) % 10)); + bytes[pos++] = (byte)('0' + (num % 10)); - num = ts.Minutes; - bytes[pos++] = (byte)('0' + ((num / 10) % 10)); - bytes[pos++] = (byte)('0' + (num % 10)); - break; + bytes[pos++] = (byte)':'; - case DateTimeKind.Unspecified: - default: - // Skip - break; - } + num = ts.Minutes; + bytes[pos++] = (byte)('0' + ((num / 10) % 10)); + bytes[pos++] = (byte)('0' + (num % 10)); + break; - return pos - byteIndex; + case DateTimeKind.Unspecified: + default: + // Skip + break; } - protected override void OnEventSourceCreated(EventSource eventSource) + return pos - byteIndex; + } + + protected override void OnEventSourceCreated(EventSource eventSource) + { + if (eventSource.Name.StartsWith(EventSourceNamePrefix, StringComparison.Ordinal)) { - if (eventSource.Name.StartsWith(EventSourceNamePrefix, StringComparison.Ordinal)) + // If there are EventSource classes already initialized as of now, this method would be called from + // the base class constructor before the first line of code in SelfDiagnosticsEventListener constructor. + // In this case logLevel is always its default value, "LogAlways". + // Thus we should save the event source and enable them later, when code runs in constructor. + if (this.eventSourcesBeforeConstructor != null) { - // If there are EventSource classes already initialized as of now, this method would be called from - // the base class constructor before the first line of code in SelfDiagnosticsEventListener constructor. - // In this case logLevel is always its default value, "LogAlways". - // Thus we should save the event source and enable them later, when code runs in constructor. - if (this.eventSourcesBeforeConstructor != null) + lock (this.lockObj) { - lock (this.lockObj) + if (this.eventSourcesBeforeConstructor != null) { - if (this.eventSourcesBeforeConstructor != null) - { - this.eventSourcesBeforeConstructor.Add(eventSource); - return; - } + this.eventSourcesBeforeConstructor.Add(eventSource); + return; } } - - this.EnableEvents(eventSource, this.logLevel, EventKeywords.All); } - base.OnEventSourceCreated(eventSource); + this.EnableEvents(eventSource, this.logLevel, EventKeywords.All); } - /// - /// This method records the events from event sources to a local file, which is provided as a stream object by - /// SelfDiagnosticsConfigRefresher class. The file size is bound to a upper limit. Once the write position - /// reaches the end, it will be reset to the beginning of the file. - /// - /// Data of the EventSource event. - protected override void OnEventWritten(EventWrittenEventArgs eventData) + base.OnEventSourceCreated(eventSource); + } + + /// + /// This method records the events from event sources to a local file, which is provided as a stream object by + /// SelfDiagnosticsConfigRefresher class. The file size is bound to a upper limit. Once the write position + /// reaches the end, it will be reset to the beginning of the file. + /// + /// Data of the EventSource event. + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + // 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) + private void Dispose(bool disposing) + { + if (this.disposedValue) { - if (this.disposedValue) - { - return; - } - - if (disposing) - { - this.writeBuffer.Dispose(); - } + return; + } - this.disposedValue = true; + if (disposing) + { + this.writeBuffer.Dispose(); } + + this.disposedValue = true; } } diff --git a/src/OpenTelemetry/Internal/Shims/IsExternalInit.cs b/src/OpenTelemetry/Internal/Shims/IsExternalInit.cs deleted file mode 100644 index de19a976dd5..00000000000 --- a/src/OpenTelemetry/Internal/Shims/IsExternalInit.cs +++ /dev/null @@ -1,25 +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. -// - -#if NETFRAMEWORK || NETSTANDARD2_0_OR_GREATER -namespace System.Runtime.CompilerServices -{ - // This enabled "init" keyword in net462 + netstandard2.0 targets. - internal sealed class IsExternalInit - { - } -} -#endif diff --git a/src/OpenTelemetry/Internal/Shims/UnconditionalSuppressMessageAttribute.cs b/src/OpenTelemetry/Internal/Shims/UnconditionalSuppressMessageAttribute.cs deleted file mode 100644 index 0364d91fa65..00000000000 --- a/src/OpenTelemetry/Internal/Shims/UnconditionalSuppressMessageAttribute.cs +++ /dev/null @@ -1,99 +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. -// -#nullable enable - -namespace System.Diagnostics.CodeAnalysis -{ - /// - /// Suppresses reporting of a specific rule violation, allowing multiple suppressions on a - /// single code artifact. - /// - /// - /// is different than - /// in that it doesn't have a - /// . So it is always preserved in the compiled assembly. - /// - [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)] - internal sealed class UnconditionalSuppressMessageAttribute : Attribute - { - /// - /// Initializes a new instance of the - /// class, specifying the category of the tool and the identifier for an analysis rule. - /// - /// The category for the attribute. - /// The identifier of the analysis rule the attribute applies to. - public UnconditionalSuppressMessageAttribute(string category, string checkId) - { - this.Category = category; - this.CheckId = checkId; - } - - /// - /// Gets the category identifying the classification of the attribute. - /// - /// - /// The property describes the tool or tool analysis category - /// for which a message suppression attribute applies. - /// - public string Category { get; } - - /// - /// Gets the identifier of the analysis tool rule to be suppressed. - /// - /// - /// Concatenated together, the and - /// properties form a unique check identifier. - /// - public string CheckId { get; } - - /// - /// Gets or sets the scope of the code that is relevant for the attribute. - /// - /// - /// The Scope property is an optional argument that specifies the metadata scope for which - /// the attribute is relevant. - /// - public string? Scope { get; set; } - - /// - /// Gets or sets a fully qualified path that represents the target of the attribute. - /// - /// - /// The property is an optional argument identifying the analysis target - /// of the attribute. An example value is "System.IO.Stream.ctor():System.Void". - /// Because it is fully qualified, it can be long, particularly for targets such as parameters. - /// The analysis tool user interface should be capable of automatically formatting the parameter. - /// - public string? Target { get; set; } - - /// - /// Gets or sets an optional argument expanding on exclusion criteria. - /// - /// - /// The property is an optional argument that specifies additional - /// exclusion where the literal metadata target is not sufficiently precise. For example, - /// the cannot be applied within a method, - /// and it may be desirable to suppress a violation against a statement in the method that will - /// give a rule violation, but not against all statements in the method. - /// - public string? MessageId { get; set; } - - /// - /// Gets or sets the justification for suppressing the code analysis message. - /// - public string? Justification { get; set; } - } -} diff --git a/src/OpenTelemetry/Internal/WildcardHelper.cs b/src/OpenTelemetry/Internal/WildcardHelper.cs index a5161fdfde8..98ee62934ae 100644 --- a/src/OpenTelemetry/Internal/WildcardHelper.cs +++ b/src/OpenTelemetry/Internal/WildcardHelper.cs @@ -1,26 +1,17 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; namespace OpenTelemetry; internal static class WildcardHelper { - public static bool ContainsWildcard(string value) + public static bool ContainsWildcard( + [NotNullWhen(true)] + string? value) { if (value == null) { @@ -30,16 +21,14 @@ public static bool ContainsWildcard(string value) return value.Contains('*') || value.Contains('?'); } - public static Regex GetWildcardRegex(IEnumerable patterns = default) + public static Regex GetWildcardRegex(IEnumerable patterns) { - if (patterns == null) - { - return null; - } + Debug.Assert(patterns?.Any() == true, "patterns was null or empty"); var convertedPattern = string.Join( "|", from p in patterns select "(?:" + Regex.Escape(p).Replace("\\*", ".*").Replace("\\?", ".") + ')'); + return new Regex("^(?:" + convertedPattern + ")$", RegexOptions.Compiled | RegexOptions.IgnoreCase); } } diff --git a/src/OpenTelemetry/Logs/BatchExportLogRecordProcessorOptions.cs b/src/OpenTelemetry/Logs/BatchExportLogRecordProcessorOptions.cs index 74441551f98..035c39820b9 100644 --- a/src/OpenTelemetry/Logs/BatchExportLogRecordProcessorOptions.cs +++ b/src/OpenTelemetry/Logs/BatchExportLogRecordProcessorOptions.cs @@ -1,20 +1,5 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.Configuration; using OpenTelemetry.Internal; diff --git a/src/OpenTelemetry/Logs/BatchLogRecordExportProcessor.cs b/src/OpenTelemetry/Logs/BatchLogRecordExportProcessor.cs index 94448dfa817..c1e341585a8 100644 --- a/src/OpenTelemetry/Logs/BatchLogRecordExportProcessor.cs +++ b/src/OpenTelemetry/Logs/BatchLogRecordExportProcessor.cs @@ -1,20 +1,5 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Logs; @@ -57,13 +42,27 @@ public override void OnEnd(LogRecord data) // happen here. Debug.Assert(data != null, "LogRecord was null."); - data!.Buffer(); + switch (data!.Source) + { + case LogRecord.LogRecordSource.FromSharedPool: + data.Buffer(); + data.AddReference(); + if (!this.TryExport(data)) + { + LogRecordSharedPool.Current.Return(data); + } - data.AddReference(); + break; + case LogRecord.LogRecordSource.CreatedManually: + data.Buffer(); + this.TryExport(data); + break; + default: + Debug.Assert(data.Source == LogRecord.LogRecordSource.FromThreadStaticPool, "LogRecord source was something unexpected"); - if (!this.TryExport(data)) - { - LogRecordSharedPool.Current.Return(data); + // Note: If we are using ThreadStatic pool we make a copy of the record. + this.TryExport(data.Copy()); + break; } } } diff --git a/src/OpenTelemetry/Logs/Builder/LoggerProviderBuilderBase.cs b/src/OpenTelemetry/Logs/Builder/LoggerProviderBuilderBase.cs index 87bf3144708..d2a7149fe69 100644 --- a/src/OpenTelemetry/Logs/Builder/LoggerProviderBuilderBase.cs +++ b/src/OpenTelemetry/Logs/Builder/LoggerProviderBuilderBase.cs @@ -1,20 +1,5 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; diff --git a/src/OpenTelemetry/Logs/Builder/LoggerProviderBuilderExtensions.cs b/src/OpenTelemetry/Logs/Builder/LoggerProviderBuilderExtensions.cs index d73d63336d2..3fad4d173dc 100644 --- a/src/OpenTelemetry/Logs/Builder/LoggerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry/Logs/Builder/LoggerProviderBuilderExtensions.cs @@ -1,21 +1,9 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using OpenTelemetry.Internal; @@ -26,17 +14,42 @@ namespace OpenTelemetry.Logs; /// /// Contains extension methods for the class. /// -public static class LoggerProviderBuilderExtensions +#if EXPOSE_EXPERIMENTAL_FEATURES +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else +internal +#endif + static class LoggerProviderBuilderExtensions { +#if EXPOSE_EXPERIMENTAL_FEATURES /// - /// Sets the from which the Resource associated with - /// this provider is built from. Overwrites currently set ResourceBuilder. - /// You should usually use instead - /// (call if desired). + /// Sets the from which the associated with + /// this provider is built from. /// + /// + /// + /// Note: Calling will override the currently set . + /// To modify the current call instead. + /// /// . /// from which Resource will be built. /// Returns for chaining. +#else + /// + /// Sets the from which the associated with + /// this provider is built from. + /// + /// + /// Note: Calling will override the currently set . + /// To modify the current call instead. + /// + /// . + /// from which Resource will be built. + /// Returns for chaining. +#endif public static LoggerProviderBuilder SetResourceBuilder(this LoggerProviderBuilder loggerProviderBuilder, ResourceBuilder resourceBuilder) { Guard.ThrowIfNull(resourceBuilder); @@ -52,13 +65,24 @@ public static LoggerProviderBuilder SetResourceBuilder(this LoggerProviderBuilde return loggerProviderBuilder; } +#if EXPOSE_EXPERIMENTAL_FEATURES /// - /// Modify the from which the Resource associated with - /// this provider is built from in-place. + /// Modify in-place the from which the associated with + /// this provider is built from. /// + /// /// . /// An action which modifies the provided in-place. /// Returns for chaining. +#else + /// + /// Modify in-place the from which the associated with + /// this provider is built from. + /// + /// . + /// An action which modifies the provided in-place. + /// Returns for chaining. +#endif public static LoggerProviderBuilder ConfigureResource(this LoggerProviderBuilder loggerProviderBuilder, Action configure) { Guard.ThrowIfNull(configure); @@ -74,12 +98,22 @@ public static LoggerProviderBuilder ConfigureResource(this LoggerProviderBuilder return loggerProviderBuilder; } +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds a processor to the provider. + /// + /// + /// . + /// LogRecord processor to add. + /// Returns for chaining. +#else /// /// Adds a processor to the provider. /// /// . /// LogRecord processor to add. /// Returns for chaining. +#endif public static LoggerProviderBuilder AddProcessor(this LoggerProviderBuilder loggerProviderBuilder, BaseProcessor processor) { Guard.ThrowIfNull(processor); @@ -95,17 +129,35 @@ public static LoggerProviderBuilder AddProcessor(this LoggerProviderBuilder logg return loggerProviderBuilder; } +#if EXPOSE_EXPERIMENTAL_FEATURES /// /// Adds a processor to the provider which will be retrieved using dependency injection. /// /// + /// /// Note: The type specified by will be /// registered as a singleton service into application services. /// /// Processor type. /// . /// The supplied for chaining. - public static LoggerProviderBuilder AddProcessor(this LoggerProviderBuilder loggerProviderBuilder) +#else + /// + /// Adds a processor to the provider which will be retrieved using dependency injection. + /// + /// + /// Note: The type specified by will be + /// registered as a singleton service into application services. + /// + /// Processor type. + /// . + /// The supplied for chaining. +#endif + public static LoggerProviderBuilder AddProcessor< +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + T>(this LoggerProviderBuilder loggerProviderBuilder) where T : BaseProcessor { loggerProviderBuilder.ConfigureServices(services => services.TryAddSingleton()); @@ -121,12 +173,22 @@ public static LoggerProviderBuilder AddProcessor(this LoggerProviderBuilder l return loggerProviderBuilder; } +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds a processor to the provider which will be retrieved using dependency injection. + /// + /// + /// . + /// The factory that creates the service. + /// The supplied for chaining. +#else /// /// Adds a processor to the provider which will be retrieved using dependency injection. /// /// . /// The factory that creates the service. /// The supplied for chaining. +#endif public static LoggerProviderBuilder AddProcessor( this LoggerProviderBuilder loggerProviderBuilder, Func> implementationFactory) @@ -144,11 +206,20 @@ public static LoggerProviderBuilder AddProcessor( return loggerProviderBuilder; } +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Run the given actions to initialize the . + /// + /// + /// . + /// . +#else /// /// Run the given actions to initialize the . /// /// . /// . +#endif public static LoggerProvider Build(this LoggerProviderBuilder loggerProviderBuilder) { if (loggerProviderBuilder is LoggerProviderBuilderBase loggerProviderBuilderBase) @@ -156,6 +227,6 @@ public static LoggerProvider Build(this LoggerProviderBuilder loggerProviderBuil return loggerProviderBuilderBase.Build(); } - return new LoggerProvider(); + throw new NotSupportedException($"Build is not supported on '{loggerProviderBuilder?.GetType().FullName ?? "null"}' instances."); } } diff --git a/src/OpenTelemetry/Logs/Builder/LoggerProviderBuilderSdk.cs b/src/OpenTelemetry/Logs/Builder/LoggerProviderBuilderSdk.cs index d9aea4c262c..10ac03cd22b 100644 --- a/src/OpenTelemetry/Logs/Builder/LoggerProviderBuilderSdk.cs +++ b/src/OpenTelemetry/Logs/Builder/LoggerProviderBuilderSdk.cs @@ -1,20 +1,5 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using Microsoft.Extensions.DependencyInjection; @@ -57,8 +42,7 @@ public void RegisterProvider(LoggerProviderSdk loggerProvider) this.loggerProvider = loggerProvider; } - public override LoggerProviderBuilder AddInstrumentation( - Func instrumentationFactory) + public override LoggerProviderBuilder AddInstrumentation(Func instrumentationFactory) { Debug.Assert(instrumentationFactory != null, "instrumentationFactory was null"); @@ -66,7 +50,7 @@ public override LoggerProviderBuilder AddInstrumentation( new InstrumentationRegistration( typeof(TInstrumentation).Name, typeof(TInstrumentation).Assembly.GetName().Version?.ToString() ?? DefaultInstrumentationVersion, - instrumentationFactory!()!)); + instrumentationFactory!())); return this; } @@ -121,9 +105,9 @@ internal readonly struct InstrumentationRegistration { public readonly string Name; public readonly string Version; - public readonly object Instance; + public readonly object? Instance; - internal InstrumentationRegistration(string name, string version, object instance) + internal InstrumentationRegistration(string name, string version, object? instance) { this.Name = name; this.Version = version; diff --git a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLogger.cs b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLogger.cs index 8fb5af320f4..3e4258d53f3 100644 --- a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLogger.cs +++ b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLogger.cs @@ -1,20 +1,5 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -39,7 +24,7 @@ internal sealed class OpenTelemetryLogger : ILogger private readonly LoggerProviderSdk provider; private readonly OpenTelemetryLoggerOptions options; - private readonly string categoryName; + private readonly InstrumentationScopeLogger instrumentationScope; internal OpenTelemetryLogger( LoggerProviderSdk provider, @@ -52,15 +37,14 @@ internal OpenTelemetryLogger( this.provider = provider!; this.options = options!; - this.categoryName = categoryName!; + this.instrumentationScope = InstrumentationScopeLogger.GetInstrumentationScopeLoggerForName(categoryName); } internal IExternalScopeProvider? ScopeProvider { get; set; } public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { - if (!this.IsEnabled(logLevel) - || Sdk.SuppressInstrumentation) + if (!this.IsEnabled(logLevel)) { return; } @@ -80,7 +64,6 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except iloggerData.TraceState = this.options.IncludeTraceState && activity != null ? activity.TraceStateString : null; - iloggerData.CategoryName = this.categoryName; iloggerData.EventId = eventId; iloggerData.Exception = exception; iloggerData.ScopeProvider = this.options.IncludeScopes ? this.ScopeProvider : null; @@ -94,7 +77,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except LogRecordData.SetActivityContext(ref data, activity); - var attributes = record.Attributes = + var attributes = record.AttributeData = ProcessState(record, ref iloggerData, in state, this.options.IncludeAttributes, this.options.ParseStateValues); if (!TryGetOriginalFormatFromAttributes(attributes, out var originalFormat)) @@ -112,7 +95,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except : null; } - record.Logger = LoggerInstrumentationScope.Instance; + record.Logger = this.instrumentationScope; processor.OnEnd(record); @@ -127,10 +110,11 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsEnabled(LogLevel logLevel) { - return logLevel != LogLevel.None; + return logLevel != LogLevel.None && !Sdk.SuppressInstrumentation; } - public IDisposable BeginScope(TState state) => this.ScopeProvider?.Push(state) ?? NullScope.Instance; + public IDisposable BeginScope(TState state) + where TState : notnull => this.ScopeProvider?.Push(state) ?? NullScope.Instance; internal static void SetLogRecordSeverityFields(ref LogRecordData logRecordData, LogLevel logLevel) { @@ -147,7 +131,7 @@ internal static void SetLogRecordSeverityFields(ref LogRecordData logRecordData, } } - private static IReadOnlyList>? ProcessState( + internal static IReadOnlyList>? ProcessState( LogRecord logRecord, ref LogRecord.LogRecordILoggerData iLoggerData, in TState state, @@ -253,19 +237,4 @@ public void Dispose() { } } - - private sealed class LoggerInstrumentationScope : Logger - { - private LoggerInstrumentationScope(string name, string version) - : base(name) - { - this.SetInstrumentationScope(version); - } - - public static LoggerInstrumentationScope Instance { get; } - = new("OpenTelemetry", typeof(OpenTelemetryLogger).Assembly.GetName().Version?.ToString() ?? "1.0.0"); - - public override void EmitLog(in LogRecordData data, in LogRecordAttributeList attributes) - => throw new NotSupportedException(); - } } diff --git a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggerOptions.cs b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggerOptions.cs index d3ff3681f2a..15575f1c71b 100644 --- a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggerOptions.cs +++ b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggerOptions.cs @@ -1,20 +1,5 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Internal; @@ -27,7 +12,7 @@ namespace OpenTelemetry.Logs; /// public class OpenTelemetryLoggerOptions { - internal readonly List> Processors = new(); + internal readonly List>> ProcessorFactories = new(); internal ResourceBuilder? ResourceBuilder; /// @@ -101,7 +86,22 @@ public OpenTelemetryLoggerOptions AddProcessor(BaseProcessor processo { Guard.ThrowIfNull(processor); - this.Processors.Add(processor); + this.ProcessorFactories.Add(_ => processor); + + return this; + } + + /// + /// Adds a processor to the provider which will be retrieved using dependency injection. + /// + /// The factory that creates the service. + /// Returns for chaining. + public OpenTelemetryLoggerOptions AddProcessor( + Func> implementationFactory) + { + Guard.ThrowIfNull(implementationFactory); + + this.ProcessorFactories.Add(implementationFactory); return this; } diff --git a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggerProvider.cs b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggerProvider.cs index 2744b617d24..0dc251da0b6 100644 --- a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggerProvider.cs +++ b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggerProvider.cs @@ -1,20 +1,5 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Collections; using System.Diagnostics; @@ -63,9 +48,9 @@ public OpenTelemetryLoggerProvider(IOptionsMonitor o builder.SetResourceBuilder(optionsInstance.ResourceBuilder); } - foreach (var processor in optionsInstance.Processors) + foreach (var processorFactory in optionsInstance.ProcessorFactories) { - builder.AddProcessor(processor); + builder.AddProcessor(processorFactory); } }) .Build(); @@ -111,6 +96,9 @@ void ISupportExternalScope.SetScopeProvider(IExternalScopeProvider scopeProvider /// public ILogger CreateLogger(string categoryName) { + // Lock-free reading leveraging Hashtable's thread safety feature. + // https://learn.microsoft.com/dotnet/api/system.collections.hashtable#thread-safety + if (this.loggers[categoryName] is not ILogger logger) { lock (this.loggers) diff --git a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggingExtensions.cs b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggingExtensions.cs index bfd6e620258..6d7afc1ee68 100644 --- a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggingExtensions.cs +++ b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggingExtensions.cs @@ -1,21 +1,13 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif +#if EXPOSE_EXPERIMENTAL_FEATURES +using System.ComponentModel; +#endif +using System.Diagnostics; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; @@ -35,23 +27,169 @@ public static class OpenTelemetryLoggingExtensions /// Adds an OpenTelemetry logger 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 . + /// Notes: + /// + /// This is safe to be called multiple times and by library authors. + /// Only a single will be created + /// for a given . + /// features available to metrics and + /// traces (for example the "ConfigureServices" extension) are NOT available + /// when using . + /// /// /// The to use. /// The supplied for call chaining. +#if EXPOSE_EXPERIMENTAL_FEATURES + // todo: [Obsolete("Call UseOpenTelemetry instead this method will be removed in a future version.")] + // Note: We hide AddOpenTelemetry from IDEs using EditorBrowsable when UseOpenTelemetry is present to reduce confusion. + [EditorBrowsable(EditorBrowsableState.Never)] +#endif 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. +#if EXPOSE_EXPERIMENTAL_FEATURES + // todo: [Obsolete("Call UseOpenTelemetry instead this method will be removed in a future version.")] + // Note: We hide AddOpenTelemetry from IDEs using EditorBrowsable when UseOpenTelemetry is present to reduce confusion. + [EditorBrowsable(EditorBrowsableState.Never)] +#endif + public static ILoggingBuilder AddOpenTelemetry( + this ILoggingBuilder builder, + Action? configure) + => AddOpenTelemetryInternal(builder, configureBuilder: null, configureOptions: configure); + +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds an OpenTelemetry logger named 'OpenTelemetry' to the . + /// + /// + /// WARNING: This is an experimental API which might change or be removed in the future. Use at your own risk. + /// Note: This is safe to be called multiple times and by library authors. + /// Only a single will be created + /// for a given . + /// + /// The to use. + /// The supplied for call chaining. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + /// + /// Adds an OpenTelemetry logger 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 to use. + /// The supplied for call chaining. + internal +#endif + static ILoggingBuilder UseOpenTelemetry( + this ILoggingBuilder builder) + => AddOpenTelemetryInternal(builder, configureBuilder: null, configureOptions: null); + +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds an OpenTelemetry logger named 'OpenTelemetry' to the . + /// + /// + /// The to use. + /// Optional configuration action. + /// The supplied for call chaining. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + /// + /// Adds an OpenTelemetry logger named 'OpenTelemetry' to the . + /// + /// + /// The to use. + /// configuration action. + /// The supplied for call chaining. + internal +#endif + static ILoggingBuilder UseOpenTelemetry( + this ILoggingBuilder builder, + Action configure) + { + Guard.ThrowIfNull(configure); + + return AddOpenTelemetryInternal(builder, configureBuilder: configure, configureOptions: null); + } + +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds an OpenTelemetry logger named 'OpenTelemetry' to the . + /// + /// + /// The to use. + /// Optional configuration action. + /// Optional configuration action. + /// The supplied for call chaining. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + /// + /// Adds an OpenTelemetry logger named 'OpenTelemetry' to the . + /// + /// + /// The to use. + /// Optional configuration action. + /// Optional configuration action. + /// The supplied for call chaining. + internal +#endif + static ILoggingBuilder UseOpenTelemetry( + this ILoggingBuilder builder, + Action? configureBuilder, + Action? configureOptions) + => AddOpenTelemetryInternal(builder, configureBuilder, configureOptions); + + private static ILoggingBuilder AddOpenTelemetryInternal( + ILoggingBuilder builder, + Action? configureBuilder, + Action? configureOptions) { Guard.ThrowIfNull(builder); builder.AddConfiguration(); - // Note: This will bind logger options element (eg "Logging:OpenTelemetry") to OpenTelemetryLoggerOptions - LoggerProviderOptions.RegisterProviderOptions(builder.Services); + var services = builder.Services; + + // Note: This will bind logger options element (e.g., "Logging:OpenTelemetry") to OpenTelemetryLoggerOptions + RegisterLoggerProviderOptions(services); + + /* Note: This ensures IConfiguration is available when using + * IServiceCollections NOT attached to a host. For example when + * performing: + * + * new ServiceCollection().AddLogging(b => b.AddOpenTelemetry()) + */ + services.AddOpenTelemetrySharedProviderBuilderServices(); + + if (configureOptions != null) + { + // Note: Order is important here so that user-supplied delegate + // fires AFTER the options are bound to Logging:OpenTelemetry + // configuration. + services.Configure(configureOptions); + } - new LoggerProviderBuilderBase(builder.Services).ConfigureBuilder( + var loggingBuilder = new LoggerProviderBuilderBase(services).ConfigureBuilder( (sp, logging) => { var options = sp.GetRequiredService>().CurrentValue; @@ -63,40 +201,78 @@ public static ILoggingBuilder AddOpenTelemetry( options.ResourceBuilder = null; } - foreach (var processor in options.Processors) + foreach (var processorFactory in options.ProcessorFactories) { - logging.AddProcessor(processor); + logging.AddProcessor(processorFactory); } - options.Processors.Clear(); + options.ProcessorFactories.Clear(); }); - builder.Services.TryAddEnumerable( + configureBuilder?.Invoke(loggingBuilder); + + services.TryAddEnumerable( ServiceDescriptor.Singleton( - sp => new OpenTelemetryLoggerProvider( - sp.GetRequiredService(), - sp.GetRequiredService>().CurrentValue, - disposeProvider: false))); + sp => + { + var state = sp.GetRequiredService(); + + var provider = state.Provider; + if (provider == null) + { + /* + * Note: + * + * There is a possibility of a circular reference when + * accessing LoggerProvider from the IServiceProvider. + * + * If LoggerProvider is the first thing accessed, and it + * requires some service which accesses ILogger (for + * example, IHttpClientFactory), then the + * OpenTelemetryLoggerProvider will try to access a new + * (second) LoggerProvider while still in the process of + * building the first one: + * + * LoggerProvider -> IHttpClientFactory -> + * ILoggerFactory -> OpenTelemetryLoggerProvider -> + * LoggerProvider + * + * This check uses the provider reference captured on + * LoggerProviderBuilderSdk during construction of + * LoggerProviderSdk to detect if a provider has already + * been created to give to OpenTelemetryLoggerProvider + * and stop the loop. + */ + provider = sp.GetRequiredService(); + Debug.Assert(provider == state.Provider, "state.Provider did not match resolved LoggerProvider."); + } + + return new OpenTelemetryLoggerProvider( + provider, + sp.GetRequiredService>().CurrentValue, + disposeProvider: false); + })); return builder; - } - /// - /// 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) + // The warning here is about the fact that the OpenTelemetryLoggerOptions will be bound to configuration using ConfigurationBinder + // That uses reflection a lot - so if any of the properties on that class were complex types reflection would be used on them + // and nothing could guarantee its correctness. + // Since currently this class only contains primitive properties this is OK. The top level properties are kept + // because the first generic argument of RegisterProviderOptions below is annotated with + // DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All) so it will preserve everything on the OpenTelemetryLoggerOptions. + // But it would not work recursively into complex property values; + // This should be fully fixed with the introduction of Configuration binder source generator in .NET 8 + // and then there should be a way to do this without any warnings. + // The correctness of these suppressions is verified by a test which validates that all properties of OpenTelemetryLoggerOptions + // are of a primitive type. +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "OpenTelemetryLoggerOptions contains only primitive properties.")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "OpenTelemetryLoggerOptions contains only primitive properties.")] +#endif + static void RegisterLoggerProviderOptions(IServiceCollection services) { - builder.Services.Configure(configure); + LoggerProviderOptions.RegisterProviderOptions(services); } - - return AddOpenTelemetry(builder); } } diff --git a/src/OpenTelemetry/Logs/LogRecord.cs b/src/OpenTelemetry/Logs/LogRecord.cs index cbc5e11ff32..58d97c38edf 100644 --- a/src/OpenTelemetry/Logs/LogRecord.cs +++ b/src/OpenTelemetry/Logs/LogRecord.cs @@ -1,22 +1,10 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif using System.Runtime.CompilerServices; using Microsoft.Extensions.Logging; using OpenTelemetry.Internal; @@ -30,8 +18,10 @@ public sealed class LogRecord { internal LogRecordData Data; internal LogRecordILoggerData ILoggerData; + internal IReadOnlyList>? AttributeData; internal List>? AttributeStorage; internal List? ScopeStorage; + internal LogRecordSource Source = LogRecordSource.CreatedManually; internal int PoolReferenceCount = int.MaxValue; private static readonly Action> AddScopeToBufferedList = (object? scope, List state) => @@ -70,7 +60,6 @@ internal LogRecord( this.ILoggerData = new() { TraceState = activity?.TraceStateString, - CategoryName = categoryName, FormattedMessage = formattedMessage, EventId = eventId, Exception = exception, @@ -87,8 +76,28 @@ internal LogRecord( this.Data.Body = template; } - this.Attributes = stateValues; + this.AttributeData = stateValues; } + + this.Logger = InstrumentationScopeLogger.GetInstrumentationScopeLoggerForName(categoryName); + } + + internal enum LogRecordSource + { + /// + /// A created manually. + /// + CreatedManually, + + /// + /// A rented from the . + /// + FromThreadStaticPool, + + /// + /// A rented from the . + /// + FromSharedPool, } /// @@ -145,22 +154,39 @@ public string? TraceState set => this.ILoggerData.TraceState = value; } +#if EXPOSE_EXPERIMENTAL_FEATURES /// /// Gets or sets the log category name. /// /// - /// Note: is only set when emitting logs through . + /// Note: is an alias for the accessed via the property. + /// Setting a new value for will result in a new + /// being set. /// +#else + /// + /// Gets or sets the log category name. + /// +#endif public string? CategoryName { - get => this.ILoggerData.CategoryName; - set => this.ILoggerData.CategoryName = value; + get => this.Logger.Name; + set + { + if (this.Logger.Name != value) + { + this.Logger = InstrumentationScopeLogger.GetInstrumentationScopeLoggerForName(value); + } + } } /// /// Gets or sets the log . /// +#if EXPOSE_EXPERIMENTAL_FEATURES [Obsolete("Use Severity instead. LogLevel will be removed in a future version.")] +#endif public LogLevel LogLevel { get @@ -238,13 +264,30 @@ public string? Body /// through . /// Set to when is enabled. + /// are automatically updated if is set directly. /// /// [Obsolete("State cannot be accessed safely outside of an ILogger.Log call stack. Use Attributes instead to safely access the data attached to a LogRecord. State will be removed in a future version.")] public object? State { get => this.ILoggerData.State; - set => this.ILoggerData.State = value; + set + { + if (ReferenceEquals(this.ILoggerData.State, value)) + { + return; + } + + if (this.AttributeData is not null) + { + this.AttributeData = OpenTelemetryLogger.ProcessState(this, ref this.ILoggerData, value, includeAttributes: true, parseStateValues: false); + } + else + { + this.ILoggerData.State = value; + } + } } /// @@ -262,15 +305,37 @@ public object? State /// Gets or sets the attributes attached to the log. /// /// - /// Note: Set when is enabled and - /// log record state implements or + /// Set when is enabled and log + /// record state implements or of s /// (where TKey is string and TValue is object) or is enabled - /// otherwise . + /// otherwise . + /// is automatically updated if are set directly. + /// /// - public IReadOnlyList>? Attributes { get; set; } + public IReadOnlyList>? Attributes + { + get => this.AttributeData; + set + { + if (ReferenceEquals(this.AttributeData, value)) + { + return; + } + + if (this.ILoggerData.State is not null) + { + this.ILoggerData.State = value; + } + + this.AttributeData = value; + } + } /// /// Gets or sets the log . @@ -284,29 +349,73 @@ public Exception? Exception set => this.ILoggerData.Exception = value; } +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Gets or sets the original string representation of the severity as it is + /// known at the source. + /// + /// +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LogsBridgeExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else /// /// Gets or sets the original string representation of the severity as it is /// known at the source. /// - public string? SeverityText + internal +#endif + string? SeverityText { get => this.Data.SeverityText; set => this.Data.SeverityText = value; } +#if EXPOSE_EXPERIMENTAL_FEATURES /// /// Gets or sets the log . /// - public LogRecordSeverity? Severity + /// +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LogsBridgeExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + /// + /// Gets or sets the log . + /// + internal +#endif + LogRecordSeverity? Severity { get => this.Data.Severity; set => this.Data.Severity = value; } +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Gets the associated with the . + /// + /// + /// + /// Note: When using the Log Bridge API (for example ) is + /// typically the which emitted the however the value may be different if is modified. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LogsBridgeExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public Logger Logger { get; internal set; } = InstrumentationScopeLogger.Default; +#else /// - /// Gets the which emitted the . + /// Gets or sets the associated with the . /// - public Logger? Logger { get; internal set; } + internal Logger Logger { get; set; } = InstrumentationScopeLogger.Default; +#endif /// /// Executes callback for each currently active scope objects in order @@ -385,7 +494,8 @@ internal LogRecord Copy() { Data = this.Data, ILoggerData = this.ILoggerData.Copy(), - Attributes = this.Attributes == null ? null : new List>(this.Attributes), + AttributeData = this.AttributeData is null ? null : new List>(this.AttributeData), + Logger = this.Logger, }; } @@ -395,7 +505,7 @@ internal LogRecord Copy() /// private void BufferLogAttributes() { - var attributes = this.Attributes; + var attributes = this.AttributeData; if (attributes == null || attributes == this.AttributeStorage) { return; @@ -410,7 +520,7 @@ private void BufferLogAttributes() // https://github.com/open-telemetry/opentelemetry-dotnet/issues/2905. attributeStorage.AddRange(attributes); - this.Attributes = attributeStorage; + this.AttributeData = attributeStorage; } /// @@ -437,7 +547,6 @@ private void BufferLogScopes() internal struct LogRecordILoggerData { public string? TraceState; - public string? CategoryName; public EventId EventId; public string? FormattedMessage; public Exception? Exception; @@ -450,7 +559,6 @@ public LogRecordILoggerData Copy() var copy = new LogRecordILoggerData { TraceState = this.TraceState, - CategoryName = this.CategoryName, EventId = this.EventId, FormattedMessage = this.FormattedMessage, Exception = this.Exception, diff --git a/src/OpenTelemetry/Logs/LogRecordExportProcessorOptions.cs b/src/OpenTelemetry/Logs/LogRecordExportProcessorOptions.cs index 8dcb3e7e5ab..95cdc830e35 100644 --- a/src/OpenTelemetry/Logs/LogRecordExportProcessorOptions.cs +++ b/src/OpenTelemetry/Logs/LogRecordExportProcessorOptions.cs @@ -1,20 +1,5 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Internal; diff --git a/src/OpenTelemetry/Logs/LogRecordScope.cs b/src/OpenTelemetry/Logs/LogRecordScope.cs index 89f944c72b0..b86230eaf32 100644 --- a/src/OpenTelemetry/Logs/LogRecordScope.cs +++ b/src/OpenTelemetry/Logs/LogRecordScope.cs @@ -1,20 +1,5 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Collections; diff --git a/src/OpenTelemetry/Logs/LoggerProviderExtensions.cs b/src/OpenTelemetry/Logs/LoggerProviderExtensions.cs index da788c1e735..66dbac5ad42 100644 --- a/src/OpenTelemetry/Logs/LoggerProviderExtensions.cs +++ b/src/OpenTelemetry/Logs/LoggerProviderExtensions.cs @@ -1,21 +1,9 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif using OpenTelemetry.Internal; namespace OpenTelemetry.Logs; @@ -23,12 +11,22 @@ namespace OpenTelemetry.Logs; /// /// Contains extension methods for the class. /// -public static class LoggerProviderExtensions +#if EXPOSE_EXPERIMENTAL_FEATURES +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else +internal +#endif + static class LoggerProviderExtensions { +#if EXPOSE_EXPERIMENTAL_FEATURES /// /// Add a processor to the . /// /// + /// /// Note: The supplied will be /// automatically disposed when then the is disposed. @@ -36,6 +34,19 @@ public static class LoggerProviderExtensions /// instance on which ForceFlush will be called. /// Log processor to add. /// The supplied for chaining. +#else + /// + /// Add a processor to the . + /// + /// + /// Note: The supplied will be + /// automatically disposed when then the is disposed. + /// + /// instance on which ForceFlush will be called. + /// Log processor to add. + /// The supplied for chaining. +#endif public static LoggerProvider AddProcessor(this LoggerProvider provider, BaseProcessor processor) { Guard.ThrowIfNull(provider); @@ -49,6 +60,7 @@ public static LoggerProvider AddProcessor(this LoggerProvider provider, BaseProc return provider; } +#if EXPOSE_EXPERIMENTAL_FEATURES /// /// Flushes all the processors registered under , blocks the current thread /// until flush completed, shutdown signaled or timed out. @@ -65,8 +77,29 @@ public static LoggerProvider AddProcessor(this LoggerProvider provider, BaseProc /// Thrown when the timeoutMilliseconds is smaller than -1. /// /// + /// /// This function guarantees thread-safety. /// +#else + /// + /// Flushes all the processors registered under , blocks the current thread + /// until flush completed, shutdown signaled or timed out. + /// + /// instance on which ForceFlush will be called. + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when force flush succeeded; otherwise, false. + /// + /// + /// Thrown when the timeoutMilliseconds is smaller than -1. + /// + /// + /// This function guarantees thread-safety. + /// +#endif public static bool ForceFlush(this LoggerProvider provider, int timeoutMilliseconds = Timeout.Infinite) { Guard.ThrowIfNull(provider); @@ -80,6 +113,28 @@ public static bool ForceFlush(this LoggerProvider provider, int timeoutMilliseco return true; } +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Attempts to shutdown the , blocks the current thread until + /// shutdown completed or timed out. + /// + /// instance on which Shutdown will be called. + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when shutdown succeeded; otherwise, false. + /// + /// + /// Thrown when the timeoutMilliseconds is smaller than -1. + /// + /// + /// + /// This function guarantees thread-safety. Only the first call will + /// win, subsequent calls will be no-op. + /// +#else /// /// Attempts to shutdown the , blocks the current thread until /// shutdown completed or timed out. @@ -99,6 +154,7 @@ public static bool ForceFlush(this LoggerProvider provider, int timeoutMilliseco /// This function guarantees thread-safety. Only the first call will /// win, subsequent calls will be no-op. /// +#endif public static bool Shutdown(this LoggerProvider provider, int timeoutMilliseconds = Timeout.Infinite) { Guard.ThrowIfNull(provider); diff --git a/src/OpenTelemetry/Logs/LoggerProviderSdk.cs b/src/OpenTelemetry/Logs/LoggerProviderSdk.cs index 1a026ca116c..f23308680e7 100644 --- a/src/OpenTelemetry/Logs/LoggerProviderSdk.cs +++ b/src/OpenTelemetry/Logs/LoggerProviderSdk.cs @@ -1,22 +1,10 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif using System.Text; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Internal; @@ -75,7 +63,11 @@ public LoggerProviderSdk( foreach (var instrumentation in state.Instrumentation) { - this.instrumentations.Add(instrumentation.Instance); + if (instrumentation.Instance is not null) + { + this.instrumentations.Add(instrumentation.Instance); + } + instrumentationFactoriesAdded.Append(instrumentation.Name); instrumentationFactoriesAdded.Append(';'); } @@ -201,7 +193,12 @@ public bool ContainsBatchProcessor(BaseProcessor processor) } /// - protected override bool TryCreateLogger(string? name, out Logger? logger) + protected override bool TryCreateLogger( + string? name, +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + [NotNullWhen(true)] +#endif + out Logger? logger) { logger = new LoggerSdk(this, name); return true; diff --git a/src/OpenTelemetry/Logs/LoggerSdk.cs b/src/OpenTelemetry/Logs/LoggerSdk.cs index e357f686122..a0bc47300d0 100644 --- a/src/OpenTelemetry/Logs/LoggerSdk.cs +++ b/src/OpenTelemetry/Logs/LoggerSdk.cs @@ -1,20 +1,5 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Internal; @@ -53,7 +38,7 @@ public override void EmitLog(in LogRecordData data, in LogRecordAttributeList at logRecord.Logger = this; - logRecord.Attributes = attributes.Export(ref logRecord.AttributeStorage); + logRecord.AttributeData = attributes.Export(ref logRecord.AttributeStorage); processor.OnEnd(logRecord); diff --git a/src/OpenTelemetry/Logs/Pool/ILogRecordPool.cs b/src/OpenTelemetry/Logs/Pool/ILogRecordPool.cs index 5cbfb20eb85..5caa7a47464 100644 --- a/src/OpenTelemetry/Logs/Pool/ILogRecordPool.cs +++ b/src/OpenTelemetry/Logs/Pool/ILogRecordPool.cs @@ -1,20 +1,5 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 namespace OpenTelemetry.Logs; diff --git a/src/OpenTelemetry/Logs/Pool/LogRecordPoolHelper.cs b/src/OpenTelemetry/Logs/Pool/LogRecordPoolHelper.cs index 076060558d5..c298bef4308 100644 --- a/src/OpenTelemetry/Logs/Pool/LogRecordPoolHelper.cs +++ b/src/OpenTelemetry/Logs/Pool/LogRecordPoolHelper.cs @@ -1,20 +1,5 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 namespace OpenTelemetry.Logs; diff --git a/src/OpenTelemetry/Logs/Pool/LogRecordSharedPool.cs b/src/OpenTelemetry/Logs/Pool/LogRecordSharedPool.cs index 65dc5a7a8c0..f5daa5a9904 100644 --- a/src/OpenTelemetry/Logs/Pool/LogRecordSharedPool.cs +++ b/src/OpenTelemetry/Logs/Pool/LogRecordSharedPool.cs @@ -1,21 +1,7 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using OpenTelemetry.Internal; @@ -32,7 +18,7 @@ internal sealed class LogRecordSharedPool : ILogRecordPool private long rentIndex; private long returnIndex; - public LogRecordSharedPool(int capacity) + private LogRecordSharedPool(int capacity) { this.Capacity = capacity; this.pool = new LogRecord?[capacity]; @@ -69,18 +55,24 @@ public LogRecord Rent() continue; } + Debug.Assert(logRecord.Source == LogRecord.LogRecordSource.FromSharedPool, "logRecord.Source was not FromSharedPool"); logRecord.ResetReferenceCount(); return logRecord; } } - var newLogRecord = new LogRecord(); + var newLogRecord = new LogRecord() + { + Source = LogRecord.LogRecordSource.FromSharedPool, + }; newLogRecord.ResetReferenceCount(); return newLogRecord; } public void Return(LogRecord logRecord) { + Debug.Assert(logRecord.Source == LogRecord.LogRecordSource.FromSharedPool, "logRecord.Source was not FromSharedPool"); + if (logRecord.RemoveReference() != 0) { return; diff --git a/src/OpenTelemetry/Logs/Pool/LogRecordThreadStaticPool.cs b/src/OpenTelemetry/Logs/Pool/LogRecordThreadStaticPool.cs index f44064985f4..8763cf8679d 100644 --- a/src/OpenTelemetry/Logs/Pool/LogRecordThreadStaticPool.cs +++ b/src/OpenTelemetry/Logs/Pool/LogRecordThreadStaticPool.cs @@ -1,20 +1,7 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; namespace OpenTelemetry.Logs; @@ -34,15 +21,23 @@ public LogRecord Rent() var logRecord = Storage; if (logRecord != null) { + Debug.Assert(logRecord.Source == LogRecord.LogRecordSource.FromThreadStaticPool, "logRecord.Source was not FromThreadStaticPool"); Storage = null; - return logRecord; + } + else + { + logRecord = new() + { + Source = LogRecord.LogRecordSource.FromThreadStaticPool, + }; } - return new(); + return logRecord; } public void Return(LogRecord logRecord) { + Debug.Assert(logRecord.Source == LogRecord.LogRecordSource.FromThreadStaticPool, "logRecord.Source was not FromThreadStaticPool"); if (Storage == null) { LogRecordPoolHelper.Clear(logRecord); diff --git a/src/OpenTelemetry/Logs/SimpleLogRecordExportProcessor.cs b/src/OpenTelemetry/Logs/SimpleLogRecordExportProcessor.cs index 8af6322a3ff..654677d8702 100644 --- a/src/OpenTelemetry/Logs/SimpleLogRecordExportProcessor.cs +++ b/src/OpenTelemetry/Logs/SimpleLogRecordExportProcessor.cs @@ -1,20 +1,5 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Logs; diff --git a/src/OpenTelemetry/Metrics/AggregationTemporality.cs b/src/OpenTelemetry/Metrics/AggregationTemporality.cs index 389c27ffd0a..2a9c036d3dc 100644 --- a/src/OpenTelemetry/Metrics/AggregationTemporality.cs +++ b/src/OpenTelemetry/Metrics/AggregationTemporality.cs @@ -1,31 +1,21 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// Enumeration used to define the aggregation temporality for a . +/// +public enum AggregationTemporality : byte { - public enum AggregationTemporality : byte - { - /// - /// Cumulative. - /// - Cumulative = 0b1, + /// + /// Cumulative. + /// + Cumulative = 0b1, - /// - /// Delta. - /// - Delta = 0b10, - } + /// + /// Delta. + /// + Delta = 0b10, } diff --git a/src/OpenTelemetry/Metrics/AggregationType.cs b/src/OpenTelemetry/Metrics/AggregationType.cs index c1b550c1538..13d3a03ad5c 100644 --- a/src/OpenTelemetry/Metrics/AggregationType.cs +++ b/src/OpenTelemetry/Metrics/AggregationType.cs @@ -1,86 +1,72 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +internal enum AggregationType { - internal enum AggregationType - { - /// - /// Invalid. - /// - Invalid = -1, + /// + /// Invalid. + /// + Invalid = -1, - /// - /// Calculate SUM from incoming delta measurements. - /// - LongSumIncomingDelta = 0, + /// + /// Calculate SUM from incoming delta measurements. + /// + LongSumIncomingDelta = 0, - /// - /// Calculate SUM from incoming cumulative measurements. - /// - LongSumIncomingCumulative = 1, + /// + /// Calculate SUM from incoming cumulative measurements. + /// + LongSumIncomingCumulative = 1, - /// - /// Calculate SUM from incoming delta measurements. - /// - DoubleSumIncomingDelta = 2, + /// + /// Calculate SUM from incoming delta measurements. + /// + DoubleSumIncomingDelta = 2, - /// - /// Calculate SUM from incoming cumulative measurements. - /// - DoubleSumIncomingCumulative = 3, + /// + /// Calculate SUM from incoming cumulative measurements. + /// + DoubleSumIncomingCumulative = 3, - /// - /// Keep LastValue. - /// - LongGauge = 4, + /// + /// Keep LastValue. + /// + LongGauge = 4, - /// - /// Keep LastValue. - /// - DoubleGauge = 5, + /// + /// Keep LastValue. + /// + DoubleGauge = 5, - /// - /// Histogram with sum, count, buckets. - /// - HistogramWithBuckets = 6, + /// + /// Histogram with sum, count, buckets. + /// + HistogramWithBuckets = 6, - /// - /// Histogram with sum, count, min, max, buckets. - /// - HistogramWithMinMaxBuckets = 7, + /// + /// Histogram with sum, count, min, max, buckets. + /// + HistogramWithMinMaxBuckets = 7, - /// - /// Histogram with sum, count. - /// - Histogram = 8, + /// + /// Histogram with sum, count. + /// + Histogram = 8, - /// - /// Histogram with sum, count, min, max. - /// - HistogramWithMinMax = 9, + /// + /// Histogram with sum, count, min, max. + /// + HistogramWithMinMax = 9, - /// - /// Exponential Histogram with sum, count. - /// - Base2ExponentialHistogram = 10, + /// + /// Exponential Histogram with sum, count. + /// + Base2ExponentialHistogram = 10, - /// - /// Exponential Histogram with sum, count, min, max. - /// - Base2ExponentialHistogramWithMinMax = 11, - } + /// + /// Exponential Histogram with sum, count, min, max. + /// + Base2ExponentialHistogramWithMinMax = 11, } diff --git a/src/OpenTelemetry/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs index 60792d14758..c74c2ff25b0 100644 --- a/src/OpenTelemetry/Metrics/AggregatorStore.cs +++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs @@ -1,280 +1,479 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Collections.Concurrent; +#if NET8_0_OR_GREATER +using System.Collections.Frozen; +#endif +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using OpenTelemetry.Internal; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +internal sealed class AggregatorStore { - internal sealed class AggregatorStore +#if NET8_0_OR_GREATER + internal readonly FrozenSet? TagKeysInteresting; +#else + internal readonly HashSet? TagKeysInteresting; +#endif + internal readonly bool OutputDelta; + internal readonly bool OutputDeltaWithUnusedMetricPointReclaimEnabled; + internal readonly int CardinalityLimit; + internal readonly bool EmitOverflowAttribute; + internal readonly ConcurrentDictionary? TagsToMetricPointIndexDictionaryDelta; + internal readonly Func? ExemplarReservoirFactory; + internal long DroppedMeasurements = 0; + + private const ExemplarFilterType DefaultExemplarFilter = ExemplarFilterType.AlwaysOff; + 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."; + private static readonly Comparison> DimensionComparisonDelegate = (x, y) => x.Key.CompareTo(y.Key); + + private readonly object lockZeroTags = new(); + private readonly object lockOverflowTag = new(); + private readonly int tagsKeysInterestingCount; + + // This holds the reclaimed MetricPoints that are available for reuse. + private readonly Queue? availableMetricPoints; + + private readonly ConcurrentDictionary tagsToMetricPointIndexDictionary = + new(); + + private readonly string name; + private readonly string metricPointCapHitMessage; + private readonly MetricPoint[] metricPoints; + private readonly int[] currentMetricPointBatch; + private readonly AggregationType aggType; + private readonly double[] histogramBounds; + private readonly int exponentialHistogramMaxSize; + private readonly int exponentialHistogramMaxScale; + private readonly UpdateLongDelegate updateLongCallback; + private readonly UpdateDoubleDelegate updateDoubleCallback; + private readonly ExemplarFilterType exemplarFilter; + private readonly Func[], int, int> lookupAggregatorStore; + + private int metricPointIndex = 0; + private int batchSize = 0; + private int metricCapHitMessageLogged; + private bool zeroTagMetricPointInitialized; + private bool overflowTagMetricPointInitialized; + + internal AggregatorStore( + MetricStreamIdentity metricStreamIdentity, + AggregationType aggType, + AggregationTemporality temporality, + int cardinalityLimit, + bool emitOverflowAttribute, + bool shouldReclaimUnusedMetricPoints, + ExemplarFilterType? exemplarFilter = null, + Func? exemplarReservoirFactory = null) { - private static readonly string MetricPointCapHitFixMessage = "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."; - private static readonly Comparison> DimensionComparisonDelegate = (x, y) => x.Key.CompareTo(y.Key); - private readonly object lockZeroTags = new(); - private readonly HashSet tagKeysInteresting; - private readonly int tagsKeysInterestingCount; - - private readonly ConcurrentDictionary tagsToMetricPointIndexDictionary = - new(); - - private readonly string name; - private readonly string metricPointCapHitMessage; - private readonly bool outputDelta; - private readonly MetricPoint[] metricPoints; - private readonly int[] currentMetricPointBatch; - private readonly AggregationType aggType; - private readonly double[] histogramBounds; - private readonly int exponentialHistogramMaxSize; - private readonly int exponentialHistogramMaxScale; - private readonly UpdateLongDelegate updateLongCallback; - private readonly UpdateDoubleDelegate updateDoubleCallback; - private readonly int maxMetricPoints; - private readonly ExemplarFilter exemplarFilter; - private int metricPointIndex = 0; - private int batchSize = 0; - private int metricCapHitMessageLogged; - private bool zeroTagMetricPointInitialized; - - internal AggregatorStore( - MetricStreamIdentity metricStreamIdentity, - AggregationType aggType, - AggregationTemporality temporality, - int maxMetricPoints, - ExemplarFilter exemplarFilter = null) - { - this.name = metricStreamIdentity.InstrumentName; - this.maxMetricPoints = maxMetricPoints; - this.metricPointCapHitMessage = $"Maximum MetricPoints limit reached for this Metric stream. Configured limit: {this.maxMetricPoints}"; - this.metricPoints = new MetricPoint[maxMetricPoints]; - this.currentMetricPointBatch = new int[maxMetricPoints]; - this.aggType = aggType; - this.outputDelta = temporality == AggregationTemporality.Delta; - this.histogramBounds = metricStreamIdentity.HistogramBucketBounds ?? Metric.DefaultHistogramBounds; - this.exponentialHistogramMaxSize = metricStreamIdentity.ExponentialHistogramMaxSize; - this.exponentialHistogramMaxScale = metricStreamIdentity.ExponentialHistogramMaxScale; - this.StartTimeExclusive = DateTimeOffset.UtcNow; - this.exemplarFilter = exemplarFilter ?? new AlwaysOffExemplarFilter(); - if (metricStreamIdentity.TagKeys == null) - { - this.updateLongCallback = this.UpdateLong; - this.updateDoubleCallback = this.UpdateDouble; - } - else - { - this.updateLongCallback = this.UpdateLongCustomTags; - this.updateDoubleCallback = this.UpdateDoubleCustomTags; - var hs = new HashSet(metricStreamIdentity.TagKeys, StringComparer.Ordinal); - this.tagKeysInteresting = hs; - this.tagsKeysInterestingCount = hs.Count; - } + this.name = metricStreamIdentity.InstrumentName; + this.CardinalityLimit = cardinalityLimit; + + this.metricPointCapHitMessage = $"Maximum MetricPoints limit reached for this Metric stream. Configured limit: {this.CardinalityLimit}"; + this.metricPoints = new MetricPoint[cardinalityLimit]; + this.currentMetricPointBatch = new int[cardinalityLimit]; + this.aggType = aggType; + this.OutputDelta = temporality == AggregationTemporality.Delta; + this.histogramBounds = metricStreamIdentity.HistogramBucketBounds ?? FindDefaultHistogramBounds(in metricStreamIdentity); + this.exponentialHistogramMaxSize = metricStreamIdentity.ExponentialHistogramMaxSize; + this.exponentialHistogramMaxScale = metricStreamIdentity.ExponentialHistogramMaxScale; + this.StartTimeExclusive = DateTimeOffset.UtcNow; + this.ExemplarReservoirFactory = exemplarReservoirFactory; + if (metricStreamIdentity.TagKeys == null) + { + this.updateLongCallback = this.UpdateLong; + this.updateDoubleCallback = this.UpdateDouble; } + else + { + this.updateLongCallback = this.UpdateLongCustomTags; + this.updateDoubleCallback = this.UpdateDoubleCustomTags; +#if NET8_0_OR_GREATER + var hs = FrozenSet.ToFrozenSet(metricStreamIdentity.TagKeys, StringComparer.Ordinal); +#else + var hs = new HashSet(metricStreamIdentity.TagKeys, StringComparer.Ordinal); +#endif + this.TagKeysInteresting = hs; + this.tagsKeysInterestingCount = hs.Count; + } + + this.EmitOverflowAttribute = emitOverflowAttribute; - private delegate void UpdateLongDelegate(long value, ReadOnlySpan> tags); + this.exemplarFilter = exemplarFilter ?? DefaultExemplarFilter; + Debug.Assert( + this.exemplarFilter == ExemplarFilterType.AlwaysOff + || this.exemplarFilter == ExemplarFilterType.AlwaysOn + || this.exemplarFilter == ExemplarFilterType.TraceBased, + "this.exemplarFilter had an unexpected value"); - private delegate void UpdateDoubleDelegate(double value, ReadOnlySpan> tags); + var reservedMetricPointsCount = 1; - internal DateTimeOffset StartTimeExclusive { get; private set; } + if (emitOverflowAttribute) + { + // Setting metricPointIndex to 1 as we would reserve the metricPoints[1] for overflow attribute. + // Newer attributes should be added starting at the index: 2 + this.metricPointIndex = 1; + reservedMetricPointsCount++; + } - internal DateTimeOffset EndTimeInclusive { get; private set; } + this.OutputDeltaWithUnusedMetricPointReclaimEnabled = shouldReclaimUnusedMetricPoints && this.OutputDelta; - internal bool IsExemplarEnabled() + if (this.OutputDeltaWithUnusedMetricPointReclaimEnabled) { - // Using this filter to indicate On/Off - // instead of another separate flag. - return this.exemplarFilter is not AlwaysOffExemplarFilter; + this.availableMetricPoints = new Queue(cardinalityLimit - reservedMetricPointsCount); + + // There is no overload which only takes capacity as the parameter + // Using the DefaultConcurrencyLevel defined in the ConcurrentDictionary class: https://github.com/dotnet/runtime/blob/v7.0.5/src/libraries/System.Collections.Concurrent/src/System/Collections/Concurrent/ConcurrentDictionary.cs#L2020 + // We expect at the most (maxMetricPoints - reservedMetricPointsCount) * 2 entries- one for sorted and one for unsorted input + this.TagsToMetricPointIndexDictionaryDelta = + new ConcurrentDictionary(concurrencyLevel: Environment.ProcessorCount, capacity: (cardinalityLimit - reservedMetricPointsCount) * 2); + + // Add all the indices except for the reserved ones to the queue so that threads have + // readily available access to these MetricPoints for their use. + for (int i = reservedMetricPointsCount; i < this.CardinalityLimit; i++) + { + this.availableMetricPoints.Enqueue(i); + } + + this.lookupAggregatorStore = this.LookupAggregatorStoreForDeltaWithReclaim; + } + else + { + this.lookupAggregatorStore = this.LookupAggregatorStore; } + } + + private delegate void UpdateLongDelegate(long value, ReadOnlySpan> tags); + + private delegate void UpdateDoubleDelegate(double value, ReadOnlySpan> tags); + + internal DateTimeOffset StartTimeExclusive { get; private set; } + + internal DateTimeOffset EndTimeInclusive { get; private set; } - internal void Update(long value, ReadOnlySpan> tags) + internal double[] HistogramBounds => this.histogramBounds; + + internal bool IsExemplarEnabled() + { + // Using this filter to indicate On/Off + // instead of another separate flag. + return this.exemplarFilter != ExemplarFilterType.AlwaysOff; + } + + internal void Update(long value, ReadOnlySpan> tags) + { + try { this.updateLongCallback(value, tags); } + catch (Exception) + { + Interlocked.Increment(ref this.DroppedMeasurements); + OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, "SDK internal error occurred.", "Contact SDK owners."); + } + } - internal void Update(double value, ReadOnlySpan> tags) + internal void Update(double value, ReadOnlySpan> tags) + { + try { this.updateDoubleCallback(value, tags); } + catch (Exception) + { + Interlocked.Increment(ref this.DroppedMeasurements); + OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, "SDK internal error occurred.", "Contact SDK owners."); + } + } - internal int Snapshot() + internal int Snapshot() + { + this.batchSize = 0; + if (this.OutputDeltaWithUnusedMetricPointReclaimEnabled) + { + this.SnapshotDeltaWithMetricPointReclaim(); + } + else if (this.OutputDelta) + { + var indexSnapshot = Math.Min(this.metricPointIndex, this.CardinalityLimit - 1); + this.SnapshotDelta(indexSnapshot); + } + else + { + var indexSnapshot = Math.Min(this.metricPointIndex, this.CardinalityLimit - 1); + this.SnapshotCumulative(indexSnapshot); + } + + this.EndTimeInclusive = DateTimeOffset.UtcNow; + return this.batchSize; + } + + internal void SnapshotDelta(int indexSnapshot) + { + for (int i = 0; i <= indexSnapshot; i++) { - this.batchSize = 0; - var indexSnapshot = Math.Min(this.metricPointIndex, this.maxMetricPoints - 1); - if (this.outputDelta) + ref var metricPoint = ref this.metricPoints[i]; + if (metricPoint.MetricPointStatus == MetricPointStatus.NoCollectPending) { - this.SnapshotDelta(indexSnapshot); + continue; + } + + if (this.IsExemplarEnabled()) + { + metricPoint.TakeSnapshotWithExemplar(outputDelta: true); } else { - this.SnapshotCumulative(indexSnapshot); + metricPoint.TakeSnapshot(outputDelta: true); } - this.EndTimeInclusive = DateTimeOffset.UtcNow; - return this.batchSize; + this.currentMetricPointBatch[this.batchSize] = i; + this.batchSize++; + } + + if (this.EndTimeInclusive != default) + { + this.StartTimeExclusive = this.EndTimeInclusive; } + } - internal void SnapshotDelta(int indexSnapshot) + internal void SnapshotDeltaWithMetricPointReclaim() + { + // Index = 0 is reserved for the case where no dimensions are provided. + ref var metricPointWithNoTags = ref this.metricPoints[0]; + if (metricPointWithNoTags.MetricPointStatus != MetricPointStatus.NoCollectPending) { - for (int i = 0; i <= indexSnapshot; i++) + if (this.IsExemplarEnabled()) { - ref var metricPoint = ref this.metricPoints[i]; - if (metricPoint.MetricPointStatus == MetricPointStatus.NoCollectPending) - { - continue; - } + metricPointWithNoTags.TakeSnapshotWithExemplar(outputDelta: true); + } + else + { + metricPointWithNoTags.TakeSnapshot(outputDelta: true); + } + + this.currentMetricPointBatch[this.batchSize] = 0; + this.batchSize++; + } + + int startIndexForReclaimableMetricPoints = 1; + if (this.EmitOverflowAttribute) + { + startIndexForReclaimableMetricPoints = 2; // Index 0 and 1 are reserved for no tags and overflow + + // TakeSnapshot for the MetricPoint for overflow + ref var metricPointForOverflow = ref this.metricPoints[1]; + if (metricPointForOverflow.MetricPointStatus != MetricPointStatus.NoCollectPending) + { if (this.IsExemplarEnabled()) { - metricPoint.TakeSnapshotWithExemplar(outputDelta: true); + metricPointForOverflow.TakeSnapshotWithExemplar(outputDelta: true); } else { - metricPoint.TakeSnapshot(outputDelta: true); + metricPointForOverflow.TakeSnapshot(outputDelta: true); } - this.currentMetricPointBatch[this.batchSize] = i; + this.currentMetricPointBatch[this.batchSize] = 1; this.batchSize++; } + } + + for (int i = startIndexForReclaimableMetricPoints; i < this.CardinalityLimit; i++) + { + ref var metricPoint = ref this.metricPoints[i]; + + if (metricPoint.MetricPointStatus == MetricPointStatus.NoCollectPending) + { + // If metricPoint.LookupData is `null` then the MetricPoint is already reclaimed and in the queue. + // If the Collect thread is successfully able to compare and swap the reference count from zero to int.MinValue, it means that + // the MetricPoint can be reused for other tags. + if (metricPoint.LookupData != null && Interlocked.CompareExchange(ref metricPoint.ReferenceCount, int.MinValue, 0) == 0) + { + var lookupData = metricPoint.LookupData; + + metricPoint.Reclaim(); + + Debug.Assert(this.TagsToMetricPointIndexDictionaryDelta != null, "this.tagsToMetricPointIndexDictionaryDelta was null"); + + lock (this.TagsToMetricPointIndexDictionaryDelta!) + { + LookupData? dictionaryValue; + if (lookupData.SortedTags != Tags.EmptyTags) + { + // Check if no other thread added a new entry for the same Tags. + // If no, then remove the existing entries. + if (this.TagsToMetricPointIndexDictionaryDelta.TryGetValue(lookupData.SortedTags, out dictionaryValue) && + dictionaryValue == lookupData) + { + this.TagsToMetricPointIndexDictionaryDelta.TryRemove(lookupData.SortedTags, out var _); + this.TagsToMetricPointIndexDictionaryDelta.TryRemove(lookupData.GivenTags, out var _); + } + } + else + { + if (this.TagsToMetricPointIndexDictionaryDelta.TryGetValue(lookupData.GivenTags, out dictionaryValue) && + dictionaryValue == lookupData) + { + this.TagsToMetricPointIndexDictionaryDelta.TryRemove(lookupData.GivenTags, out var _); + } + } + + Debug.Assert(this.availableMetricPoints != null, "this.availableMetricPoints was null"); + + this.availableMetricPoints!.Enqueue(i); + } + } + + continue; + } - if (this.EndTimeInclusive != default) + if (this.IsExemplarEnabled()) { - this.StartTimeExclusive = this.EndTimeInclusive; + metricPoint.TakeSnapshotWithExemplar(outputDelta: true); } + else + { + metricPoint.TakeSnapshot(outputDelta: true); + } + + this.currentMetricPointBatch[this.batchSize] = i; + this.batchSize++; + } + + if (this.EndTimeInclusive != default) + { + this.StartTimeExclusive = this.EndTimeInclusive; } + } - internal void SnapshotCumulative(int indexSnapshot) + internal void SnapshotCumulative(int indexSnapshot) + { + for (int i = 0; i <= indexSnapshot; i++) { - for (int i = 0; i <= indexSnapshot; i++) + ref var metricPoint = ref this.metricPoints[i]; + if (!metricPoint.IsInitialized) { - ref var metricPoint = ref this.metricPoints[i]; - if (!metricPoint.IsInitialized) - { - continue; - } + continue; + } - if (this.IsExemplarEnabled()) - { - metricPoint.TakeSnapshotWithExemplar(outputDelta: false); - } - else - { - metricPoint.TakeSnapshot(outputDelta: false); - } + if (this.IsExemplarEnabled()) + { + metricPoint.TakeSnapshotWithExemplar(outputDelta: false); + } + else + { + metricPoint.TakeSnapshot(outputDelta: false); + } - this.currentMetricPointBatch[this.batchSize] = i; - this.batchSize++; + this.currentMetricPointBatch[this.batchSize] = i; + this.batchSize++; + } + } + + internal MetricPointsAccessor GetMetricPoints() + => new(this.metricPoints, this.currentMetricPointBatch, this.batchSize); + + private static double[] FindDefaultHistogramBounds(in MetricStreamIdentity metricStreamIdentity) + { + if (metricStreamIdentity.Unit == "s") + { + if (Metric.DefaultHistogramBoundShortMappings + .Contains((metricStreamIdentity.MeterName, metricStreamIdentity.InstrumentName))) + { + return Metric.DefaultHistogramBoundsShortSeconds; + } + + if (Metric.DefaultHistogramBoundLongMappings + .Contains((metricStreamIdentity.MeterName, metricStreamIdentity.InstrumentName))) + { + return Metric.DefaultHistogramBoundsLongSeconds; } } - internal MetricPointsAccessor GetMetricPoints() - => new(this.metricPoints, this.currentMetricPointBatch, this.batchSize); + return Metric.DefaultHistogramBounds; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void InitializeZeroTagPointIfNotInitialized() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void InitializeZeroTagPointIfNotInitialized() + { + if (!this.zeroTagMetricPointInitialized) { - if (!this.zeroTagMetricPointInitialized) + lock (this.lockZeroTags) { - lock (this.lockZeroTags) + if (!this.zeroTagMetricPointInitialized) { - if (!this.zeroTagMetricPointInitialized) + if (this.OutputDelta) + { + var lookupData = new LookupData(0, Tags.EmptyTags, Tags.EmptyTags); + this.metricPoints[0] = new MetricPoint(this, this.aggType, null, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale, lookupData); + } + else { this.metricPoints[0] = new MetricPoint(this, this.aggType, null, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale); - this.zeroTagMetricPointInitialized = true; } + + this.zeroTagMetricPointInitialized = true; } } } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int LookupAggregatorStore(KeyValuePair[] tagKeysAndValues, int length) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void InitializeOverflowTagPointIfNotInitialized() + { + if (!this.overflowTagMetricPointInitialized) { - var givenTags = new Tags(tagKeysAndValues); - - if (!this.tagsToMetricPointIndexDictionary.TryGetValue(givenTags, out var aggregatorIndex)) + lock (this.lockOverflowTag) { - if (length > 1) + if (!this.overflowTagMetricPointInitialized) { - // Note: We are using storage from ThreadStatic, so need to make a deep copy for Dictionary storage. - // Create or obtain new arrays to temporarily hold the sorted tag Keys and Values - var storage = ThreadStaticStorage.GetStorage(); - storage.CloneKeysAndValues(tagKeysAndValues, length, out var tempSortedTagKeysAndValues); + var keyValuePairs = new KeyValuePair[] { new("otel.metric.overflow", true) }; + var tags = new Tags(keyValuePairs); - Array.Sort(tempSortedTagKeysAndValues, DimensionComparisonDelegate); + if (this.OutputDelta) + { + var lookupData = new LookupData(1, tags, tags); + this.metricPoints[1] = new MetricPoint(this, this.aggType, keyValuePairs, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale, lookupData); + } + else + { + this.metricPoints[1] = new MetricPoint(this, this.aggType, keyValuePairs, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale); + } - var sortedTags = new Tags(tempSortedTagKeysAndValues); + this.overflowTagMetricPointInitialized = true; + } + } + } + } - if (!this.tagsToMetricPointIndexDictionary.TryGetValue(sortedTags, out aggregatorIndex)) - { - aggregatorIndex = this.metricPointIndex; - if (aggregatorIndex >= this.maxMetricPoints) - { - // sorry! out of data points. - // TODO: Once we support cleanup of - // unused points (typically with delta) - // we can re-claim them here. - return -1; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int LookupAggregatorStore(KeyValuePair[] tagKeysAndValues, int length) + { + var givenTags = new Tags(tagKeysAndValues); - // Note: We are using storage from ThreadStatic (for upto MaxTagCacheSize tags) for both the input order of tags and the sorted order of tags, - // so we need to make a deep copy for Dictionary storage. - if (length <= ThreadStaticStorage.MaxTagCacheSize) - { - var givenTagKeysAndValues = new KeyValuePair[length]; - tagKeysAndValues.CopyTo(givenTagKeysAndValues.AsSpan()); + if (!this.tagsToMetricPointIndexDictionary.TryGetValue(givenTags, out var aggregatorIndex)) + { + if (length > 1) + { + // Note: We are using storage from ThreadStatic, so need to make a deep copy for Dictionary storage. + // Create or obtain new arrays to temporarily hold the sorted tag Keys and Values + var storage = ThreadStaticStorage.GetStorage(); + storage.CloneKeysAndValues(tagKeysAndValues, length, out var tempSortedTagKeysAndValues); - var sortedTagKeysAndValues = new KeyValuePair[length]; - tempSortedTagKeysAndValues.CopyTo(sortedTagKeysAndValues.AsSpan()); + Array.Sort(tempSortedTagKeysAndValues, DimensionComparisonDelegate); - givenTags = new Tags(givenTagKeysAndValues); - sortedTags = new Tags(sortedTagKeysAndValues); - } + var sortedTags = new Tags(tempSortedTagKeysAndValues); - lock (this.tagsToMetricPointIndexDictionary) - { - // check again after acquiring lock. - if (!this.tagsToMetricPointIndexDictionary.TryGetValue(sortedTags, out aggregatorIndex)) - { - aggregatorIndex = ++this.metricPointIndex; - if (aggregatorIndex >= this.maxMetricPoints) - { - // sorry! out of data points. - // TODO: Once we support cleanup of - // unused points (typically with delta) - // we can re-claim them here. - return -1; - } - - ref var metricPoint = ref this.metricPoints[aggregatorIndex]; - metricPoint = new MetricPoint(this, this.aggType, sortedTags.KeyValuePairs, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale); - - // Add to dictionary *after* initializing MetricPoint - // as other threads can start writing to the - // MetricPoint, if dictionary entry found. - - // Add the sorted order along with the given order of tags - this.tagsToMetricPointIndexDictionary.TryAdd(sortedTags, aggregatorIndex); - this.tagsToMetricPointIndexDictionary.TryAdd(givenTags, aggregatorIndex); - } - } - } - } - else + if (!this.tagsToMetricPointIndexDictionary.TryGetValue(sortedTags, out aggregatorIndex)) { - // This else block is for tag length = 1 aggregatorIndex = this.metricPointIndex; - if (aggregatorIndex >= this.maxMetricPoints) + if (aggregatorIndex >= this.CardinalityLimit) { // sorry! out of data points. // TODO: Once we support cleanup of @@ -283,20 +482,27 @@ private int LookupAggregatorStore(KeyValuePair[] tagKeysAndValue return -1; } - // Note: We are using storage from ThreadStatic, so need to make a deep copy for Dictionary storage. - var givenTagKeysAndValues = new KeyValuePair[length]; + // Note: We are using storage from ThreadStatic (for upto MaxTagCacheSize tags) for both the input order of tags and the sorted order of tags, + // so we need to make a deep copy for Dictionary storage. + if (length <= ThreadStaticStorage.MaxTagCacheSize) + { + var givenTagKeysAndValues = new KeyValuePair[length]; + tagKeysAndValues.CopyTo(givenTagKeysAndValues.AsSpan()); - tagKeysAndValues.CopyTo(givenTagKeysAndValues.AsSpan()); + var sortedTagKeysAndValues = new KeyValuePair[length]; + tempSortedTagKeysAndValues.CopyTo(sortedTagKeysAndValues.AsSpan()); - givenTags = new Tags(givenTagKeysAndValues); + givenTags = new Tags(givenTagKeysAndValues); + sortedTags = new Tags(sortedTagKeysAndValues); + } lock (this.tagsToMetricPointIndexDictionary) { // check again after acquiring lock. - if (!this.tagsToMetricPointIndexDictionary.TryGetValue(givenTags, out aggregatorIndex)) + if (!this.tagsToMetricPointIndexDictionary.TryGetValue(sortedTags, out aggregatorIndex)) { aggregatorIndex = ++this.metricPointIndex; - if (aggregatorIndex >= this.maxMetricPoints) + if (aggregatorIndex >= this.CardinalityLimit) { // sorry! out of data points. // TODO: Once we support cleanup of @@ -306,192 +512,599 @@ private int LookupAggregatorStore(KeyValuePair[] tagKeysAndValue } ref var metricPoint = ref this.metricPoints[aggregatorIndex]; - metricPoint = new MetricPoint(this, this.aggType, givenTags.KeyValuePairs, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale); + metricPoint = new MetricPoint(this, this.aggType, sortedTags.KeyValuePairs, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale); // Add to dictionary *after* initializing MetricPoint // as other threads can start writing to the // MetricPoint, if dictionary entry found. - // givenTags will always be sorted when tags length == 1 + // Add the sorted order along with the given order of tags + this.tagsToMetricPointIndexDictionary.TryAdd(sortedTags, aggregatorIndex); this.tagsToMetricPointIndexDictionary.TryAdd(givenTags, aggregatorIndex); } } } } + else + { + // This else block is for tag length = 1 + aggregatorIndex = this.metricPointIndex; + if (aggregatorIndex >= this.CardinalityLimit) + { + // sorry! out of data points. + // TODO: Once we support cleanup of + // unused points (typically with delta) + // we can re-claim them here. + return -1; + } + + // Note: We are using storage from ThreadStatic, so need to make a deep copy for Dictionary storage. + var givenTagKeysAndValues = new KeyValuePair[length]; + + tagKeysAndValues.CopyTo(givenTagKeysAndValues.AsSpan()); + + givenTags = new Tags(givenTagKeysAndValues); + + lock (this.tagsToMetricPointIndexDictionary) + { + // check again after acquiring lock. + if (!this.tagsToMetricPointIndexDictionary.TryGetValue(givenTags, out aggregatorIndex)) + { + aggregatorIndex = ++this.metricPointIndex; + if (aggregatorIndex >= this.CardinalityLimit) + { + // sorry! out of data points. + // TODO: Once we support cleanup of + // unused points (typically with delta) + // we can re-claim them here. + return -1; + } + + ref var metricPoint = ref this.metricPoints[aggregatorIndex]; + metricPoint = new MetricPoint(this, this.aggType, givenTags.KeyValuePairs, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale); + + // Add to dictionary *after* initializing MetricPoint + // as other threads can start writing to the + // MetricPoint, if dictionary entry found. - return aggregatorIndex; + // givenTags will always be sorted when tags length == 1 + this.tagsToMetricPointIndexDictionary.TryAdd(givenTags, aggregatorIndex); + } + } + } } - private void UpdateLong(long value, ReadOnlySpan> tags) + return aggregatorIndex; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int LookupAggregatorStoreForDeltaWithReclaim(KeyValuePair[] tagKeysAndValues, int length) + { + int index; + var givenTags = new Tags(tagKeysAndValues); + + Debug.Assert(this.TagsToMetricPointIndexDictionaryDelta != null, "this.tagsToMetricPointIndexDictionaryDelta was null"); + + bool newMetricPointCreated = false; + + if (!this.TagsToMetricPointIndexDictionaryDelta!.TryGetValue(givenTags, out var lookupData)) { - try + if (length > 1) { - var index = this.FindMetricAggregatorsDefault(tags); - if (index < 0) + // Note: We are using storage from ThreadStatic, so need to make a deep copy for Dictionary storage. + // Create or obtain new arrays to temporarily hold the sorted tag Keys and Values + var storage = ThreadStaticStorage.GetStorage(); + storage.CloneKeysAndValues(tagKeysAndValues, length, out var tempSortedTagKeysAndValues); + + Array.Sort(tempSortedTagKeysAndValues, DimensionComparisonDelegate); + + var sortedTags = new Tags(tempSortedTagKeysAndValues); + + if (!this.TagsToMetricPointIndexDictionaryDelta.TryGetValue(sortedTags, out lookupData)) { - if (Interlocked.CompareExchange(ref this.metricCapHitMessageLogged, 1, 0) == 0) + // Note: We are using storage from ThreadStatic (for up to MaxTagCacheSize tags) for both the input order of tags and the sorted order of tags, + // so we need to make a deep copy for Dictionary storage. + if (length <= ThreadStaticStorage.MaxTagCacheSize) { - OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, this.metricPointCapHitMessage, MetricPointCapHitFixMessage); + var givenTagKeysAndValues = new KeyValuePair[length]; + tagKeysAndValues.CopyTo(givenTagKeysAndValues.AsSpan()); + + var sortedTagKeysAndValues = new KeyValuePair[length]; + tempSortedTagKeysAndValues.CopyTo(sortedTagKeysAndValues.AsSpan()); + + givenTags = new Tags(givenTagKeysAndValues); + sortedTags = new Tags(sortedTagKeysAndValues); } - return; - } + Debug.Assert(this.availableMetricPoints != null, "this.availableMetricPoints was null"); - // TODO: can special case built-in filters to be bit faster. - if (this.IsExemplarEnabled()) - { - var shouldSample = this.exemplarFilter.ShouldSample(value, tags); - this.metricPoints[index].UpdateWithExemplar(value, tags: default, shouldSample); + lock (this.TagsToMetricPointIndexDictionaryDelta) + { + // check again after acquiring lock. + if (!this.TagsToMetricPointIndexDictionaryDelta.TryGetValue(sortedTags, out lookupData)) + { + // Check for an available MetricPoint + if (this.availableMetricPoints!.Count > 0) + { + index = this.availableMetricPoints.Dequeue(); + } + else + { + // No MetricPoint is available for reuse + return -1; + } + + lookupData = new LookupData(index, sortedTags, givenTags); + + ref var metricPoint = ref this.metricPoints[index]; + metricPoint = new MetricPoint(this, this.aggType, sortedTags.KeyValuePairs, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale, lookupData); + newMetricPointCreated = true; + + // Add to dictionary *after* initializing MetricPoint + // as other threads can start writing to the + // MetricPoint, if dictionary entry found. + + // Add the sorted order along with the given order of tags + this.TagsToMetricPointIndexDictionaryDelta.TryAdd(sortedTags, lookupData); + this.TagsToMetricPointIndexDictionaryDelta.TryAdd(givenTags, lookupData); + } + } } - else + } + else + { + // This else block is for tag length = 1 + + // Note: We are using storage from ThreadStatic, so need to make a deep copy for Dictionary storage. + var givenTagKeysAndValues = new KeyValuePair[length]; + + tagKeysAndValues.CopyTo(givenTagKeysAndValues.AsSpan()); + + givenTags = new Tags(givenTagKeysAndValues); + + Debug.Assert(this.availableMetricPoints != null, "this.availableMetricPoints was null"); + + lock (this.TagsToMetricPointIndexDictionaryDelta) { - this.metricPoints[index].Update(value); + // check again after acquiring lock. + if (!this.TagsToMetricPointIndexDictionaryDelta.TryGetValue(givenTags, out lookupData)) + { + // Check for an available MetricPoint + if (this.availableMetricPoints!.Count > 0) + { + index = this.availableMetricPoints.Dequeue(); + } + else + { + // No MetricPoint is available for reuse + return -1; + } + + lookupData = new LookupData(index, Tags.EmptyTags, givenTags); + + ref var metricPoint = ref this.metricPoints[index]; + metricPoint = new MetricPoint(this, this.aggType, givenTags.KeyValuePairs, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale, lookupData); + newMetricPointCreated = true; + + // Add to dictionary *after* initializing MetricPoint + // as other threads can start writing to the + // MetricPoint, if dictionary entry found. + + // givenTags will always be sorted when tags length == 1 + this.TagsToMetricPointIndexDictionaryDelta.TryAdd(givenTags, lookupData); + } } } - catch (Exception) + } + + // Found the MetricPoint + index = lookupData.Index; + + // If the running thread created a new MetricPoint, then the Snapshot method cannot reclaim that MetricPoint because MetricPoint is initialized with a ReferenceCount of 1. + // It can simply return the index. + + if (!newMetricPointCreated) + { + // If the running thread did not create the MetricPoint, it could be working on an index that has been reclaimed by Snapshot method. + // This could happen if the thread get switched out by CPU after it retrieves the index but the Snapshot method reclaims it before the thread wakes up again. + + ref var metricPointAtIndex = ref this.metricPoints[index]; + var referenceCount = Interlocked.Increment(ref metricPointAtIndex.ReferenceCount); + + if (referenceCount < 0) + { + // Rare case: Snapshot method had already marked the MetricPoint available for reuse as it has not been updated in last collect cycle. + + // Example scenario: + // Thread T1 wants to record a measurement for (k1,v1). + // Thread T1 creates a new MetricPoint at index 100 and adds an entry for (k1,v1) in the dictionary with the relevant LookupData value; ReferenceCount of the MetricPoint is 1 at this point. + // Thread T1 completes the update and decrements the ReferenceCount to 0. + // Later, another update thread (could be T1 as well) wants to record a measurement for (k1,v1) + // It looks up the dictionary and retrieves the index as 100. ReferenceCount for the MetricPoint is 0 at this point. + // This update thread gets switched out by the CPU. + // With the reclaim behavior, Snapshot method reclaims the index 100 as the MetricPoint for the index has NoCollectPending and has a ReferenceCount of 0. + // Snapshot thread sets the ReferenceCount to int.MinValue. + // The update thread wakes up and increments the ReferenceCount but finds the value to be negative. + + // Retry attempt to get a MetricPoint. + index = this.RemoveStaleEntriesAndGetAvailableMetricPointRare(lookupData, length); + } + else if (metricPointAtIndex.LookupData != lookupData) { - OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, "SDK internal error occurred.", "Contact SDK owners."); + // Rare case: Another thread with different input tags could have reclaimed this MetricPoint if it was freed up by Snapshot method. + + // Example scenario: + // Thread T1 wants to record a measurement for (k1,v1). + // Thread T1 creates a new MetricPoint at index 100 and adds an entry for (k1,v1) in the dictionary with the relevant LookupData value; ReferenceCount of the MetricPoint is 1 at this point. + // Thread T1 completes the update and decrements the ReferenceCount to 0. + // Later, another update thread T2 (could be T1 as well) wants to record a measurement for (k1,v1) + // It looks up the dictionary and retrieves the index as 100. ReferenceCount for the MetricPoint is 0 at this point. + // This update thread T2 gets switched out by the CPU. + // With the reclaim behavior, Snapshot method reclaims the index 100 as the MetricPoint for the index has NoCollectPending and has a ReferenceCount of 0. + // Snapshot thread sets the ReferenceCount to int.MinValue. + // An update thread T3 wants to record a measurement for (k2,v2). + // Thread T3 looks for an available index from the queue and finds index 100. + // Thread T3 creates a new MetricPoint at index 100 and adds an entry for (k2,v2) in the dictionary with the LookupData value for (k2,v2). ReferenceCount of the MetricPoint is 1 at this point. + // The update thread T2 wakes up and increments the ReferenceCount and finds the value to be positive but the LookupData value does not match the one for (k1,v1). + + // Remove reference since its not the right MetricPoint. + Interlocked.Decrement(ref metricPointAtIndex.ReferenceCount); + + // Retry attempt to get a MetricPoint. + index = this.RemoveStaleEntriesAndGetAvailableMetricPointRare(lookupData, length); } } - private void UpdateLongCustomTags(long value, ReadOnlySpan> tags) + return index; + } + + // This method is always called under `lock(this.tagsToMetricPointIndexDictionaryDelta)` so it's safe with other code that adds or removes + // entries from `this.tagsToMetricPointIndexDictionaryDelta` + private bool TryGetAvailableMetricPointRare( + Tags givenTags, + Tags sortedTags, + int length, + [NotNullWhen(true)] + out LookupData? lookupData, + out bool newMetricPointCreated) + { + Debug.Assert(this.TagsToMetricPointIndexDictionaryDelta != null, "this.tagsToMetricPointIndexDictionaryDelta was null"); + Debug.Assert(this.availableMetricPoints != null, "this.availableMetricPoints was null"); + + int index; + newMetricPointCreated = false; + + if (length > 1) { - try + // check again after acquiring lock. + if (!this.TagsToMetricPointIndexDictionaryDelta!.TryGetValue(givenTags, out lookupData) && + !this.TagsToMetricPointIndexDictionaryDelta.TryGetValue(sortedTags, out lookupData)) { - var index = this.FindMetricAggregatorsCustomTag(tags); - if (index < 0) + // Check for an available MetricPoint + if (this.availableMetricPoints!.Count > 0) { - if (Interlocked.CompareExchange(ref this.metricCapHitMessageLogged, 1, 0) == 0) - { - OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, this.metricPointCapHitMessage, MetricPointCapHitFixMessage); - } - - return; + index = this.availableMetricPoints.Dequeue(); + } + else + { + // No MetricPoint is available for reuse + return false; } - // TODO: can special case built-in filters to be bit faster. - if (this.IsExemplarEnabled()) + lookupData = new LookupData(index, sortedTags, givenTags); + + ref var metricPoint = ref this.metricPoints[index]; + metricPoint = new MetricPoint(this, this.aggType, sortedTags.KeyValuePairs, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale, lookupData); + newMetricPointCreated = true; + + // Add to dictionary *after* initializing MetricPoint + // as other threads can start writing to the + // MetricPoint, if dictionary entry found. + + // Add the sorted order along with the given order of tags + this.TagsToMetricPointIndexDictionaryDelta.TryAdd(sortedTags, lookupData); + this.TagsToMetricPointIndexDictionaryDelta.TryAdd(givenTags, lookupData); + } + } + else + { + // check again after acquiring lock. + if (!this.TagsToMetricPointIndexDictionaryDelta!.TryGetValue(givenTags, out lookupData)) + { + // Check for an available MetricPoint + if (this.availableMetricPoints!.Count > 0) { - var shouldSample = this.exemplarFilter.ShouldSample(value, tags); - this.metricPoints[index].UpdateWithExemplar(value, tags: tags, shouldSample); + index = this.availableMetricPoints.Dequeue(); } else { - this.metricPoints[index].Update(value); + // No MetricPoint is available for reuse + return false; } - } - catch (Exception) - { - OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, "SDK internal error occurred.", "Contact SDK owners."); + + lookupData = new LookupData(index, Tags.EmptyTags, givenTags); + + ref var metricPoint = ref this.metricPoints[index]; + metricPoint = new MetricPoint(this, this.aggType, givenTags.KeyValuePairs, this.histogramBounds, this.exponentialHistogramMaxSize, this.exponentialHistogramMaxScale, lookupData); + newMetricPointCreated = true; + + // Add to dictionary *after* initializing MetricPoint + // as other threads can start writing to the + // MetricPoint, if dictionary entry found. + + // givenTags will always be sorted when tags length == 1 + this.TagsToMetricPointIndexDictionaryDelta.TryAdd(givenTags, lookupData); } } - private void UpdateDouble(double value, ReadOnlySpan> tags) + return true; + } + + // This method is essentially a retry attempt for when `LookupAggregatorStoreForDeltaWithReclaim` cannot find a MetricPoint. + // If we still fail to get a MetricPoint in this method, we don't retry any further and simply drop the measurement. + // This method acquires `lock (this.tagsToMetricPointIndexDictionaryDelta)` + private int RemoveStaleEntriesAndGetAvailableMetricPointRare(LookupData lookupData, int length) + { + bool foundMetricPoint = false; + bool newMetricPointCreated = false; + var sortedTags = lookupData.SortedTags; + var inputTags = lookupData.GivenTags; + + // Acquire lock + // Try to remove stale entries from dictionary + // Get the index for a new MetricPoint (it could be self-claimed or from another thread that added a fresh entry) + // If self-claimed, then add a fresh entry to the dictionary + // If an available MetricPoint is found, then only increment the ReferenceCount + + Debug.Assert(this.TagsToMetricPointIndexDictionaryDelta != null, "this.tagsToMetricPointIndexDictionaryDelta was null"); + + // Delete the entry for these Tags and get another MetricPoint. + lock (this.TagsToMetricPointIndexDictionaryDelta!) { - try + LookupData? dictionaryValue; + if (lookupData.SortedTags != Tags.EmptyTags) { - var index = this.FindMetricAggregatorsDefault(tags); - if (index < 0) + // Check if no other thread added a new entry for the same Tags in the meantime. + // If no, then remove the existing entries. + if (this.TagsToMetricPointIndexDictionaryDelta.TryGetValue(lookupData.SortedTags, out dictionaryValue)) { - if (Interlocked.CompareExchange(ref this.metricCapHitMessageLogged, 1, 0) == 0) + if (dictionaryValue == lookupData) { - OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, this.metricPointCapHitMessage, MetricPointCapHitFixMessage); + // No other thread added a new entry for the same Tags. + this.TagsToMetricPointIndexDictionaryDelta.TryRemove(lookupData.SortedTags, out _); + this.TagsToMetricPointIndexDictionaryDelta.TryRemove(lookupData.GivenTags, out _); + } + else + { + // Some other thread added a new entry for these Tags. Use the new MetricPoint + lookupData = dictionaryValue; + foundMetricPoint = true; } - - return; - } - - // TODO: can special case built-in filters to be bit faster. - if (this.IsExemplarEnabled()) - { - var shouldSample = this.exemplarFilter.ShouldSample(value, tags); - this.metricPoints[index].UpdateWithExemplar(value, tags: default, shouldSample); } - else + } + else + { + if (this.TagsToMetricPointIndexDictionaryDelta.TryGetValue(lookupData.GivenTags, out dictionaryValue)) { - this.metricPoints[index].Update(value); + if (dictionaryValue == lookupData) + { + // No other thread added a new entry for the same Tags. + this.TagsToMetricPointIndexDictionaryDelta.TryRemove(lookupData.GivenTags, out _); + } + else + { + // Some other thread added a new entry for these Tags. Use the new MetricPoint + lookupData = dictionaryValue; + foundMetricPoint = true; + } } } - catch (Exception) + + if (!foundMetricPoint + && this.TryGetAvailableMetricPointRare(inputTags, sortedTags, length, out var tempLookupData, out newMetricPointCreated)) { - OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, "SDK internal error occurred.", "Contact SDK owners."); + foundMetricPoint = true; + lookupData = tempLookupData; } } - private void UpdateDoubleCustomTags(double value, ReadOnlySpan> tags) + if (foundMetricPoint) { - try + var index = lookupData.Index; + + // If the running thread created a new MetricPoint, then the Snapshot method cannot reclaim that MetricPoint because MetricPoint is initialized with a ReferenceCount of 1. + // It can simply return the index. + + if (!newMetricPointCreated) { - var index = this.FindMetricAggregatorsCustomTag(tags); - if (index < 0) - { - if (Interlocked.CompareExchange(ref this.metricCapHitMessageLogged, 1, 0) == 0) - { - OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, this.metricPointCapHitMessage, MetricPointCapHitFixMessage); - } + // If the running thread did not create the MetricPoint, it could be working on an index that has been reclaimed by Snapshot method. + // This could happen if the thread get switched out by CPU after it retrieves the index but the Snapshot method reclaims it before the thread wakes up again. - return; - } + ref var metricPointAtIndex = ref this.metricPoints[index]; + var referenceCount = Interlocked.Increment(ref metricPointAtIndex.ReferenceCount); - // TODO: can special case built-in filters to be bit faster. - if (this.IsExemplarEnabled()) + if (referenceCount < 0) { - var shouldSample = this.exemplarFilter.ShouldSample(value, tags); - this.metricPoints[index].UpdateWithExemplar(value, tags: tags, shouldSample); + // Super rare case: Snapshot method had already marked the MetricPoint available for reuse as it has not been updated in last collect cycle even in the retry attempt. + // Example scenario mentioned in `LookupAggregatorStoreForDeltaWithReclaim` method. + + // Don't retry again and drop the measurement. + return -1; } - else + else if (metricPointAtIndex.LookupData != lookupData) { - this.metricPoints[index].Update(value); + // Rare case: Another thread with different input tags could have reclaimed this MetricPoint if it was freed up by Snapshot method even in the retry attempt. + // Example scenario mentioned in `LookupAggregatorStoreForDeltaWithReclaim` method. + + // Remove reference since its not the right MetricPoint. + Interlocked.Decrement(ref metricPointAtIndex.ReferenceCount); + + // Don't retry again and drop the measurement. + return -1; } } - catch (Exception) - { - OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, "SDK internal error occurred.", "Contact SDK owners."); - } + + return index; + } + else + { + // No MetricPoint is available for reuse + return -1; } + } + + private void UpdateLong(long value, ReadOnlySpan> tags) + { + var index = this.FindMetricAggregatorsDefault(tags); - private int FindMetricAggregatorsDefault(ReadOnlySpan> tags) + this.UpdateLongMetricPoint(index, value, tags); + } + + private void UpdateLongCustomTags(long value, ReadOnlySpan> tags) + { + var index = this.FindMetricAggregatorsCustomTag(tags); + + this.UpdateLongMetricPoint(index, value, tags); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void UpdateLongMetricPoint(int metricPointIndex, long value, ReadOnlySpan> tags) + { + if (metricPointIndex < 0) { - int tagLength = tags.Length; - if (tagLength == 0) + Interlocked.Increment(ref this.DroppedMeasurements); + + if (this.EmitOverflowAttribute) { - this.InitializeZeroTagPointIfNotInitialized(); - return 0; + this.InitializeOverflowTagPointIfNotInitialized(); + this.metricPoints[1].Update(value); + } + else if (Interlocked.CompareExchange(ref this.metricCapHitMessageLogged, 1, 0) == 0) + { + OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, this.metricPointCapHitMessage, MetricPointCapHitFixMessage); } - var storage = ThreadStaticStorage.GetStorage(); - - storage.SplitToKeysAndValues(tags, tagLength, out var tagKeysAndValues); + return; + } - return this.LookupAggregatorStore(tagKeysAndValues, tagLength); + var exemplarFilterType = this.exemplarFilter; + if (exemplarFilterType == ExemplarFilterType.AlwaysOff) + { + this.metricPoints[metricPointIndex].Update(value); + } + else if (exemplarFilterType == ExemplarFilterType.AlwaysOn) + { + this.metricPoints[metricPointIndex].UpdateWithExemplar( + value, + tags, + isSampled: true); + } + else + { + this.metricPoints[metricPointIndex].UpdateWithExemplar( + value, + tags, + isSampled: Activity.Current?.Recorded ?? false); } + } + + private void UpdateDouble(double value, ReadOnlySpan> tags) + { + var index = this.FindMetricAggregatorsDefault(tags); - private int FindMetricAggregatorsCustomTag(ReadOnlySpan> tags) + this.UpdateDoubleMetricPoint(index, value, tags); + } + + private void UpdateDoubleCustomTags(double value, ReadOnlySpan> tags) + { + var index = this.FindMetricAggregatorsCustomTag(tags); + + this.UpdateDoubleMetricPoint(index, value, tags); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void UpdateDoubleMetricPoint(int metricPointIndex, double value, ReadOnlySpan> tags) + { + if (metricPointIndex < 0) { - int tagLength = tags.Length; - if (tagLength == 0 || this.tagsKeysInterestingCount == 0) + Interlocked.Increment(ref this.DroppedMeasurements); + + if (this.EmitOverflowAttribute) { - this.InitializeZeroTagPointIfNotInitialized(); - return 0; + this.InitializeOverflowTagPointIfNotInitialized(); + this.metricPoints[1].Update(value); + } + else if (Interlocked.CompareExchange(ref this.metricCapHitMessageLogged, 1, 0) == 0) + { + OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, this.metricPointCapHitMessage, MetricPointCapHitFixMessage); } - // TODO: Get only interesting tags - // from the incoming tags + return; + } - var storage = ThreadStaticStorage.GetStorage(); + var exemplarFilterType = this.exemplarFilter; + if (exemplarFilterType == ExemplarFilterType.AlwaysOff) + { + this.metricPoints[metricPointIndex].Update(value); + } + else if (exemplarFilterType == ExemplarFilterType.AlwaysOn) + { + this.metricPoints[metricPointIndex].UpdateWithExemplar( + value, + tags, + isSampled: true); + } + else + { + this.metricPoints[metricPointIndex].UpdateWithExemplar( + value, + tags, + isSampled: Activity.Current?.Recorded ?? false); + } + } - storage.SplitToKeysAndValues(tags, tagLength, this.tagKeysInteresting, out var tagKeysAndValues, out var actualLength); + private int FindMetricAggregatorsDefault(ReadOnlySpan> tags) + { + int tagLength = tags.Length; + if (tagLength == 0) + { + this.InitializeZeroTagPointIfNotInitialized(); + return 0; + } - // Actual number of tags depend on how many - // of the incoming tags has user opted to - // select. - if (actualLength == 0) - { - this.InitializeZeroTagPointIfNotInitialized(); - return 0; - } + var storage = ThreadStaticStorage.GetStorage(); + + storage.SplitToKeysAndValues(tags, tagLength, out var tagKeysAndValues); + + return this.lookupAggregatorStore(tagKeysAndValues, tagLength); + } - return this.LookupAggregatorStore(tagKeysAndValues, actualLength); + private int FindMetricAggregatorsCustomTag(ReadOnlySpan> tags) + { + int tagLength = tags.Length; + if (tagLength == 0 || this.tagsKeysInterestingCount == 0) + { + this.InitializeZeroTagPointIfNotInitialized(); + return 0; } + + var storage = ThreadStaticStorage.GetStorage(); + + Debug.Assert(this.TagKeysInteresting != null, "this.tagKeysInteresting was null"); + + storage.SplitToKeysAndValues(tags, tagLength, this.TagKeysInteresting!, out var tagKeysAndValues, out var actualLength); + + // Actual number of tags depend on how many + // of the incoming tags has user opted to + // select. + if (actualLength == 0) + { + this.InitializeZeroTagPointIfNotInitialized(); + return 0; + } + + Debug.Assert(tagKeysAndValues != null, "tagKeysAndValues was null"); + + return this.lookupAggregatorStore(tagKeysAndValues!, actualLength); } } diff --git a/src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogram.cs b/src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogram.cs index e47fce6f260..1d63d898443 100644 --- a/src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogram.cs +++ b/src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogram.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Internal; @@ -36,9 +23,7 @@ internal sealed partial class Base2ExponentialBucketHistogram internal double RunningMax = double.NegativeInfinity; internal double SnapshotMax; - internal int IsCriticalSectionOccupied = 0; - - internal ExponentialHistogramData SnapshotExponentialHistogramData = new ExponentialHistogramData(); + internal ExponentialHistogramData SnapshotExponentialHistogramData = new(); private int scale; private double scalingFactor; // 2 ^ scale / log(2) @@ -237,11 +222,13 @@ internal ExponentialHistogramData GetExponentialHistogramData() internal Base2ExponentialBucketHistogram Copy() { Debug.Assert(this.PositiveBuckets.Capacity == this.NegativeBuckets.Capacity, "Capacity of positive and negative buckets are not equal."); - var copy = new Base2ExponentialBucketHistogram(this.PositiveBuckets.Capacity, this.SnapshotExponentialHistogramData.Scale); - copy.SnapshotSum = this.SnapshotSum; - copy.SnapshotMin = this.SnapshotMin; - copy.SnapshotMax = this.SnapshotMax; - copy.SnapshotExponentialHistogramData = this.SnapshotExponentialHistogramData.Copy(); - return copy; + + return new Base2ExponentialBucketHistogram(this.PositiveBuckets.Capacity, this.SnapshotExponentialHistogramData.Scale) + { + SnapshotSum = this.SnapshotSum, + SnapshotMin = this.SnapshotMin, + SnapshotMax = this.SnapshotMax, + SnapshotExponentialHistogramData = this.SnapshotExponentialHistogramData.Copy(), + }; } } diff --git a/src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogramConfiguration.cs b/src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogramConfiguration.cs index 1394d3a8715..d029e64fe4a 100644 --- a/src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogramConfiguration.cs +++ b/src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogramConfiguration.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 namespace OpenTelemetry.Metrics; diff --git a/src/OpenTelemetry/Metrics/BaseExportingMetricReader.cs b/src/OpenTelemetry/Metrics/BaseExportingMetricReader.cs index 72582295ef8..80f96cc143a 100644 --- a/src/OpenTelemetry/Metrics/BaseExportingMetricReader.cs +++ b/src/OpenTelemetry/Metrics/BaseExportingMetricReader.cs @@ -1,178 +1,171 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Internal; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// MetricReader implementation which exports metrics to the configured +/// MetricExporter upon . +/// +public class BaseExportingMetricReader : MetricReader { /// - /// MetricReader implementation which exports metrics to the configured - /// MetricExporter upon . + /// Gets the exporter used by the metric reader. + /// + protected readonly BaseExporter exporter; + + private readonly ExportModes supportedExportModes = ExportModes.Push | ExportModes.Pull; + private readonly string exportCalledMessage; + private readonly string exportSucceededMessage; + private readonly string exportFailedMessage; + private bool disposed; + + /// + /// Initializes a new instance of the class. /// - public class BaseExportingMetricReader : MetricReader + /// Exporter instance to export Metrics to. + public BaseExportingMetricReader(BaseExporter exporter) { - protected readonly BaseExporter exporter; - private readonly ExportModes supportedExportModes = ExportModes.Push | ExportModes.Pull; - private readonly string exportCalledMessage; - private readonly string exportSucceededMessage; - private readonly string exportFailedMessage; - private bool disposed; - - /// - /// Initializes a new instance of the class. - /// - /// Exporter instance to export Metrics to. - public BaseExportingMetricReader(BaseExporter exporter) - { - Guard.ThrowIfNull(exporter); + Guard.ThrowIfNull(exporter); + + this.exporter = exporter; - this.exporter = exporter; + var exporterType = exporter.GetType(); + var attributes = exporterType.GetCustomAttributes(typeof(ExportModesAttribute), true); + if (attributes.Length > 0) + { + var attr = (ExportModesAttribute)attributes[attributes.Length - 1]; + this.supportedExportModes = attr.Supported; + } - var exporterType = exporter.GetType(); - var attributes = exporterType.GetCustomAttributes(typeof(ExportModesAttribute), true); - if (attributes.Length > 0) + if (exporter is IPullMetricExporter pullExporter) + { + if (this.supportedExportModes.HasFlag(ExportModes.Push)) { - var attr = (ExportModesAttribute)attributes[attributes.Length - 1]; - this.supportedExportModes = attr.Supported; + pullExporter.Collect = this.Collect; } - - if (exporter is IPullMetricExporter pullExporter) + else { - if (this.supportedExportModes.HasFlag(ExportModes.Push)) + pullExporter.Collect = (timeoutMilliseconds) => { - pullExporter.Collect = this.Collect; - } - else - { - pullExporter.Collect = (timeoutMilliseconds) => + using (PullMetricScope.Begin()) { - using (PullMetricScope.Begin()) - { - return this.Collect(timeoutMilliseconds); - } - }; - } + return this.Collect(timeoutMilliseconds); + } + }; } - - this.exportCalledMessage = $"{nameof(BaseExportingMetricReader)} calling {this.Exporter}.{nameof(this.Exporter.Export)} method."; - this.exportSucceededMessage = $"{this.Exporter}.{nameof(this.Exporter.Export)} succeeded."; - this.exportFailedMessage = $"{this.Exporter}.{nameof(this.Exporter.Export)} failed."; } - internal BaseExporter Exporter => this.exporter; + this.exportCalledMessage = $"{nameof(BaseExportingMetricReader)} calling {this.Exporter}.{nameof(this.Exporter.Export)} method."; + this.exportSucceededMessage = $"{this.Exporter}.{nameof(this.Exporter.Export)} succeeded."; + this.exportFailedMessage = $"{this.Exporter}.{nameof(this.Exporter.Export)} failed."; + } - protected ExportModes SupportedExportModes => this.supportedExportModes; + internal BaseExporter Exporter => this.exporter; - internal override void SetParentProvider(BaseProvider parentProvider) - { - base.SetParentProvider(parentProvider); - this.exporter.ParentProvider = parentProvider; - } + /// + /// Gets the supported . + /// + protected ExportModes SupportedExportModes => this.supportedExportModes; + + internal override void SetParentProvider(BaseProvider parentProvider) + { + base.SetParentProvider(parentProvider); + this.exporter.ParentProvider = parentProvider; + } - /// - internal override bool ProcessMetrics(in Batch metrics, int timeoutMilliseconds) + /// + internal override bool ProcessMetrics(in Batch metrics, int timeoutMilliseconds) + { + // TODO: Do we need to consider timeout here? + try { - // TODO: Do we need to consider timeout here? - try + OpenTelemetrySdkEventSource.Log.MetricReaderEvent(this.exportCalledMessage); + var result = this.exporter.Export(metrics); + if (result == ExportResult.Success) { - OpenTelemetrySdkEventSource.Log.MetricReaderEvent(this.exportCalledMessage); - var result = this.exporter.Export(metrics); - if (result == ExportResult.Success) - { - OpenTelemetrySdkEventSource.Log.MetricReaderEvent(this.exportSucceededMessage); - return true; - } - else - { - OpenTelemetrySdkEventSource.Log.MetricReaderEvent(this.exportFailedMessage); - return false; - } + OpenTelemetrySdkEventSource.Log.MetricReaderEvent(this.exportSucceededMessage); + return true; } - catch (Exception ex) + else { - OpenTelemetrySdkEventSource.Log.MetricReaderException(nameof(this.ProcessMetrics), ex); + OpenTelemetrySdkEventSource.Log.MetricReaderEvent(this.exportFailedMessage); return false; } } - - /// - protected override bool OnCollect(int timeoutMilliseconds) + catch (Exception ex) { - if (this.supportedExportModes.HasFlag(ExportModes.Push)) - { - return base.OnCollect(timeoutMilliseconds); - } - - if (this.supportedExportModes.HasFlag(ExportModes.Pull) && PullMetricScope.IsPullAllowed) - { - return base.OnCollect(timeoutMilliseconds); - } - - // TODO: add some error log + OpenTelemetrySdkEventSource.Log.MetricReaderException(nameof(this.ProcessMetrics), ex); return false; } + } - /// - protected override bool OnShutdown(int timeoutMilliseconds) + /// + protected override bool OnCollect(int timeoutMilliseconds) + { + if (this.supportedExportModes.HasFlag(ExportModes.Push)) { - var result = true; + return base.OnCollect(timeoutMilliseconds); + } - if (timeoutMilliseconds == Timeout.Infinite) - { - result = this.Collect(Timeout.Infinite) && result; - result = this.exporter.Shutdown(Timeout.Infinite) && result; - } - else - { - var sw = Stopwatch.StartNew(); - result = this.Collect(timeoutMilliseconds) && result; - var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; - result = this.exporter.Shutdown((int)Math.Max(timeout, 0)) && result; - } + if (this.supportedExportModes.HasFlag(ExportModes.Pull) && PullMetricScope.IsPullAllowed) + { + return base.OnCollect(timeoutMilliseconds); + } + + // TODO: add some error log + return false; + } + + /// + protected override bool OnShutdown(int timeoutMilliseconds) + { + var result = true; - return result; + if (timeoutMilliseconds == Timeout.Infinite) + { + result = this.Collect(Timeout.Infinite) && result; + result = this.exporter.Shutdown(Timeout.Infinite) && result; } + else + { + var sw = Stopwatch.StartNew(); + result = this.Collect(timeoutMilliseconds) && result; + var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; + result = this.exporter.Shutdown((int)Math.Max(timeout, 0)) && result; + } + + return result; + } - /// - protected override void Dispose(bool disposing) + /// + protected override void Dispose(bool disposing) + { + if (!this.disposed) { - if (!this.disposed) + if (disposing) { - if (disposing) + try { - try + if (this.exporter is IPullMetricExporter pullExporter) { - if (this.exporter is IPullMetricExporter pullExporter) - { - pullExporter.Collect = null; - } - - this.exporter.Dispose(); - } - catch (Exception ex) - { - OpenTelemetrySdkEventSource.Log.MetricReaderException(nameof(this.Dispose), ex); + pullExporter.Collect = null; } - } - this.disposed = true; + this.exporter.Dispose(); + } + catch (Exception ex) + { + OpenTelemetrySdkEventSource.Log.MetricReaderException(nameof(this.Dispose), ex); + } } - base.Dispose(disposing); + this.disposed = true; } + + base.Dispose(disposing); } } diff --git a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderBase.cs b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderBase.cs index 4994eae7286..49b519b66c2 100644 --- a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderBase.cs +++ b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderBase.cs @@ -1,20 +1,5 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -30,6 +15,9 @@ public class MeterProviderBuilderBase : MeterProviderBuilder, IMeterProviderBuil private readonly bool allowBuild; private readonly MeterProviderServiceCollectionBuilder innerBuilder; + /// + /// Initializes a new instance of the class. + /// public MeterProviderBuilderBase() { var services = new ServiceCollection(); diff --git a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs index 21763741efe..6a36562aa45 100644 --- a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs @@ -1,21 +1,9 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif using System.Diagnostics.Metrics; using System.Text.RegularExpressions; using Microsoft.Extensions.DependencyInjection; @@ -23,321 +11,361 @@ using OpenTelemetry.Internal; using OpenTelemetry.Resources; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// Contains extension methods for the class. +/// +public static class MeterProviderBuilderExtensions { /// - /// Contains extension methods for the class. + /// Adds a reader to the provider. /// - public static class MeterProviderBuilderExtensions + /// . + /// . + /// The supplied for chaining. + public static MeterProviderBuilder AddReader(this MeterProviderBuilder meterProviderBuilder, MetricReader reader) { - /// - /// Adds a reader to the provider. - /// - /// . - /// . - /// The supplied for chaining. - public static MeterProviderBuilder AddReader(this MeterProviderBuilder meterProviderBuilder, MetricReader reader) - { - Guard.ThrowIfNull(reader); - - meterProviderBuilder.ConfigureBuilder((sp, builder) => - { - if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) - { - meterProviderBuilderSdk.AddReader(reader); - } - }); - - return meterProviderBuilder; - } + Guard.ThrowIfNull(reader); - /// - /// Adds a reader to the provider. - /// - /// - /// Note: The type specified by will be - /// registered as a singleton service into application services. - /// - /// Reader type. - /// . - /// The supplied for chaining. - public static MeterProviderBuilder AddReader(this MeterProviderBuilder meterProviderBuilder) - where T : MetricReader + meterProviderBuilder.ConfigureBuilder((sp, builder) => { - meterProviderBuilder.ConfigureServices(services => services.TryAddSingleton()); - - meterProviderBuilder.ConfigureBuilder((sp, builder) => + if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) { - if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) - { - meterProviderBuilderSdk.AddReader(sp.GetRequiredService()); - } - }); - - return meterProviderBuilder; - } - - /// - /// Adds a reader to the provider. - /// - /// . - /// The factory that creates the service. - /// The supplied for chaining. - public static MeterProviderBuilder AddReader( - this MeterProviderBuilder meterProviderBuilder, - Func implementationFactory) - { - Guard.ThrowIfNull(implementationFactory); + meterProviderBuilderSdk.AddReader(reader); + } + }); - meterProviderBuilder.ConfigureBuilder((sp, builder) => - { - if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) - { - meterProviderBuilderSdk.AddReader(implementationFactory(sp)); - } - }); + return meterProviderBuilder; + } - return meterProviderBuilder; - } + /// + /// Adds a reader to the provider. + /// + /// + /// Note: The type specified by will be + /// registered as a singleton service into application services. + /// + /// Reader type. + /// . + /// The supplied for chaining. + public static MeterProviderBuilder AddReader< +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + T>(this MeterProviderBuilder meterProviderBuilder) + where T : MetricReader + { + meterProviderBuilder.ConfigureServices(services => services.TryAddSingleton()); - /// - /// Add metric view, which can be used to customize the Metrics outputted - /// from the SDK. The views are applied in the order they are added. - /// - /// See View specification here : https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view. - /// . - /// Name of the instrument, to be used as part of Instrument selection criteria. - /// Name of the view. This will be used as name of resulting metrics stream. - /// The supplied for chaining. - public static MeterProviderBuilder AddView(this MeterProviderBuilder meterProviderBuilder, string instrumentName, string name) + meterProviderBuilder.ConfigureBuilder((sp, builder) => { - if (!MeterProviderBuilderSdk.IsValidInstrumentName(name)) + if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) { - throw new ArgumentException($"Custom view name {name} is invalid.", nameof(name)); + meterProviderBuilderSdk.AddReader(sp.GetRequiredService()); } + }); + + return meterProviderBuilder; + } - if (instrumentName.IndexOf('*') != -1) + /// + /// Adds a reader to the provider. + /// + /// . + /// The factory that creates the service. + /// The supplied for chaining. + public static MeterProviderBuilder AddReader( + this MeterProviderBuilder meterProviderBuilder, + Func implementationFactory) + { + Guard.ThrowIfNull(implementationFactory); + + meterProviderBuilder.ConfigureBuilder((sp, builder) => + { + if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) { - throw new ArgumentException( - $"Instrument selection criteria is invalid. Instrument name '{instrumentName}' " + - $"contains a wildcard character. This is not allowed when using a view to " + - $"rename a metric stream as it would lead to conflicting metric stream names.", - nameof(instrumentName)); + meterProviderBuilderSdk.AddReader(implementationFactory(sp)); } + }); - meterProviderBuilder.AddView(instrumentName, new MetricStreamConfiguration { Name = name }); + return meterProviderBuilder; + } - return meterProviderBuilder; + /// + /// Add metric view, which can be used to customize the Metrics outputted + /// from the SDK. The views are applied in the order they are added. + /// + /// See View specification here : https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view. + /// . + /// Name of the instrument, to be used as part of Instrument selection criteria. + /// Name of the view. This will be used as name of resulting metrics stream. + /// The supplied for chaining. + public static MeterProviderBuilder AddView(this MeterProviderBuilder meterProviderBuilder, string instrumentName, string name) + { + if (!MeterProviderBuilderSdk.IsValidInstrumentName(name)) + { + throw new ArgumentException($"Custom view name {name} is invalid.", nameof(name)); } - /// - /// Add metric view, which can be used to customize the Metrics outputted - /// from the SDK. The views are applied in the order they are added. - /// - /// See View specification here : https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view. - /// . - /// Name of the instrument, to be used as part of Instrument selection criteria. - /// Aggregation configuration used to produce metrics stream. - /// The supplied for chaining. - public static MeterProviderBuilder AddView(this MeterProviderBuilder meterProviderBuilder, string instrumentName, MetricStreamConfiguration metricStreamConfiguration) + if (instrumentName.IndexOf('*') != -1) { - Guard.ThrowIfNullOrWhitespace(instrumentName); - Guard.ThrowIfNull(metricStreamConfiguration); + throw new ArgumentException( + $"Instrument selection criteria is invalid. Instrument name '{instrumentName}' " + + $"contains a wildcard character. This is not allowed when using a view to " + + $"rename a metric stream as it would lead to conflicting metric stream names.", + nameof(instrumentName)); + } - if (metricStreamConfiguration.Name != null && instrumentName.IndexOf('*') != -1) - { - throw new ArgumentException( - $"Instrument selection criteria is invalid. Instrument name '{instrumentName}' " + - $"contains a wildcard character. This is not allowed when using a view to " + - $"rename a metric stream as it would lead to conflicting metric stream names.", - nameof(instrumentName)); - } + meterProviderBuilder.AddView(instrumentName, new MetricStreamConfiguration { Name = name }); - meterProviderBuilder.ConfigureBuilder((sp, builder) => - { - if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) - { - if (instrumentName.IndexOf('*') != -1) - { - var pattern = '^' + Regex.Escape(instrumentName).Replace("\\*", ".*"); - var regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase); - meterProviderBuilderSdk.AddView(instrument => regex.IsMatch(instrument.Name) ? metricStreamConfiguration : null); - } - else - { - meterProviderBuilderSdk.AddView(instrument => instrument.Name.Equals(instrumentName, StringComparison.OrdinalIgnoreCase) ? metricStreamConfiguration : null); - } - } - }); + return meterProviderBuilder; + } - return meterProviderBuilder; - } + /// + /// Add metric view, which can be used to customize the Metrics outputted + /// from the SDK. The views are applied in the order they are added. + /// + /// See View specification here : https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view. + /// . + /// Name of the instrument, to be used as part of Instrument selection criteria. + /// Aggregation configuration used to produce metrics stream. + /// The supplied for chaining. + public static MeterProviderBuilder AddView(this MeterProviderBuilder meterProviderBuilder, string instrumentName, MetricStreamConfiguration metricStreamConfiguration) + { + Guard.ThrowIfNullOrWhitespace(instrumentName); + Guard.ThrowIfNull(metricStreamConfiguration); - /// - /// Add metric view, which can be used to customize the Metrics outputted - /// from the SDK. The views are applied in the order they are added. - /// - /// - /// - /// Note: An invalid - /// returned from will cause the - /// view to be ignored, no error will be - /// thrown at runtime. - /// See View specification here : https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view. - /// - /// - /// . - /// Function to configure aggregation based on the instrument. - /// The supplied for chaining. - public static MeterProviderBuilder AddView(this MeterProviderBuilder meterProviderBuilder, Func viewConfig) + if (metricStreamConfiguration.Name != null && instrumentName.IndexOf('*') != -1) { - Guard.ThrowIfNull(viewConfig); + throw new ArgumentException( + $"Instrument selection criteria is invalid. Instrument name '{instrumentName}' " + + $"contains a wildcard character. This is not allowed when using a view to " + + $"rename a metric stream as it would lead to conflicting metric stream names.", + nameof(instrumentName)); + } - meterProviderBuilder.ConfigureBuilder((sp, builder) => + meterProviderBuilder.ConfigureBuilder((sp, builder) => + { + if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) { - if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) + if (instrumentName.IndexOf('*') != -1) + { + var pattern = '^' + Regex.Escape(instrumentName).Replace("\\*", ".*"); + var regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase); + meterProviderBuilderSdk.AddView(instrument => regex.IsMatch(instrument.Name) ? metricStreamConfiguration : null); + } + else { - meterProviderBuilderSdk.AddView(viewConfig); + meterProviderBuilderSdk.AddView(instrument => instrument.Name.Equals(instrumentName, StringComparison.OrdinalIgnoreCase) ? metricStreamConfiguration : null); } - }); + } + }); - return meterProviderBuilder; - } + return meterProviderBuilder; + } - /// - /// Sets the maximum number of Metric streams supported by the MeterProvider. - /// When no Views are configured, every instrument will result in one metric stream, - /// so this control the numbers of instruments supported. - /// When Views are configured, a single instrument can result in multiple metric streams, - /// so this control the number of streams. - /// - /// - /// If an instrument is created, but disposed later, this will still be contributing to the limit. - /// This may change in the future. - /// - /// . - /// Maximum number of metric streams allowed. - /// The supplied for chaining. - public static MeterProviderBuilder SetMaxMetricStreams(this MeterProviderBuilder meterProviderBuilder, int maxMetricStreams) - { - Guard.ThrowIfOutOfRange(maxMetricStreams, min: 1); + /// + /// Add metric view, which can be used to customize the Metrics outputted + /// from the SDK. The views are applied in the order they are added. + /// + /// + /// + /// Note: An invalid + /// returned from will cause the + /// view to be ignored, no error will be + /// thrown at runtime. + /// See View specification here : https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view. + /// + /// + /// . + /// Function to configure aggregation based on the instrument. + /// The supplied for chaining. + public static MeterProviderBuilder AddView(this MeterProviderBuilder meterProviderBuilder, Func viewConfig) + { + Guard.ThrowIfNull(viewConfig); - meterProviderBuilder.ConfigureBuilder((sp, builder) => + meterProviderBuilder.ConfigureBuilder((sp, builder) => + { + if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) { - if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) - { - meterProviderBuilderSdk.SetMaxMetricStreams(maxMetricStreams); - } - }); + meterProviderBuilderSdk.AddView(viewConfig); + } + }); - return meterProviderBuilder; - } + return meterProviderBuilder; + } - /// - /// Sets the maximum number of MetricPoints allowed per metric stream. - /// This limits the number of unique combinations of key/value pairs used - /// for reporting measurements. - /// - /// - /// If a particular key/value pair combination is used at least once, - /// it will contribute to the limit for the life of the process. - /// This may change in the future. See: https://github.com/open-telemetry/opentelemetry-dotnet/issues/2360. - /// - /// . - /// Maximum maximum number of metric points allowed per metric stream. - /// The supplied for chaining. - public static MeterProviderBuilder SetMaxMetricPointsPerMetricStream(this MeterProviderBuilder meterProviderBuilder, int maxMetricPointsPerMetricStream) - { - Guard.ThrowIfOutOfRange(maxMetricPointsPerMetricStream, min: 1); + /// + /// Sets the maximum number of Metric streams supported by the MeterProvider. + /// When no Views are configured, every instrument will result in one metric stream, + /// so this control the numbers of instruments supported. + /// When Views are configured, a single instrument can result in multiple metric streams, + /// so this control the number of streams. + /// + /// + /// If an instrument is created, but disposed later, this will still be contributing to the limit. + /// This may change in the future. + /// + /// . + /// Maximum number of metric streams allowed. + /// The supplied for chaining. + public static MeterProviderBuilder SetMaxMetricStreams(this MeterProviderBuilder meterProviderBuilder, int maxMetricStreams) + { + Guard.ThrowIfOutOfRange(maxMetricStreams, min: 1); - meterProviderBuilder.ConfigureBuilder((sp, builder) => + meterProviderBuilder.ConfigureBuilder((sp, builder) => + { + if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) { - if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) - { - meterProviderBuilderSdk.SetMaxMetricPointsPerMetricStream(maxMetricPointsPerMetricStream); - } - }); + meterProviderBuilderSdk.SetMetricLimit(maxMetricStreams); + } + }); - return meterProviderBuilder; - } + return meterProviderBuilder; + } - /// - /// Sets the from which the Resource associated with - /// this provider is built from. Overwrites currently set ResourceBuilder. - /// You should usually use instead - /// (call if desired). - /// - /// . - /// from which Resource will be built. - /// The supplied for chaining. - public static MeterProviderBuilder SetResourceBuilder(this MeterProviderBuilder meterProviderBuilder, ResourceBuilder resourceBuilder) + /// + /// Sets the maximum number of MetricPoints allowed per metric stream. + /// This limits the number of unique combinations of key/value pairs used + /// for reporting measurements. + /// + /// + /// If a particular key/value pair combination is used at least once, + /// it will contribute to the limit for the life of the process. + /// This may change in the future. See: https://github.com/open-telemetry/opentelemetry-dotnet/issues/2360. + /// + /// . + /// Maximum number of metric points allowed per metric stream. + /// The supplied for chaining. +#if EXPOSE_EXPERIMENTAL_FEATURES + [Obsolete("Use MetricStreamConfiguration.CardinalityLimit via the AddView API instead. This method will be removed in a future version.")] +#endif + public static MeterProviderBuilder SetMaxMetricPointsPerMetricStream(this MeterProviderBuilder meterProviderBuilder, int maxMetricPointsPerMetricStream) + { + Guard.ThrowIfOutOfRange(maxMetricPointsPerMetricStream, min: 1); + + meterProviderBuilder.ConfigureBuilder((sp, builder) => { - meterProviderBuilder.ConfigureBuilder((sp, builder) => + if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) { - if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) - { - meterProviderBuilderSdk.SetResourceBuilder(resourceBuilder); - } - }); + meterProviderBuilderSdk.SetDefaultCardinalityLimit(maxMetricPointsPerMetricStream); + } + }); - return meterProviderBuilder; - } + return meterProviderBuilder; + } - /// - /// Modify the from which the Resource associated with - /// this provider is built from in-place. - /// - /// . - /// An action which modifies the provided in-place. - /// The supplied for chaining. - public static MeterProviderBuilder ConfigureResource(this MeterProviderBuilder meterProviderBuilder, Action configure) + /// + /// Sets the from which the Resource associated with + /// this provider is built from. Overwrites currently set ResourceBuilder. + /// You should usually use instead + /// (call if desired). + /// + /// . + /// from which Resource will be built. + /// The supplied for chaining. + public static MeterProviderBuilder SetResourceBuilder(this MeterProviderBuilder meterProviderBuilder, ResourceBuilder resourceBuilder) + { + Guard.ThrowIfNull(resourceBuilder); + + meterProviderBuilder.ConfigureBuilder((sp, builder) => { - meterProviderBuilder.ConfigureBuilder((sp, builder) => + if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) { - if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) - { - meterProviderBuilderSdk.ConfigureResource(configure); - } - }); + meterProviderBuilderSdk.SetResourceBuilder(resourceBuilder); + } + }); - return meterProviderBuilder; - } + return meterProviderBuilder; + } - /// - /// Sets the to be used for this provider. - /// This is applied to all the metrics from this provider. - /// - /// . - /// ExemplarFilter to use. - /// The supplied for chaining. - public static MeterProviderBuilder SetExemplarFilter(this MeterProviderBuilder meterProviderBuilder, ExemplarFilter exemplarFilter) - { - Guard.ThrowIfNull(exemplarFilter); + /// + /// Modify the from which the Resource associated with + /// this provider is built from in-place. + /// + /// . + /// An action which modifies the provided in-place. + /// The supplied for chaining. + public static MeterProviderBuilder ConfigureResource(this MeterProviderBuilder meterProviderBuilder, Action configure) + { + Guard.ThrowIfNull(configure); - meterProviderBuilder.ConfigureBuilder((sp, builder) => + meterProviderBuilder.ConfigureBuilder((sp, builder) => + { + if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) { - if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) - { - meterProviderBuilderSdk.SetExemplarFilter(exemplarFilter); - } - }); + meterProviderBuilderSdk.ConfigureResource(configure); + } + }); - return meterProviderBuilder; + return meterProviderBuilder; + } + + /// + /// Run the given actions to initialize the . + /// + /// . + /// . + public static MeterProvider Build(this MeterProviderBuilder meterProviderBuilder) + { + if (meterProviderBuilder is MeterProviderBuilderBase meterProviderBuilderBase) + { + return meterProviderBuilderBase.InvokeBuild(); } - /// - /// Run the given actions to initialize the . - /// - /// . - /// . - public static MeterProvider? Build(this MeterProviderBuilder meterProviderBuilder) + throw new NotSupportedException($"Build is not supported on '{meterProviderBuilder?.GetType().FullName ?? "null"}' instances."); + } + +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Sets the to be used for this provider + /// which controls how measurements will be offered to exemplar reservoirs. + /// Default provider configuration: . + /// + /// + /// + /// Note: Use or to enable exemplars. + /// Specification: . + /// + /// . + /// to + /// use. + /// The supplied for + /// chaining. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + internal +#endif + static MeterProviderBuilder SetExemplarFilter( + this MeterProviderBuilder meterProviderBuilder, + ExemplarFilterType exemplarFilter = ExemplarFilterType.TraceBased) + { + meterProviderBuilder.ConfigureBuilder((sp, builder) => { - if (meterProviderBuilder is MeterProviderBuilderBase meterProviderBuilderBase) + if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) { - return meterProviderBuilderBase.InvokeBuild(); + switch (exemplarFilter) + { + case ExemplarFilterType.AlwaysOn: + case ExemplarFilterType.AlwaysOff: + case ExemplarFilterType.TraceBased: + meterProviderBuilderSdk.SetExemplarFilter(exemplarFilter); + break; + default: + throw new NotSupportedException($"SdkExemplarFilter '{exemplarFilter}' is not supported."); + } } + }); - return null; - } + return meterProviderBuilder; } } diff --git a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderSdk.cs b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderSdk.cs index da4571184ae..11084ec7f0d 100644 --- a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderSdk.cs +++ b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderSdk.cs @@ -1,20 +1,5 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Diagnostics.Metrics; @@ -23,229 +8,224 @@ using OpenTelemetry.Internal; using OpenTelemetry.Resources; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// Stores state used to build a . +/// +internal sealed class MeterProviderBuilderSdk : MeterProviderBuilder, IMeterProviderBuilder { - /// - /// Stores state used to build a . - /// - internal sealed class MeterProviderBuilderSdk : MeterProviderBuilder, IMeterProviderBuilder - { - public const int MaxMetricsDefault = 1000; - public const int MaxMetricPointsPerMetricDefault = 2000; - private const string DefaultInstrumentationVersion = "1.0.0.0"; + public const int DefaultMetricLimit = 1000; + public const int DefaultCardinalityLimit = 2000; + private const string DefaultInstrumentationVersion = "1.0.0.0"; - private readonly IServiceProvider serviceProvider; - private MeterProviderSdk? meterProvider; + private readonly IServiceProvider serviceProvider; + private MeterProviderSdk? meterProvider; - public MeterProviderBuilderSdk(IServiceProvider serviceProvider) - { - this.serviceProvider = serviceProvider; - } + public MeterProviderBuilderSdk(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } - // Note: We don't use static readonly here because some customers - // replace this using reflection which is not allowed on initonly static - // fields. See: https://github.com/dotnet/runtime/issues/11571. - // Customers: This is not guaranteed to work forever. We may change this - // mechanism in the future do this at your own risk. - public static Regex InstrumentNameRegex { get; set; } = new( - @"^[a-z][a-z0-9-._]{0,62}$", RegexOptions.IgnoreCase | RegexOptions.Compiled); + // Note: We don't use static readonly here because some customers + // replace this using reflection which is not allowed on initonly static + // fields. See: https://github.com/dotnet/runtime/issues/11571. + // Customers: This is not guaranteed to work forever. We may change this + // mechanism in the future do this at your own risk. + public static Regex InstrumentNameRegex { get; set; } = new( + @"^[a-z][a-z0-9-._/]{0,254}$", RegexOptions.IgnoreCase | RegexOptions.Compiled); - public List Instrumentation { get; } = new(); + public List Instrumentation { get; } = new(); - public ResourceBuilder? ResourceBuilder { get; private set; } + public ResourceBuilder? ResourceBuilder { get; private set; } - public ExemplarFilter? ExemplarFilter { get; private set; } + public ExemplarFilterType? ExemplarFilter { get; private set; } - public MeterProvider? Provider => this.meterProvider; + public MeterProvider? Provider => this.meterProvider; - public List Readers { get; } = new(); + public List Readers { get; } = new(); - public List MeterSources { get; } = new(); + public List MeterSources { get; } = new(); - public List> ViewConfigs { get; } = new(); + public List> ViewConfigs { get; } = new(); - public int MaxMetricStreams { get; private set; } = MaxMetricsDefault; + public int MetricLimit { get; private set; } = DefaultMetricLimit; - public int MaxMetricPointsPerMetricStream { get; private set; } = MaxMetricPointsPerMetricDefault; + public int CardinalityLimit { get; private set; } = DefaultCardinalityLimit; - /// - /// Returns whether the given instrument name is valid according to the specification. - /// - /// See specification: . - /// The instrument name. - /// Boolean indicating if the instrument is valid. - public static bool IsValidInstrumentName(string instrumentName) + /// + /// Returns whether the given instrument name is valid according to the specification. + /// + /// See specification: . + /// The instrument name. + /// Boolean indicating if the instrument is valid. + public static bool IsValidInstrumentName(string instrumentName) + { + if (string.IsNullOrWhiteSpace(instrumentName)) { - if (string.IsNullOrWhiteSpace(instrumentName)) - { - return false; - } - - return InstrumentNameRegex.IsMatch(instrumentName); + return false; } - /// - /// Returns whether the given custom view name is valid according to the specification. - /// - /// See specification: . - /// The view name. - /// Boolean indicating if the instrument is valid. - public static bool IsValidViewName(string customViewName) - { - // Only validate the view name in case it's not null. In case it's null, the view name will be the instrument name as per the spec. - if (customViewName == null) - { - return true; - } + return InstrumentNameRegex.IsMatch(instrumentName); + } - return InstrumentNameRegex.IsMatch(customViewName); + /// + /// Returns whether the given custom view name is valid according to the specification. + /// + /// See specification: . + /// The view name. + /// Boolean indicating if the instrument is valid. + public static bool IsValidViewName(string customViewName) + { + // Only validate the view name in case it's not null. In case it's null, the view name will be the instrument name as per the spec. + if (customViewName == null) + { + return true; } - public void RegisterProvider(MeterProviderSdk meterProvider) - { - Debug.Assert(meterProvider != null, "meterProvider was null"); + return InstrumentNameRegex.IsMatch(customViewName); + } - if (this.meterProvider != null) - { - throw new NotSupportedException("MeterProvider cannot be accessed while build is executing."); - } + public void RegisterProvider(MeterProviderSdk meterProvider) + { + Debug.Assert(meterProvider != null, "meterProvider was null"); - this.meterProvider = meterProvider; + if (this.meterProvider != null) + { + throw new NotSupportedException("MeterProvider cannot be accessed while build is executing."); } - public override MeterProviderBuilder AddInstrumentation( - Func instrumentationFactory) - { - Debug.Assert(instrumentationFactory != null, "instrumentationFactory was null"); + this.meterProvider = meterProvider; + } - return this.AddInstrumentation( - typeof(TInstrumentation).Name, - typeof(TInstrumentation).Assembly.GetName().Version?.ToString() ?? DefaultInstrumentationVersion, - instrumentationFactory!()); - } + public override MeterProviderBuilder AddInstrumentation(Func instrumentationFactory) + { + Debug.Assert(instrumentationFactory != null, "instrumentationFactory was null"); - public MeterProviderBuilder AddInstrumentation( - string instrumentationName, - string instrumentationVersion, - object instrumentation) - { - Debug.Assert(!string.IsNullOrWhiteSpace(instrumentationName), "instrumentationName was null or whitespace"); - Debug.Assert(!string.IsNullOrWhiteSpace(instrumentationVersion), "instrumentationVersion was null or whitespace"); - Debug.Assert(instrumentation != null, "instrumentation was null"); + return this.AddInstrumentation( + typeof(TInstrumentation).Name, + typeof(TInstrumentation).Assembly.GetName().Version?.ToString() ?? DefaultInstrumentationVersion, + instrumentationFactory!()); + } - this.Instrumentation.Add( - new InstrumentationRegistration( - instrumentationName, - instrumentationVersion, - instrumentation!)); + public MeterProviderBuilder AddInstrumentation( + string instrumentationName, + string instrumentationVersion, + object? instrumentation) + { + Debug.Assert(!string.IsNullOrWhiteSpace(instrumentationName), "instrumentationName was null or whitespace"); + Debug.Assert(!string.IsNullOrWhiteSpace(instrumentationVersion), "instrumentationVersion was null or whitespace"); - return this; - } + this.Instrumentation.Add( + new InstrumentationRegistration( + instrumentationName, + instrumentationVersion, + instrumentation)); - public MeterProviderBuilder ConfigureResource(Action configure) - { - Debug.Assert(configure != null, "configure was null"); + return this; + } - var resourceBuilder = this.ResourceBuilder ??= ResourceBuilder.CreateDefault(); + public MeterProviderBuilder ConfigureResource(Action configure) + { + Debug.Assert(configure != null, "configure was null"); - configure!(resourceBuilder); + var resourceBuilder = this.ResourceBuilder ??= ResourceBuilder.CreateDefault(); - return this; - } + configure!(resourceBuilder); - public MeterProviderBuilder SetResourceBuilder(ResourceBuilder resourceBuilder) - { - Debug.Assert(resourceBuilder != null, "resourceBuilder was null"); + return this; + } - this.ResourceBuilder = resourceBuilder; + public MeterProviderBuilder SetResourceBuilder(ResourceBuilder resourceBuilder) + { + Debug.Assert(resourceBuilder != null, "resourceBuilder was null"); - return this; - } + this.ResourceBuilder = resourceBuilder; - public MeterProviderBuilder SetExemplarFilter(ExemplarFilter exemplarFilter) - { - Debug.Assert(exemplarFilter != null, "exemplarFilter was null"); + return this; + } - this.ExemplarFilter = exemplarFilter; + public MeterProviderBuilder SetExemplarFilter(ExemplarFilterType exemplarFilter) + { + this.ExemplarFilter = exemplarFilter; - return this; - } + return this; + } + + public override MeterProviderBuilder AddMeter(params string[] names) + { + Debug.Assert(names != null, "names was null"); - public override MeterProviderBuilder AddMeter(params string[] names) + foreach (var name in names!) { - Debug.Assert(names != null, "names was null"); + Guard.ThrowIfNullOrWhitespace(name); - foreach (var name in names!) - { - Guard.ThrowIfNullOrWhitespace(name); + this.MeterSources.Add(name); + } - this.MeterSources.Add(name); - } + return this; + } - return this; - } + public MeterProviderBuilder AddReader(MetricReader reader) + { + Debug.Assert(reader != null, "reader was null"); - public MeterProviderBuilder AddReader(MetricReader reader) - { - Debug.Assert(reader != null, "reader was null"); + this.Readers.Add(reader!); - this.Readers.Add(reader!); + return this; + } - return this; - } + public MeterProviderBuilder AddView(Func viewConfig) + { + Debug.Assert(viewConfig != null, "viewConfig was null"); - public MeterProviderBuilder AddView(Func viewConfig) - { - Debug.Assert(viewConfig != null, "viewConfig was null"); + this.ViewConfigs.Add(viewConfig!); - this.ViewConfigs.Add(viewConfig!); + return this; + } - return this; - } + public MeterProviderBuilder SetMetricLimit(int metricLimit) + { + this.MetricLimit = metricLimit; - public MeterProviderBuilder SetMaxMetricStreams(int maxMetricStreams) - { - this.MaxMetricStreams = maxMetricStreams; + return this; + } - return this; - } + public MeterProviderBuilder SetDefaultCardinalityLimit(int cardinalityLimit) + { + this.CardinalityLimit = cardinalityLimit; - public MeterProviderBuilder SetMaxMetricPointsPerMetricStream(int maxMetricPointsPerMetricStream) - { - this.MaxMetricPointsPerMetricStream = maxMetricPointsPerMetricStream; + return this; + } - return this; - } + public MeterProviderBuilder ConfigureBuilder(Action configure) + { + Debug.Assert(configure != null, "configure was null"); - public MeterProviderBuilder ConfigureBuilder(Action configure) - { - Debug.Assert(configure != null, "configure was null"); + configure!(this.serviceProvider, this); - configure!(this.serviceProvider, this); + return this; + } - return this; - } + public MeterProviderBuilder ConfigureServices(Action configure) + { + throw new NotSupportedException("Services cannot be configured after ServiceProvider has been created."); + } - public MeterProviderBuilder ConfigureServices(Action configure) - { - throw new NotSupportedException("Services cannot be configured after ServiceProvider has been created."); - } + MeterProviderBuilder IDeferredMeterProviderBuilder.Configure(Action configure) + => this.ConfigureBuilder(configure); - MeterProviderBuilder IDeferredMeterProviderBuilder.Configure(Action configure) - => this.ConfigureBuilder(configure); + internal readonly struct InstrumentationRegistration + { + public readonly string Name; + public readonly string Version; + public readonly object? Instance; - internal readonly struct InstrumentationRegistration + internal InstrumentationRegistration(string name, string version, object? instance) { - public readonly string Name; - public readonly string Version; - public readonly object Instance; - - internal InstrumentationRegistration(string name, string version, object instance) - { - this.Name = name; - this.Version = version; - this.Instance = instance; - } + this.Name = name; + this.Version = version; + this.Instance = instance; } } } diff --git a/src/OpenTelemetry/Metrics/CircularBufferBuckets.cs b/src/OpenTelemetry/Metrics/CircularBufferBuckets.cs index d5abef60510..a7205a41eab 100644 --- a/src/OpenTelemetry/Metrics/CircularBufferBuckets.cs +++ b/src/OpenTelemetry/Metrics/CircularBufferBuckets.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Runtime.CompilerServices; @@ -25,7 +12,7 @@ namespace OpenTelemetry.Metrics; /// internal sealed class CircularBufferBuckets { - private long[] trait; + private long[]? trait; private int begin = 0; private int end = -1; @@ -62,7 +49,12 @@ public CircularBufferBuckets(int capacity) /// public long this[int index] { - get => this.trait[this.ModuloIndex(index)]; + get + { + Debug.Assert(this.trait != null, "trait was null"); + + return this.trait![this.ModuloIndex(index)]; + } } /// diff --git a/src/OpenTelemetry/Metrics/CompositeMetricReader.cs b/src/OpenTelemetry/Metrics/CompositeMetricReader.cs index fa3ddbc2968..a213db5285f 100644 --- a/src/OpenTelemetry/Metrics/CompositeMetricReader.cs +++ b/src/OpenTelemetry/Metrics/CompositeMetricReader.cs @@ -1,193 +1,181 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using OpenTelemetry.Internal; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// CompositeMetricReader that does not deal with adding metrics and recording measurements. +/// +internal sealed partial class CompositeMetricReader : MetricReader { - /// - /// CompositeMetricReader that does not deal with adding metrics and recording measurements. - /// - internal sealed partial class CompositeMetricReader : MetricReader + public readonly DoublyLinkedListNode Head; + private DoublyLinkedListNode tail; + private bool disposed; + private int count; + + public CompositeMetricReader(IEnumerable readers) { - public readonly DoublyLinkedListNode Head; - private DoublyLinkedListNode tail; - private bool disposed; - private int count; + Guard.ThrowIfNull(readers); - public CompositeMetricReader(IEnumerable readers) + using var iter = readers.GetEnumerator(); + if (!iter.MoveNext()) { - Guard.ThrowIfNull(readers); - - using var iter = readers.GetEnumerator(); - if (!iter.MoveNext()) - { - throw new ArgumentException($"'{iter}' is null or empty", nameof(iter)); - } + throw new ArgumentException($"'{iter}' is null or empty", nameof(iter)); + } - this.Head = new DoublyLinkedListNode(iter.Current); - this.tail = this.Head; - this.count++; + this.Head = new DoublyLinkedListNode(iter.Current); + this.tail = this.Head; + this.count++; - while (iter.MoveNext()) - { - this.AddReader(iter.Current); - } + while (iter.MoveNext()) + { + this.AddReader(iter.Current); } + } + + public CompositeMetricReader AddReader(MetricReader reader) + { + Guard.ThrowIfNull(reader); - public CompositeMetricReader AddReader(MetricReader reader) + var node = new DoublyLinkedListNode(reader) { - Guard.ThrowIfNull(reader); + Previous = this.tail, + }; + this.tail.Next = node; + this.tail = node; + this.count++; - var node = new DoublyLinkedListNode(reader) - { - Previous = this.tail, - }; - this.tail.Next = node; - this.tail = node; - this.count++; + return this; + } - return this; - } + public Enumerator GetEnumerator() => new(this.Head); - public Enumerator GetEnumerator() => new(this.Head); + /// + internal override bool ProcessMetrics(in Batch metrics, int timeoutMilliseconds) + { + // CompositeMetricReader delegates the work to its underlying readers, + // so CompositeMetricReader.ProcessMetrics should never be called. + throw new NotSupportedException(); + } - /// - internal override bool ProcessMetrics(in Batch metrics, int timeoutMilliseconds) - { - // CompositeMetricReader delegates the work to its underlying readers, - // so CompositeMetricReader.ProcessMetrics should never be called. - throw new NotSupportedException(); - } + /// + protected override bool OnCollect(int timeoutMilliseconds = Timeout.Infinite) + { + var result = true; + var sw = timeoutMilliseconds == Timeout.Infinite + ? null + : Stopwatch.StartNew(); - /// - protected override bool OnCollect(int timeoutMilliseconds = Timeout.Infinite) + for (var cur = this.Head; cur != null; cur = cur.Next) { - var result = true; - var sw = timeoutMilliseconds == Timeout.Infinite - ? null - : Stopwatch.StartNew(); - - for (var cur = this.Head; cur != null; cur = cur.Next) + if (sw == null) { - if (sw == null) - { - result = cur.Value.Collect(Timeout.Infinite) && result; - } - else - { - var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; - - // notify all the readers, even if we run overtime - result = cur.Value.Collect((int)Math.Max(timeout, 0)) && result; - } + result = cur.Value.Collect(Timeout.Infinite) && result; } + else + { + var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; - return result; + // notify all the readers, even if we run overtime + result = cur.Value.Collect((int)Math.Max(timeout, 0)) && result; + } } - /// - protected override bool OnShutdown(int timeoutMilliseconds) - { - var result = true; - var sw = timeoutMilliseconds == Timeout.Infinite - ? null - : Stopwatch.StartNew(); + return result; + } - for (var cur = this.Head; cur != null; cur = cur.Next) - { - if (sw == null) - { - result = cur.Value.Shutdown(Timeout.Infinite) && result; - } - else - { - var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; + /// + protected override bool OnShutdown(int timeoutMilliseconds) + { + var result = true; + var sw = timeoutMilliseconds == Timeout.Infinite + ? null + : Stopwatch.StartNew(); - // notify all the readers, even if we run overtime - result = cur.Value.Shutdown((int)Math.Max(timeout, 0)) && result; - } + for (var cur = this.Head; cur != null; cur = cur.Next) + { + if (sw == null) + { + result = cur.Value.Shutdown(Timeout.Infinite) && result; } + else + { + var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; - return result; + // notify all the readers, even if we run overtime + result = cur.Value.Shutdown((int)Math.Max(timeout, 0)) && result; + } } - protected override void Dispose(bool disposing) + return result; + } + + protected override void Dispose(bool disposing) + { + if (!this.disposed) { - if (!this.disposed) + if (disposing) { - if (disposing) + for (var cur = this.Head; cur != null; cur = cur.Next) { - for (var cur = this.Head; cur != null; cur = cur.Next) + try { - try - { - cur.Value?.Dispose(); - } - catch (Exception ex) - { - OpenTelemetrySdkEventSource.Log.MetricReaderException(nameof(this.Dispose), ex); - } + cur.Value?.Dispose(); + } + catch (Exception ex) + { + OpenTelemetrySdkEventSource.Log.MetricReaderException(nameof(this.Dispose), ex); } } - - this.disposed = true; } - base.Dispose(disposing); + this.disposed = true; } - public struct Enumerator - { - private DoublyLinkedListNode node; - - internal Enumerator(DoublyLinkedListNode node) - { - this.node = node; - this.Current = null; - } - - public MetricReader Current { get; private set; } + base.Dispose(disposing); + } - public bool MoveNext() - { - if (this.node != null) - { - this.Current = this.node.Value; - this.node = this.node.Next; - return true; - } + public struct Enumerator + { + private DoublyLinkedListNode? node; - return false; - } + internal Enumerator(DoublyLinkedListNode node) + { + this.node = node; + this.Current = null; } - internal sealed class DoublyLinkedListNode - { - public readonly MetricReader Value; + [AllowNull] + public MetricReader Current { get; private set; } - public DoublyLinkedListNode(MetricReader value) + public bool MoveNext() + { + if (this.node != null) { - this.Value = value; + this.Current = this.node.Value; + this.node = this.node.Next; + return true; } - public DoublyLinkedListNode Previous { get; set; } + return false; + } + } + + internal sealed class DoublyLinkedListNode + { + public readonly MetricReader Value; - public DoublyLinkedListNode Next { get; set; } + public DoublyLinkedListNode(MetricReader value) + { + this.Value = value; } + + public DoublyLinkedListNode? Previous { get; set; } + + public DoublyLinkedListNode? Next { get; set; } } } diff --git a/src/OpenTelemetry/Metrics/CompositeMetricReaderExt.cs b/src/OpenTelemetry/Metrics/CompositeMetricReaderExt.cs index a32b9572dbd..a68b3e71070 100644 --- a/src/OpenTelemetry/Metrics/CompositeMetricReaderExt.cs +++ b/src/OpenTelemetry/Metrics/CompositeMetricReaderExt.cs @@ -1,147 +1,47 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Diagnostics.Metrics; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// CompositeMetricReader that deals with adding metrics and recording measurements. +/// +internal sealed partial class CompositeMetricReader { - /// - /// CompositeMetricReader that deals with adding metrics and recording measurements. - /// - internal sealed partial class CompositeMetricReader + internal override List AddMetricWithNoViews(Instrument instrument) { - internal List AddMetricsWithNoViews(Instrument instrument) - { - var metrics = new List(this.count); - for (var cur = this.Head; cur != null; cur = cur.Next) - { - var metric = cur.Value.AddMetricWithNoViews(instrument); - metrics.Add(metric); - } - - return metrics; - } - - internal void RecordSingleStreamLongMeasurements(List metrics, long value, ReadOnlySpan> tags) - { - Debug.Assert(metrics.Count == this.count, "The count of metrics to be updated for a CompositeReader must match the number of individual readers."); - - int index = 0; - for (var cur = this.Head; cur != null; cur = cur.Next) - { - if (metrics[index] != null) - { - cur.Value.RecordSingleStreamLongMeasurement(metrics[index], value, tags); - } - - index++; - } - } - - internal void RecordSingleStreamDoubleMeasurements(List metrics, double value, ReadOnlySpan> tags) - { - Debug.Assert(metrics.Count == this.count, "The count of metrics to be updated for a CompositeReader must match the number of individual readers."); - - int index = 0; - for (var cur = this.Head; cur != null; cur = cur.Next) - { - if (metrics[index] != null) - { - cur.Value.RecordSingleStreamDoubleMeasurement(metrics[index], value, tags); - } - - index++; - } - } - - internal List> AddMetricsSuperListWithViews(Instrument instrument, List metricStreamConfigs) - { - var metricsSuperList = new List>(this.count); - for (var cur = this.Head; cur != null; cur = cur.Next) - { - var metrics = cur.Value.AddMetricsListWithViews(instrument, metricStreamConfigs); - metricsSuperList.Add(metrics); - } + var metrics = new List(this.count); - return metricsSuperList; - } - - internal void RecordLongMeasurements(List> metricsSuperList, long value, ReadOnlySpan> tags) + for (var cur = this.Head; cur != null; cur = cur.Next) { - Debug.Assert(metricsSuperList.Count == this.count, "The count of metrics to be updated for a CompositeReader must match the number of individual readers."); - - int index = 0; - for (var cur = this.Head; cur != null; cur = cur.Next) + var innerMetrics = cur.Value.AddMetricWithNoViews(instrument); + if (innerMetrics.Count > 0) { - if (metricsSuperList[index].Count > 0) - { - cur.Value.RecordLongMeasurement(metricsSuperList[index], value, tags); - } + Debug.Assert(innerMetrics.Count == 1, "Multiple metrics returned without view configuration"); - index++; + metrics.AddRange(innerMetrics); } } - internal void RecordDoubleMeasurements(List> metricsSuperList, double value, ReadOnlySpan> tags) - { - Debug.Assert(metricsSuperList.Count == this.count, "The count of metrics to be updated for a CompositeReader must match the number of individual readers."); + return metrics; + } - int index = 0; - for (var cur = this.Head; cur != null; cur = cur.Next) - { - if (metricsSuperList[index].Count > 0) - { - cur.Value.RecordDoubleMeasurement(metricsSuperList[index], value, tags); - } + internal override List AddMetricWithViews(Instrument instrument, List metricStreamConfigs) + { + Debug.Assert(metricStreamConfigs != null, "metricStreamConfigs was null"); - index++; - } - } + var metrics = new List(this.count * metricStreamConfigs!.Count); - internal void CompleteSingleStreamMeasurements(List metrics) + for (var cur = this.Head; cur != null; cur = cur.Next) { - Debug.Assert(metrics.Count == this.count, "The count of metrics to be updated for a CompositeReader must match the number of individual readers."); - - int index = 0; - for (var cur = this.Head; cur != null; cur = cur.Next) - { - if (metrics[index] != null) - { - cur.Value.CompleteSingleStreamMeasurement(metrics[index]); - } + var innerMetrics = cur.Value.AddMetricWithViews(instrument, metricStreamConfigs); - index++; - } + metrics.AddRange(innerMetrics); } - internal void CompleteMeasurements(List> metricsSuperList) - { - Debug.Assert(metricsSuperList.Count == this.count, "The count of metrics to be updated for a CompositeReader must match the number of individual readers."); - - int index = 0; - for (var cur = this.Head; cur != null; cur = cur.Next) - { - if (metricsSuperList[index].Count > 0) - { - cur.Value.CompleteMeasurement(metricsSuperList[index]); - } - - index++; - } - } + return metrics; } } diff --git a/src/OpenTelemetry/Metrics/Exemplar/AlignedHistogramBucketExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/AlignedHistogramBucketExemplarReservoir.cs index 73090ce9949..ce99dd85f74 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/AlignedHistogramBucketExemplarReservoir.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/AlignedHistogramBucketExemplarReservoir.cs @@ -1,114 +1,35 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; namespace OpenTelemetry.Metrics; /// -/// The AlignedHistogramBucketExemplarReservoir implementation. +/// AlignedHistogramBucketExemplarReservoir implementation. /// -internal sealed class AlignedHistogramBucketExemplarReservoir : ExemplarReservoir +/// +/// Specification: . +/// +internal sealed class AlignedHistogramBucketExemplarReservoir : FixedSizeExemplarReservoir { - private readonly int length; - private readonly Exemplar[] runningExemplars; - private readonly Exemplar[] tempExemplars; - - public AlignedHistogramBucketExemplarReservoir(int length) - { - this.length = length; - this.runningExemplars = new Exemplar[length + 1]; - this.tempExemplars = new Exemplar[length + 1]; - } - - public override void Offer(long value, ReadOnlySpan> tags, int index = default) + public AlignedHistogramBucketExemplarReservoir(int numberOfBuckets) + : base(numberOfBuckets + 1) { - this.OfferAtBoundary(value, tags, index); } - public override void Offer(double value, ReadOnlySpan> tags, int index = default) + public override void Offer(in ExemplarMeasurement measurement) { - this.OfferAtBoundary(value, tags, index); + Debug.Fail("AlignedHistogramBucketExemplarReservoir shouldn't be used with long values"); } - public override Exemplar[] Collect(ReadOnlyTagCollection actualTags, bool reset) + public override void Offer(in ExemplarMeasurement measurement) { - for (int i = 0; i < this.runningExemplars.Length; i++) - { - this.tempExemplars[i] = this.runningExemplars[i]; - if (this.runningExemplars[i].FilteredTags != null) - { - // TODO: Better data structure to avoid this Linq. - // This is doing filtered = alltags - storedtags. - // TODO: At this stage, this logic is done inside Reservoir. - // Kinda hard for end users who write own reservoirs. - // Evaluate if this logic can be moved elsewhere. - // TODO: The cost is paid irrespective of whether the - // Exporter supports Exemplar or not. One idea is to - // defer this until first exporter attempts read. - this.tempExemplars[i].FilteredTags = this.runningExemplars[i].FilteredTags.Except(actualTags.KeyAndValues.ToList()).ToList(); - } - - if (reset) - { - this.runningExemplars[i].Timestamp = default; - } - } - - return this.tempExemplars; - } - - private void OfferAtBoundary(double value, ReadOnlySpan> tags, int index) - { - ref var exemplar = ref this.runningExemplars[index]; - exemplar.Timestamp = DateTimeOffset.UtcNow; - exemplar.DoubleValue = value; - exemplar.TraceId = Activity.Current?.TraceId; - exemplar.SpanId = Activity.Current?.SpanId; - - if (tags == default) - { - // default tag is used to indicate - // the special case where all tags provided at measurement - // recording time are stored. - // In this case, Exemplars does not have to store any tags. - // In other words, FilteredTags will be empty. - return; - } - - if (exemplar.FilteredTags == null) - { - exemplar.FilteredTags = new List>(tags.Length); - } - else - { - // Keep the list, but clear contents. - exemplar.FilteredTags.Clear(); - } + Debug.Assert( + measurement.ExplicitBucketHistogramBucketIndex != -1, + "ExplicitBucketHistogramBucketIndex was -1"); - // Though only those tags that are filtered need to be - // stored, finding filtered list from the full tag list - // is expensive. So all the tags are stored in hot path (this). - // During snapshot, the filtered list is calculated. - // TODO: Evaluate alternative approaches based on perf. - // TODO: This is not user friendly to Reservoir authors - // and must be handled as transparently as feasible. - foreach (var tag in tags) - { - exemplar.FilteredTags.Add(tag); - } + this.UpdateExemplar(measurement.ExplicitBucketHistogramBucketIndex, in measurement); } } diff --git a/src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs b/src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs deleted file mode 100644 index 2d3f3cab898..00000000000 --- a/src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs +++ /dev/null @@ -1,34 +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. -// - -namespace OpenTelemetry.Metrics; - -/// -/// An ExemplarFilter which makes no measurements eligible for being an Exemplar. -/// Using this ExemplarFilter is as good as disabling Exemplar feature. -/// -public sealed class AlwaysOffExemplarFilter : ExemplarFilter -{ - public override bool ShouldSample(long value, ReadOnlySpan> tags) - { - return false; - } - - public override bool ShouldSample(double value, ReadOnlySpan> tags) - { - return false; - } -} diff --git a/src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs b/src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs deleted file mode 100644 index 79adb9eeba3..00000000000 --- a/src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs +++ /dev/null @@ -1,33 +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. -// - -namespace OpenTelemetry.Metrics; - -/// -/// An ExemplarFilter which makes all measurements eligible for being an Exemplar. -/// -public sealed class AlwaysOnExemplarFilter : ExemplarFilter -{ - public override bool ShouldSample(long value, ReadOnlySpan> tags) - { - return true; - } - - public override bool ShouldSample(double value, ReadOnlySpan> tags) - { - return true; - } -} diff --git a/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs b/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs index 46342011abb..d9eda128f12 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs @@ -1,56 +1,182 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +#if NET8_0_OR_GREATER +using System.Collections.Frozen; +#endif using System.Diagnostics; +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +using OpenTelemetry.Internal; +#endif -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +#if EXPOSE_EXPERIMENTAL_FEATURES +/// +/// Exemplar implementation. +/// +/// +/// WARNING: This is an experimental API which might change or be removed in the future. Use at your own risk. +/// Specification: . +/// +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else +internal +#endif + struct Exemplar { -#pragma warning disable SA1623 // The property's documentation summary text should begin with: `Gets or sets` +#if NET8_0_OR_GREATER + internal FrozenSet? ViewDefinedTagKeys; +#else + internal HashSet? ViewDefinedTagKeys; +#endif + + private static readonly ReadOnlyFilteredTagCollection Empty = new(excludedKeys: null, Array.Empty>(), count: 0); + private int tagCount; + private KeyValuePair[]? tagStorage; + private MetricPointValueStorage valueStorage; + + /// + /// Gets the timestamp. + /// + public DateTimeOffset Timestamp { readonly get; private set; } + + /// + /// Gets the TraceId. + /// + public ActivityTraceId TraceId { readonly get; private set; } + + /// + /// Gets the SpanId. + /// + public ActivitySpanId SpanId { readonly get; private set; } + /// - /// Represents an Exemplar data. + /// Gets the long value. /// - public struct Exemplar + public long LongValue { - /// - /// Gets the timestamp (UTC). - /// - public DateTimeOffset Timestamp { get; internal set; } + readonly get => this.valueStorage.AsLong; + private set => this.valueStorage.AsLong = value; + } - /// - /// Gets the TraceId. - /// - public ActivityTraceId? TraceId { get; internal set; } + /// + /// Gets the double value. + /// + public double DoubleValue + { + readonly get => this.valueStorage.AsDouble; + private set => this.valueStorage.AsDouble = value; + } - /// - /// Gets the SpanId. - /// - public ActivitySpanId? SpanId { get; internal set; } + /// + /// Gets the filtered tags. + /// + /// + /// Note: represents the set of tags which were + /// supplied at measurement but dropped due to filtering configured by a + /// view (). If view tag + /// filtering is not configured will be empty. + /// + public readonly ReadOnlyFilteredTagCollection FilteredTags + { + get + { + if (this.tagCount == 0) + { + return Empty; + } + else + { + Debug.Assert(this.tagStorage != null, "tagStorage was null"); - // TODO: Leverage MetricPointValueStorage - // and allow double/long instead of double only. + return new(this.ViewDefinedTagKeys, this.tagStorage!, this.tagCount); + } + } + } + + internal void Update(in ExemplarMeasurement measurement) + where T : struct + { + this.Timestamp = DateTimeOffset.UtcNow; + + if (typeof(T) == typeof(long)) + { + this.LongValue = (long)(object)measurement.Value; + } + else if (typeof(T) == typeof(double)) + { + this.DoubleValue = (double)(object)measurement.Value; + } + else + { + Debug.Fail("Invalid value type"); + this.DoubleValue = Convert.ToDouble(measurement.Value); + } + + var currentActivity = Activity.Current; + if (currentActivity != null) + { + this.TraceId = currentActivity.TraceId; + this.SpanId = currentActivity.SpanId; + } + else + { + this.TraceId = default; + this.SpanId = default; + } + + if (this.ViewDefinedTagKeys != null) + { + this.StoreRawTags(measurement.Tags); + } + } + + internal void Reset() + { + this.Timestamp = default; + } + + internal readonly bool IsUpdated() + { + return this.Timestamp != default; + } + + internal readonly void Copy(ref Exemplar destination) + { + destination.Timestamp = this.Timestamp; + destination.TraceId = this.TraceId; + destination.SpanId = this.SpanId; + destination.valueStorage = this.valueStorage; + destination.ViewDefinedTagKeys = this.ViewDefinedTagKeys; + destination.tagCount = this.tagCount; + if (destination.tagCount > 0) + { + Debug.Assert(this.tagStorage != null, "tagStorage was null"); + + destination.tagStorage = new KeyValuePair[destination.tagCount]; + Array.Copy(this.tagStorage!, 0, destination.tagStorage, 0, destination.tagCount); + } + } + + private void StoreRawTags(ReadOnlySpan> tags) + { + this.tagCount = tags.Length; + if (tags.Length == 0) + { + return; + } - /// - /// Gets the double value. - /// - public double DoubleValue { get; internal set; } + if (this.tagStorage == null || this.tagStorage.Length < this.tagCount) + { + this.tagStorage = new KeyValuePair[this.tagCount]; + } - /// - /// Gets the FilteredTags (i.e any tags that were dropped during aggregation). - /// - public List> FilteredTags { get; internal set; } + tags.CopyTo(this.tagStorage); } -#pragma warning restore SA1623 } diff --git a/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilter.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilter.cs deleted file mode 100644 index 9e79570ce77..00000000000 --- a/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilter.cs +++ /dev/null @@ -1,58 +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. -// -namespace OpenTelemetry.Metrics; - -/// -/// The base class for defining Exemplar Filter. -/// -public abstract class ExemplarFilter -{ - /// - /// Determines if a given measurement is eligible for being - /// considered for becoming Exemplar. - /// - /// The value of the measurement. - /// The complete set of tags provided with the measurement. - /// - /// Returns - /// true to indicate this measurement is eligible to become Exemplar - /// and will be given to an ExemplarReservoir. - /// Reservoir may further sample, so a true here does not mean that this - /// measurement will become an exemplar, it just means it'll be - /// eligible for being Exemplar. - /// false to indicate this measurement is not eligible to become Exemplar - /// and will not be given to the ExemplarReservoir. - /// - public abstract bool ShouldSample(long value, ReadOnlySpan> tags); - - /// - /// Determines if a given measurement is eligible for being - /// considered for becoming Exemplar. - /// - /// The value of the measurement. - /// The complete set of tags provided with the measurement. - /// - /// Returns - /// true to indicate this measurement is eligible to become Exemplar - /// and will be given to an ExemplarReservoir. - /// Reservoir may further sample, so a true here does not mean that this - /// measurement will become an exemplar, it just means it'll be - /// eligible for being Exemplar. - /// false to indicate this measurement is not eligible to become Exemplar - /// and will not be given to the ExemplarReservoir. - /// - public abstract bool ShouldSample(double value, ReadOnlySpan> tags); -} diff --git a/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilterType.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilterType.cs new file mode 100644 index 00000000000..959d1f8e42f --- /dev/null +++ b/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilterType.cs @@ -0,0 +1,62 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +using OpenTelemetry.Internal; +#endif + +namespace OpenTelemetry.Metrics; + +#if EXPOSE_EXPERIMENTAL_FEATURES +/// +/// Defines the supported exemplar filters. +/// +/// +/// +/// Specification: . +/// +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else +internal +#endif + enum ExemplarFilterType +{ + /// + /// An exemplar filter which makes no measurements eligible for becoming an + /// . + /// + /// + /// Note: Setting on a meter provider + /// effectively disables exemplars. + /// Specification: . + /// + AlwaysOff, + + /// + /// An exemplar filter which makes all measurements eligible for becoming an + /// . + /// + /// + /// Specification: . + /// + AlwaysOn, + + /// + /// An exemplar filter which makes measurements recorded in the context of a + /// sampled (span) eligible for becoming an . + /// + /// + /// Specification: . + /// + TraceBased, +} diff --git a/src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs new file mode 100644 index 00000000000..8c86753e7a2 --- /dev/null +++ b/src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs @@ -0,0 +1,62 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +using OpenTelemetry.Internal; +#endif + +namespace OpenTelemetry.Metrics; + +#if EXPOSE_EXPERIMENTAL_FEATURES +/// +/// Represents an Exemplar measurement. +/// +/// +/// Measurement type. +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else +internal +#endif + readonly ref struct ExemplarMeasurement + where T : struct +{ + internal ExemplarMeasurement( + T value, + ReadOnlySpan> tags) + { + this.Value = value; + this.Tags = tags; + this.ExplicitBucketHistogramBucketIndex = -1; + } + + internal ExemplarMeasurement( + T value, + ReadOnlySpan> tags, + int explicitBucketHistogramIndex) + { + this.Value = value; + this.Tags = tags; + this.ExplicitBucketHistogramBucketIndex = explicitBucketHistogramIndex; + } + + /// + /// Gets the measurement value. + /// + public T Value { get; } + + /// + /// Gets the measurement tags. + /// + /// + /// Note: represents the full set of tags supplied at + /// measurement regardless of any filtering configured by a view (). + /// + public ReadOnlySpan> Tags { get; } + + internal int ExplicitBucketHistogramBucketIndex { get; } +} diff --git a/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs index 59b530e9d56..9fd1fc1b9f8 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs @@ -1,52 +1,50 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + namespace OpenTelemetry.Metrics; /// -/// The base class for defining Exemplar Reservoir. +/// ExemplarReservoir base implementation and contract. /// +/// +/// Specification: . +/// internal abstract class ExemplarReservoir { /// - /// Offers measurement to the reservoir. + /// Gets a value indicating whether or not the should reset its state when performing + /// collection. /// - /// The value of the measurement. - /// The complete set of tags provided with the measurement. - /// The histogram bucket index where this measurement is going to be stored. - /// This is optional and is only relevant for Histogram with buckets. - public abstract void Offer(long value, ReadOnlySpan> tags, int index = default); + /// + /// Note: is set to for + /// s using delta aggregation temporality and for s using cumulative + /// aggregation temporality. + /// + public bool ResetOnCollect { get; private set; } /// - /// Offers measurement to the reservoir. + /// Offers a measurement to the reservoir. /// - /// The value of the measurement. - /// The complete set of tags provided with the measurement. - /// The histogram bucket index where this measurement is going to be stored. - /// This is optional and is only relevant for Histogram with buckets. - public abstract void Offer(double value, ReadOnlySpan> tags, int index = default); + /// . + public abstract void Offer(in ExemplarMeasurement measurement); + + /// + /// Offers a measurement to the reservoir. + /// + /// . + public abstract void Offer(in ExemplarMeasurement measurement); /// /// Collects all the exemplars accumulated by the Reservoir. /// - /// The actual tags that are part of the metric. Exemplars are - /// only expected to contain any filtered tags, so this will allow the reservoir - /// to prepare the filtered tags from all the tags it is given by doing the - /// equivalent of filtered tags = all tags - actual tags. - /// - /// Flag to indicate if the reservoir should be reset after this call. - /// Array of Exemplars. - public abstract Exemplar[] Collect(ReadOnlyTagCollection actualTags, bool reset); + /// . + public abstract ReadOnlyExemplarCollection Collect(); + + internal virtual void Initialize(AggregatorStore aggregatorStore) + { + this.ResetOnCollect = aggregatorStore.OutputDelta; + } } diff --git a/src/OpenTelemetry/Metrics/Exemplar/FixedSizeExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/FixedSizeExemplarReservoir.cs new file mode 100644 index 00000000000..3d7057f85d0 --- /dev/null +++ b/src/OpenTelemetry/Metrics/Exemplar/FixedSizeExemplarReservoir.cs @@ -0,0 +1,91 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Metrics; + +internal abstract class FixedSizeExemplarReservoir : ExemplarReservoir +{ + private readonly Exemplar[] runningExemplars; + private readonly Exemplar[] snapshotExemplars; + + protected FixedSizeExemplarReservoir(int capacity) + { + Guard.ThrowIfOutOfRange(capacity, min: 1); + + this.runningExemplars = new Exemplar[capacity]; + this.snapshotExemplars = new Exemplar[capacity]; + this.Capacity = capacity; + } + + internal int Capacity { get; } + + /// + /// Collects all the exemplars accumulated by the Reservoir. + /// + /// . + public sealed override ReadOnlyExemplarCollection Collect() + { + var runningExemplars = this.runningExemplars; + + if (this.ResetOnCollect) + { + for (int i = 0; i < runningExemplars.Length; i++) + { + ref var running = ref runningExemplars[i]; + if (running.IsUpdated()) + { + running.Copy(ref this.snapshotExemplars[i]); + running.Reset(); + } + else + { + this.snapshotExemplars[i].Reset(); + } + } + } + else + { + for (int i = 0; i < runningExemplars.Length; i++) + { + ref var running = ref runningExemplars[i]; + if (running.IsUpdated()) + { + running.Copy(ref this.snapshotExemplars[i]); + } + else + { + this.snapshotExemplars[i].Reset(); + } + } + } + + this.OnCollected(); + + return new(this.snapshotExemplars); + } + + internal sealed override void Initialize(AggregatorStore aggregatorStore) + { + var viewDefinedTagKeys = aggregatorStore.TagKeysInteresting; + + for (int i = 0; i < this.runningExemplars.Length; i++) + { + this.runningExemplars[i].ViewDefinedTagKeys = viewDefinedTagKeys; + this.snapshotExemplars[i].ViewDefinedTagKeys = viewDefinedTagKeys; + } + + base.Initialize(aggregatorStore); + } + + protected virtual void OnCollected() + { + } + + protected void UpdateExemplar(int exemplarIndex, in ExemplarMeasurement measurement) + where T : struct + { + this.runningExemplars[exemplarIndex].Update(in measurement); + } +} diff --git a/src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs b/src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs new file mode 100644 index 00000000000..4a43bf3ebc6 --- /dev/null +++ b/src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs @@ -0,0 +1,139 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +using OpenTelemetry.Internal; +#endif + +namespace OpenTelemetry.Metrics; + +#if EXPOSE_EXPERIMENTAL_FEATURES +/// +/// A read-only collection of s. +/// +/// +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else +internal +#endif + readonly struct ReadOnlyExemplarCollection +{ + internal static readonly ReadOnlyExemplarCollection Empty = new(Array.Empty()); + private readonly Exemplar[] exemplars; + + internal ReadOnlyExemplarCollection(Exemplar[] exemplars) + { + Debug.Assert(exemplars != null, "exemplars was null"); + + this.exemplars = exemplars!; + } + + /// + /// Gets the maximum number of s in the collection. + /// + /// + /// Note: Enumerating the collection may return fewer results depending on + /// which s in the collection received updates. + /// + internal int MaximumCount => this.exemplars.Length; + + /// + /// Returns an enumerator that iterates through the s. + /// + /// . + public Enumerator GetEnumerator() + => new(this.exemplars); + + internal ReadOnlyExemplarCollection Copy() + { + var maximumCount = this.MaximumCount; + + if (maximumCount > 0) + { + var exemplarCopies = new Exemplar[maximumCount]; + + int i = 0; + foreach (ref readonly var exemplar in this) + { + if (exemplar.IsUpdated()) + { + exemplar.Copy(ref exemplarCopies[i++]); + } + } + + return new ReadOnlyExemplarCollection(exemplarCopies); + } + + return Empty; + } + + internal IReadOnlyList ToReadOnlyList() + { + var list = new List(this.MaximumCount); + + foreach (var exemplar in this) + { + // Note: If ToReadOnlyList is ever made public it should make sure + // to take copies of exemplars or make sure the instance was first + // copied using the Copy method above. + list.Add(exemplar); + } + + return list; + } + + /// + /// Enumerates the elements of a . + /// + public struct Enumerator + { + private readonly Exemplar[] exemplars; + private int index; + + internal Enumerator(Exemplar[] exemplars) + { + this.exemplars = exemplars; + this.index = -1; + } + + /// + /// Gets the at the current position of the enumerator. + /// + public readonly ref readonly Exemplar Current + => ref this.exemplars[this.index]; + + /// + /// Advances the enumerator to the next element of the . + /// + /// if the enumerator was + /// successfully advanced to the next element; if the enumerator has passed the end of the + /// collection. + public bool MoveNext() + { + var exemplars = this.exemplars; + + while (true) + { + var index = ++this.index; + if (index < exemplars.Length) + { + if (!exemplars[index].IsUpdated()) + { + continue; + } + + return true; + } + + return false; + } + } + } +} diff --git a/src/OpenTelemetry/Metrics/Exemplar/SimpleExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/SimpleExemplarReservoir.cs deleted file mode 100644 index c42efee38be..00000000000 --- a/src/OpenTelemetry/Metrics/Exemplar/SimpleExemplarReservoir.cs +++ /dev/null @@ -1,149 +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; - -namespace OpenTelemetry.Metrics; - -/// -/// The SimpleExemplarReservoir implementation. -/// -internal sealed class SimpleExemplarReservoir : ExemplarReservoir -{ - private readonly int poolSize; - private readonly Random random; - private readonly Exemplar[] runningExemplars; - private readonly Exemplar[] tempExemplars; - - private long measurementsSeen; - - public SimpleExemplarReservoir(int poolSize) - { - this.poolSize = poolSize; - this.runningExemplars = new Exemplar[poolSize]; - this.tempExemplars = new Exemplar[poolSize]; - this.measurementsSeen = 0; - this.random = new Random(); - } - - public override void Offer(long value, ReadOnlySpan> tags, int index = default) - { - this.Offer(value, tags); - } - - public override void Offer(double value, ReadOnlySpan> tags, int index = default) - { - this.Offer(value, tags); - } - - public override Exemplar[] Collect(ReadOnlyTagCollection actualTags, bool reset) - { - for (int i = 0; i < this.runningExemplars.Length; i++) - { - this.tempExemplars[i] = this.runningExemplars[i]; - if (this.runningExemplars[i].FilteredTags != null) - { - // TODO: Better data structure to avoid this Linq. - // This is doing filtered = alltags - storedtags. - // TODO: At this stage, this logic is done inside Reservoir. - // Kinda hard for end users who write own reservoirs. - // Evaluate if this logic can be moved elsewhere. - // TODO: The cost is paid irrespective of whether the - // Exporter supports Exemplar or not. One idea is to - // defer this until first exporter attempts read. - this.tempExemplars[i].FilteredTags = this.runningExemplars[i].FilteredTags.Except(actualTags.KeyAndValues.ToList()).ToList(); - } - - if (reset) - { - this.runningExemplars[i].Timestamp = default; - this.measurementsSeen = 0; - } - } - - return this.tempExemplars; - } - - private void Offer(double value, ReadOnlySpan> tags) - { - if (this.measurementsSeen < this.poolSize) - { - ref var exemplar = ref this.runningExemplars[this.measurementsSeen]; - exemplar.Timestamp = DateTimeOffset.UtcNow; - exemplar.DoubleValue = value; - exemplar.TraceId = Activity.Current?.TraceId; - exemplar.SpanId = Activity.Current?.SpanId; - this.StoreTags(ref exemplar, tags); - } - else - { - // TODO: RandomNext64 is only available in .NET 6 or newer. - int upperBound = 0; - unchecked - { - upperBound = (int)this.measurementsSeen; - } - - var index = this.random.Next(0, upperBound); - if (index < this.poolSize) - { - ref var exemplar = ref this.runningExemplars[index]; - exemplar.Timestamp = DateTimeOffset.UtcNow; - exemplar.DoubleValue = value; - exemplar.TraceId = Activity.Current?.TraceId; - exemplar.SpanId = Activity.Current?.SpanId; - this.StoreTags(ref exemplar, tags); - } - } - - this.measurementsSeen++; - } - - private void StoreTags(ref Exemplar exemplar, ReadOnlySpan> tags) - { - if (tags == default) - { - // default tag is used to indicate - // the special case where all tags provided at measurement - // recording time are stored. - // In this case, Exemplars does not have to store any tags. - // In other words, FilteredTags will be empty. - return; - } - - if (exemplar.FilteredTags == null) - { - exemplar.FilteredTags = new List>(tags.Length); - } - else - { - // Keep the list, but clear contents. - exemplar.FilteredTags.Clear(); - } - - // Though only those tags that are filtered need to be - // stored, finding filtered list from the full tag list - // is expensive. So all the tags are stored in hot path (this). - // During snapshot, the filtered list is calculated. - // TODO: Evaluate alternative approaches based on perf. - // TODO: This is not user friendly to Reservoir authors - // and must be handled as transparently as feasible. - foreach (var tag in tags) - { - exemplar.FilteredTags.Add(tag); - } - } -} diff --git a/src/OpenTelemetry/Metrics/Exemplar/SimpleFixedSizeExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/SimpleFixedSizeExemplarReservoir.cs new file mode 100644 index 00000000000..34dc945fabb --- /dev/null +++ b/src/OpenTelemetry/Metrics/Exemplar/SimpleFixedSizeExemplarReservoir.cs @@ -0,0 +1,60 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Metrics; + +/// +/// SimpleFixedSizeExemplarReservoir implementation. +/// +/// +/// Specification: . +/// +internal sealed class SimpleFixedSizeExemplarReservoir : FixedSizeExemplarReservoir +{ + private readonly Random random = new(); + + private int measurementsSeen; + + public SimpleFixedSizeExemplarReservoir(int poolSize) + : base(poolSize) + { + } + + public override void Offer(in ExemplarMeasurement measurement) + { + this.Offer(in measurement); + } + + public override void Offer(in ExemplarMeasurement measurement) + { + this.Offer(in measurement); + } + + protected override void OnCollected() + { + // Reset internal state irrespective of temporality. + // This ensures incoming measurements have fair chance + // of making it to the reservoir. + this.measurementsSeen = 0; + } + + private void Offer(in ExemplarMeasurement measurement) + where T : struct + { + var measurementNumber = this.measurementsSeen++; + + if (measurementNumber < this.Capacity) + { + this.UpdateExemplar(measurementNumber, in measurement); + } + else + { + var index = this.random.Next(0, measurementNumber); + if (index < this.Capacity) + { + this.UpdateExemplar(index, in measurement); + } + } + } +} diff --git a/src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs b/src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs deleted file mode 100644 index 69c4c29e9f8..00000000000 --- a/src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs +++ /dev/null @@ -1,36 +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; - -namespace OpenTelemetry.Metrics; - -/// -/// An ExemplarFilter which makes those measurements eligible for being an Exemplar, -/// which are recorded in the context of a sampled parent activity (span). -/// -public sealed class TraceBasedExemplarFilter : ExemplarFilter -{ - public override bool ShouldSample(long value, ReadOnlySpan> tags) - { - return Activity.Current?.Recorded ?? false; - } - - public override bool ShouldSample(double value, ReadOnlySpan> tags) - { - return Activity.Current?.Recorded ?? false; - } -} diff --git a/src/OpenTelemetry/Metrics/ExplicitBucketHistogramConfiguration.cs b/src/OpenTelemetry/Metrics/ExplicitBucketHistogramConfiguration.cs index 4ae3654210c..8d040da86a5 100644 --- a/src/OpenTelemetry/Metrics/ExplicitBucketHistogramConfiguration.cs +++ b/src/OpenTelemetry/Metrics/ExplicitBucketHistogramConfiguration.cs @@ -1,88 +1,74 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// Stores configuration for a histogram metric stream with explicit bucket boundaries. +/// +public class ExplicitBucketHistogramConfiguration : HistogramConfiguration { /// - /// Stores configuration for a histogram metric stream with explicit bucket boundaries. + /// Gets or sets the optional boundaries of the histogram metric stream. /// - public class ExplicitBucketHistogramConfiguration : HistogramConfiguration + /// + /// Requirements: + /// + /// The array must be in ascending order with distinct + /// values. + /// An empty array would result in no histogram buckets being + /// calculated. + /// A null value would result in default bucket boundaries being + /// used. + /// + /// Note: A copy is made of the provided array. + /// + public double[]? Boundaries { - /// - /// Gets or sets the optional boundaries of the histogram metric stream. - /// - /// - /// Requirements: - /// - /// The array must be in ascending order with distinct - /// values. - /// An empty array would result in no histogram buckets being - /// calculated. - /// A null value would result in default bucket boundaries being - /// used. - /// - /// Note: A copy is made of the provided array. - /// - public double[] Boundaries + get { - get + if (this.CopiedBoundaries != null) { - if (this.CopiedBoundaries != null) - { - double[] copy = new double[this.CopiedBoundaries.Length]; - this.CopiedBoundaries.AsSpan().CopyTo(copy); - return copy; - } - - return null; + double[] copy = new double[this.CopiedBoundaries.Length]; + this.CopiedBoundaries.AsSpan().CopyTo(copy); + return copy; } - set - { - if (value != null) - { - if (!IsSortedAndDistinct(value)) - { - throw new ArgumentException($"Histogram boundaries are invalid. Histogram boundaries must be in ascending order with distinct values.", nameof(value)); - } + return null; + } - double[] copy = new double[value.Length]; - value.AsSpan().CopyTo(copy); - this.CopiedBoundaries = copy; - } - else + set + { + if (value != null) + { + if (!IsSortedAndDistinct(value)) { - this.CopiedBoundaries = null; + throw new ArgumentException($"Histogram boundaries are invalid. Histogram boundaries must be in ascending order with distinct values.", nameof(value)); } + + double[] copy = new double[value.Length]; + value.AsSpan().CopyTo(copy); + this.CopiedBoundaries = copy; + } + else + { + this.CopiedBoundaries = null; } } + } - internal double[] CopiedBoundaries { get; private set; } + internal double[]? CopiedBoundaries { get; private set; } - private static bool IsSortedAndDistinct(double[] values) + private static bool IsSortedAndDistinct(double[] values) + { + for (int i = 1; i < values.Length; i++) { - for (int i = 1; i < values.Length; i++) + if (values[i] <= values[i - 1]) { - if (values[i] <= values[i - 1]) - { - return false; - } + return false; } - - return true; } + + return true; } } diff --git a/src/OpenTelemetry/Metrics/ExponentialHistogramBuckets.cs b/src/OpenTelemetry/Metrics/ExponentialHistogramBuckets.cs index 84db5a4a0bf..a48b8e40c60 100644 --- a/src/OpenTelemetry/Metrics/ExponentialHistogramBuckets.cs +++ b/src/OpenTelemetry/Metrics/ExponentialHistogramBuckets.cs @@ -1,23 +1,12 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 namespace OpenTelemetry.Metrics; +/// +/// Contains the buckets of an exponential histogram. +/// +// Note: Does not implement IEnumerable<> to prevent accidental boxing. public sealed class ExponentialHistogramBuckets { private long[] buckets = Array.Empty(); @@ -27,8 +16,15 @@ internal ExponentialHistogramBuckets() { } + /// + /// Gets the exponential histogram offset. + /// public int Offset { get; private set; } + /// + /// Returns an enumerator that iterates through the . + /// + /// . public Enumerator GetEnumerator() => new(this.buckets, this.size); internal void SnapshotBuckets(CircularBufferBuckets buckets) @@ -45,10 +41,12 @@ internal void SnapshotBuckets(CircularBufferBuckets buckets) internal ExponentialHistogramBuckets Copy() { - var copy = new ExponentialHistogramBuckets(); - copy.size = this.size; - copy.Offset = this.Offset; - copy.buckets = new long[this.buckets.Length]; + var copy = new ExponentialHistogramBuckets + { + size = this.size, + Offset = this.Offset, + buckets = new long[this.buckets.Length], + }; Array.Copy(this.buckets, copy.buckets, this.buckets.Length); return copy; } diff --git a/src/OpenTelemetry/Metrics/ExponentialHistogramData.cs b/src/OpenTelemetry/Metrics/ExponentialHistogramData.cs index 2cae22cb512..b9bc09a40d1 100644 --- a/src/OpenTelemetry/Metrics/ExponentialHistogramData.cs +++ b/src/OpenTelemetry/Metrics/ExponentialHistogramData.cs @@ -1,23 +1,11 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 namespace OpenTelemetry.Metrics; +/// +/// Contains the data for an exponential histogram. +/// public sealed class ExponentialHistogramData { internal ExponentialHistogramData() @@ -26,21 +14,35 @@ internal ExponentialHistogramData() this.NegativeBuckets = new(); } + /// + /// Gets the exponential histogram scale. + /// public int Scale { get; internal set; } + /// + /// Gets the exponential histogram zero count. + /// public long ZeroCount { get; internal set; } + /// + /// Gets the exponential histogram positive buckets. + /// public ExponentialHistogramBuckets PositiveBuckets { get; private set; } + /// + /// Gets the exponential histogram negative buckets. + /// internal ExponentialHistogramBuckets NegativeBuckets { get; private set; } internal ExponentialHistogramData Copy() { - var copy = new ExponentialHistogramData(); - copy.Scale = this.Scale; - copy.ZeroCount = this.ZeroCount; - copy.PositiveBuckets = this.PositiveBuckets.Copy(); - copy.NegativeBuckets = this.NegativeBuckets.Copy(); + var copy = new ExponentialHistogramData + { + Scale = this.Scale, + ZeroCount = this.ZeroCount, + PositiveBuckets = this.PositiveBuckets.Copy(), + NegativeBuckets = this.NegativeBuckets.Copy(), + }; return copy; } } diff --git a/src/OpenTelemetry/Metrics/ExportModes.cs b/src/OpenTelemetry/Metrics/ExportModes.cs index 91f237cd410..2e3def35a4b 100644 --- a/src/OpenTelemetry/Metrics/ExportModes.cs +++ b/src/OpenTelemetry/Metrics/ExportModes.cs @@ -1,45 +1,34 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// Describes the mode of a metric exporter. +/// +[Flags] +public enum ExportModes : byte { - [Flags] - public enum ExportModes : byte - { - /* - 0 0 0 0 0 0 0 0 - | | | | | | | | - | | | | | | | +--- Push - | | | | | | +----- Pull - | | | | | +------- (reserved) - | | | | +--------- (reserved) - | | | +----------- (reserved) - | | +------------- (reserved) - | +--------------- (reserved) - +----------------- (reserved) - */ + /* + 0 0 0 0 0 0 0 0 + | | | | | | | | + | | | | | | | +--- Push + | | | | | | +----- Pull + | | | | | +------- (reserved) + | | | | +--------- (reserved) + | | | +----------- (reserved) + | | +------------- (reserved) + | +--------------- (reserved) + +----------------- (reserved) + */ - /// - /// Push. - /// - Push = 0b1, + /// + /// Push. + /// + Push = 0b1, - /// - /// Pull. - /// - Pull = 0b10, - } + /// + /// Pull. + /// + Pull = 0b10, } diff --git a/src/OpenTelemetry/Metrics/ExportModesAttribute.cs b/src/OpenTelemetry/Metrics/ExportModesAttribute.cs index 6859b70724f..65b88ae3f34 100644 --- a/src/OpenTelemetry/Metrics/ExportModesAttribute.cs +++ b/src/OpenTelemetry/Metrics/ExportModesAttribute.cs @@ -1,31 +1,27 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Metrics -{ - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] - public sealed class ExportModesAttribute : Attribute - { - private readonly ExportModes supportedExportModes; +namespace OpenTelemetry.Metrics; - public ExportModesAttribute(ExportModes supported) - { - this.supportedExportModes = supported; - } +/// +/// An attribute for declaring the supported of a metric exporter. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] +public sealed class ExportModesAttribute : Attribute +{ + private readonly ExportModes supportedExportModes; - public ExportModes Supported => this.supportedExportModes; + /// + /// Initializes a new instance of the class. + /// + /// . + public ExportModesAttribute(ExportModes supported) + { + this.supportedExportModes = supported; } + + /// + /// Gets the supported . + /// + public ExportModes Supported => this.supportedExportModes; } diff --git a/src/OpenTelemetry/Metrics/HistogramBucket.cs b/src/OpenTelemetry/Metrics/HistogramBucket.cs index fc876fa4cf1..b5c01697e8c 100644 --- a/src/OpenTelemetry/Metrics/HistogramBucket.cs +++ b/src/OpenTelemetry/Metrics/HistogramBucket.cs @@ -1,41 +1,27 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// Represents a bucket in the histogram metric type. +/// +public readonly struct HistogramBucket { - /// - /// Represents a bucket in the histogram metric type. - /// - public readonly struct HistogramBucket + internal HistogramBucket(double explicitBound, long bucketCount) { - internal HistogramBucket(double explicitBound, long bucketCount) - { - this.ExplicitBound = explicitBound; - this.BucketCount = bucketCount; - } + this.ExplicitBound = explicitBound; + this.BucketCount = bucketCount; + } - /// - /// Gets the configured bounds for the bucket or for the catch-all bucket. - /// - public double ExplicitBound { get; } + /// + /// Gets the configured bounds for the bucket or for the catch-all bucket. + /// + public double ExplicitBound { get; } - /// - /// Gets the count of items in the bucket. - /// - public long BucketCount { get; } - } + /// + /// Gets the count of items in the bucket. + /// + public long BucketCount { get; } } diff --git a/src/OpenTelemetry/Metrics/HistogramBuckets.cs b/src/OpenTelemetry/Metrics/HistogramBuckets.cs index fbe17269950..6ef36eb6867 100644 --- a/src/OpenTelemetry/Metrics/HistogramBuckets.cs +++ b/src/OpenTelemetry/Metrics/HistogramBuckets.cs @@ -1,206 +1,227 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Runtime.CompilerServices; -namespace OpenTelemetry.Metrics -{ - /// - /// A collection of s associated with a histogram metric type. - /// - // Note: Does not implement IEnumerable<> to prevent accidental boxing. - public class HistogramBuckets - { - internal const int DefaultBoundaryCountForBinarySearch = 50; +namespace OpenTelemetry.Metrics; - internal readonly double[] ExplicitBounds; +/// +/// A collection of s associated with a histogram metric type. +/// +// Note: Does not implement IEnumerable<> to prevent accidental boxing. +public class HistogramBuckets +{ + internal const int DefaultBoundaryCountForBinarySearch = 50; - internal readonly long[] RunningBucketCounts; - internal readonly long[] SnapshotBucketCounts; + internal readonly double[]? ExplicitBounds; - internal double RunningSum; - internal double SnapshotSum; + internal readonly HistogramBucketValues[] BucketCounts; - internal double RunningMin = double.PositiveInfinity; - internal double SnapshotMin; + internal double RunningSum; + internal double SnapshotSum; - internal double RunningMax = double.NegativeInfinity; - internal double SnapshotMax; + internal double RunningMin = double.PositiveInfinity; + internal double SnapshotMin; - internal int IsCriticalSectionOccupied = 0; + internal double RunningMax = double.NegativeInfinity; + internal double SnapshotMax; - private readonly BucketLookupNode bucketLookupTreeRoot; + private readonly BucketLookupNode? bucketLookupTreeRoot; - private readonly Func findHistogramBucketIndex; + private readonly Func findHistogramBucketIndex; - internal HistogramBuckets(double[] explicitBounds) + internal HistogramBuckets(double[]? explicitBounds) + { + this.ExplicitBounds = explicitBounds; + this.findHistogramBucketIndex = this.FindBucketIndexLinear; + if (explicitBounds != null && explicitBounds.Length >= DefaultBoundaryCountForBinarySearch) { - this.ExplicitBounds = explicitBounds; - this.findHistogramBucketIndex = this.FindBucketIndexLinear; - if (explicitBounds != null && explicitBounds.Length >= DefaultBoundaryCountForBinarySearch) - { - this.bucketLookupTreeRoot = ConstructBalancedBST(explicitBounds, 0, explicitBounds.Length); - this.findHistogramBucketIndex = this.FindBucketIndexBinary; + this.bucketLookupTreeRoot = ConstructBalancedBST(explicitBounds, 0, explicitBounds.Length)!; + this.findHistogramBucketIndex = this.FindBucketIndexBinary; - static BucketLookupNode ConstructBalancedBST(double[] values, int min, int max) + static BucketLookupNode? ConstructBalancedBST(double[] values, int min, int max) + { + if (min == max) { - if (min == max) - { - return null; - } - - int median = min + ((max - min) / 2); - return new BucketLookupNode - { - Index = median, - UpperBoundInclusive = values[median], - LowerBoundExclusive = median > 0 ? values[median - 1] : double.NegativeInfinity, - Left = ConstructBalancedBST(values, min, median), - Right = ConstructBalancedBST(values, median + 1, max), - }; + return null; } - } - this.RunningBucketCounts = explicitBounds != null ? new long[explicitBounds.Length + 1] : null; - this.SnapshotBucketCounts = explicitBounds != null ? new long[explicitBounds.Length + 1] : new long[0]; + int median = min + ((max - min) / 2); + return new BucketLookupNode + { + Index = median, + UpperBoundInclusive = values[median], + LowerBoundExclusive = median > 0 ? values[median - 1] : double.NegativeInfinity, + Left = ConstructBalancedBST(values, min, median), + Right = ConstructBalancedBST(values, median + 1, max), + }; + } } - public Enumerator GetEnumerator() => new(this); + this.BucketCounts = explicitBounds != null ? new HistogramBucketValues[explicitBounds.Length + 1] : Array.Empty(); + } - internal HistogramBuckets Copy() - { - HistogramBuckets copy = new HistogramBuckets(this.ExplicitBounds); + /// + /// Returns an enumerator that iterates through the . + /// + /// . + public Enumerator GetEnumerator() => new(this); - Array.Copy(this.SnapshotBucketCounts, copy.SnapshotBucketCounts, this.SnapshotBucketCounts.Length); - copy.SnapshotSum = this.SnapshotSum; - copy.SnapshotMin = this.SnapshotMin; - copy.SnapshotMax = this.SnapshotMax; + internal HistogramBuckets Copy() + { + HistogramBuckets copy = new HistogramBuckets(this.ExplicitBounds); - return copy; - } + Array.Copy(this.BucketCounts, copy.BucketCounts, this.BucketCounts.Length); + copy.SnapshotSum = this.SnapshotSum; + copy.SnapshotMin = this.SnapshotMin; + copy.SnapshotMax = this.SnapshotMax; + + return copy; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal int FindBucketIndex(double value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal int FindBucketIndex(double value) + { + return this.findHistogramBucketIndex(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal int FindBucketIndexBinary(double value) + { + BucketLookupNode? current = this.bucketLookupTreeRoot; + + Debug.Assert(current != null, "Bucket root was null."); + + do { - return this.findHistogramBucketIndex(value); + if (value <= current!.LowerBoundExclusive) + { + current = current.Left; + } + else if (value > current.UpperBoundInclusive) + { + current = current.Right; + } + else + { + return current.Index; + } } + while (current != null); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal int FindBucketIndexBinary(double value) - { - BucketLookupNode current = this.bucketLookupTreeRoot; + Debug.Assert(this.ExplicitBounds != null, "ExplicitBounds was null."); - Debug.Assert(current != null, "Bucket root was null."); + return this.ExplicitBounds!.Length; + } - do + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal int FindBucketIndexLinear(double value) + { + Debug.Assert(this.ExplicitBounds != null, "ExplicitBounds was null."); + + int i; + for (i = 0; i < this.ExplicitBounds!.Length; i++) + { + // Upper bound is inclusive + if (value <= this.ExplicitBounds[i]) { - if (value <= current.LowerBoundExclusive) - { - current = current.Left; - } - else if (value > current.UpperBoundInclusive) - { - current = current.Right; - } - else - { - return current.Index; - } + break; } - while (current != null); - - return this.ExplicitBounds.Length; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal int FindBucketIndexLinear(double value) + return i; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void Snapshot(bool outputDelta) + { + var bucketCounts = this.BucketCounts; + + if (outputDelta) { - int i; - for (i = 0; i < this.ExplicitBounds.Length; i++) + for (int i = 0; i < bucketCounts.Length; i++) { - // Upper bound is inclusive - if (value <= this.ExplicitBounds[i]) - { - break; - } + ref var values = ref bucketCounts[i]; + ref var running = ref values.RunningValue; + values.SnapshotValue = running; + running = 0L; + } + } + else + { + for (int i = 0; i < bucketCounts.Length; i++) + { + ref var values = ref bucketCounts[i]; + values.SnapshotValue = values.RunningValue; } + } + } + + /// + /// Enumerates the elements of a . + /// + // Note: Does not implement IEnumerator<> to prevent accidental boxing. + public struct Enumerator + { + private readonly int numberOfBuckets; + private readonly HistogramBuckets histogramMeasurements; + private int index; - return i; + internal Enumerator(HistogramBuckets histogramMeasurements) + { + this.histogramMeasurements = histogramMeasurements; + this.index = 0; + this.Current = default; + this.numberOfBuckets = histogramMeasurements.BucketCounts.Length; } /// - /// Enumerates the elements of a . + /// Gets the at the current position of the enumerator. /// - // Note: Does not implement IEnumerator<> to prevent accidental boxing. - public struct Enumerator - { - private readonly int numberOfBuckets; - private readonly HistogramBuckets histogramMeasurements; - private int index; + public HistogramBucket Current { get; private set; } - internal Enumerator(HistogramBuckets histogramMeasurements) + /// + /// Advances the enumerator to the next element of the . + /// + /// if the enumerator was + /// successfully advanced to the next element; if the enumerator has passed the end of the + /// collection. + public bool MoveNext() + { + if (this.index < this.numberOfBuckets) { - this.histogramMeasurements = histogramMeasurements; - this.index = 0; - this.Current = default; - this.numberOfBuckets = histogramMeasurements.SnapshotBucketCounts.Length; + double explicitBound = this.index < this.numberOfBuckets - 1 + ? this.histogramMeasurements.ExplicitBounds![this.index] + : double.PositiveInfinity; + long bucketCount = this.histogramMeasurements.BucketCounts[this.index].SnapshotValue; + this.Current = new HistogramBucket(explicitBound, bucketCount); + this.index++; + return true; } - /// - /// Gets the at the current position of the enumerator. - /// - public HistogramBucket Current { get; private set; } - - /// - /// Advances the enumerator to the next element of the . - /// - /// if the enumerator was - /// successfully advanced to the next element; if the enumerator has passed the end of the - /// collection. - public bool MoveNext() - { - if (this.index < this.numberOfBuckets) - { - double explicitBound = this.index < this.numberOfBuckets - 1 - ? this.histogramMeasurements.ExplicitBounds[this.index] - : double.PositiveInfinity; - long bucketCount = this.histogramMeasurements.SnapshotBucketCounts[this.index]; - this.Current = new HistogramBucket(explicitBound, bucketCount); - this.index++; - return true; - } - - return false; - } + return false; } + } - private sealed class BucketLookupNode - { - public double UpperBoundInclusive { get; set; } + internal struct HistogramBucketValues + { + public long RunningValue; + public long SnapshotValue; + } + + private sealed class BucketLookupNode + { + public double UpperBoundInclusive { get; set; } - public double LowerBoundExclusive { get; set; } + public double LowerBoundExclusive { get; set; } - public int Index { get; set; } + public int Index { get; set; } - public BucketLookupNode Left { get; set; } + public BucketLookupNode? Left { get; set; } - public BucketLookupNode Right { get; set; } - } + public BucketLookupNode? Right { get; set; } } } diff --git a/src/OpenTelemetry/Metrics/HistogramConfiguration.cs b/src/OpenTelemetry/Metrics/HistogramConfiguration.cs index 37980098937..9d8ee472fcd 100644 --- a/src/OpenTelemetry/Metrics/HistogramConfiguration.cs +++ b/src/OpenTelemetry/Metrics/HistogramConfiguration.cs @@ -1,21 +1,11 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 namespace OpenTelemetry.Metrics; +/// +/// Stores configuration for a histogram MetricStream. +/// public class HistogramConfiguration : MetricStreamConfiguration { /// diff --git a/src/OpenTelemetry/Metrics/IMetricsListener/OpenTelemetryMetricsBuilderExtensions.cs b/src/OpenTelemetry/Metrics/IMetricsListener/OpenTelemetryMetricsBuilderExtensions.cs new file mode 100644 index 00000000000..5d00bcf192a --- /dev/null +++ b/src/OpenTelemetry/Metrics/IMetricsListener/OpenTelemetryMetricsBuilderExtensions.cs @@ -0,0 +1,68 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +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/Metrics/IMetricsListener/OpenTelemetryMetricsListener.cs b/src/OpenTelemetry/Metrics/IMetricsListener/OpenTelemetryMetricsListener.cs new file mode 100644 index 00000000000..6c66ba9474a --- /dev/null +++ b/src/OpenTelemetry/Metrics/IMetricsListener/OpenTelemetryMetricsListener.cs @@ -0,0 +1,70 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +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) + => MeterProviderSdk.MeasurementRecordedLong(instrument, value, tags, state), + ShortHandler = (instrument, value, tags, state) + => MeterProviderSdk.MeasurementRecordedLong(instrument, value, tags, state), + IntHandler = (instrument, value, tags, state) + => MeterProviderSdk.MeasurementRecordedLong(instrument, value, tags, state), + LongHandler = MeterProviderSdk.MeasurementRecordedLong, + FloatHandler = (instrument, value, tags, state) + => MeterProviderSdk.MeasurementRecordedDouble(instrument, value, tags, state), + DoubleHandler = MeterProviderSdk.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) + { + MeterProviderSdk.MeasurementsCompleted(instrument, userState); + } + + public void Initialize(IObservableInstrumentsSource source) + { + this.observableInstrumentsSource = source; + } + + private void OnCollectObservableInstruments() + { + this.observableInstrumentsSource?.RecordObservableInstruments(); + } +} diff --git a/src/OpenTelemetry/Metrics/IPullMetricExporter.cs b/src/OpenTelemetry/Metrics/IPullMetricExporter.cs index 29fc3c8ce33..8f3e1bc0bd1 100644 --- a/src/OpenTelemetry/Metrics/IPullMetricExporter.cs +++ b/src/OpenTelemetry/Metrics/IPullMetricExporter.cs @@ -1,26 +1,15 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// Describes a type of which supports . +/// +public interface IPullMetricExporter { /// - /// Describes a type of which supports . + /// Gets or sets the Collect delegate. /// - public interface IPullMetricExporter - { - Func Collect { get; set; } - } + Func? Collect { get; set; } } diff --git a/src/OpenTelemetry/Metrics/LookupData.cs b/src/OpenTelemetry/Metrics/LookupData.cs new file mode 100644 index 00000000000..a77cf4d18be --- /dev/null +++ b/src/OpenTelemetry/Metrics/LookupData.cs @@ -0,0 +1,18 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Metrics; + +internal sealed class LookupData +{ + public int Index; + public Tags SortedTags; + public Tags GivenTags; + + public LookupData(int index, in Tags sortedTags, in Tags givenTags) + { + this.Index = index; + this.SortedTags = sortedTags; + this.GivenTags = givenTags; + } +} diff --git a/src/OpenTelemetry/Metrics/MeterProviderExtensions.cs b/src/OpenTelemetry/Metrics/MeterProviderExtensions.cs index 59ce3ad9d9a..472e170d5f3 100644 --- a/src/OpenTelemetry/Metrics/MeterProviderExtensions.cs +++ b/src/OpenTelemetry/Metrics/MeterProviderExtensions.cs @@ -1,149 +1,142 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics.CodeAnalysis; using OpenTelemetry.Internal; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// Contains extension methods for the class. +/// +public static class MeterProviderExtensions { - public static class MeterProviderExtensions + /// + /// Flushes all the readers registered under MeterProviderSdk, blocks the current thread + /// until flush completed, shutdown signaled or timed out. + /// + /// MeterProviderSdk instance on which ForceFlush will be called. + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when force flush succeeded; otherwise, false. + /// + /// + /// Thrown when the timeoutMilliseconds is smaller than -1. + /// + /// + /// This function guarantees thread-safety. + /// + public static bool ForceFlush(this MeterProvider provider, int timeoutMilliseconds = Timeout.Infinite) { - /// - /// Flushes all the readers registered under MeterProviderSdk, blocks the current thread - /// until flush completed, shutdown signaled or timed out. - /// - /// MeterProviderSdk instance on which ForceFlush will be called. - /// - /// The number (non-negative) of milliseconds to wait, or - /// Timeout.Infinite to wait indefinitely. - /// - /// - /// Returns true when force flush succeeded; otherwise, false. - /// - /// - /// Thrown when the timeoutMilliseconds is smaller than -1. - /// - /// - /// This function guarantees thread-safety. - /// - public static bool ForceFlush(this MeterProvider provider, int timeoutMilliseconds = Timeout.Infinite) - { - Guard.ThrowIfNull(provider); - Guard.ThrowIfInvalidTimeout(timeoutMilliseconds); + Guard.ThrowIfNull(provider); + Guard.ThrowIfInvalidTimeout(timeoutMilliseconds); - if (provider is MeterProviderSdk meterProviderSdk) + if (provider is MeterProviderSdk meterProviderSdk) + { + try { - try - { - return meterProviderSdk.OnForceFlush(timeoutMilliseconds); - } - catch (Exception ex) - { - OpenTelemetrySdkEventSource.Log.MeterProviderException(nameof(meterProviderSdk.OnForceFlush), ex); - return false; - } + return meterProviderSdk.OnForceFlush(timeoutMilliseconds); + } + catch (Exception ex) + { + OpenTelemetrySdkEventSource.Log.MeterProviderException(nameof(meterProviderSdk.OnForceFlush), ex); + return false; } - - return true; } - /// - /// Attempts to shutdown the MeterProviderSdk, blocks the current thread until - /// shutdown completed or timed out. - /// - /// MeterProviderSdk instance on which Shutdown will be called. - /// - /// The number (non-negative) of milliseconds to wait, or - /// Timeout.Infinite to wait indefinitely. - /// - /// - /// Returns true when shutdown succeeded; otherwise, false. - /// - /// - /// Thrown when the timeoutMilliseconds is smaller than -1. - /// - /// - /// This function guarantees thread-safety. Only the first call will - /// win, subsequent calls will be no-op. - /// - public static bool Shutdown(this MeterProvider provider, int timeoutMilliseconds = Timeout.Infinite) - { - Guard.ThrowIfNull(provider); - Guard.ThrowIfInvalidTimeout(timeoutMilliseconds); + return true; + } + + /// + /// Attempts to shutdown the MeterProviderSdk, blocks the current thread until + /// shutdown completed or timed out. + /// + /// MeterProviderSdk instance on which Shutdown will be called. + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when shutdown succeeded; otherwise, false. + /// + /// + /// Thrown when the timeoutMilliseconds is smaller than -1. + /// + /// + /// This function guarantees thread-safety. Only the first call will + /// win, subsequent calls will be no-op. + /// + public static bool Shutdown(this MeterProvider provider, int timeoutMilliseconds = Timeout.Infinite) + { + Guard.ThrowIfNull(provider); + Guard.ThrowIfInvalidTimeout(timeoutMilliseconds); - if (provider is MeterProviderSdk meterProviderSdk) + if (provider is MeterProviderSdk meterProviderSdk) + { + if (Interlocked.Increment(ref meterProviderSdk.ShutdownCount) > 1) { - if (Interlocked.Increment(ref meterProviderSdk.ShutdownCount) > 1) - { - return false; // shutdown already called - } + return false; // shutdown already called + } - try - { - return meterProviderSdk.OnShutdown(timeoutMilliseconds); - } - catch (Exception ex) - { - OpenTelemetrySdkEventSource.Log.MeterProviderException(nameof(meterProviderSdk.OnShutdown), ex); - return false; - } + try + { + return meterProviderSdk.OnShutdown(timeoutMilliseconds); + } + catch (Exception ex) + { + OpenTelemetrySdkEventSource.Log.MeterProviderException(nameof(meterProviderSdk.OnShutdown), ex); + return false; } + } - return true; + return true; + } + + /// + /// Finds the Metric exporter of the given type from the provider. + /// + /// The type of the Exporter. + /// The MeterProvider from which Exporter should be found. + /// The exporter instance. + /// true if the exporter of specified Type is found; otherwise false. + internal static bool TryFindExporter( + this MeterProvider provider, + [NotNullWhen(true)] + out T? exporter) + where T : BaseExporter + { + if (provider is MeterProviderSdk meterProviderSdk) + { + return TryFindExporter(meterProviderSdk.Reader, out exporter); } - /// - /// Finds the Metric exporter of the given type from the provider. - /// - /// The type of the Exporter. - /// The MeterProvider from which Exporter should be found. - /// The exporter instance. - /// true if the exporter of specified Type is found; otherwise false. - internal static bool TryFindExporter(this MeterProvider provider, out T exporter) - where T : BaseExporter + exporter = null; + return false; + + static bool TryFindExporter(MetricReader? reader, out T? exporter) { - if (provider is MeterProviderSdk meterProviderSdk) + if (reader is BaseExportingMetricReader exportingMetricReader) { - return TryFindExporter(meterProviderSdk.Reader, out exporter); + exporter = exportingMetricReader.Exporter as T; + return exporter != null; } - exporter = null; - return false; - - static bool TryFindExporter(MetricReader reader, out T exporter) + if (reader is CompositeMetricReader compositeMetricReader) { - if (reader is BaseExportingMetricReader exportingMetricReader) - { - exporter = exportingMetricReader.Exporter as T; - return exporter != null; - } - - if (reader is CompositeMetricReader compositeMetricReader) + foreach (MetricReader childReader in compositeMetricReader) { - foreach (MetricReader childReader in compositeMetricReader) + if (TryFindExporter(childReader, out exporter)) { - if (TryFindExporter(childReader, out exporter)) - { - return true; - } + return true; } } - - exporter = null; - return false; } + + exporter = null; + return false; } } } diff --git a/src/OpenTelemetry/Metrics/MeterProviderSdk.cs b/src/OpenTelemetry/Metrics/MeterProviderSdk.cs index 73123215dad..22f49cb986a 100644 --- a/src/OpenTelemetry/Metrics/MeterProviderSdk.cs +++ b/src/OpenTelemetry/Metrics/MeterProviderSdk.cs @@ -1,621 +1,534 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Diagnostics.Metrics; using System.Text; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Internal; using OpenTelemetry.Resources; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +internal sealed class MeterProviderSdk : MeterProvider { - internal sealed class MeterProviderSdk : MeterProvider + internal const string EmitOverFlowAttributeConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_EMIT_OVERFLOW_ATTRIBUTE"; + internal const string ReclaimUnusedMetricPointsConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_RECLAIM_UNUSED_METRIC_POINTS"; + internal const string ExemplarFilterConfigKey = "OTEL_METRICS_EXEMPLAR_FILTER"; + + internal readonly IServiceProvider ServiceProvider; + internal readonly IDisposable? OwnedServiceProvider; + internal int ShutdownCount; + internal bool Disposed; + internal bool EmitOverflowAttribute; + internal bool ReclaimUnusedMetricPoints; + internal ExemplarFilterType? ExemplarFilter; + internal Action? OnCollectObservableInstruments; + + private readonly List instrumentations = new(); + private readonly List> viewConfigs; + private readonly object collectLock = new(); + private readonly MeterListener listener; + private readonly MetricReader? reader; + private readonly CompositeMetricReader? compositeMetricReader; + private readonly Func shouldListenTo = instrument => false; + + internal MeterProviderSdk( + IServiceProvider serviceProvider, + bool ownsServiceProvider) { - internal readonly IServiceProvider ServiceProvider; - internal readonly IDisposable? OwnedServiceProvider; - internal int ShutdownCount; - internal bool Disposed; - - private readonly List instrumentations = new(); - private readonly List> viewConfigs; - private readonly object collectLock = new(); - private readonly MeterListener listener; - private readonly MetricReader? reader; - private readonly CompositeMetricReader? compositeMetricReader; - - internal MeterProviderSdk( - IServiceProvider serviceProvider, - bool ownsServiceProvider) - { - Debug.Assert(serviceProvider != null, "serviceProvider was null"); + Debug.Assert(serviceProvider != null, "serviceProvider was null"); - var state = serviceProvider!.GetRequiredService(); - state.RegisterProvider(this); + var state = serviceProvider!.GetRequiredService(); + state.RegisterProvider(this); - this.ServiceProvider = serviceProvider!; + this.ServiceProvider = serviceProvider!; - if (ownsServiceProvider) - { - this.OwnedServiceProvider = serviceProvider as IDisposable; - Debug.Assert(this.OwnedServiceProvider != null, "serviceProvider was not IDisposable"); - } + if (ownsServiceProvider) + { + this.OwnedServiceProvider = serviceProvider as IDisposable; + Debug.Assert(this.OwnedServiceProvider != null, "serviceProvider was not IDisposable"); + } - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent("Building MeterProvider."); + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent("Building MeterProvider."); - var configureProviderBuilders = serviceProvider!.GetServices(); - foreach (var configureProviderBuilder in configureProviderBuilders) - { - configureProviderBuilder.ConfigureBuilder(serviceProvider!, state); - } + var configureProviderBuilders = serviceProvider!.GetServices(); + foreach (var configureProviderBuilder in configureProviderBuilders) + { + configureProviderBuilder.ConfigureBuilder(serviceProvider!, state); + } - StringBuilder exportersAdded = new StringBuilder(); - StringBuilder instrumentationFactoriesAdded = new StringBuilder(); + this.ExemplarFilter = state.ExemplarFilter; - var resourceBuilder = state.ResourceBuilder ?? ResourceBuilder.CreateDefault(); - resourceBuilder.ServiceProvider = serviceProvider; - this.Resource = resourceBuilder.Build(); + this.ApplySpecificationConfigurationKeys(serviceProvider!.GetRequiredService()); - this.viewConfigs = state.ViewConfigs; + StringBuilder exportersAdded = new StringBuilder(); + StringBuilder instrumentationFactoriesAdded = new StringBuilder(); - foreach (var reader in state.Readers) - { - Guard.ThrowIfNull(reader); + var resourceBuilder = state.ResourceBuilder ?? ResourceBuilder.CreateDefault(); + resourceBuilder.ServiceProvider = serviceProvider; + this.Resource = resourceBuilder.Build(); - reader.SetParentProvider(this); - reader.SetMaxMetricStreams(state.MaxMetricStreams); - reader.SetMaxMetricPointsPerMetricStream(state.MaxMetricPointsPerMetricStream); - reader.SetExemplarFilter(state.ExemplarFilter); + this.viewConfigs = state.ViewConfigs; - if (this.reader == null) - { - this.reader = reader; - } - else if (this.reader is CompositeMetricReader compositeReader) - { - compositeReader.AddReader(reader); - } - else - { - this.reader = new CompositeMetricReader(new[] { this.reader, reader }); - } + foreach (var reader in state.Readers) + { + Guard.ThrowIfNull(reader); - if (reader is PeriodicExportingMetricReader periodicExportingMetricReader) - { - exportersAdded.Append(periodicExportingMetricReader.Exporter); - exportersAdded.Append(" (Paired with PeriodicExportingMetricReader exporting at "); - exportersAdded.Append(periodicExportingMetricReader.ExportIntervalMilliseconds); - exportersAdded.Append(" milliseconds intervals.)"); - exportersAdded.Append(';'); - } - else if (reader is BaseExportingMetricReader baseExportingMetricReader) - { - exportersAdded.Append(baseExportingMetricReader.Exporter); - exportersAdded.Append(" (Paired with a MetricReader requiring manual trigger to export.)"); - exportersAdded.Append(';'); - } - } + reader.SetParentProvider(this); - if (exportersAdded.Length != 0) + reader.ApplyParentProviderSettings( + state.MetricLimit, + state.CardinalityLimit, + this.EmitOverflowAttribute, + this.ReclaimUnusedMetricPoints, + this.ExemplarFilter); + + if (this.reader == null) { - exportersAdded.Remove(exportersAdded.Length - 1, 1); - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Exporters added = \"{exportersAdded}\"."); + this.reader = reader; } - - this.compositeMetricReader = this.reader as CompositeMetricReader; - - if (state.Instrumentation.Any()) + else if (this.reader is CompositeMetricReader compositeReader) { - foreach (var instrumentation in state.Instrumentation) - { - this.instrumentations.Add(instrumentation.Instance); - instrumentationFactoriesAdded.Append(instrumentation.Name); - instrumentationFactoriesAdded.Append(';'); - } + compositeReader.AddReader(reader); } - - if (instrumentationFactoriesAdded.Length != 0) + else { - instrumentationFactoriesAdded.Remove(instrumentationFactoriesAdded.Length - 1, 1); - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Instrumentations added = \"{instrumentationFactoriesAdded}\"."); + this.reader = new CompositeMetricReader(new[] { this.reader, reader }); } - // Setup Listener - Func shouldListenTo = instrument => false; - if (state.MeterSources.Any(s => WildcardHelper.ContainsWildcard(s))) + if (reader is PeriodicExportingMetricReader periodicExportingMetricReader) { - var regex = WildcardHelper.GetWildcardRegex(state.MeterSources); - shouldListenTo = instrument => regex.IsMatch(instrument.Meter.Name); + exportersAdded.Append(periodicExportingMetricReader.Exporter); + exportersAdded.Append(" (Paired with PeriodicExportingMetricReader exporting at "); + exportersAdded.Append(periodicExportingMetricReader.ExportIntervalMilliseconds); + exportersAdded.Append(" milliseconds intervals.)"); + exportersAdded.Append(';'); } - else if (state.MeterSources.Any()) + else if (reader is BaseExportingMetricReader baseExportingMetricReader) { - var meterSourcesToSubscribe = new HashSet(state.MeterSources, StringComparer.OrdinalIgnoreCase); - shouldListenTo = instrument => meterSourcesToSubscribe.Contains(instrument.Meter.Name); + exportersAdded.Append(baseExportingMetricReader.Exporter); + exportersAdded.Append(" (Paired with a MetricReader requiring manual trigger to export.)"); + exportersAdded.Append(';'); } + } - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Listening to following meters = \"{string.Join(";", state.MeterSources)}\"."); - - this.listener = new MeterListener(); - var viewConfigCount = this.viewConfigs.Count; + if (exportersAdded.Length != 0) + { + exportersAdded.Remove(exportersAdded.Length - 1, 1); + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Exporters added = \"{exportersAdded}\"."); + } - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Number of views configured = {viewConfigCount}."); + this.compositeMetricReader = this.reader as CompositeMetricReader; - // 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) + if (state.Instrumentation.Any()) + { + foreach (var instrumentation in state.Instrumentation) { - this.listener.InstrumentPublished = (instrument, listener) => + if (instrumentation.Instance is not null) { - 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; - } + this.instrumentations.Add(instrumentation.Instance); + } - 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); - } - } + instrumentationFactoriesAdded.Append(instrumentation.Name); + instrumentationFactoriesAdded.Append(';'); + } + } - 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 (instrumentationFactoriesAdded.Length != 0) + { + instrumentationFactoriesAdded.Remove(instrumentationFactoriesAdded.Length - 1, 1); + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Instrumentations added = \"{instrumentationFactoriesAdded}\"."); + } - 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; - } - } - } + // Setup Listener + if (state.MeterSources.Any(s => WildcardHelper.ContainsWildcard(s))) + { + var regex = WildcardHelper.GetWildcardRegex(state.MeterSources); + this.shouldListenTo = instrument => regex.IsMatch(instrument.Meter.Name); + } + else if (state.MeterSources.Any()) + { + var meterSourcesToSubscribe = new HashSet(state.MeterSources, StringComparer.OrdinalIgnoreCase); + this.shouldListenTo = instrument => meterSourcesToSubscribe.Contains(instrument.Meter.Name); + } - 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."); - } - }; + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Listening to following meters = \"{string.Join(";", state.MeterSources)}\"."); - // Everything double - this.listener.SetMeasurementEventCallback(this.MeasurementRecordedDouble); - this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedDouble(instrument, value, tags, state)); + this.listener = new MeterListener(); + var viewConfigCount = this.viewConfigs.Count; - // Everything long - this.listener.SetMeasurementEventCallback(this.MeasurementRecordedLong); - this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedLong(instrument, value, tags, state)); - this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedLong(instrument, value, tags, state)); - this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedLong(instrument, value, tags, state)); + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Number of views configured = {viewConfigCount}."); - this.listener.MeasurementsCompleted = (instrument, state) => this.MeasurementsCompleted(instrument, state); - } - else + this.listener.InstrumentPublished = (instrument, listener) => + { + object? state = this.InstrumentPublished(instrument, listeningIsManagedExternally: false); + if (state != null) { - 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; - } + listener.EnableMeasurementEvents(instrument, state); + } + }; - try - { - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Started publishing Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\"."); + // Everything double + this.listener.SetMeasurementEventCallback(MeasurementRecordedDouble); + this.listener.SetMeasurementEventCallback(static (instrument, value, tags, state) => MeasurementRecordedDouble(instrument, value, tags, state)); - if (!MeterProviderBuilderSdk.IsValidInstrumentName(instrument.Name)) - { - OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored( - instrument.Name, - instrument.Meter.Name, - "Instrument name is invalid.", - "The name must comply with the OpenTelemetry specification"); + // Everything long + this.listener.SetMeasurementEventCallback(MeasurementRecordedLong); + this.listener.SetMeasurementEventCallback(static (instrument, value, tags, state) => MeasurementRecordedLong(instrument, value, tags, state)); + this.listener.SetMeasurementEventCallback(static (instrument, value, tags, state) => MeasurementRecordedLong(instrument, value, tags, state)); + this.listener.SetMeasurementEventCallback(static (instrument, value, tags, state) => MeasurementRecordedLong(instrument, value, tags, state)); - return; - } + this.listener.MeasurementsCompleted = MeasurementsCompleted; - if (this.reader != null) - { - if (this.compositeMetricReader == null) - { - var metric = this.reader.AddMetricWithNoViews(instrument); - if (metric != null) - { - listener.EnableMeasurementEvents(instrument, metric); - enabledMeasurements = true; - } - } - else - { - var metrics = this.compositeMetricReader.AddMetricsWithNoViews(instrument); - if (metrics.Any(metric => metric != null)) - { - listener.EnableMeasurementEvents(instrument, metrics); - enabledMeasurements = true; - } - } - } + this.listener.Start(); - 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."); - } - }; + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent("MeterProvider built successfully."); + } - // Everything double - this.listener.SetMeasurementEventCallback(this.MeasurementRecordedDoubleSingleStream); - this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedDoubleSingleStream(instrument, value, tags, state)); + internal Resource Resource { get; } - // 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)); + internal List Instrumentations => this.instrumentations; - this.listener.MeasurementsCompleted = (instrument, state) => this.MeasurementsCompletedSingleStream(instrument, state); - } + internal MetricReader? Reader => this.reader; - this.listener.Start(); + internal int ViewCount => this.viewConfigs.Count; - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent("MeterProvider built successfully."); + internal static void MeasurementsCompleted(Instrument instrument, object? state) + { + if (state is not MetricState metricState) + { + // todo: Log + return; } - internal Resource Resource { get; } + metricState.CompleteMeasurement(); + } - internal List Instrumentations => this.instrumentations; + internal static void MeasurementRecordedLong(Instrument instrument, long value, ReadOnlySpan> tags, object? state) + { + if (state is not MetricState metricState) + { + OpenTelemetrySdkEventSource.Log.MeasurementDropped(instrument?.Name ?? "UnknownInstrument", "SDK internal error occurred.", "Contact SDK owners."); + return; + } - internal MetricReader? Reader => this.reader; + metricState.RecordMeasurementLong(value, tags); + } - internal void MeasurementsCompletedSingleStream(Instrument instrument, object? state) + internal static void MeasurementRecordedDouble(Instrument instrument, double value, ReadOnlySpan> tags, object? state) + { + if (state is not MetricState metricState) { - Debug.Assert(instrument != null, "instrument must be non-null."); + OpenTelemetrySdkEventSource.Log.MeasurementDropped(instrument?.Name ?? "UnknownInstrument", "SDK internal error occurred.", "Contact SDK owners."); + return; + } - if (this.compositeMetricReader == null) - { - if (state is not Metric metric) - { - // TODO: log - return; - } + metricState.RecordMeasurementDouble(value, tags); + } - this.reader?.CompleteSingleStreamMeasurement(metric); - } - else - { - if (state is not List metrics) - { - // TODO: log - return; - } + internal object? InstrumentPublished(Instrument instrument, bool listeningIsManagedExternally) + { + var listenToInstrumentUsingSdkConfiguration = this.shouldListenTo(instrument); - this.compositeMetricReader.CompleteSingleStreamMeasurements(metrics); - } + 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; } - internal void MeasurementsCompleted(Instrument instrument, object? state) + object? state = null; + var viewConfigCount = this.viewConfigs.Count; + + try { - Debug.Assert(instrument != null, "instrument must be non-null."); + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Started publishing Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\"."); - if (this.compositeMetricReader == null) + if (viewConfigCount <= 0) { - if (state is not List metrics) + if (!MeterProviderBuilderSdk.IsValidInstrumentName(instrument.Name)) { - // TODO: log - return; + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored( + instrument.Name, + instrument.Meter.Name, + "Instrument name is invalid.", + "The name must comply with the OpenTelemetry specification"); + return null; } - this.reader?.CompleteMeasurement(metrics); + if (this.reader != null) + { + var metrics = this.reader.AddMetricWithNoViews(instrument); + if (metrics.Count == 1) + { + state = MetricState.BuildForSingleMetric(metrics[0]); + } + else if (metrics.Count > 0) + { + state = MetricState.BuildForMetricList(metrics); + } + } } else { - if (state is not List> metricsSuperList) + // 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) { - // TODO: log - return; - } + var viewConfig = this.viewConfigs[i]; + MetricStreamConfiguration? metricStreamConfig = null; - this.compositeMetricReader.CompleteMeasurements(metricsSuperList); - } - } - - internal void MeasurementRecordedDouble(Instrument instrument, double value, ReadOnlySpan> tagsRos, object? state) - { - Debug.Assert(instrument != null, "instrument must be non-null."); + try + { + metricStreamConfig = viewConfig(instrument); - if (this.compositeMetricReader == null) - { - if (state is not List metrics) - { - OpenTelemetrySdkEventSource.Log.MeasurementDropped(instrument!.Name, "SDK internal error occurred.", "Contact SDK owners."); - return; - } + // 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; + } - this.reader?.RecordDoubleMeasurement(metrics, value, tagsRos); - } - else - { - if (state is not List> metricsSuperList) - { - OpenTelemetrySdkEventSource.Log.MeasurementDropped(instrument!.Name, "SDK internal error occurred.", "Contact SDK owners."); - return; - } + if (metricStreamConfig is HistogramConfiguration + && instrument.GetType().GetGenericTypeDefinition() != typeof(Histogram<>)) + { + metricStreamConfig = null; - this.compositeMetricReader.RecordDoubleMeasurements(metricsSuperList, value, tagsRos); - } - } + 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."); + } - internal void MeasurementRecordedLong(Instrument instrument, long value, ReadOnlySpan> tagsRos, object? state) - { - Debug.Assert(instrument != null, "instrument must be non-null."); + if (metricStreamConfig != null) + { + metricStreamConfigs.Add(metricStreamConfig); + } + } - if (this.compositeMetricReader == null) - { - if (state is not List metrics) + if (metricStreamConfigs.Count == 0) { - OpenTelemetrySdkEventSource.Log.MeasurementDropped(instrument!.Name, "SDK internal error occurred.", "Contact SDK owners."); - return; + // 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); } - this.reader?.RecordLongMeasurement(metrics, value, tagsRos); - } - else - { - if (state is not List> metricsSuperList) + if (this.reader != null) { - OpenTelemetrySdkEventSource.Log.MeasurementDropped(instrument!.Name, "SDK internal error occurred.", "Contact SDK owners."); - return; + var metrics = this.reader.AddMetricWithViews(instrument, metricStreamConfigs); + if (metrics.Count == 1) + { + state = MetricState.BuildForSingleMetric(metrics[0]); + } + else if (metrics.Count > 0) + { + state = MetricState.BuildForMetricList(metrics); + } } - - this.compositeMetricReader.RecordLongMeasurements(metricsSuperList, value, tagsRos); } - } - - internal void MeasurementRecordedLongSingleStream(Instrument instrument, long value, ReadOnlySpan> tagsRos, object? state) - { - Debug.Assert(instrument != null, "instrument must be non-null."); - if (this.compositeMetricReader == null) + if (state != null) { - if (state is not Metric metric) - { - OpenTelemetrySdkEventSource.Log.MeasurementDropped(instrument!.Name, "SDK internal error occurred.", "Contact SDK owners."); - return; - } - - this.reader?.RecordSingleStreamLongMeasurement(metric, value, tagsRos); + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Measurements for Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\" will be processed and aggregated by the SDK."); + return state; } else { - if (state is not List metrics) - { - OpenTelemetrySdkEventSource.Log.MeasurementDropped(instrument!.Name, "SDK internal error occurred.", "Contact SDK owners."); - return; - } - - this.compositeMetricReader.RecordSingleStreamLongMeasurements(metrics, value, tagsRos); + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Measurements for Instrument = \"{instrument.Name}\" of Meter = \"{instrument.Meter.Name}\" will be dropped by the SDK."); + return null; } } - - internal void MeasurementRecordedDoubleSingleStream(Instrument instrument, double value, ReadOnlySpan> tagsRos, object? state) +#if DEBUG + catch (Exception ex) + { + throw new InvalidOperationException("SDK internal error occurred.", ex); + } +#else + catch (Exception) { - Debug.Assert(instrument != null, "instrument must be non-null."); + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(instrument.Name, instrument.Meter.Name, "SDK internal error occurred.", "Contact SDK owners."); + return null; + } +#endif + } - if (this.compositeMetricReader == null) + internal void CollectObservableInstruments() + { + lock (this.collectLock) + { + // Record all observable instruments + try { - if (state is not Metric metric) - { - OpenTelemetrySdkEventSource.Log.MeasurementDropped(instrument!.Name, "SDK internal error occurred.", "Contact SDK owners."); - return; - } + this.listener.RecordObservableInstruments(); - this.reader?.RecordSingleStreamDoubleMeasurement(metric, value, tagsRos); + this.OnCollectObservableInstruments?.Invoke(); } - else + catch (Exception exception) { - if (state is not List metrics) - { - OpenTelemetrySdkEventSource.Log.MeasurementDropped(instrument!.Name, "SDK internal error occurred.", "Contact SDK owners."); - return; - } - - this.compositeMetricReader.RecordSingleStreamDoubleMeasurements(metrics, value, tagsRos); + // TODO: + // It doesn't looks like we can find which instrument callback + // threw. + OpenTelemetrySdkEventSource.Log.MetricObserverCallbackException(exception); } } + } - internal void CollectObservableInstruments() + /// + /// Called by ForceFlush. This function should block the current + /// thread until flush completed or timed out. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when flush succeeded; otherwise, false. + /// + /// + /// This function is called synchronously on the thread which made the + /// first call to ForceFlush. This function should not throw + /// exceptions. + /// + internal bool OnForceFlush(int timeoutMilliseconds) + { + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"{nameof(MeterProviderSdk)}.{nameof(this.OnForceFlush)} called with {nameof(timeoutMilliseconds)} = {timeoutMilliseconds}."); + return this.reader?.Collect(timeoutMilliseconds) ?? true; + } + + /// + /// Called by Shutdown. This function should block the current + /// thread until shutdown completed or timed out. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when shutdown succeeded; otherwise, false. + /// + /// + /// This function is called synchronously on the thread which made the + /// first call to Shutdown. This function should not throw + /// exceptions. + /// + internal bool OnShutdown(int timeoutMilliseconds) + { + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"{nameof(MeterProviderSdk)}.{nameof(this.OnShutdown)} called with {nameof(timeoutMilliseconds)} = {timeoutMilliseconds}."); + return this.reader?.Shutdown(timeoutMilliseconds) ?? true; + } + + protected override void Dispose(bool disposing) + { + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"{nameof(MeterProviderSdk)}.{nameof(this.Dispose)} started."); + if (!this.Disposed) { - lock (this.collectLock) + if (disposing) { - // Record all observable instruments - try + if (this.instrumentations != null) { - this.listener.RecordObservableInstruments(); - } - catch (Exception exception) - { - // TODO: - // It doesn't looks like we can find which instrument callback - // threw. - OpenTelemetrySdkEventSource.Log.MetricObserverCallbackException(exception); + foreach (var item in this.instrumentations) + { + (item as IDisposable)?.Dispose(); + } + + this.instrumentations.Clear(); } + + // Wait for up to 5 seconds grace period + this.reader?.Shutdown(5000); + this.reader?.Dispose(); + this.compositeMetricReader?.Dispose(); + + this.listener?.Dispose(); + + this.OwnedServiceProvider?.Dispose(); } + + this.Disposed = true; + OpenTelemetrySdkEventSource.Log.ProviderDisposed(nameof(MeterProvider)); } - /// - /// Called by ForceFlush. This function should block the current - /// thread until flush completed or timed out. - /// - /// - /// The number (non-negative) of milliseconds to wait, or - /// Timeout.Infinite to wait indefinitely. - /// - /// - /// Returns true when flush succeeded; otherwise, false. - /// - /// - /// This function is called synchronously on the thread which made the - /// first call to ForceFlush. This function should not throw - /// exceptions. - /// - internal bool OnForceFlush(int timeoutMilliseconds) + base.Dispose(disposing); + } + + private void ApplySpecificationConfigurationKeys(IConfiguration configuration) + { + if (configuration.TryGetBoolValue(EmitOverFlowAttributeConfigKey, out this.EmitOverflowAttribute)) { - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"{nameof(MeterProviderSdk)}.{nameof(this.OnForceFlush)} called with {nameof(timeoutMilliseconds)} = {timeoutMilliseconds}."); - return this.reader?.Collect(timeoutMilliseconds) ?? true; + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent("Overflow attribute feature enabled via configuration."); } - /// - /// Called by Shutdown. This function should block the current - /// thread until shutdown completed or timed out. - /// - /// - /// The number (non-negative) of milliseconds to wait, or - /// Timeout.Infinite to wait indefinitely. - /// - /// - /// Returns true when shutdown succeeded; otherwise, false. - /// - /// - /// This function is called synchronously on the thread which made the - /// first call to Shutdown. This function should not throw - /// exceptions. - /// - internal bool OnShutdown(int timeoutMilliseconds) + if (configuration.TryGetBoolValue(ReclaimUnusedMetricPointsConfigKey, out this.ReclaimUnusedMetricPoints)) { - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"{nameof(MeterProviderSdk)}.{nameof(this.OnShutdown)} called with {nameof(timeoutMilliseconds)} = {timeoutMilliseconds}."); - return this.reader?.Shutdown(timeoutMilliseconds) ?? true; + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent("Reclaim unused metric point feature enabled via configuration."); } - protected override void Dispose(bool disposing) +#if EXPOSE_EXPERIMENTAL_FEATURES + if (configuration.TryGetStringValue(ExemplarFilterConfigKey, out var configValue)) { - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"{nameof(MeterProviderSdk)}.{nameof(this.Dispose)} started."); - if (!this.Disposed) + if (this.ExemplarFilter.HasValue) { - if (disposing) - { - if (this.instrumentations != null) - { - foreach (var item in this.instrumentations) - { - (item as IDisposable)?.Dispose(); - } - - this.instrumentations.Clear(); - } - - // Wait for up to 5 seconds grace period - this.reader?.Shutdown(5000); - this.reader?.Dispose(); - this.compositeMetricReader?.Dispose(); - - this.listener?.Dispose(); - - this.OwnedServiceProvider?.Dispose(); - } + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent( + $"Exemplar filter configuration value '{configValue}' has been ignored because a value '{this.ExemplarFilter}' was set programmatically."); + return; + } - this.Disposed = true; - OpenTelemetrySdkEventSource.Log.ProviderDisposed(nameof(MeterProvider)); + ExemplarFilterType? exemplarFilter; + if (string.Equals("always_off", configValue, StringComparison.OrdinalIgnoreCase)) + { + exemplarFilter = ExemplarFilterType.AlwaysOff; + } + else if (string.Equals("always_on", configValue, StringComparison.OrdinalIgnoreCase)) + { + exemplarFilter = ExemplarFilterType.AlwaysOn; + } + else if (string.Equals("trace_based", configValue, StringComparison.OrdinalIgnoreCase)) + { + exemplarFilter = ExemplarFilterType.TraceBased; + } + else + { + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Exemplar filter configuration was found but the value '{configValue}' is invalid and will be ignored."); + return; } - base.Dispose(disposing); + this.ExemplarFilter = exemplarFilter; + + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent($"Exemplar filter set to '{exemplarFilter}' from configuration."); + } +#else + if (configuration.TryGetStringValue(ExemplarFilterConfigKey, out var configValue)) + { + OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent( + $"Exemplar filter configuration value '{configValue}' has been ignored because exemplars are an experimental feature not available in stable builds."); } +#endif } } diff --git a/src/OpenTelemetry/Metrics/Metric.cs b/src/OpenTelemetry/Metrics/Metric.cs index 1b13793ff4b..32b107c5b2f 100644 --- a/src/OpenTelemetry/Metrics/Metric.cs +++ b/src/OpenTelemetry/Metrics/Metric.cs @@ -1,187 +1,233 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Metrics; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// Represents a metric stream which can contain multiple metric points. +/// +public sealed class Metric { - /// - /// Represents a Metric stream which can contain multiple MetricPoints. - /// - public sealed class Metric - { - internal const int DefaultExponentialHistogramMaxBuckets = 160; + internal const int DefaultExponentialHistogramMaxBuckets = 160; - internal const int DefaultExponentialHistogramMaxScale = 20; + internal const int DefaultExponentialHistogramMaxScale = 20; - internal static readonly double[] DefaultHistogramBounds = new double[] { 0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000 }; + internal static readonly double[] DefaultHistogramBounds = new double[] { 0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000 }; - private readonly AggregatorStore aggStore; + // Short default histogram bounds. Based on the recommended semantic convention values for http.server.request.duration. + internal static readonly double[] DefaultHistogramBoundsShortSeconds = new double[] { 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 }; + internal static readonly HashSet<(string, string)> DefaultHistogramBoundShortMappings = new() + { + ("Microsoft.AspNetCore.Hosting", "http.server.request.duration"), + ("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.lookup.duration"), + }; + + // Long default histogram bounds. Not based on a standard. May change in the future. + internal static readonly double[] DefaultHistogramBoundsLongSeconds = new double[] { 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60, 120, 300 }; + internal static readonly HashSet<(string, string)> DefaultHistogramBoundLongMappings = new() + { + ("Microsoft.AspNetCore.Http.Connections", "signalr.server.connection.duration"), + ("Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"), + ("System.Net.Http", "http.client.connection.duration"), + }; + + internal readonly AggregatorStore AggregatorStore; + + internal Metric( + MetricStreamIdentity instrumentIdentity, + AggregationTemporality temporality, + int cardinalityLimit, + bool emitOverflowAttribute, + bool shouldReclaimUnusedMetricPoints, + ExemplarFilterType? exemplarFilter = null, + Func? exemplarReservoirFactory = null) + { + this.InstrumentIdentity = instrumentIdentity; - internal Metric( - MetricStreamIdentity instrumentIdentity, - AggregationTemporality temporality, - int maxMetricPointsPerMetricStream, - ExemplarFilter exemplarFilter = null) + AggregationType aggType; + if (instrumentIdentity.InstrumentType == typeof(ObservableCounter) + || instrumentIdentity.InstrumentType == typeof(ObservableCounter) + || instrumentIdentity.InstrumentType == typeof(ObservableCounter) + || instrumentIdentity.InstrumentType == typeof(ObservableCounter)) + { + aggType = AggregationType.LongSumIncomingCumulative; + this.MetricType = MetricType.LongSum; + } + else if (instrumentIdentity.InstrumentType == typeof(Counter) + || instrumentIdentity.InstrumentType == typeof(Counter) + || instrumentIdentity.InstrumentType == typeof(Counter) + || instrumentIdentity.InstrumentType == typeof(Counter)) + { + aggType = AggregationType.LongSumIncomingDelta; + this.MetricType = MetricType.LongSum; + } + else if (instrumentIdentity.InstrumentType == typeof(Counter) + || instrumentIdentity.InstrumentType == typeof(Counter)) + { + aggType = AggregationType.DoubleSumIncomingDelta; + this.MetricType = MetricType.DoubleSum; + } + else if (instrumentIdentity.InstrumentType == typeof(ObservableCounter) + || instrumentIdentity.InstrumentType == typeof(ObservableCounter)) + { + aggType = AggregationType.DoubleSumIncomingCumulative; + this.MetricType = MetricType.DoubleSum; + } + else if (instrumentIdentity.InstrumentType == typeof(ObservableUpDownCounter) + || instrumentIdentity.InstrumentType == typeof(ObservableUpDownCounter) + || instrumentIdentity.InstrumentType == typeof(ObservableUpDownCounter) + || instrumentIdentity.InstrumentType == typeof(ObservableUpDownCounter)) + { + aggType = AggregationType.LongSumIncomingCumulative; + this.MetricType = MetricType.LongSumNonMonotonic; + } + else if (instrumentIdentity.InstrumentType == typeof(UpDownCounter) + || instrumentIdentity.InstrumentType == typeof(UpDownCounter) + || instrumentIdentity.InstrumentType == typeof(UpDownCounter) + || instrumentIdentity.InstrumentType == typeof(UpDownCounter)) + { + aggType = AggregationType.LongSumIncomingDelta; + this.MetricType = MetricType.LongSumNonMonotonic; + } + else if (instrumentIdentity.InstrumentType == typeof(UpDownCounter) + || instrumentIdentity.InstrumentType == typeof(UpDownCounter)) + { + aggType = AggregationType.DoubleSumIncomingDelta; + this.MetricType = MetricType.DoubleSumNonMonotonic; + } + else if (instrumentIdentity.InstrumentType == typeof(ObservableUpDownCounter) + || instrumentIdentity.InstrumentType == typeof(ObservableUpDownCounter)) { - this.InstrumentIdentity = instrumentIdentity; + aggType = AggregationType.DoubleSumIncomingCumulative; + this.MetricType = MetricType.DoubleSumNonMonotonic; + } + else if (instrumentIdentity.InstrumentType == typeof(ObservableGauge) + || instrumentIdentity.InstrumentType == typeof(ObservableGauge)) + { + aggType = AggregationType.DoubleGauge; + this.MetricType = MetricType.DoubleGauge; + } + else if (instrumentIdentity.InstrumentType == typeof(ObservableGauge) + || instrumentIdentity.InstrumentType == typeof(ObservableGauge) + || instrumentIdentity.InstrumentType == typeof(ObservableGauge) + || instrumentIdentity.InstrumentType == typeof(ObservableGauge)) + { + aggType = AggregationType.LongGauge; + this.MetricType = MetricType.LongGauge; + } + else if (instrumentIdentity.InstrumentType == typeof(Histogram) + || instrumentIdentity.InstrumentType == typeof(Histogram) + || instrumentIdentity.InstrumentType == typeof(Histogram) + || instrumentIdentity.InstrumentType == typeof(Histogram) + || instrumentIdentity.InstrumentType == typeof(Histogram) + || instrumentIdentity.InstrumentType == typeof(Histogram)) + { + var explicitBucketBounds = instrumentIdentity.HistogramBucketBounds; + var exponentialMaxSize = instrumentIdentity.ExponentialHistogramMaxSize; + var histogramRecordMinMax = instrumentIdentity.HistogramRecordMinMax; - AggregationType aggType; - if (instrumentIdentity.InstrumentType == typeof(ObservableCounter) - || instrumentIdentity.InstrumentType == typeof(ObservableCounter) - || instrumentIdentity.InstrumentType == typeof(ObservableCounter) - || instrumentIdentity.InstrumentType == typeof(ObservableCounter)) - { - aggType = AggregationType.LongSumIncomingCumulative; - this.MetricType = MetricType.LongSum; - } - else if (instrumentIdentity.InstrumentType == typeof(Counter) - || instrumentIdentity.InstrumentType == typeof(Counter) - || instrumentIdentity.InstrumentType == typeof(Counter) - || instrumentIdentity.InstrumentType == typeof(Counter)) - { - aggType = AggregationType.LongSumIncomingDelta; - this.MetricType = MetricType.LongSum; - } - else if (instrumentIdentity.InstrumentType == typeof(Counter) - || instrumentIdentity.InstrumentType == typeof(Counter)) - { - aggType = AggregationType.DoubleSumIncomingDelta; - this.MetricType = MetricType.DoubleSum; - } - else if (instrumentIdentity.InstrumentType == typeof(ObservableCounter) - || instrumentIdentity.InstrumentType == typeof(ObservableCounter)) - { - aggType = AggregationType.DoubleSumIncomingCumulative; - this.MetricType = MetricType.DoubleSum; - } - else if (instrumentIdentity.InstrumentType == typeof(ObservableUpDownCounter) - || instrumentIdentity.InstrumentType == typeof(ObservableUpDownCounter) - || instrumentIdentity.InstrumentType == typeof(ObservableUpDownCounter) - || instrumentIdentity.InstrumentType == typeof(ObservableUpDownCounter)) - { - aggType = AggregationType.LongSumIncomingCumulative; - this.MetricType = MetricType.LongSumNonMonotonic; - } - else if (instrumentIdentity.InstrumentType == typeof(UpDownCounter) - || instrumentIdentity.InstrumentType == typeof(UpDownCounter) - || instrumentIdentity.InstrumentType == typeof(UpDownCounter) - || instrumentIdentity.InstrumentType == typeof(UpDownCounter)) - { - aggType = AggregationType.LongSumIncomingDelta; - this.MetricType = MetricType.LongSumNonMonotonic; - } - else if (instrumentIdentity.InstrumentType == typeof(UpDownCounter) - || instrumentIdentity.InstrumentType == typeof(UpDownCounter)) - { - aggType = AggregationType.DoubleSumIncomingDelta; - this.MetricType = MetricType.DoubleSumNonMonotonic; - } - else if (instrumentIdentity.InstrumentType == typeof(ObservableUpDownCounter) - || instrumentIdentity.InstrumentType == typeof(ObservableUpDownCounter)) - { - aggType = AggregationType.DoubleSumIncomingCumulative; - this.MetricType = MetricType.DoubleSumNonMonotonic; - } - else if (instrumentIdentity.InstrumentType == typeof(ObservableGauge) - || instrumentIdentity.InstrumentType == typeof(ObservableGauge)) - { - aggType = AggregationType.DoubleGauge; - this.MetricType = MetricType.DoubleGauge; - } - else if (instrumentIdentity.InstrumentType == typeof(ObservableGauge) - || instrumentIdentity.InstrumentType == typeof(ObservableGauge) - || instrumentIdentity.InstrumentType == typeof(ObservableGauge) - || instrumentIdentity.InstrumentType == typeof(ObservableGauge)) - { - aggType = AggregationType.LongGauge; - this.MetricType = MetricType.LongGauge; - } - else if (instrumentIdentity.InstrumentType == typeof(Histogram) - || instrumentIdentity.InstrumentType == typeof(Histogram) - || instrumentIdentity.InstrumentType == typeof(Histogram) - || instrumentIdentity.InstrumentType == typeof(Histogram) - || instrumentIdentity.InstrumentType == typeof(Histogram) - || instrumentIdentity.InstrumentType == typeof(Histogram)) + this.MetricType = exponentialMaxSize == 0 + ? MetricType.Histogram + : MetricType.ExponentialHistogram; + + if (this.MetricType == MetricType.Histogram) { - var explicitBucketBounds = instrumentIdentity.HistogramBucketBounds; - var exponentialMaxSize = instrumentIdentity.ExponentialHistogramMaxSize; - var histogramRecordMinMax = instrumentIdentity.HistogramRecordMinMax; - - this.MetricType = exponentialMaxSize == 0 - ? MetricType.Histogram - : MetricType.ExponentialHistogram; - - if (this.MetricType == MetricType.Histogram) - { - aggType = explicitBucketBounds != null && explicitBucketBounds.Length == 0 - ? (histogramRecordMinMax ? AggregationType.HistogramWithMinMax : AggregationType.Histogram) - : (histogramRecordMinMax ? AggregationType.HistogramWithMinMaxBuckets : AggregationType.HistogramWithBuckets); - } - else - { - aggType = histogramRecordMinMax ? AggregationType.Base2ExponentialHistogramWithMinMax : AggregationType.Base2ExponentialHistogram; - } + aggType = explicitBucketBounds != null && explicitBucketBounds.Length == 0 + ? (histogramRecordMinMax ? AggregationType.HistogramWithMinMax : AggregationType.Histogram) + : (histogramRecordMinMax ? AggregationType.HistogramWithMinMaxBuckets : AggregationType.HistogramWithBuckets); } else { - throw new NotSupportedException($"Unsupported Instrument Type: {instrumentIdentity.InstrumentType.FullName}"); + aggType = histogramRecordMinMax ? AggregationType.Base2ExponentialHistogramWithMinMax : AggregationType.Base2ExponentialHistogram; } - - this.aggStore = new AggregatorStore(instrumentIdentity, aggType, temporality, maxMetricPointsPerMetricStream, exemplarFilter); - this.Temporality = temporality; - this.InstrumentDisposed = false; + } + else + { + throw new NotSupportedException($"Unsupported Instrument Type: {instrumentIdentity.InstrumentType.FullName}"); } - public MetricType MetricType { get; private set; } + this.AggregatorStore = new AggregatorStore( + instrumentIdentity, + aggType, + temporality, + cardinalityLimit, + emitOverflowAttribute, + shouldReclaimUnusedMetricPoints, + exemplarFilter, + exemplarReservoirFactory); + this.Temporality = temporality; + } - public AggregationTemporality Temporality { get; private set; } + /// + /// Gets the for the metric stream. + /// + public MetricType MetricType { get; private set; } - public string Name => this.InstrumentIdentity.InstrumentName; + /// + /// Gets the for the metric stream. + /// + public AggregationTemporality Temporality { get; private set; } - public string Description => this.InstrumentIdentity.Description; + /// + /// Gets the name for the metric stream. + /// + public string Name => this.InstrumentIdentity.InstrumentName; - public string Unit => this.InstrumentIdentity.Unit; + /// + /// Gets the description for the metric stream. + /// + public string Description => this.InstrumentIdentity.Description; - public string MeterName => this.InstrumentIdentity.MeterName; + /// + /// Gets the unit for the metric stream. + /// + public string Unit => this.InstrumentIdentity.Unit; - public string MeterVersion => this.InstrumentIdentity.MeterVersion; + /// + /// Gets the meter name for the metric stream. + /// + public string MeterName => this.InstrumentIdentity.MeterName; - internal MetricStreamIdentity InstrumentIdentity { get; private set; } + /// + /// Gets the meter version for the metric stream. + /// + public string MeterVersion => this.InstrumentIdentity.MeterVersion; - internal bool InstrumentDisposed { get; set; } + /// + /// Gets the attributes (tags) for the metric stream. + /// + public IEnumerable>? MeterTags => this.InstrumentIdentity.MeterTags; - public MetricPointsAccessor GetMetricPoints() - { - return this.aggStore.GetMetricPoints(); - } + /// + /// Gets the for the metric stream. + /// + internal MetricStreamIdentity InstrumentIdentity { get; private set; } - internal void UpdateLong(long value, ReadOnlySpan> tags) - { - this.aggStore.Update(value, tags); - } + internal bool Active { get; set; } = true; - internal void UpdateDouble(double value, ReadOnlySpan> tags) - { - this.aggStore.Update(value, tags); - } + /// + /// Get the metric points for the metric stream. + /// + /// . + public MetricPointsAccessor GetMetricPoints() + => this.AggregatorStore.GetMetricPoints(); - internal int Snapshot() - { - return this.aggStore.Snapshot(); - } - } + internal void UpdateLong(long value, ReadOnlySpan> tags) + => this.AggregatorStore.Update(value, tags); + + internal void UpdateDouble(double value, ReadOnlySpan> tags) + => this.AggregatorStore.Update(value, tags); + + internal int Snapshot() + => this.AggregatorStore.Snapshot(); } diff --git a/src/OpenTelemetry/Metrics/MetricPoint.cs b/src/OpenTelemetry/Metrics/MetricPoint.cs index f0c3a0b70f0..e1fe8e1695c 100644 --- a/src/OpenTelemetry/Metrics/MetricPoint.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint.cs @@ -1,1653 +1,1422 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Runtime.CompilerServices; +using OpenTelemetry.Internal; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// Represents a metric data point. +/// +public struct MetricPoint { - /// - /// Represents a metric data point. - /// - public struct MetricPoint - { - // TODO: Ask spec to define a default value for this. - private const int DefaultSimpleReservoirPoolSize = 10; + // Represents the number of update threads using this MetricPoint at any given point of time. + // If the value is equal to int.MinValue which is -2147483648, it means that this MetricPoint is available for reuse. + // We never increment the ReferenceCount for MetricPoint with no tags (index == 0) and the MetricPoint for overflow attribute, + // but we always decrement it (in the Update methods). This should be fine. + // ReferenceCount doesn't matter for MetricPoint with no tags and overflow attribute as they are never reclaimed. + internal int ReferenceCount; - private readonly AggregatorStore aggregatorStore; + private const int DefaultSimpleReservoirPoolSize = 1; - private readonly AggregationType aggType; + private readonly AggregatorStore aggregatorStore; - private MetricPointOptionalComponents? mpComponents; + private readonly AggregationType aggType; - // Represents temporality adjusted "value" for double/long metric types or "count" when histogram - private MetricPointValueStorage runningValue; + private MetricPointOptionalComponents? mpComponents; - // Represents either "value" for double/long metric types or "count" when histogram - private MetricPointValueStorage snapshotValue; + // Represents temporality adjusted "value" for double/long metric types or "count" when histogram + private MetricPointValueStorage runningValue; - private MetricPointValueStorage deltaLastValue; + // Represents either "value" for double/long metric types or "count" when histogram + private MetricPointValueStorage snapshotValue; - internal MetricPoint( - AggregatorStore aggregatorStore, - AggregationType aggType, - KeyValuePair[] tagKeysAndValues, - double[] histogramExplicitBounds, - int exponentialHistogramMaxSize, - int exponentialHistogramMaxScale) + private MetricPointValueStorage deltaLastValue; + + internal MetricPoint( + AggregatorStore aggregatorStore, + AggregationType aggType, + KeyValuePair[]? tagKeysAndValues, + double[] histogramExplicitBounds, + int exponentialHistogramMaxSize, + int exponentialHistogramMaxScale, + LookupData? lookupData = null) + { + Debug.Assert(aggregatorStore != null, "AggregatorStore was null."); + Debug.Assert(histogramExplicitBounds != null, "Histogram explicit Bounds was null."); + Debug.Assert(!aggregatorStore!.OutputDeltaWithUnusedMetricPointReclaimEnabled || lookupData != null, "LookupData was null."); + + this.aggType = aggType; + this.Tags = new ReadOnlyTagCollection(tagKeysAndValues); + this.runningValue = default; + this.snapshotValue = default; + this.deltaLastValue = default; + this.MetricPointStatus = MetricPointStatus.NoCollectPending; + this.ReferenceCount = 1; + this.LookupData = lookupData; + + var isExemplarEnabled = aggregatorStore!.IsExemplarEnabled(); + + ExemplarReservoir? reservoir; + try { - Debug.Assert(aggregatorStore != null, "AggregatorStore was null."); - Debug.Assert(histogramExplicitBounds != null, "Histogram explicit Bounds was null."); - - this.aggType = aggType; - this.Tags = new ReadOnlyTagCollection(tagKeysAndValues); - this.runningValue = default; - this.snapshotValue = default; - this.deltaLastValue = default; - this.MetricPointStatus = MetricPointStatus.NoCollectPending; - - ExemplarReservoir? reservoir = null; - if (this.aggType == AggregationType.HistogramWithBuckets || - this.aggType == AggregationType.HistogramWithMinMaxBuckets) - { - this.mpComponents = new MetricPointOptionalComponents(); - this.mpComponents.HistogramBuckets = new HistogramBuckets(histogramExplicitBounds); - if (aggregatorStore!.IsExemplarEnabled()) - { - reservoir = new AlignedHistogramBucketExemplarReservoir(histogramExplicitBounds!.Length); - } - } - else if (this.aggType == AggregationType.Histogram || - this.aggType == AggregationType.HistogramWithMinMax) - { - this.mpComponents = new MetricPointOptionalComponents(); - this.mpComponents.HistogramBuckets = new HistogramBuckets(null); - } - else if (this.aggType == AggregationType.Base2ExponentialHistogram || - this.aggType == AggregationType.Base2ExponentialHistogramWithMinMax) - { - this.mpComponents = new MetricPointOptionalComponents(); - this.mpComponents.Base2ExponentialBucketHistogram = new Base2ExponentialBucketHistogram(exponentialHistogramMaxSize, exponentialHistogramMaxScale); - } - else - { - this.mpComponents = null; - } + reservoir = aggregatorStore.ExemplarReservoirFactory?.Invoke(); + } + catch + { + // TODO : Log that the factory on view threw an exception, once view exposes that capability + reservoir = null; + } - if (aggregatorStore!.IsExemplarEnabled() && reservoir == null) + if (this.aggType == AggregationType.HistogramWithBuckets || + this.aggType == AggregationType.HistogramWithMinMaxBuckets) + { + this.mpComponents = new MetricPointOptionalComponents(); + this.mpComponents.HistogramBuckets = new HistogramBuckets(histogramExplicitBounds); + if (isExemplarEnabled && reservoir == null) { - reservoir = new SimpleExemplarReservoir(DefaultSimpleReservoirPoolSize); + reservoir = new AlignedHistogramBucketExemplarReservoir(histogramExplicitBounds!.Length); } - - if (reservoir != null) + } + else if (this.aggType == AggregationType.Histogram || + this.aggType == AggregationType.HistogramWithMinMax) + { + this.mpComponents = new MetricPointOptionalComponents(); + this.mpComponents.HistogramBuckets = new HistogramBuckets(null); + } + else if (this.aggType == AggregationType.Base2ExponentialHistogram || + this.aggType == AggregationType.Base2ExponentialHistogramWithMinMax) + { + this.mpComponents = new MetricPointOptionalComponents(); + this.mpComponents.Base2ExponentialBucketHistogram = new Base2ExponentialBucketHistogram(exponentialHistogramMaxSize, exponentialHistogramMaxScale); + if (isExemplarEnabled && reservoir == null) { - if (this.mpComponents == null) - { - this.mpComponents = new MetricPointOptionalComponents(); - } - - this.mpComponents.ExemplarReservoir = reservoir; + reservoir = new SimpleFixedSizeExemplarReservoir(Math.Min(20, exponentialHistogramMaxSize)); } - - // Note: Intentionally set last because this is used to detect valid MetricPoints. - this.aggregatorStore = aggregatorStore; } - - /// - /// Gets the tags associated with the metric point. - /// - public readonly ReadOnlyTagCollection Tags + else { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get; + this.mpComponents = null; } - /// - /// Gets the start time (UTC) associated with the metric point. - /// - public readonly DateTimeOffset StartTime => this.aggregatorStore.StartTimeExclusive; - - /// - /// Gets the end time (UTC) associated with the metric point. - /// - public readonly DateTimeOffset EndTime => this.aggregatorStore.EndTimeInclusive; - - internal MetricPointStatus MetricPointStatus + if (isExemplarEnabled && reservoir == null) { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - readonly get; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private set; + reservoir = new SimpleFixedSizeExemplarReservoir(DefaultSimpleReservoirPoolSize); } - internal readonly bool IsInitialized => this.aggregatorStore != null; - - /// - /// Gets the sum long value associated with the metric point. - /// - /// - /// Applies to metric type. - /// - /// Long sum value. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly long GetSumLong() + if (reservoir != null) { - if (this.aggType != AggregationType.LongSumIncomingDelta && this.aggType != AggregationType.LongSumIncomingCumulative) + if (this.mpComponents == null) { - this.ThrowNotSupportedMetricTypeException(nameof(this.GetSumLong)); + this.mpComponents = new MetricPointOptionalComponents(); } - return this.snapshotValue.AsLong; + reservoir.Initialize(aggregatorStore); + + this.mpComponents.ExemplarReservoir = reservoir; } - /// - /// Gets the sum double value associated with the metric point. - /// - /// - /// Applies to metric type. - /// - /// Double sum value. + // Note: Intentionally set last because this is used to detect valid MetricPoints. + this.aggregatorStore = aggregatorStore; + } + + /// + /// Gets the tags associated with the metric point. + /// + public readonly ReadOnlyTagCollection Tags + { [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly double GetSumDouble() - { - if (this.aggType != AggregationType.DoubleSumIncomingDelta && this.aggType != AggregationType.DoubleSumIncomingCumulative) - { - this.ThrowNotSupportedMetricTypeException(nameof(this.GetSumDouble)); - } + get; + } - return this.snapshotValue.AsDouble; - } + /// + /// Gets the start time (UTC) associated with the metric point. + /// + public readonly DateTimeOffset StartTime => this.aggregatorStore.StartTimeExclusive; - /// - /// Gets the last long value of the gauge associated with the metric point. - /// - /// - /// Applies to metric type. - /// - /// Long gauge value. + /// + /// Gets the end time (UTC) associated with the metric point. + /// + public readonly DateTimeOffset EndTime => this.aggregatorStore.EndTimeInclusive; + + internal MetricPointStatus MetricPointStatus + { [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly long GetGaugeLastValueLong() + readonly get; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private set; + } + + // When the AggregatorStore is reclaiming MetricPoints, this serves the purpose of validating the a given thread is using the right + // MetricPoint for update by checking it against what as added in the Dictionary. Also, when a thread finds out that the MetricPoint + // that its using is already reclaimed, this helps avoid sorting of the tags for adding a new Dictionary entry. + // Snapshot method can use this to skip trying to reclaim indices which have already been reclaimed and added to the queue. + internal LookupData? LookupData { readonly get; private set; } + + internal readonly bool IsInitialized => this.aggregatorStore != null; + + /// + /// Gets the sum long value associated with the metric point. + /// + /// + /// Applies to metric type. + /// + /// Long sum value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly long GetSumLong() + { + if (this.aggType != AggregationType.LongSumIncomingDelta && this.aggType != AggregationType.LongSumIncomingCumulative) { - if (this.aggType != AggregationType.LongGauge) - { - this.ThrowNotSupportedMetricTypeException(nameof(this.GetGaugeLastValueLong)); - } + this.ThrowNotSupportedMetricTypeException(nameof(this.GetSumLong)); + } + + return this.snapshotValue.AsLong; + } - return this.snapshotValue.AsLong; + /// + /// Gets the sum double value associated with the metric point. + /// + /// + /// Applies to metric type. + /// + /// Double sum value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly double GetSumDouble() + { + if (this.aggType != AggregationType.DoubleSumIncomingDelta && this.aggType != AggregationType.DoubleSumIncomingCumulative) + { + this.ThrowNotSupportedMetricTypeException(nameof(this.GetSumDouble)); } - /// - /// Gets the last double value of the gauge associated with the metric point. - /// - /// - /// Applies to metric type. - /// - /// Double gauge value. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly double GetGaugeLastValueDouble() + return this.snapshotValue.AsDouble; + } + + /// + /// Gets the last long value of the gauge associated with the metric point. + /// + /// + /// Applies to metric type. + /// + /// Long gauge value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly long GetGaugeLastValueLong() + { + if (this.aggType != AggregationType.LongGauge) { - if (this.aggType != AggregationType.DoubleGauge) - { - this.ThrowNotSupportedMetricTypeException(nameof(this.GetGaugeLastValueDouble)); - } + this.ThrowNotSupportedMetricTypeException(nameof(this.GetGaugeLastValueLong)); + } + + return this.snapshotValue.AsLong; + } - return this.snapshotValue.AsDouble; + /// + /// Gets the last double value of the gauge associated with the metric point. + /// + /// + /// Applies to metric type. + /// + /// Double gauge value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly double GetGaugeLastValueDouble() + { + if (this.aggType != AggregationType.DoubleGauge) + { + this.ThrowNotSupportedMetricTypeException(nameof(this.GetGaugeLastValueDouble)); } - /// - /// Gets the count value of the histogram associated with the metric point. - /// - /// - /// Applies to metric type. - /// - /// Count value. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly long GetHistogramCount() + return this.snapshotValue.AsDouble; + } + + /// + /// Gets the count value of the histogram associated with the metric point. + /// + /// + /// Applies to metric type. + /// + /// Count value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly long GetHistogramCount() + { + if (this.aggType != AggregationType.HistogramWithBuckets && + this.aggType != AggregationType.Histogram && + this.aggType != AggregationType.HistogramWithMinMaxBuckets && + this.aggType != AggregationType.HistogramWithMinMax && + this.aggType != AggregationType.Base2ExponentialHistogram && + this.aggType != AggregationType.Base2ExponentialHistogramWithMinMax) { - if (this.aggType != AggregationType.HistogramWithBuckets && - this.aggType != AggregationType.Histogram && - this.aggType != AggregationType.HistogramWithMinMaxBuckets && - this.aggType != AggregationType.HistogramWithMinMax && - this.aggType != AggregationType.Base2ExponentialHistogram && - this.aggType != AggregationType.Base2ExponentialHistogramWithMinMax) - { - this.ThrowNotSupportedMetricTypeException(nameof(this.GetHistogramCount)); - } + this.ThrowNotSupportedMetricTypeException(nameof(this.GetHistogramCount)); + } + + return this.snapshotValue.AsLong; + } - return this.snapshotValue.AsLong; + /// + /// Gets the sum value of the histogram associated with the metric point. + /// + /// + /// Applies to metric type. + /// + /// Sum value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly double GetHistogramSum() + { + if (this.aggType != AggregationType.HistogramWithBuckets && + this.aggType != AggregationType.Histogram && + this.aggType != AggregationType.HistogramWithMinMaxBuckets && + this.aggType != AggregationType.HistogramWithMinMax && + this.aggType != AggregationType.Base2ExponentialHistogram && + this.aggType != AggregationType.Base2ExponentialHistogramWithMinMax) + { + this.ThrowNotSupportedMetricTypeException(nameof(this.GetHistogramSum)); } - /// - /// Gets the sum value of the histogram associated with the metric point. - /// - /// - /// Applies to metric type. - /// - /// Sum value. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly double GetHistogramSum() + Debug.Assert( + this.mpComponents?.HistogramBuckets != null + || this.mpComponents?.Base2ExponentialBucketHistogram != null, + "HistogramBuckets and Base2ExponentialBucketHistogram were both null"); + + return this.mpComponents!.HistogramBuckets != null + ? this.mpComponents.HistogramBuckets.SnapshotSum + : this.mpComponents.Base2ExponentialBucketHistogram!.SnapshotSum; + } + + /// + /// Gets the buckets of the histogram associated with the metric point. + /// + /// + /// Applies to metric type. + /// + /// . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly HistogramBuckets GetHistogramBuckets() + { + if (this.aggType != AggregationType.HistogramWithBuckets && + this.aggType != AggregationType.Histogram && + this.aggType != AggregationType.HistogramWithMinMaxBuckets && + this.aggType != AggregationType.HistogramWithMinMax) { - if (this.aggType != AggregationType.HistogramWithBuckets && - this.aggType != AggregationType.Histogram && - this.aggType != AggregationType.HistogramWithMinMaxBuckets && - this.aggType != AggregationType.HistogramWithMinMax && - this.aggType != AggregationType.Base2ExponentialHistogram && - this.aggType != AggregationType.Base2ExponentialHistogramWithMinMax) - { - this.ThrowNotSupportedMetricTypeException(nameof(this.GetHistogramSum)); - } + this.ThrowNotSupportedMetricTypeException(nameof(this.GetHistogramBuckets)); + } - return this.mpComponents!.HistogramBuckets != null - ? this.mpComponents.HistogramBuckets.SnapshotSum - : this.mpComponents.Base2ExponentialBucketHistogram.SnapshotSum; + Debug.Assert(this.mpComponents?.HistogramBuckets != null, "HistogramBuckets was null"); + + return this.mpComponents!.HistogramBuckets!; + } + + /// + /// Gets the exponential histogram data associated with the metric point. + /// + /// + /// Applies to metric type. + /// + /// . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ExponentialHistogramData GetExponentialHistogramData() + { + if (this.aggType != AggregationType.Base2ExponentialHistogram && + this.aggType != AggregationType.Base2ExponentialHistogramWithMinMax) + { + this.ThrowNotSupportedMetricTypeException(nameof(this.GetExponentialHistogramData)); } - /// - /// Gets the buckets of the histogram associated with the metric point. - /// - /// - /// Applies to metric type. - /// - /// . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly HistogramBuckets GetHistogramBuckets() + Debug.Assert(this.mpComponents?.Base2ExponentialBucketHistogram != null, "Base2ExponentialBucketHistogram was null"); + + return this.mpComponents!.Base2ExponentialBucketHistogram!.GetExponentialHistogramData(); + } + + /// + /// Gets the Histogram Min and Max values. + /// + /// The histogram minimum value. + /// The histogram maximum value. + /// True if minimum and maximum value exist, false otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool TryGetHistogramMinMaxValues(out double min, out double max) + { + if (this.aggType == AggregationType.HistogramWithMinMax + || this.aggType == AggregationType.HistogramWithMinMaxBuckets) { - if (this.aggType != AggregationType.HistogramWithBuckets && - this.aggType != AggregationType.Histogram && - this.aggType != AggregationType.HistogramWithMinMaxBuckets && - this.aggType != AggregationType.HistogramWithMinMax) - { - this.ThrowNotSupportedMetricTypeException(nameof(this.GetHistogramBuckets)); - } + Debug.Assert(this.mpComponents?.HistogramBuckets != null, "HistogramBuckets was null"); - return this.mpComponents!.HistogramBuckets; + min = this.mpComponents!.HistogramBuckets!.SnapshotMin; + max = this.mpComponents.HistogramBuckets.SnapshotMax; + return true; } - /// - /// Gets the exponential histogram data associated with the metric point. - /// - /// - /// Applies to metric type. - /// - /// . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ExponentialHistogramData GetExponentialHistogramData() + if (this.aggType == AggregationType.Base2ExponentialHistogramWithMinMax) { - if (this.aggType != AggregationType.Base2ExponentialHistogram && - this.aggType != AggregationType.Base2ExponentialHistogramWithMinMax) - { - this.ThrowNotSupportedMetricTypeException(nameof(this.GetExponentialHistogramData)); - } + Debug.Assert(this.mpComponents?.Base2ExponentialBucketHistogram != null, "Base2ExponentialBucketHistogram was null"); - return this.mpComponents!.Base2ExponentialBucketHistogram.GetExponentialHistogramData(); + min = this.mpComponents!.Base2ExponentialBucketHistogram!.SnapshotMin; + max = this.mpComponents.Base2ExponentialBucketHistogram.SnapshotMax; + return true; } - /// - /// Gets the Histogram Min and Max values. - /// - /// The histogram minimum value. - /// The histogram maximum value. - /// True if minimum and maximum value exist, false otherwise. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryGetHistogramMinMaxValues(out double min, out double max) + min = 0; + max = 0; + return false; + } + +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Gets the exemplars associated with the metric point. + /// + /// + /// . + /// if exemplars exist; otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal +#endif + readonly bool TryGetExemplars(out ReadOnlyExemplarCollection exemplars) + { + exemplars = this.mpComponents?.Exemplars ?? ReadOnlyExemplarCollection.Empty; + return exemplars.MaximumCount > 0; + } + + internal readonly MetricPoint Copy() + { + MetricPoint copy = this; + copy.mpComponents = this.mpComponents?.Copy(); + return copy; + } + + internal void Update(long number) + { + switch (this.aggType) { - if (this.aggType == AggregationType.HistogramWithMinMax || - this.aggType == AggregationType.HistogramWithMinMaxBuckets) - { - Debug.Assert(this.mpComponents!.HistogramBuckets != null, "histogramBuckets was null"); + case AggregationType.LongSumIncomingDelta: + { + Interlocked.Add(ref this.runningValue.AsLong, number); + break; + } - min = this.mpComponents!.HistogramBuckets!.SnapshotMin; - max = this.mpComponents!.HistogramBuckets!.SnapshotMax; - return true; - } + case AggregationType.LongSumIncomingCumulative: + { + Interlocked.Exchange(ref this.runningValue.AsLong, number); + break; + } - if (this.aggType == AggregationType.Base2ExponentialHistogramWithMinMax) - { - Debug.Assert(this.mpComponents!.Base2ExponentialBucketHistogram != null, "base2ExponentialBucketHistogram was null"); + case AggregationType.LongGauge: + { + Interlocked.Exchange(ref this.runningValue.AsLong, number); + break; + } - min = this.mpComponents!.Base2ExponentialBucketHistogram!.SnapshotMin; - max = this.mpComponents!.Base2ExponentialBucketHistogram!.SnapshotMax; - return true; - } + case AggregationType.Histogram: + { + this.UpdateHistogram((double)number); + break; + } + + case AggregationType.HistogramWithMinMax: + { + this.UpdateHistogramWithMinMax((double)number); + break; + } + + case AggregationType.HistogramWithBuckets: + { + this.UpdateHistogramWithBuckets((double)number); + break; + } - min = 0; - max = 0; - return false; + case AggregationType.HistogramWithMinMaxBuckets: + { + this.UpdateHistogramWithBucketsAndMinMax((double)number); + break; + } + + case AggregationType.Base2ExponentialHistogram: + { + this.UpdateBase2ExponentialHistogram((double)number); + break; + } + + case AggregationType.Base2ExponentialHistogramWithMinMax: + { + this.UpdateBase2ExponentialHistogramWithMinMax((double)number); + break; + } } - /// - /// Gets the exemplars associated with the metric point. - /// - /// . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly Exemplar[] GetExemplars() + this.CompleteUpdate(); + } + + internal void UpdateWithExemplar(long number, ReadOnlySpan> tags, bool isSampled) + { + Debug.Assert(this.mpComponents != null, "this.mpComponents was null"); + + switch (this.aggType) { - // TODO: Do not expose Exemplar data structure (array now) - return this.mpComponents?.Exemplars ?? Array.Empty(); + case AggregationType.LongSumIncomingDelta: + { + this.mpComponents!.AcquireLock(); + + unchecked + { + this.runningValue.AsLong += number; + } + + this.OfferExemplarIfSampled(number, tags, isSampled); + + this.mpComponents.ReleaseLock(); + + break; + } + + case AggregationType.LongSumIncomingCumulative: + { + this.mpComponents!.AcquireLock(); + + this.runningValue.AsLong = number; + + this.OfferExemplarIfSampled(number, tags, isSampled); + + this.mpComponents.ReleaseLock(); + + break; + } + + case AggregationType.LongGauge: + { + this.mpComponents!.AcquireLock(); + + this.runningValue.AsLong = number; + + this.OfferExemplarIfSampled(number, tags, isSampled); + + this.mpComponents.ReleaseLock(); + + break; + } + + case AggregationType.Histogram: + { + this.UpdateHistogram((double)number, tags, isSampled); + break; + } + + case AggregationType.HistogramWithMinMax: + { + this.UpdateHistogramWithMinMax((double)number, tags, isSampled); + break; + } + + case AggregationType.HistogramWithBuckets: + { + this.UpdateHistogramWithBuckets((double)number, tags, isSampled); + break; + } + + case AggregationType.HistogramWithMinMaxBuckets: + { + this.UpdateHistogramWithBucketsAndMinMax((double)number, tags, isSampled); + break; + } + + case AggregationType.Base2ExponentialHistogram: + { + this.UpdateBase2ExponentialHistogram((double)number, tags, isSampled); + break; + } + + case AggregationType.Base2ExponentialHistogramWithMinMax: + { + this.UpdateBase2ExponentialHistogramWithMinMax((double)number, tags, isSampled); + break; + } } - internal readonly MetricPoint Copy() + this.CompleteUpdate(); + } + + internal void Update(double number) + { + switch (this.aggType) { - MetricPoint copy = this; - copy.mpComponents = this.mpComponents?.Copy(); - return copy; + case AggregationType.DoubleSumIncomingDelta: + { + InterlockedHelper.Add(ref this.runningValue.AsDouble, number); + break; + } + + case AggregationType.DoubleSumIncomingCumulative: + { + Interlocked.Exchange(ref this.runningValue.AsDouble, number); + break; + } + + case AggregationType.DoubleGauge: + { + Interlocked.Exchange(ref this.runningValue.AsDouble, number); + break; + } + + case AggregationType.Histogram: + { + this.UpdateHistogram(number); + break; + } + + case AggregationType.HistogramWithMinMax: + { + this.UpdateHistogramWithMinMax(number); + break; + } + + case AggregationType.HistogramWithBuckets: + { + this.UpdateHistogramWithBuckets(number); + break; + } + + case AggregationType.HistogramWithMinMaxBuckets: + { + this.UpdateHistogramWithBucketsAndMinMax(number); + break; + } + + case AggregationType.Base2ExponentialHistogram: + { + this.UpdateBase2ExponentialHistogram(number); + break; + } + + case AggregationType.Base2ExponentialHistogramWithMinMax: + { + this.UpdateBase2ExponentialHistogramWithMinMax(number); + break; + } } - internal void Update(long number) + this.CompleteUpdate(); + } + + internal void UpdateWithExemplar(double number, ReadOnlySpan> tags, bool isSampled) + { + Debug.Assert(this.mpComponents != null, "this.mpComponents was null"); + + switch (this.aggType) { - switch (this.aggType) - { - case AggregationType.LongSumIncomingDelta: - { - Interlocked.Add(ref this.runningValue.AsLong, number); - break; - } + case AggregationType.DoubleSumIncomingDelta: + { + this.mpComponents!.AcquireLock(); - case AggregationType.LongSumIncomingCumulative: + unchecked { - Interlocked.Exchange(ref this.runningValue.AsLong, number); - break; + this.runningValue.AsDouble += number; } - case AggregationType.LongGauge: - { - Interlocked.Exchange(ref this.runningValue.AsLong, number); - break; - } + this.OfferExemplarIfSampled(number, tags, isSampled); - case AggregationType.Histogram: - { - this.UpdateHistogram((double)number); - break; - } + this.mpComponents.ReleaseLock(); - case AggregationType.HistogramWithMinMax: - { - this.UpdateHistogramWithMinMax((double)number); - break; - } + break; + } - case AggregationType.HistogramWithBuckets: - { - this.UpdateHistogramWithBuckets((double)number); - break; - } + case AggregationType.DoubleSumIncomingCumulative: + { + this.mpComponents!.AcquireLock(); - case AggregationType.HistogramWithMinMaxBuckets: + unchecked { - this.UpdateHistogramWithBucketsAndMinMax((double)number); - break; + this.runningValue.AsDouble = number; } - case AggregationType.Base2ExponentialHistogram: - { - this.UpdateBase2ExponentialHistogram((double)number); - break; - } + this.OfferExemplarIfSampled(number, tags, isSampled); + + this.mpComponents.ReleaseLock(); + + break; + } + + case AggregationType.DoubleGauge: + { + this.mpComponents!.AcquireLock(); - case AggregationType.Base2ExponentialHistogramWithMinMax: + unchecked { - this.UpdateBase2ExponentialHistogramWithMinMax((double)number); - break; + this.runningValue.AsDouble = number; } - } - // There is a race with Snapshot: - // Update() updates the value - // Snapshot snapshots the value - // Snapshot sets status to NoCollectPending - // Update sets status to CollectPending -- this is not right as the Snapshot - // already included the updated value. - // In the absence of any new Update call until next Snapshot, - // this results in exporting an Update even though - // it had no update. - // TODO: For Delta, this can be mitigated - // by ignoring Zero points - this.MetricPointStatus = MetricPointStatus.CollectPending; + this.OfferExemplarIfSampled(number, tags, isSampled); + + this.mpComponents.ReleaseLock(); + + break; + } + + case AggregationType.Histogram: + { + this.UpdateHistogram(number, tags, isSampled); + break; + } + + case AggregationType.HistogramWithMinMax: + { + this.UpdateHistogramWithMinMax(number, tags, isSampled); + break; + } + + case AggregationType.HistogramWithBuckets: + { + this.UpdateHistogramWithBuckets(number, tags, isSampled); + break; + } + + case AggregationType.HistogramWithMinMaxBuckets: + { + this.UpdateHistogramWithBucketsAndMinMax(number, tags, isSampled); + break; + } + + case AggregationType.Base2ExponentialHistogram: + { + this.UpdateBase2ExponentialHistogram(number, tags, isSampled); + break; + } + + case AggregationType.Base2ExponentialHistogramWithMinMax: + { + this.UpdateBase2ExponentialHistogramWithMinMax(number, tags, isSampled); + break; + } } - internal void UpdateWithExemplar(long number, ReadOnlySpan> tags, bool isSampled) + this.CompleteUpdate(); + } + + internal void TakeSnapshot(bool outputDelta) + { + switch (this.aggType) { - switch (this.aggType) - { - case AggregationType.LongSumIncomingDelta: + case AggregationType.LongSumIncomingDelta: + case AggregationType.LongSumIncomingCumulative: + { + if (outputDelta) { - var sw = default(SpinWait); - while (true) + long initValue = Interlocked.Read(ref this.runningValue.AsLong); + this.snapshotValue.AsLong = initValue - this.deltaLastValue.AsLong; + this.deltaLastValue.AsLong = initValue; + this.MetricPointStatus = MetricPointStatus.NoCollectPending; + + // Check again if value got updated, if yes reset status. + // This ensures no Updates get Lost. + if (initValue != Interlocked.Read(ref this.runningValue.AsLong)) { - if (Interlocked.Exchange(ref this.mpComponents!.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - unchecked - { - this.runningValue.AsLong += number; - } - - if (isSampled) - { - this.mpComponents.ExemplarReservoir.Offer(number, tags); - } - - // Release lock - Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 0); - break; - } - - sw.SpinOnce(); + this.MetricPointStatus = MetricPointStatus.CollectPending; } - - break; } - - case AggregationType.LongSumIncomingCumulative: + else { - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref this.mpComponents!.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - this.runningValue.AsLong = number; - this.mpComponents.ExemplarReservoir.Offer(number, tags); - - // Release lock - Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 0); - break; - } - - sw.SpinOnce(); - } - - break; + this.snapshotValue.AsLong = Interlocked.Read(ref this.runningValue.AsLong); } - case AggregationType.LongGauge: + break; + } + + case AggregationType.DoubleSumIncomingDelta: + case AggregationType.DoubleSumIncomingCumulative: + { + if (outputDelta) { - var sw = default(SpinWait); - while (true) + double initValue = InterlockedHelper.Read(ref this.runningValue.AsDouble); + this.snapshotValue.AsDouble = initValue - this.deltaLastValue.AsDouble; + this.deltaLastValue.AsDouble = initValue; + this.MetricPointStatus = MetricPointStatus.NoCollectPending; + + // Check again if value got updated, if yes reset status. + // This ensures no Updates get Lost. + if (initValue != InterlockedHelper.Read(ref this.runningValue.AsDouble)) { - if (Interlocked.Exchange(ref this.mpComponents!.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - this.runningValue.AsLong = number; - this.mpComponents.ExemplarReservoir.Offer(number, tags); - - // Release lock - Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 0); - break; - } - - sw.SpinOnce(); + this.MetricPointStatus = MetricPointStatus.CollectPending; } - - break; } - - case AggregationType.Histogram: + else { - this.UpdateHistogram((double)number, tags, true); - break; + this.snapshotValue.AsDouble = InterlockedHelper.Read(ref this.runningValue.AsDouble); } - case AggregationType.HistogramWithMinMax: - { - this.UpdateHistogramWithMinMax((double)number, tags, true); - break; - } + break; + } - case AggregationType.HistogramWithBuckets: + case AggregationType.LongGauge: + { + this.snapshotValue.AsLong = Interlocked.Read(ref this.runningValue.AsLong); + this.MetricPointStatus = MetricPointStatus.NoCollectPending; + + // Check again if value got updated, if yes reset status. + // This ensures no Updates get Lost. + if (this.snapshotValue.AsLong != Interlocked.Read(ref this.runningValue.AsLong)) { - this.UpdateHistogramWithBuckets((double)number, tags, true); - break; + this.MetricPointStatus = MetricPointStatus.CollectPending; } - case AggregationType.HistogramWithMinMaxBuckets: + break; + } + + case AggregationType.DoubleGauge: + { + this.snapshotValue.AsDouble = InterlockedHelper.Read(ref this.runningValue.AsDouble); + this.MetricPointStatus = MetricPointStatus.NoCollectPending; + + // Check again if value got updated, if yes reset status. + // This ensures no Updates get Lost. + if (this.snapshotValue.AsDouble != InterlockedHelper.Read(ref this.runningValue.AsDouble)) { - this.UpdateHistogramWithBucketsAndMinMax((double)number, tags, true); - break; + this.MetricPointStatus = MetricPointStatus.CollectPending; } - case AggregationType.Base2ExponentialHistogram: + break; + } + + case AggregationType.HistogramWithBuckets: + { + Debug.Assert(this.mpComponents?.HistogramBuckets != null, "HistogramBuckets was null"); + + var histogramBuckets = this.mpComponents!.HistogramBuckets!; + + this.mpComponents.AcquireLock(); + + this.snapshotValue.AsLong = this.runningValue.AsLong; + histogramBuckets.SnapshotSum = histogramBuckets.RunningSum; + + if (outputDelta) { - this.UpdateBase2ExponentialHistogram((double)number, tags, true); - break; + this.runningValue.AsLong = 0; + histogramBuckets.RunningSum = 0; } - case AggregationType.Base2ExponentialHistogramWithMinMax: + histogramBuckets.Snapshot(outputDelta); + + this.MetricPointStatus = MetricPointStatus.NoCollectPending; + + this.mpComponents.ReleaseLock(); + + break; + } + + case AggregationType.Histogram: + { + Debug.Assert(this.mpComponents?.HistogramBuckets != null, "HistogramBuckets was null"); + + var histogramBuckets = this.mpComponents!.HistogramBuckets!; + + this.mpComponents.AcquireLock(); + + this.snapshotValue.AsLong = this.runningValue.AsLong; + histogramBuckets.SnapshotSum = histogramBuckets.RunningSum; + + if (outputDelta) { - this.UpdateBase2ExponentialHistogramWithMinMax((double)number, tags, true); - break; + this.runningValue.AsLong = 0; + histogramBuckets.RunningSum = 0; } - } - // There is a race with Snapshot: - // Update() updates the value - // Snapshot snapshots the value - // Snapshot sets status to NoCollectPending - // Update sets status to CollectPending -- this is not right as the Snapshot - // already included the updated value. - // In the absence of any new Update call until next Snapshot, - // this results in exporting an Update even though - // it had no update. - // TODO: For Delta, this can be mitigated - // by ignoring Zero points - this.MetricPointStatus = MetricPointStatus.CollectPending; - } + this.MetricPointStatus = MetricPointStatus.NoCollectPending; - internal void Update(double number) - { - switch (this.aggType) - { - case AggregationType.DoubleSumIncomingDelta: - { - double initValue, newValue; - var sw = default(SpinWait); - while (true) - { - initValue = this.runningValue.AsDouble; + this.mpComponents.ReleaseLock(); - unchecked - { - newValue = initValue + number; - } + break; + } - if (initValue == Interlocked.CompareExchange(ref this.runningValue.AsDouble, newValue, initValue)) - { - break; - } + case AggregationType.HistogramWithMinMaxBuckets: + { + Debug.Assert(this.mpComponents?.HistogramBuckets != null, "HistogramBuckets was null"); - sw.SpinOnce(); - } + var histogramBuckets = this.mpComponents!.HistogramBuckets!; - break; - } + this.mpComponents.AcquireLock(); + + this.snapshotValue.AsLong = this.runningValue.AsLong; + histogramBuckets.SnapshotSum = histogramBuckets.RunningSum; + histogramBuckets.SnapshotMin = histogramBuckets.RunningMin; + histogramBuckets.SnapshotMax = histogramBuckets.RunningMax; - case AggregationType.DoubleSumIncomingCumulative: + if (outputDelta) { - Interlocked.Exchange(ref this.runningValue.AsDouble, number); - break; + this.runningValue.AsLong = 0; + histogramBuckets.RunningSum = 0; + histogramBuckets.RunningMin = double.PositiveInfinity; + histogramBuckets.RunningMax = double.NegativeInfinity; } - case AggregationType.DoubleGauge: + histogramBuckets.Snapshot(outputDelta); + + this.MetricPointStatus = MetricPointStatus.NoCollectPending; + + this.mpComponents.ReleaseLock(); + + break; + } + + case AggregationType.HistogramWithMinMax: + { + Debug.Assert(this.mpComponents?.HistogramBuckets != null, "HistogramBuckets was null"); + + var histogramBuckets = this.mpComponents!.HistogramBuckets!; + + this.mpComponents.AcquireLock(); + + this.snapshotValue.AsLong = this.runningValue.AsLong; + histogramBuckets.SnapshotSum = histogramBuckets.RunningSum; + histogramBuckets.SnapshotMin = histogramBuckets.RunningMin; + histogramBuckets.SnapshotMax = histogramBuckets.RunningMax; + + if (outputDelta) { - Interlocked.Exchange(ref this.runningValue.AsDouble, number); - break; + this.runningValue.AsLong = 0; + histogramBuckets.RunningSum = 0; + histogramBuckets.RunningMin = double.PositiveInfinity; + histogramBuckets.RunningMax = double.NegativeInfinity; } - case AggregationType.Histogram: + this.MetricPointStatus = MetricPointStatus.NoCollectPending; + + this.mpComponents.ReleaseLock(); + + break; + } + + case AggregationType.Base2ExponentialHistogram: + { + Debug.Assert(this.mpComponents?.Base2ExponentialBucketHistogram != null, "Base2ExponentialBucketHistogram was null"); + + var histogram = this.mpComponents!.Base2ExponentialBucketHistogram!; + + this.mpComponents.AcquireLock(); + + this.snapshotValue.AsLong = this.runningValue.AsLong; + histogram.SnapshotSum = histogram.RunningSum; + histogram.Snapshot(); + + if (outputDelta) { - this.UpdateHistogram(number); - break; + this.runningValue.AsLong = 0; + histogram.RunningSum = 0; + histogram.Reset(); } - case AggregationType.HistogramWithMinMax: + this.MetricPointStatus = MetricPointStatus.NoCollectPending; + + this.mpComponents.ReleaseLock(); + + break; + } + + case AggregationType.Base2ExponentialHistogramWithMinMax: + { + Debug.Assert(this.mpComponents?.Base2ExponentialBucketHistogram != null, "Base2ExponentialBucketHistogram was null"); + + var histogram = this.mpComponents!.Base2ExponentialBucketHistogram!; + + this.mpComponents.AcquireLock(); + + this.snapshotValue.AsLong = this.runningValue.AsLong; + histogram.SnapshotSum = histogram.RunningSum; + histogram.Snapshot(); + histogram.SnapshotMin = histogram.RunningMin; + histogram.SnapshotMax = histogram.RunningMax; + + if (outputDelta) { - this.UpdateHistogramWithMinMax(number); - break; + this.runningValue.AsLong = 0; + histogram.RunningSum = 0; + histogram.Reset(); + histogram.RunningMin = double.PositiveInfinity; + histogram.RunningMax = double.NegativeInfinity; } - case AggregationType.HistogramWithBuckets: + this.MetricPointStatus = MetricPointStatus.NoCollectPending; + + this.mpComponents.ReleaseLock(); + + break; + } + } + } + + internal void TakeSnapshotWithExemplar(bool outputDelta) + { + Debug.Assert(this.mpComponents != null, "this.mpComponents was null"); + Debug.Assert(this.mpComponents!.ExemplarReservoir != null, "this.mpComponents.ExemplarReservoir was null"); + + switch (this.aggType) + { + case AggregationType.LongSumIncomingDelta: + case AggregationType.LongSumIncomingCumulative: + { + this.mpComponents.AcquireLock(); + + if (outputDelta) { - this.UpdateHistogramWithBuckets(number); - break; + long initValue = this.runningValue.AsLong; + this.snapshotValue.AsLong = initValue - this.deltaLastValue.AsLong; + this.deltaLastValue.AsLong = initValue; + this.MetricPointStatus = MetricPointStatus.NoCollectPending; } - - case AggregationType.HistogramWithMinMaxBuckets: + else { - this.UpdateHistogramWithBucketsAndMinMax(number); - break; + this.snapshotValue.AsLong = this.runningValue.AsLong; } - case AggregationType.Base2ExponentialHistogram: + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir!.Collect(); + + this.mpComponents.ReleaseLock(); + + break; + } + + case AggregationType.DoubleSumIncomingDelta: + case AggregationType.DoubleSumIncomingCumulative: + { + this.mpComponents!.AcquireLock(); + + if (outputDelta) { - this.UpdateBase2ExponentialHistogram(number); - break; + double initValue = this.runningValue.AsDouble; + this.snapshotValue.AsDouble = initValue - this.deltaLastValue.AsDouble; + this.deltaLastValue.AsDouble = initValue; + this.MetricPointStatus = MetricPointStatus.NoCollectPending; } - - case AggregationType.Base2ExponentialHistogramWithMinMax: + else { - this.UpdateBase2ExponentialHistogramWithMinMax(number); - break; + this.snapshotValue.AsDouble = this.runningValue.AsDouble; } - } - // There is a race with Snapshot: - // Update() updates the value - // Snapshot snapshots the value - // Snapshot sets status to NoCollectPending - // Update sets status to CollectPending -- this is not right as the Snapshot - // already included the updated value. - // In the absence of any new Update call until next Snapshot, - // this results in exporting an Update even though - // it had no update. - // TODO: For Delta, this can be mitigated - // by ignoring Zero points - this.MetricPointStatus = MetricPointStatus.CollectPending; - } + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir!.Collect(); - internal void UpdateWithExemplar(double number, ReadOnlySpan> tags, bool isSampled) - { - switch (this.aggType) - { - case AggregationType.DoubleSumIncomingDelta: - { - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref this.mpComponents!.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - unchecked - { - this.runningValue.AsDouble += number; - } - - if (isSampled) - { - this.mpComponents.ExemplarReservoir.Offer(number, tags); - } - - // Release lock - Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 0); - break; - } - - sw.SpinOnce(); - } + this.mpComponents.ReleaseLock(); - break; - } + break; + } - case AggregationType.DoubleSumIncomingCumulative: - { - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref this.mpComponents!.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - unchecked - { - this.runningValue.AsDouble = number; - } - - this.mpComponents.ExemplarReservoir.Offer(number, tags); - - // Release lock - Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 0); - break; - } - - sw.SpinOnce(); - } + case AggregationType.LongGauge: + { + this.mpComponents.AcquireLock(); + + this.snapshotValue.AsLong = this.runningValue.AsLong; + this.MetricPointStatus = MetricPointStatus.NoCollectPending; + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir!.Collect(); + + this.mpComponents.ReleaseLock(); + + break; + } + + case AggregationType.DoubleGauge: + { + this.mpComponents.AcquireLock(); + + this.snapshotValue.AsDouble = this.runningValue.AsDouble; + this.MetricPointStatus = MetricPointStatus.NoCollectPending; + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir!.Collect(); + + this.mpComponents.ReleaseLock(); - break; - } + break; + } - case AggregationType.DoubleGauge: - { - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref this.mpComponents!.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - unchecked - { - this.runningValue.AsDouble = number; - } - - this.mpComponents.ExemplarReservoir.Offer(number, tags); - - // Release lock - Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 0); - break; - } - - sw.SpinOnce(); - } + case AggregationType.HistogramWithBuckets: + { + Debug.Assert(this.mpComponents.HistogramBuckets != null, "HistogramBuckets was null"); - break; - } + var histogramBuckets = this.mpComponents.HistogramBuckets!; - case AggregationType.Histogram: - { - this.UpdateHistogram(number, tags, true); - break; - } + this.mpComponents.AcquireLock(); - case AggregationType.HistogramWithMinMax: - { - this.UpdateHistogramWithMinMax(number, tags, true); - break; - } + this.snapshotValue.AsLong = this.runningValue.AsLong; + histogramBuckets.SnapshotSum = histogramBuckets.RunningSum; - case AggregationType.HistogramWithBuckets: + if (outputDelta) { - this.UpdateHistogramWithBuckets(number, tags, true); - break; + this.runningValue.AsLong = 0; + histogramBuckets.RunningSum = 0; } - case AggregationType.HistogramWithMinMaxBuckets: - { - this.UpdateHistogramWithBucketsAndMinMax(number, tags, true); - break; - } + histogramBuckets.Snapshot(outputDelta); - case AggregationType.Base2ExponentialHistogram: - { - this.UpdateBase2ExponentialHistogram(number, tags, true); - break; - } + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir!.Collect(); - case AggregationType.Base2ExponentialHistogramWithMinMax: - { - this.UpdateBase2ExponentialHistogramWithMinMax(number, tags, true); - break; - } - } + this.MetricPointStatus = MetricPointStatus.NoCollectPending; - // There is a race with Snapshot: - // Update() updates the value - // Snapshot snapshots the value - // Snapshot sets status to NoCollectPending - // Update sets status to CollectPending -- this is not right as the Snapshot - // already included the updated value. - // In the absence of any new Update call until next Snapshot, - // this results in exporting an Update even though - // it had no update. - // TODO: For Delta, this can be mitigated - // by ignoring Zero points - this.MetricPointStatus = MetricPointStatus.CollectPending; - } + this.mpComponents.ReleaseLock(); - internal void TakeSnapshot(bool outputDelta) - { - switch (this.aggType) - { - case AggregationType.LongSumIncomingDelta: - case AggregationType.LongSumIncomingCumulative: - { - if (outputDelta) - { - long initValue = Interlocked.Read(ref this.runningValue.AsLong); - this.snapshotValue.AsLong = initValue - this.deltaLastValue.AsLong; - this.deltaLastValue.AsLong = initValue; - this.MetricPointStatus = MetricPointStatus.NoCollectPending; - - // Check again if value got updated, if yes reset status. - // This ensures no Updates get Lost. - if (initValue != Interlocked.Read(ref this.runningValue.AsLong)) - { - this.MetricPointStatus = MetricPointStatus.CollectPending; - } - } - else - { - this.snapshotValue.AsLong = Interlocked.Read(ref this.runningValue.AsLong); - } + break; + } - break; - } + case AggregationType.Histogram: + { + Debug.Assert(this.mpComponents.HistogramBuckets != null, "HistogramBuckets was null"); - case AggregationType.DoubleSumIncomingDelta: - case AggregationType.DoubleSumIncomingCumulative: - { - if (outputDelta) - { - // TODO: - // Is this thread-safe way to read double? - // As long as the value is not -ve infinity, - // the exchange (to 0.0) will never occur, - // but we get the original value atomically. - double initValue = Interlocked.CompareExchange(ref this.runningValue.AsDouble, 0.0, double.NegativeInfinity); - this.snapshotValue.AsDouble = initValue - this.deltaLastValue.AsDouble; - this.deltaLastValue.AsDouble = initValue; - this.MetricPointStatus = MetricPointStatus.NoCollectPending; - - // Check again if value got updated, if yes reset status. - // This ensures no Updates get Lost. - if (initValue != Interlocked.CompareExchange(ref this.runningValue.AsDouble, 0.0, double.NegativeInfinity)) - { - this.MetricPointStatus = MetricPointStatus.CollectPending; - } - } - else - { - // TODO: - // Is this thread-safe way to read double? - // As long as the value is not -ve infinity, - // the exchange (to 0.0) will never occur, - // but we get the original value atomically. - this.snapshotValue.AsDouble = Interlocked.CompareExchange(ref this.runningValue.AsDouble, 0.0, double.NegativeInfinity); - } + var histogramBuckets = this.mpComponents.HistogramBuckets!; - break; - } + this.mpComponents.AcquireLock(); + + this.snapshotValue.AsLong = this.runningValue.AsLong; + histogramBuckets.SnapshotSum = histogramBuckets.RunningSum; - case AggregationType.LongGauge: + if (outputDelta) { - this.snapshotValue.AsLong = Interlocked.Read(ref this.runningValue.AsLong); - this.MetricPointStatus = MetricPointStatus.NoCollectPending; + this.runningValue.AsLong = 0; + histogramBuckets.RunningSum = 0; + } - // Check again if value got updated, if yes reset status. - // This ensures no Updates get Lost. - if (this.snapshotValue.AsLong != Interlocked.Read(ref this.runningValue.AsLong)) - { - this.MetricPointStatus = MetricPointStatus.CollectPending; - } + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir!.Collect(); + this.MetricPointStatus = MetricPointStatus.NoCollectPending; - break; - } + this.mpComponents.ReleaseLock(); - case AggregationType.DoubleGauge: - { - // TODO: - // Is this thread-safe way to read double? - // As long as the value is not -ve infinity, - // the exchange (to 0.0) will never occur, - // but we get the original value atomically. - this.snapshotValue.AsDouble = Interlocked.CompareExchange(ref this.runningValue.AsDouble, 0.0, double.NegativeInfinity); - this.MetricPointStatus = MetricPointStatus.NoCollectPending; + break; + } - // Check again if value got updated, if yes reset status. - // This ensures no Updates get Lost. - if (this.snapshotValue.AsDouble != Interlocked.CompareExchange(ref this.runningValue.AsDouble, 0.0, double.NegativeInfinity)) - { - this.MetricPointStatus = MetricPointStatus.CollectPending; - } + case AggregationType.HistogramWithMinMaxBuckets: + { + Debug.Assert(this.mpComponents.HistogramBuckets != null, "HistogramBuckets was null"); - break; - } + var histogramBuckets = this.mpComponents.HistogramBuckets!; - case AggregationType.HistogramWithBuckets: - { - var histogramBuckets = this.mpComponents!.HistogramBuckets; - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - this.snapshotValue.AsLong = this.runningValue.AsLong; - histogramBuckets.SnapshotSum = histogramBuckets.RunningSum; - - if (outputDelta) - { - this.runningValue.AsLong = 0; - histogramBuckets.RunningSum = 0; - } - - for (int i = 0; i < histogramBuckets.RunningBucketCounts.Length; i++) - { - histogramBuckets.SnapshotBucketCounts[i] = histogramBuckets.RunningBucketCounts[i]; - if (outputDelta) - { - histogramBuckets.RunningBucketCounts[i] = 0; - } - } - - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); - - this.MetricPointStatus = MetricPointStatus.NoCollectPending; - - // Release lock - Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0); - break; - } - - sw.SpinOnce(); - } + this.mpComponents.AcquireLock(); - break; - } + this.snapshotValue.AsLong = this.runningValue.AsLong; + histogramBuckets.SnapshotSum = histogramBuckets.RunningSum; + histogramBuckets.SnapshotMin = histogramBuckets.RunningMin; + histogramBuckets.SnapshotMax = histogramBuckets.RunningMax; - case AggregationType.Histogram: + if (outputDelta) { - var histogramBuckets = this.mpComponents!.HistogramBuckets; - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - this.snapshotValue.AsLong = this.runningValue.AsLong; - histogramBuckets.SnapshotSum = histogramBuckets.RunningSum; - - if (outputDelta) - { - this.runningValue.AsLong = 0; - histogramBuckets.RunningSum = 0; - } - - this.MetricPointStatus = MetricPointStatus.NoCollectPending; - - // Release lock - Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0); - break; - } - - sw.SpinOnce(); - } - - break; + this.runningValue.AsLong = 0; + histogramBuckets.RunningSum = 0; + histogramBuckets.RunningMin = double.PositiveInfinity; + histogramBuckets.RunningMax = double.NegativeInfinity; } - case AggregationType.HistogramWithMinMaxBuckets: - { - var histogramBuckets = this.mpComponents!.HistogramBuckets; - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - this.snapshotValue.AsLong = this.runningValue.AsLong; - histogramBuckets.SnapshotSum = histogramBuckets.RunningSum; - histogramBuckets.SnapshotMin = histogramBuckets.RunningMin; - histogramBuckets.SnapshotMax = histogramBuckets.RunningMax; - - if (outputDelta) - { - this.runningValue.AsLong = 0; - histogramBuckets.RunningSum = 0; - histogramBuckets.RunningMin = double.PositiveInfinity; - histogramBuckets.RunningMax = double.NegativeInfinity; - } - - for (int i = 0; i < histogramBuckets.RunningBucketCounts.Length; i++) - { - histogramBuckets.SnapshotBucketCounts[i] = histogramBuckets.RunningBucketCounts[i]; - if (outputDelta) - { - histogramBuckets.RunningBucketCounts[i] = 0; - } - } - - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); - this.MetricPointStatus = MetricPointStatus.NoCollectPending; - - // Release lock - Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0); - break; - } - - sw.SpinOnce(); - } + histogramBuckets.Snapshot(outputDelta); - break; - } + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir!.Collect(); + this.MetricPointStatus = MetricPointStatus.NoCollectPending; - case AggregationType.HistogramWithMinMax: - { - var histogramBuckets = this.mpComponents!.HistogramBuckets; - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - this.snapshotValue.AsLong = this.runningValue.AsLong; - histogramBuckets.SnapshotSum = histogramBuckets.RunningSum; - histogramBuckets.SnapshotMin = histogramBuckets.RunningMin; - histogramBuckets.SnapshotMax = histogramBuckets.RunningMax; - - if (outputDelta) - { - this.runningValue.AsLong = 0; - histogramBuckets.RunningSum = 0; - histogramBuckets.RunningMin = double.PositiveInfinity; - histogramBuckets.RunningMax = double.NegativeInfinity; - } - - this.MetricPointStatus = MetricPointStatus.NoCollectPending; - - // Release lock - Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0); - break; - } - - sw.SpinOnce(); - } + this.mpComponents.ReleaseLock(); - break; - } + break; + } - case AggregationType.Base2ExponentialHistogram: - { - var histogram = this.mpComponents!.Base2ExponentialBucketHistogram; - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref histogram.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - this.snapshotValue.AsLong = this.runningValue.AsLong; - histogram.SnapshotSum = histogram.RunningSum; - histogram.Snapshot(); - - if (outputDelta) - { - this.runningValue.AsLong = 0; - histogram.RunningSum = 0; - histogram.Reset(); - } - - this.MetricPointStatus = MetricPointStatus.NoCollectPending; - - // Release lock - Interlocked.Exchange(ref histogram.IsCriticalSectionOccupied, 0); - break; - } - - sw.SpinOnce(); - } + case AggregationType.HistogramWithMinMax: + { + Debug.Assert(this.mpComponents.HistogramBuckets != null, "HistogramBuckets was null"); - break; - } + var histogramBuckets = this.mpComponents.HistogramBuckets!; - case AggregationType.Base2ExponentialHistogramWithMinMax: - { - var histogram = this.mpComponents!.Base2ExponentialBucketHistogram; - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref histogram.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - this.snapshotValue.AsLong = this.runningValue.AsLong; - histogram.SnapshotSum = histogram.RunningSum; - histogram.Snapshot(); - histogram.SnapshotMin = histogram.RunningMin; - histogram.SnapshotMax = histogram.RunningMax; - - if (outputDelta) - { - this.runningValue.AsLong = 0; - histogram.RunningSum = 0; - histogram.Reset(); - histogram.RunningMin = double.PositiveInfinity; - histogram.RunningMax = double.NegativeInfinity; - } - - this.MetricPointStatus = MetricPointStatus.NoCollectPending; - - // Release lock - Interlocked.Exchange(ref histogram.IsCriticalSectionOccupied, 0); - break; - } - - sw.SpinOnce(); - } + this.mpComponents.AcquireLock(); - break; - } - } - } + this.snapshotValue.AsLong = this.runningValue.AsLong; + histogramBuckets.SnapshotSum = histogramBuckets.RunningSum; + histogramBuckets.SnapshotMin = histogramBuckets.RunningMin; + histogramBuckets.SnapshotMax = histogramBuckets.RunningMax; - internal void TakeSnapshotWithExemplar(bool outputDelta) - { - switch (this.aggType) - { - case AggregationType.LongSumIncomingDelta: - case AggregationType.LongSumIncomingCumulative: + if (outputDelta) { - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref this.mpComponents!.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - - if (outputDelta) - { - long initValue = this.runningValue.AsLong; - this.snapshotValue.AsLong = initValue - this.deltaLastValue.AsLong; - this.deltaLastValue.AsLong = initValue; - this.MetricPointStatus = MetricPointStatus.NoCollectPending; - } - else - { - this.snapshotValue.AsLong = this.runningValue.AsLong; - } - - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); - - // Release lock - Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 0); - break; - } - - sw.SpinOnce(); - } - - break; + this.runningValue.AsLong = 0; + histogramBuckets.RunningSum = 0; + histogramBuckets.RunningMin = double.PositiveInfinity; + histogramBuckets.RunningMax = double.NegativeInfinity; } - case AggregationType.DoubleSumIncomingDelta: - case AggregationType.DoubleSumIncomingCumulative: - { - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref this.mpComponents!.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - - if (outputDelta) - { - double initValue = this.runningValue.AsDouble; - this.snapshotValue.AsDouble = initValue - this.deltaLastValue.AsDouble; - this.deltaLastValue.AsDouble = initValue; - this.MetricPointStatus = MetricPointStatus.NoCollectPending; - } - else - { - this.snapshotValue.AsDouble = this.runningValue.AsDouble; - } - - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); - - // Release lock - Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 0); - break; - } - - sw.SpinOnce(); - } + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir!.Collect(); + this.MetricPointStatus = MetricPointStatus.NoCollectPending; - break; - } + this.mpComponents.ReleaseLock(); - case AggregationType.LongGauge: - { - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref this.mpComponents!.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired + break; + } - this.snapshotValue.AsLong = this.runningValue.AsLong; - this.MetricPointStatus = MetricPointStatus.NoCollectPending; - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); + case AggregationType.Base2ExponentialHistogram: + { + Debug.Assert(this.mpComponents.Base2ExponentialBucketHistogram != null, "Base2ExponentialBucketHistogram was null"); - // Release lock - Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 0); - break; - } + var histogram = this.mpComponents.Base2ExponentialBucketHistogram!; - sw.SpinOnce(); - } + this.mpComponents.AcquireLock(); - break; - } + this.snapshotValue.AsLong = this.runningValue.AsLong; + histogram.SnapshotSum = histogram.RunningSum; + histogram.Snapshot(); - case AggregationType.DoubleGauge: + if (outputDelta) { - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref this.mpComponents!.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired + this.runningValue.AsLong = 0; + histogram.RunningSum = 0; + histogram.Reset(); + } - this.snapshotValue.AsDouble = this.runningValue.AsDouble; - this.MetricPointStatus = MetricPointStatus.NoCollectPending; - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir!.Collect(); + this.MetricPointStatus = MetricPointStatus.NoCollectPending; - // Release lock - Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 0); - break; - } + this.mpComponents.ReleaseLock(); - sw.SpinOnce(); - } + break; + } - break; - } + case AggregationType.Base2ExponentialHistogramWithMinMax: + { + Debug.Assert(this.mpComponents.Base2ExponentialBucketHistogram != null, "Base2ExponentialBucketHistogram was null"); - case AggregationType.HistogramWithBuckets: - { - var histogramBuckets = this.mpComponents!.HistogramBuckets; - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - this.snapshotValue.AsLong = this.runningValue.AsLong; - histogramBuckets.SnapshotSum = histogramBuckets.RunningSum; - - if (outputDelta) - { - this.runningValue.AsLong = 0; - histogramBuckets.RunningSum = 0; - } - - for (int i = 0; i < histogramBuckets.RunningBucketCounts.Length; i++) - { - histogramBuckets.SnapshotBucketCounts[i] = histogramBuckets.RunningBucketCounts[i]; - if (outputDelta) - { - histogramBuckets.RunningBucketCounts[i] = 0; - } - } - - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); - - this.MetricPointStatus = MetricPointStatus.NoCollectPending; - - // Release lock - Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0); - break; - } - - sw.SpinOnce(); - } + var histogram = this.mpComponents.Base2ExponentialBucketHistogram!; - break; - } + this.mpComponents.AcquireLock(); - case AggregationType.Histogram: - { - var histogramBuckets = this.mpComponents!.HistogramBuckets; - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - this.snapshotValue.AsLong = this.runningValue.AsLong; - histogramBuckets.SnapshotSum = histogramBuckets.RunningSum; - - if (outputDelta) - { - this.runningValue.AsLong = 0; - histogramBuckets.RunningSum = 0; - } - - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); - this.MetricPointStatus = MetricPointStatus.NoCollectPending; - - // Release lock - Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0); - break; - } - - sw.SpinOnce(); - } + this.snapshotValue.AsLong = this.runningValue.AsLong; + histogram.SnapshotSum = histogram.RunningSum; + histogram.Snapshot(); + histogram.SnapshotMin = histogram.RunningMin; + histogram.SnapshotMax = histogram.RunningMax; - break; + if (outputDelta) + { + this.runningValue.AsLong = 0; + histogram.RunningSum = 0; + histogram.Reset(); + histogram.RunningMin = double.PositiveInfinity; + histogram.RunningMax = double.NegativeInfinity; } - case AggregationType.HistogramWithMinMaxBuckets: - { - var histogramBuckets = this.mpComponents!.HistogramBuckets; - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - this.snapshotValue.AsLong = this.runningValue.AsLong; - histogramBuckets.SnapshotSum = histogramBuckets.RunningSum; - histogramBuckets.SnapshotMin = histogramBuckets.RunningMin; - histogramBuckets.SnapshotMax = histogramBuckets.RunningMax; - - if (outputDelta) - { - this.runningValue.AsLong = 0; - histogramBuckets.RunningSum = 0; - histogramBuckets.RunningMin = double.PositiveInfinity; - histogramBuckets.RunningMax = double.NegativeInfinity; - } - - for (int i = 0; i < histogramBuckets.RunningBucketCounts.Length; i++) - { - histogramBuckets.SnapshotBucketCounts[i] = histogramBuckets.RunningBucketCounts[i]; - if (outputDelta) - { - histogramBuckets.RunningBucketCounts[i] = 0; - } - } - - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); - this.MetricPointStatus = MetricPointStatus.NoCollectPending; - - // Release lock - Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0); - break; - } - - sw.SpinOnce(); - } + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir!.Collect(); + this.MetricPointStatus = MetricPointStatus.NoCollectPending; - break; - } + this.mpComponents.ReleaseLock(); - case AggregationType.HistogramWithMinMax: - { - var histogramBuckets = this.mpComponents!.HistogramBuckets; - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - this.snapshotValue.AsLong = this.runningValue.AsLong; - histogramBuckets.SnapshotSum = histogramBuckets.RunningSum; - histogramBuckets.SnapshotMin = histogramBuckets.RunningMin; - histogramBuckets.SnapshotMax = histogramBuckets.RunningMax; - - if (outputDelta) - { - this.runningValue.AsLong = 0; - histogramBuckets.RunningSum = 0; - histogramBuckets.RunningMin = double.PositiveInfinity; - histogramBuckets.RunningMax = double.NegativeInfinity; - } - - this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); - this.MetricPointStatus = MetricPointStatus.NoCollectPending; - - // Release lock - Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0); - break; - } - - sw.SpinOnce(); - } + break; + } + } + } - break; - } + /// + /// Denote that this MetricPoint is reclaimed. + /// + internal void Reclaim() + { + this.LookupData = null; + this.mpComponents = null; + } - case AggregationType.Base2ExponentialHistogram: - { - var histogram = this.mpComponents!.Base2ExponentialBucketHistogram; - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref histogram.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - this.snapshotValue.AsLong = this.runningValue.AsLong; - histogram.SnapshotSum = histogram.RunningSum; - histogram.Snapshot(); - - if (outputDelta) - { - this.runningValue.AsLong = 0; - histogram.RunningSum = 0; - histogram.Reset(); - } - - this.MetricPointStatus = MetricPointStatus.NoCollectPending; - - // Release lock - Interlocked.Exchange(ref histogram.IsCriticalSectionOccupied, 0); - break; - } - - sw.SpinOnce(); - } + private void UpdateHistogram(double number, ReadOnlySpan> tags = default, bool isSampled = false) + { + Debug.Assert(this.mpComponents?.HistogramBuckets != null, "HistogramBuckets was null"); - break; - } + var histogramBuckets = this.mpComponents!.HistogramBuckets!; - case AggregationType.Base2ExponentialHistogramWithMinMax: - { - var histogram = this.mpComponents!.Base2ExponentialBucketHistogram; - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref histogram.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - this.snapshotValue.AsLong = this.runningValue.AsLong; - histogram.SnapshotSum = histogram.RunningSum; - histogram.Snapshot(); - histogram.SnapshotMin = histogram.RunningMin; - histogram.SnapshotMax = histogram.RunningMax; - - if (outputDelta) - { - this.runningValue.AsLong = 0; - histogram.RunningSum = 0; - histogram.Reset(); - histogram.RunningMin = double.PositiveInfinity; - histogram.RunningMax = double.NegativeInfinity; - } - - this.MetricPointStatus = MetricPointStatus.NoCollectPending; - - // Release lock - Interlocked.Exchange(ref histogram.IsCriticalSectionOccupied, 0); - break; - } - - sw.SpinOnce(); - } + this.mpComponents.AcquireLock(); - break; - } - } + unchecked + { + this.runningValue.AsLong++; + histogramBuckets.RunningSum += number; } - private void UpdateHistogram(double number, ReadOnlySpan> tags = default, bool reportExemplar = false) - { - var histogramBuckets = this.mpComponents!.HistogramBuckets; - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - unchecked - { - this.runningValue.AsLong++; - histogramBuckets.RunningSum += number; - } + this.OfferExemplarIfSampled(number, tags, isSampled); - if (reportExemplar) - { - this.mpComponents.ExemplarReservoir.Offer(number, tags); - } + this.mpComponents.ReleaseLock(); + } - // Release lock - Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0); - break; - } + private void UpdateHistogramWithMinMax(double number, ReadOnlySpan> tags = default, bool isSampled = false) + { + Debug.Assert(this.mpComponents?.HistogramBuckets != null, "HistogramBuckets was null"); - sw.SpinOnce(); - } - } + var histogramBuckets = this.mpComponents!.HistogramBuckets!; - private void UpdateHistogramWithMinMax(double number, ReadOnlySpan> tags = default, bool reportExemplar = false) + this.mpComponents.AcquireLock(); + + unchecked { - var histogramBuckets = this.mpComponents!.HistogramBuckets; - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - unchecked - { - this.runningValue.AsLong++; - histogramBuckets.RunningSum += number; - histogramBuckets.RunningMin = Math.Min(histogramBuckets.RunningMin, number); - histogramBuckets.RunningMax = Math.Max(histogramBuckets.RunningMax, number); - } + this.runningValue.AsLong++; + histogramBuckets.RunningSum += number; + histogramBuckets.RunningMin = Math.Min(histogramBuckets.RunningMin, number); + histogramBuckets.RunningMax = Math.Max(histogramBuckets.RunningMax, number); + } - if (reportExemplar) - { - this.mpComponents.ExemplarReservoir.Offer(number, tags); - } + this.OfferExemplarIfSampled(number, tags, isSampled); - // Release lock - Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0); - break; - } + this.mpComponents.ReleaseLock(); + } - sw.SpinOnce(); - } - } + private void UpdateHistogramWithBuckets(double number, ReadOnlySpan> tags = default, bool isSampled = false) + { + Debug.Assert(this.mpComponents?.HistogramBuckets != null, "HistogramBuckets was null"); + + var histogramBuckets = this.mpComponents!.HistogramBuckets; + + int bucketIndex = histogramBuckets!.FindBucketIndex(number); + + this.mpComponents.AcquireLock(); - private void UpdateHistogramWithBuckets(double number, ReadOnlySpan> tags = default, bool reportExemplar = false) + unchecked { - var histogramBuckets = this.mpComponents!.HistogramBuckets; - int i = histogramBuckets.FindBucketIndex(number); + this.runningValue.AsLong++; + histogramBuckets.RunningSum += number; + histogramBuckets.BucketCounts[bucketIndex].RunningValue++; + } - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - unchecked - { - this.runningValue.AsLong++; - histogramBuckets.RunningSum += number; - histogramBuckets.RunningBucketCounts[i]++; - if (reportExemplar) - { - this.mpComponents.ExemplarReservoir.Offer(number, tags, i); - } - } + this.OfferExplicitBucketHistogramExemplarIfSampled(number, tags, bucketIndex, isSampled); - // Release lock - Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0); - break; - } + this.mpComponents.ReleaseLock(); + } - sw.SpinOnce(); - } + private void UpdateHistogramWithBucketsAndMinMax(double number, ReadOnlySpan> tags = default, bool isSampled = false) + { + Debug.Assert(this.mpComponents?.HistogramBuckets != null, "histogramBuckets was null"); + + var histogramBuckets = this.mpComponents!.HistogramBuckets; + + int bucketIndex = histogramBuckets!.FindBucketIndex(number); + + this.mpComponents.AcquireLock(); + + unchecked + { + this.runningValue.AsLong++; + histogramBuckets.RunningSum += number; + histogramBuckets.BucketCounts[bucketIndex].RunningValue++; + + histogramBuckets.RunningMin = Math.Min(histogramBuckets.RunningMin, number); + histogramBuckets.RunningMax = Math.Max(histogramBuckets.RunningMax, number); } - private void UpdateHistogramWithBucketsAndMinMax(double number, ReadOnlySpan> tags = default, bool reportExemplar = false) + this.OfferExplicitBucketHistogramExemplarIfSampled(number, tags, bucketIndex, isSampled); + + this.mpComponents.ReleaseLock(); + } + + private void UpdateBase2ExponentialHistogram(double number, ReadOnlySpan> tags = default, bool isSampled = false) + { + if (number < 0) { - var histogramBuckets = this.mpComponents!.HistogramBuckets; - int i = histogramBuckets.FindBucketIndex(number); + return; + } - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - unchecked - { - this.runningValue.AsLong++; - histogramBuckets.RunningSum += number; - histogramBuckets.RunningBucketCounts[i]++; - if (reportExemplar) - { - this.mpComponents.ExemplarReservoir.Offer(number, tags, i); - } + Debug.Assert(this.mpComponents?.Base2ExponentialBucketHistogram != null, "Base2ExponentialBucketHistogram was null"); - histogramBuckets.RunningMin = Math.Min(histogramBuckets.RunningMin, number); - histogramBuckets.RunningMax = Math.Max(histogramBuckets.RunningMax, number); - } + var histogram = this.mpComponents!.Base2ExponentialBucketHistogram!; - // Release lock - Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0); - break; - } + this.mpComponents.AcquireLock(); - sw.SpinOnce(); - } + unchecked + { + this.runningValue.AsLong++; + histogram.RunningSum += number; + histogram.Record(number); } -#pragma warning disable IDE0060 // Remove unused parameter: Exemplars for exponential histograms will be a follow up PR - private void UpdateBase2ExponentialHistogram(double number, ReadOnlySpan> tags = default, bool reportExemplar = false) -#pragma warning restore IDE0060 // Remove unused parameter + this.OfferExemplarIfSampled(number, tags, isSampled); + + this.mpComponents.ReleaseLock(); + } + + private void UpdateBase2ExponentialHistogramWithMinMax(double number, ReadOnlySpan> tags = default, bool isSampled = false) + { + if (number < 0) { - if (number < 0) - { - return; - } + return; + } - var histogram = this.mpComponents!.Base2ExponentialBucketHistogram; + Debug.Assert(this.mpComponents?.Base2ExponentialBucketHistogram != null, "Base2ExponentialBucketHistogram was null"); - var sw = default(SpinWait); - while (true) - { - if (Interlocked.Exchange(ref histogram.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - unchecked - { - this.runningValue.AsLong++; - histogram.RunningSum += number; - histogram.Record(number); - } + var histogram = this.mpComponents!.Base2ExponentialBucketHistogram!; - // Release lock - Interlocked.Exchange(ref histogram.IsCriticalSectionOccupied, 0); - break; - } + this.mpComponents.AcquireLock(); - sw.SpinOnce(); - } + unchecked + { + this.runningValue.AsLong++; + histogram.RunningSum += number; + histogram.Record(number); + + histogram.RunningMin = Math.Min(histogram.RunningMin, number); + histogram.RunningMax = Math.Max(histogram.RunningMax, number); } -#pragma warning disable IDE0060 // Remove unused parameter: Exemplars for exponential histograms will be a follow up PR - private void UpdateBase2ExponentialHistogramWithMinMax(double number, ReadOnlySpan> tags = default, bool reportExemplar = false) -#pragma warning restore IDE0060 // Remove unused parameter + this.OfferExemplarIfSampled(number, tags, isSampled); + + this.mpComponents.ReleaseLock(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private readonly void OfferExemplarIfSampled(T number, ReadOnlySpan> tags, bool isSampled) + where T : struct + { + if (isSampled) { - if (number < 0) + Debug.Assert(this.mpComponents?.ExemplarReservoir != null, "ExemplarReservoir was null"); + + // TODO: Need to ensure that the lock is always released. + // A custom implementation of `ExemplarReservoir.Offer` might throw an exception. + if (typeof(T) == typeof(long)) { - return; + this.mpComponents!.ExemplarReservoir!.Offer( + new ExemplarMeasurement((long)(object)number, tags)); } - - var histogram = this.mpComponents!.Base2ExponentialBucketHistogram; - - var sw = default(SpinWait); - while (true) + else if (typeof(T) == typeof(double)) { - if (Interlocked.Exchange(ref histogram.IsCriticalSectionOccupied, 1) == 0) - { - // Lock acquired - unchecked - { - this.runningValue.AsLong++; - histogram.RunningSum += number; - histogram.Record(number); - - histogram.RunningMin = Math.Min(histogram.RunningMin, number); - histogram.RunningMax = Math.Max(histogram.RunningMax, number); - } + this.mpComponents!.ExemplarReservoir!.Offer( + new ExemplarMeasurement((double)(object)number, tags)); + } + else + { + Debug.Fail("Unexpected type"); + this.mpComponents!.ExemplarReservoir!.Offer( + new ExemplarMeasurement(Convert.ToDouble(number), tags)); + } + } + } - // Release lock - Interlocked.Exchange(ref histogram.IsCriticalSectionOccupied, 0); - break; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private readonly void OfferExplicitBucketHistogramExemplarIfSampled( + double number, + ReadOnlySpan> tags, + int bucketIndex, + bool isSampled) + { + if (isSampled) + { + Debug.Assert(this.mpComponents?.ExemplarReservoir != null, "ExemplarReservoir was null"); - sw.SpinOnce(); - } + // TODO: Need to ensure that the lock is always released. + // A custom implementation of `ExemplarReservoir.Offer` might throw an exception. + this.mpComponents!.ExemplarReservoir!.Offer( + new ExemplarMeasurement(number, tags, bucketIndex)); } + } - [MethodImpl(MethodImplOptions.NoInlining)] - private readonly void ThrowNotSupportedMetricTypeException(string methodName) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CompleteUpdate() + { + // There is a race with Snapshot: + // Update() updates the value + // Snapshot snapshots the value + // Snapshot sets status to NoCollectPending + // Update sets status to CollectPending -- this is not right as the Snapshot + // already included the updated value. + // In the absence of any new Update call until next Snapshot, + // this results in exporting an Update even though + // it had no update. + // TODO: For Delta, this can be mitigated + // by ignoring Zero points + this.MetricPointStatus = MetricPointStatus.CollectPending; + + if (this.aggregatorStore.OutputDeltaWithUnusedMetricPointReclaimEnabled) { - throw new NotSupportedException($"{methodName} is not supported for this metric type."); + Interlocked.Decrement(ref this.ReferenceCount); } } + + [MethodImpl(MethodImplOptions.NoInlining)] + private readonly void ThrowNotSupportedMetricTypeException(string methodName) + { + throw new NotSupportedException($"{methodName} is not supported for this metric type."); + } } diff --git a/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs b/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs index c9ec98ebbd7..440a9cc36b6 100644 --- a/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs +++ b/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs @@ -1,51 +1,66 @@ -// // 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. -// - -namespace OpenTelemetry.Metrics +// SPDX-License-Identifier: Apache-2.0 + +using System.Runtime.CompilerServices; + +namespace OpenTelemetry.Metrics; + +/// +/// Stores optional components of a metric point. +/// Histogram, Exemplar are current components. +/// ExponentialHistogram is a future component. +/// This is done to keep the MetricPoint (struct) +/// size in control. +/// +internal sealed class MetricPointOptionalComponents { - /// - /// Stores optional components of a metric point. - /// Histogram, Exemplar are current components. - /// ExponentialHistogram is a future component. - /// This is done to keep the MetricPoint (struct) - /// size in control. - /// - internal sealed class MetricPointOptionalComponents - { - public HistogramBuckets HistogramBuckets; + public HistogramBuckets? HistogramBuckets; + + public Base2ExponentialBucketHistogram? Base2ExponentialBucketHistogram; - public Base2ExponentialBucketHistogram Base2ExponentialBucketHistogram; + public ExemplarReservoir? ExemplarReservoir; + + public ReadOnlyExemplarCollection Exemplars = ReadOnlyExemplarCollection.Empty; + + private int isCriticalSectionOccupied = 0; + + public MetricPointOptionalComponents Copy() + { + MetricPointOptionalComponents copy = new MetricPointOptionalComponents + { + HistogramBuckets = this.HistogramBuckets?.Copy(), + Base2ExponentialBucketHistogram = this.Base2ExponentialBucketHistogram?.Copy(), + Exemplars = this.Exemplars.Copy(), + }; - public ExemplarReservoir ExemplarReservoir; + return copy; + } - public Exemplar[] Exemplars; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AcquireLock() + { + if (Interlocked.Exchange(ref this.isCriticalSectionOccupied, 1) != 0) + { + this.AcquireLockRare(); + } + } - public int IsCriticalSectionOccupied = 0; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReleaseLock() + { + Interlocked.Exchange(ref this.isCriticalSectionOccupied, 0); + } - internal MetricPointOptionalComponents Copy() + // Note: This method is marked as NoInlining because the whole point of it + // is to avoid the initialization of SpinWait unless it is needed. + [MethodImpl(MethodImplOptions.NoInlining)] + private void AcquireLockRare() + { + var sw = default(SpinWait); + do { - MetricPointOptionalComponents copy = new MetricPointOptionalComponents(); - copy.HistogramBuckets = this.HistogramBuckets?.Copy(); - copy.Base2ExponentialBucketHistogram = this.Base2ExponentialBucketHistogram?.Copy(); - if (this.Exemplars != null) - { - Array.Copy(this.Exemplars, copy.Exemplars, this.Exemplars.Length); - } - - return copy; + sw.SpinOnce(); } + while (Interlocked.Exchange(ref this.isCriticalSectionOccupied, 1) != 0); } } diff --git a/src/OpenTelemetry/Metrics/MetricPointStatus.cs b/src/OpenTelemetry/Metrics/MetricPointStatus.cs index 0ecd2a0c062..20667fb2575 100644 --- a/src/OpenTelemetry/Metrics/MetricPointStatus.cs +++ b/src/OpenTelemetry/Metrics/MetricPointStatus.cs @@ -1,33 +1,19 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +internal enum MetricPointStatus { - internal enum MetricPointStatus - { - /// - /// This status is applied to s with status after a Collect. - /// If an update occurs, status will be moved to . - /// - NoCollectPending, + /// + /// This status is applied to s with status after a Collect. + /// If an update occurs, status will be moved to . + /// + NoCollectPending, - /// - /// The has been updated since the previous Collect cycle. - /// Collect will move it to . - /// - CollectPending, - } + /// + /// The has been updated since the previous Collect cycle. + /// Collect will move it to . + /// + CollectPending, } diff --git a/src/OpenTelemetry/Metrics/MetricPointValueStorage.cs b/src/OpenTelemetry/Metrics/MetricPointValueStorage.cs index 778e97885bb..2e9ef64d95d 100644 --- a/src/OpenTelemetry/Metrics/MetricPointValueStorage.cs +++ b/src/OpenTelemetry/Metrics/MetricPointValueStorage.cs @@ -1,30 +1,16 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Runtime.InteropServices; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +[StructLayout(LayoutKind.Explicit)] +internal struct MetricPointValueStorage { - [StructLayout(LayoutKind.Explicit)] - internal struct MetricPointValueStorage - { - [FieldOffset(0)] - public long AsLong; + [FieldOffset(0)] + public long AsLong; - [FieldOffset(0)] - public double AsDouble; - } + [FieldOffset(0)] + public double AsDouble; } diff --git a/src/OpenTelemetry/Metrics/MetricPointsAccessor.cs b/src/OpenTelemetry/Metrics/MetricPointsAccessor.cs index 91fefd09bc1..b79c1ac7096 100644 --- a/src/OpenTelemetry/Metrics/MetricPointsAccessor.cs +++ b/src/OpenTelemetry/Metrics/MetricPointsAccessor.cs @@ -1,83 +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. -// +// SPDX-License-Identifier: Apache-2.0 -using OpenTelemetry.Internal; +using System.Diagnostics; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// A struct for accessing the s collected for a +/// . +/// +public readonly struct MetricPointsAccessor { + private readonly MetricPoint[] metricsPoints; + private readonly int[] metricPointsToProcess; + private readonly long targetCount; + + internal MetricPointsAccessor(MetricPoint[] metricsPoints, int[] metricPointsToProcess, long targetCount) + { + Debug.Assert(metricsPoints != null, "metricPoints was null"); + Debug.Assert(metricPointsToProcess != null, "metricPointsToProcess was null"); + + this.metricsPoints = metricsPoints!; + this.metricPointsToProcess = metricPointsToProcess!; + this.targetCount = targetCount; + } + /// - /// A struct for accessing the s collected for a - /// . + /// Returns an enumerator that iterates through the . /// - public readonly struct MetricPointsAccessor + /// . + public Enumerator GetEnumerator() + => new(this.metricsPoints, this.metricPointsToProcess, this.targetCount); + + /// + /// Enumerates the elements of a . + /// + public struct Enumerator { private readonly MetricPoint[] metricsPoints; private readonly int[] metricPointsToProcess; private readonly long targetCount; + private long index; - internal MetricPointsAccessor(MetricPoint[] metricsPoints, int[] metricPointsToProcess, long targetCount) + internal Enumerator(MetricPoint[] metricsPoints, int[] metricPointsToProcess, long targetCount) { - Guard.ThrowIfNull(metricsPoints); - this.metricsPoints = metricsPoints; this.metricPointsToProcess = metricPointsToProcess; this.targetCount = targetCount; + this.index = -1; } /// - /// Returns an enumerator that iterates through the . + /// Gets the at the current position of the enumerator. /// - /// . - public Enumerator GetEnumerator() - => new(this.metricsPoints, this.metricPointsToProcess, this.targetCount); + public readonly ref readonly MetricPoint Current + => ref this.metricsPoints[this.metricPointsToProcess[this.index]]; /// - /// Enumerates the elements of a . + /// Advances the enumerator to the next element of the . /// - public struct Enumerator - { - private readonly MetricPoint[] metricsPoints; - private readonly int[] metricPointsToProcess; - private readonly long targetCount; - private long index; - - internal Enumerator(MetricPoint[] metricsPoints, int[] metricPointsToProcess, long targetCount) - { - this.metricsPoints = metricsPoints; - this.metricPointsToProcess = metricPointsToProcess; - this.targetCount = targetCount; - this.index = -1; - } - - /// - /// Gets the at the current position of the enumerator. - /// - public ref readonly MetricPoint Current - => ref this.metricsPoints[this.metricPointsToProcess[this.index]]; - - /// - /// Advances the enumerator to the next element of the . - /// - /// if the enumerator was - /// successfully advanced to the next element; if the enumerator has passed the end of the - /// collection. - public bool MoveNext() - => ++this.index < this.targetCount; - } + /// if the enumerator was + /// successfully advanced to the next element; if the enumerator has passed the end of the + /// collection. + public bool MoveNext() + => ++this.index < this.targetCount; } } diff --git a/src/OpenTelemetry/Metrics/MetricReader.cs b/src/OpenTelemetry/Metrics/MetricReader.cs index c29b986edfc..decbf8ad70d 100644 --- a/src/OpenTelemetry/Metrics/MetricReader.cs +++ b/src/OpenTelemetry/Metrics/MetricReader.cs @@ -1,361 +1,345 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Diagnostics.Metrics; using OpenTelemetry.Internal; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// MetricReader base class. +/// +public abstract partial class MetricReader : IDisposable { - /// - /// MetricReader base class. - /// - public abstract partial class MetricReader : IDisposable - { - private const MetricReaderTemporalityPreference MetricReaderTemporalityPreferenceUnspecified = (MetricReaderTemporalityPreference)0; + private const MetricReaderTemporalityPreference MetricReaderTemporalityPreferenceUnspecified = (MetricReaderTemporalityPreference)0; - private static Func cumulativeTemporalityPreferenceFunc = - (instrumentType) => AggregationTemporality.Cumulative; + private static readonly Func CumulativeTemporalityPreferenceFunc = + (instrumentType) => AggregationTemporality.Cumulative; - private static Func monotonicDeltaTemporalityPreferenceFunc = (instrumentType) => + private static readonly Func MonotonicDeltaTemporalityPreferenceFunc = (instrumentType) => + { + return instrumentType.GetGenericTypeDefinition() switch { - return instrumentType.GetGenericTypeDefinition() switch - { - var type when type == typeof(Counter<>) => AggregationTemporality.Delta, - var type when type == typeof(ObservableCounter<>) => AggregationTemporality.Delta, - var type when type == typeof(Histogram<>) => AggregationTemporality.Delta, + var type when type == typeof(Counter<>) => AggregationTemporality.Delta, + var type when type == typeof(ObservableCounter<>) => AggregationTemporality.Delta, + var type when type == typeof(Histogram<>) => AggregationTemporality.Delta, - // Temporality is not defined for gauges, so this does not really affect anything. - var type when type == typeof(ObservableGauge<>) => AggregationTemporality.Delta, + // Temporality is not defined for gauges, so this does not really affect anything. + var type when type == typeof(ObservableGauge<>) => AggregationTemporality.Delta, - var type when type == typeof(UpDownCounter<>) => AggregationTemporality.Cumulative, - var type when type == typeof(ObservableUpDownCounter<>) => AggregationTemporality.Cumulative, + var type when type == typeof(UpDownCounter<>) => AggregationTemporality.Cumulative, + var type when type == typeof(ObservableUpDownCounter<>) => AggregationTemporality.Cumulative, - // TODO: Consider logging here because we should not fall through to this case. - _ => AggregationTemporality.Delta, - }; + // TODO: Consider logging here because we should not fall through to this case. + _ => AggregationTemporality.Delta, }; + }; - private readonly object newTaskLock = new(); - private readonly object onCollectLock = new(); - private readonly TaskCompletionSource shutdownTcs = new(); - private MetricReaderTemporalityPreference temporalityPreference = MetricReaderTemporalityPreferenceUnspecified; - private Func temporalityFunc = cumulativeTemporalityPreferenceFunc; - private int shutdownCount; - private TaskCompletionSource collectionTcs; - private BaseProvider parentProvider; + private readonly object newTaskLock = new(); + private readonly object onCollectLock = new(); + private readonly TaskCompletionSource shutdownTcs = new(); + private MetricReaderTemporalityPreference temporalityPreference = MetricReaderTemporalityPreferenceUnspecified; + private Func temporalityFunc = CumulativeTemporalityPreferenceFunc; + private int shutdownCount; + private TaskCompletionSource? collectionTcs; + private BaseProvider? parentProvider; - public MetricReaderTemporalityPreference TemporalityPreference + /// + /// Gets or sets the metric reader temporality preference. + /// + public MetricReaderTemporalityPreference TemporalityPreference + { + get { - get + if (this.temporalityPreference == MetricReaderTemporalityPreferenceUnspecified) { - if (this.temporalityPreference == MetricReaderTemporalityPreferenceUnspecified) - { - this.temporalityPreference = MetricReaderTemporalityPreference.Cumulative; - } - - return this.temporalityPreference; + this.temporalityPreference = MetricReaderTemporalityPreference.Cumulative; } - set - { - if (this.temporalityPreference != MetricReaderTemporalityPreferenceUnspecified) - { - throw new NotSupportedException($"The temporality preference cannot be modified (the current value is {this.temporalityPreference})."); - } - - this.temporalityPreference = value; - switch (value) - { - case MetricReaderTemporalityPreference.Delta: - this.temporalityFunc = monotonicDeltaTemporalityPreferenceFunc; - break; - case MetricReaderTemporalityPreference.Cumulative: - default: - this.temporalityFunc = cumulativeTemporalityPreferenceFunc; - break; - } - } + return this.temporalityPreference; } - /// - /// Attempts to collect the metrics, blocks the current thread until - /// metrics collection completed, shutdown signaled or timed out. - /// If there are asynchronous instruments involved, their callback - /// functions will be triggered. - /// - /// - /// The number (non-negative) of milliseconds to wait, or - /// Timeout.Infinite to wait indefinitely. - /// - /// - /// Returns true when metrics collection succeeded; otherwise, - /// false. - /// - /// - /// Thrown when the timeoutMilliseconds is smaller than -1. - /// - /// - /// This function guarantees thread-safety. If multiple calls occurred - /// simultaneously, they might get folded and result in less calls to - /// the OnCollect callback for improved performance, as long as - /// the semantic can be preserved. - /// - public bool Collect(int timeoutMilliseconds = Timeout.Infinite) + set { - Guard.ThrowIfInvalidTimeout(timeoutMilliseconds); - - OpenTelemetrySdkEventSource.Log.MetricReaderEvent("MetricReader.Collect method called."); - var shouldRunCollect = false; - var tcs = this.collectionTcs; - - if (tcs == null) + if (this.temporalityPreference != MetricReaderTemporalityPreferenceUnspecified) { - lock (this.newTaskLock) - { - tcs = this.collectionTcs; - - if (tcs == null) - { - shouldRunCollect = true; - tcs = new TaskCompletionSource(); - this.collectionTcs = tcs; - } - } + throw new NotSupportedException($"The temporality preference cannot be modified (the current value is {this.temporalityPreference})."); } - if (!shouldRunCollect) + this.temporalityPreference = value; + this.temporalityFunc = value switch { - return Task.WaitAny(tcs.Task, this.shutdownTcs.Task, Task.Delay(timeoutMilliseconds)) == 0 && tcs.Task.Result; - } + MetricReaderTemporalityPreference.Delta => MonotonicDeltaTemporalityPreferenceFunc, + _ => CumulativeTemporalityPreferenceFunc, + }; + } + } + + /// + /// Attempts to collect the metrics, blocks the current thread until + /// metrics collection completed, shutdown signaled or timed out. + /// If there are asynchronous instruments involved, their callback + /// functions will be triggered. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when metrics collection succeeded; otherwise, + /// false. + /// + /// + /// Thrown when the timeoutMilliseconds is smaller than -1. + /// + /// + /// This function guarantees thread-safety. If multiple calls occurred + /// simultaneously, they might get folded and result in less calls to + /// the OnCollect callback for improved performance, as long as + /// the semantic can be preserved. + /// + public bool Collect(int timeoutMilliseconds = Timeout.Infinite) + { + Guard.ThrowIfInvalidTimeout(timeoutMilliseconds); + + OpenTelemetrySdkEventSource.Log.MetricReaderEvent("MetricReader.Collect method called."); + var shouldRunCollect = false; + var tcs = this.collectionTcs; - var result = false; - try + if (tcs == null) + { + lock (this.newTaskLock) { - lock (this.onCollectLock) + tcs = this.collectionTcs; + + if (tcs == null) { - this.collectionTcs = null; - result = this.OnCollect(timeoutMilliseconds); + shouldRunCollect = true; + tcs = new TaskCompletionSource(); + this.collectionTcs = tcs; } } - catch (Exception ex) - { - OpenTelemetrySdkEventSource.Log.MetricReaderException(nameof(this.Collect), ex); - } + } - tcs.TrySetResult(result); + if (!shouldRunCollect) + { + return Task.WaitAny(tcs.Task, this.shutdownTcs.Task, Task.Delay(timeoutMilliseconds)) == 0 && tcs.Task.Result; + } - if (result) - { - OpenTelemetrySdkEventSource.Log.MetricReaderEvent("MetricReader.Collect succeeded."); - } - else + var result = false; + try + { + lock (this.onCollectLock) { - OpenTelemetrySdkEventSource.Log.MetricReaderEvent("MetricReader.Collect failed."); + this.collectionTcs = null; + result = this.OnCollect(timeoutMilliseconds); } - - return result; } - - /// - /// Attempts to shutdown the processor, blocks the current thread until - /// shutdown completed or timed out. - /// - /// - /// The number (non-negative) of milliseconds to wait, or - /// Timeout.Infinite to wait indefinitely. - /// - /// - /// Returns true when shutdown succeeded; otherwise, false. - /// - /// - /// Thrown when the timeoutMilliseconds is smaller than -1. - /// - /// - /// This function guarantees thread-safety. Only the first call will - /// win, subsequent calls will be no-op. - /// - public bool Shutdown(int timeoutMilliseconds = Timeout.Infinite) + catch (Exception ex) { - Guard.ThrowIfInvalidTimeout(timeoutMilliseconds); + OpenTelemetrySdkEventSource.Log.MetricReaderException(nameof(this.Collect), ex); + } - OpenTelemetrySdkEventSource.Log.MetricReaderEvent("MetricReader.Shutdown called."); + tcs.TrySetResult(result); - if (Interlocked.CompareExchange(ref this.shutdownCount, 1, 0) != 0) - { - return false; // shutdown already called - } + if (result) + { + OpenTelemetrySdkEventSource.Log.MetricReaderEvent("MetricReader.Collect succeeded."); + } + else + { + OpenTelemetrySdkEventSource.Log.MetricReaderEvent("MetricReader.Collect failed."); + } - var result = false; - try - { - result = this.OnShutdown(timeoutMilliseconds); - } - catch (Exception ex) - { - OpenTelemetrySdkEventSource.Log.MetricReaderException(nameof(this.Shutdown), ex); - } + return result; + } - this.shutdownTcs.TrySetResult(result); + /// + /// Attempts to shutdown the processor, blocks the current thread until + /// shutdown completed or timed out. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when shutdown succeeded; otherwise, false. + /// + /// + /// Thrown when the timeoutMilliseconds is smaller than -1. + /// + /// + /// This function guarantees thread-safety. Only the first call will + /// win, subsequent calls will be no-op. + /// + public bool Shutdown(int timeoutMilliseconds = Timeout.Infinite) + { + Guard.ThrowIfInvalidTimeout(timeoutMilliseconds); - if (result) - { - OpenTelemetrySdkEventSource.Log.MetricReaderEvent("MetricReader.Shutdown succeeded."); - } - else - { - OpenTelemetrySdkEventSource.Log.MetricReaderEvent("MetricReader.Shutdown failed."); - } + OpenTelemetrySdkEventSource.Log.MetricReaderEvent("MetricReader.Shutdown called."); - return result; + if (Interlocked.CompareExchange(ref this.shutdownCount, 1, 0) != 0) + { + return false; // shutdown already called } - /// - public void Dispose() + var result = false; + try { - this.Dispose(true); - GC.SuppressFinalize(this); + result = this.OnShutdown(timeoutMilliseconds); } - - internal virtual void SetParentProvider(BaseProvider parentProvider) + catch (Exception ex) { - this.parentProvider = parentProvider; + OpenTelemetrySdkEventSource.Log.MetricReaderException(nameof(this.Shutdown), ex); } - /// - /// Processes a batch of metrics. - /// - /// Batch of metrics to be processed. - /// - /// The number (non-negative) of milliseconds to wait, or - /// Timeout.Infinite to wait indefinitely. - /// - /// - /// Returns true when metrics processing succeeded; otherwise, - /// false. - /// - internal virtual bool ProcessMetrics(in Batch metrics, int timeoutMilliseconds) + this.shutdownTcs.TrySetResult(result); + + if (result) { - return true; + OpenTelemetrySdkEventSource.Log.MetricReaderEvent("MetricReader.Shutdown succeeded."); } - - /// - /// Called by Collect. This function should block the current - /// thread until metrics collection completed, shutdown signaled or - /// timed out. - /// - /// - /// The number (non-negative) of milliseconds to wait, or - /// Timeout.Infinite to wait indefinitely. - /// - /// - /// Returns true when metrics collection succeeded; otherwise, - /// false. - /// - /// - /// This function is called synchronously on the threads which called - /// Collect. This function should not throw exceptions. - /// - protected virtual bool OnCollect(int timeoutMilliseconds) + else { - OpenTelemetrySdkEventSource.Log.MetricReaderEvent("MetricReader.OnCollect called."); + OpenTelemetrySdkEventSource.Log.MetricReaderEvent("MetricReader.Shutdown failed."); + } - var sw = timeoutMilliseconds == Timeout.Infinite - ? null - : Stopwatch.StartNew(); + return result; + } - var meterProviderSdk = this.parentProvider as MeterProviderSdk; - meterProviderSdk?.CollectObservableInstruments(); + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } - OpenTelemetrySdkEventSource.Log.MetricReaderEvent("Observable instruments collected."); + internal virtual void SetParentProvider(BaseProvider parentProvider) + { + this.parentProvider = parentProvider; + } - var metrics = this.GetMetricsBatch(); + /// + /// Processes a batch of metrics. + /// + /// Batch of metrics to be processed. + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when metrics processing succeeded; otherwise, + /// false. + /// + internal virtual bool ProcessMetrics(in Batch metrics, int timeoutMilliseconds) + { + return true; + } - bool result; - if (sw == null) - { - OpenTelemetrySdkEventSource.Log.MetricReaderEvent("ProcessMetrics called."); - result = this.ProcessMetrics(metrics, Timeout.Infinite); - if (result) - { - OpenTelemetrySdkEventSource.Log.MetricReaderEvent("ProcessMetrics succeeded."); - } - else - { - OpenTelemetrySdkEventSource.Log.MetricReaderEvent("ProcessMetrics failed."); - } + /// + /// Called by Collect. This function should block the current + /// thread until metrics collection completed, shutdown signaled or + /// timed out. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when metrics collection succeeded; otherwise, + /// false. + /// + /// + /// This function is called synchronously on the threads which called + /// Collect. This function should not throw exceptions. + /// + protected virtual bool OnCollect(int timeoutMilliseconds) + { + OpenTelemetrySdkEventSource.Log.MetricReaderEvent("MetricReader.OnCollect called."); + + var sw = timeoutMilliseconds == Timeout.Infinite + ? null + : Stopwatch.StartNew(); + + var meterProviderSdk = this.parentProvider as MeterProviderSdk; + meterProviderSdk?.CollectObservableInstruments(); - return result; + OpenTelemetrySdkEventSource.Log.MetricReaderEvent("Observable instruments collected."); + + var metrics = this.GetMetricsBatch(); + + bool result; + if (sw == null) + { + OpenTelemetrySdkEventSource.Log.MetricReaderEvent("ProcessMetrics called."); + result = this.ProcessMetrics(metrics, Timeout.Infinite); + if (result) + { + OpenTelemetrySdkEventSource.Log.MetricReaderEvent("ProcessMetrics succeeded."); } else { - var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; + OpenTelemetrySdkEventSource.Log.MetricReaderEvent("ProcessMetrics failed."); + } - if (timeout <= 0) - { - OpenTelemetrySdkEventSource.Log.MetricReaderEvent("OnCollect failed timeout period has elapsed."); - return false; - } + return result; + } + else + { + var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; - OpenTelemetrySdkEventSource.Log.MetricReaderEvent("ProcessMetrics called."); - result = this.ProcessMetrics(metrics, (int)timeout); - if (result) - { - OpenTelemetrySdkEventSource.Log.MetricReaderEvent("ProcessMetrics succeeded."); - } - else - { - OpenTelemetrySdkEventSource.Log.MetricReaderEvent("ProcessMetrics failed."); - } + if (timeout <= 0) + { + OpenTelemetrySdkEventSource.Log.MetricReaderEvent("OnCollect failed timeout period has elapsed."); + return false; + } - return result; + OpenTelemetrySdkEventSource.Log.MetricReaderEvent("ProcessMetrics called."); + result = this.ProcessMetrics(metrics, (int)timeout); + if (result) + { + OpenTelemetrySdkEventSource.Log.MetricReaderEvent("ProcessMetrics succeeded."); + } + else + { + OpenTelemetrySdkEventSource.Log.MetricReaderEvent("ProcessMetrics failed."); } - } - /// - /// Called by Shutdown. This function should block the current - /// thread until shutdown completed or timed out. - /// - /// - /// The number (non-negative) of milliseconds to wait, or - /// Timeout.Infinite to wait indefinitely. - /// - /// - /// Returns true when shutdown succeeded; otherwise, false. - /// - /// - /// This function is called synchronously on the thread which made the - /// first call to Shutdown. This function should not throw - /// exceptions. - /// - protected virtual bool OnShutdown(int timeoutMilliseconds) - { - return this.Collect(timeoutMilliseconds); + return result; } + } - /// - /// Releases the unmanaged resources used by this class and optionally - /// releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) - { - } + /// + /// Called by Shutdown. This function should block the current + /// thread until shutdown completed or timed out. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when shutdown succeeded; otherwise, false. + /// + /// + /// This function is called synchronously on the thread which made the + /// first call to Shutdown. This function should not throw + /// exceptions. + /// + protected virtual bool OnShutdown(int timeoutMilliseconds) + { + return this.Collect(timeoutMilliseconds); + } + + /// + /// Releases the unmanaged resources used by this class and optionally + /// releases the managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { } } diff --git a/src/OpenTelemetry/Metrics/MetricReaderExt.cs b/src/OpenTelemetry/Metrics/MetricReaderExt.cs index 6b1fb2c48d4..b64938e4c39 100644 --- a/src/OpenTelemetry/Metrics/MetricReaderExt.cs +++ b/src/OpenTelemetry/Metrics/MetricReaderExt.cs @@ -1,278 +1,268 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Collections.Concurrent; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; using OpenTelemetry.Internal; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// MetricReader base class. +/// +public abstract partial class MetricReader { - /// - /// MetricReader base class. - /// - public abstract partial class MetricReader + private readonly HashSet metricStreamNames = new(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary instrumentIdentityToMetric = new(); + private readonly object instrumentCreationLock = new(); + private int metricLimit; + private int cardinalityLimit; + private Metric?[]? metrics; + private Metric[]? metricsCurrentBatch; + private int metricIndex = -1; + private bool emitOverflowAttribute; + private bool reclaimUnusedMetricPoints; + private ExemplarFilterType? exemplarFilter; + + internal static void DeactivateMetric(Metric metric) { - private readonly HashSet metricStreamNames = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary instrumentIdentityToMetric = new(); - private readonly object instrumentCreationLock = new(); - private int maxMetricStreams; - private int maxMetricPointsPerMetricStream; - private Metric[] metrics; - private Metric[] metricsCurrentBatch; - private int metricIndex = -1; - - private ExemplarFilter exemplarFilter; - - internal AggregationTemporality GetAggregationTemporality(Type instrumentType) + if (metric.Active) { - return this.temporalityFunc(instrumentType); + // 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); } + } + + internal AggregationTemporality GetAggregationTemporality(Type instrumentType) + { + return this.temporalityFunc(instrumentType); + } - internal Metric AddMetricWithNoViews(Instrument instrument) + internal virtual List AddMetricWithNoViews(Instrument instrument) + { + Debug.Assert(instrument != null, "instrument was null"); + Debug.Assert(this.metrics != null, "this.metrics was null"); + + var metricStreamIdentity = new MetricStreamIdentity(instrument!, metricStreamConfiguration: null); + lock (this.instrumentCreationLock) { - var metricStreamIdentity = new MetricStreamIdentity(instrument, metricStreamConfiguration: null); - lock (this.instrumentCreationLock) + if (this.TryGetExistingMetric(in metricStreamIdentity, out var existingMetric)) + { + return new() { existingMetric }; + } + + var index = ++this.metricIndex; + if (index >= this.metricLimit) + { + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricStreamIdentity.InstrumentName, metricStreamIdentity.MeterName, "Maximum allowed Metric streams for the provider exceeded.", "Use MeterProviderBuilder.AddView to drop unused instruments. Or use MeterProviderBuilder.SetMaxMetricStreams to configure MeterProvider to allow higher limit."); + return new(); + } + else { - if (this.instrumentIdentityToMetric.TryGetValue(metricStreamIdentity, out var existingMetric)) + Metric? metric = null; + try + { + metric = new Metric(metricStreamIdentity, this.GetAggregationTemporality(metricStreamIdentity.InstrumentType), this.cardinalityLimit, this.emitOverflowAttribute, this.reclaimUnusedMetricPoints, this.exemplarFilter); + } + catch (NotSupportedException nse) { - return existingMetric; + // TODO: This allocates string even if none listening. + // Could be improved with separate Event. + // Also the message could call out what Instruments + // and types (eg: int, long etc) are supported. + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricStreamIdentity.InstrumentName, metricStreamIdentity.MeterName, "Unsupported instrument. Details: " + nse.Message, "Switch to a supported instrument type."); + return new(); } - if (this.metricStreamNames.Contains(metricStreamIdentity.MetricStreamName)) + this.instrumentIdentityToMetric[metricStreamIdentity] = metric; + this.metrics![index] = metric; + + this.CreateOrUpdateMetricStreamRegistration(in metricStreamIdentity); + + return new() { metric }; + } + } + } + + internal virtual List AddMetricWithViews(Instrument instrument, List metricStreamConfigs) + { + Debug.Assert(instrument != null, "instrument was null"); + Debug.Assert(metricStreamConfigs != null, "metricStreamConfigs was null"); + Debug.Assert(this.metrics != null, "this.metrics was null"); + + var maxCountMetricsToBeCreated = metricStreamConfigs!.Count; + + // Create list with initial capacity as the max metric count. + // Due to duplicate/max limit, we may not end up using them + // all, and that memory is wasted until Meter disposed. + // TODO: Revisit to see if we need to do metrics.TrimExcess() + var metrics = new List(maxCountMetricsToBeCreated); + lock (this.instrumentCreationLock) + { + for (int i = 0; i < maxCountMetricsToBeCreated; i++) + { + var metricStreamConfig = metricStreamConfigs[i]; + var metricStreamIdentity = new MetricStreamIdentity(instrument!, metricStreamConfig); + + if (!MeterProviderBuilderSdk.IsValidInstrumentName(metricStreamIdentity.InstrumentName)) { - OpenTelemetrySdkEventSource.Log.DuplicateMetricInstrument( + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored( 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."); + "Metric name is invalid.", + "The name must comply with the OpenTelemetry specification."); + + continue; + } + + if (this.TryGetExistingMetric(in metricStreamIdentity, out var existingMetric)) + { + metrics.Add(existingMetric); + continue; + } + + if (metricStreamConfig == MetricStreamConfiguration.Drop) + { + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricStreamIdentity.InstrumentName, metricStreamIdentity.MeterName, "View configuration asks to drop this instrument.", "Modify view configuration to allow this instrument, if desired."); + continue; } var index = ++this.metricIndex; - if (index >= this.maxMetricStreams) + if (index >= this.metricLimit) { OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricStreamIdentity.InstrumentName, metricStreamIdentity.MeterName, "Maximum allowed Metric streams for the provider exceeded.", "Use MeterProviderBuilder.AddView to drop unused instruments. Or use MeterProviderBuilder.SetMaxMetricStreams to configure MeterProvider to allow higher limit."); - return null; } else { - Metric metric = null; - try - { - metric = new Metric(metricStreamIdentity, this.GetAggregationTemporality(metricStreamIdentity.InstrumentType), this.maxMetricPointsPerMetricStream, exemplarFilter: this.exemplarFilter); - } - catch (NotSupportedException nse) - { - // TODO: This allocates string even if none listening. - // Could be improved with separate Event. - // Also the message could call out what Instruments - // and types (eg: int, long etc) are supported. - OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricStreamIdentity.InstrumentName, metricStreamIdentity.MeterName, "Unsupported instrument. Details: " + nse.Message, "Switch to a supported instrument type."); - return null; - } + Metric metric = new( + metricStreamIdentity, + this.GetAggregationTemporality(metricStreamIdentity.InstrumentType), + metricStreamConfig?.CardinalityLimit ?? this.cardinalityLimit, + this.emitOverflowAttribute, + this.reclaimUnusedMetricPoints, + this.exemplarFilter, + metricStreamConfig?.ExemplarReservoirFactory); this.instrumentIdentityToMetric[metricStreamIdentity] = metric; - this.metrics[index] = metric; - this.metricStreamNames.Add(metricStreamIdentity.MetricStreamName); - return metric; + this.metrics![index] = metric; + metrics.Add(metric); + + this.CreateOrUpdateMetricStreamRegistration(in metricStreamIdentity); } } + + return metrics; } + } + + internal void ApplyParentProviderSettings( + int metricLimit, + int cardinalityLimit, + bool emitOverflowAttribute, + bool reclaimUnusedMetricPoints, + ExemplarFilterType? exemplarFilter) + { + this.metricLimit = metricLimit; + this.metrics = new Metric[metricLimit]; + this.metricsCurrentBatch = new Metric[metricLimit]; + this.cardinalityLimit = cardinalityLimit; + this.reclaimUnusedMetricPoints = reclaimUnusedMetricPoints; + this.exemplarFilter = exemplarFilter; - internal void RecordSingleStreamLongMeasurement(Metric metric, long value, ReadOnlySpan> tags) + if (emitOverflowAttribute) { - metric.UpdateLong(value, tags); + // We need at least two metric points. One is reserved for zero tags and the other one for overflow attribute + if (cardinalityLimit > 1) + { + this.emitOverflowAttribute = true; + } } + } + + private bool TryGetExistingMetric(in MetricStreamIdentity metricStreamIdentity, [NotNullWhen(true)] out Metric? existingMetric) + => this.instrumentIdentityToMetric.TryGetValue(metricStreamIdentity, out existingMetric) + && existingMetric.Active; - internal void RecordSingleStreamDoubleMeasurement(Metric metric, double value, ReadOnlySpan> tags) + private void CreateOrUpdateMetricStreamRegistration(in MetricStreamIdentity metricStreamIdentity) + { + if (!this.metricStreamNames.Add(metricStreamIdentity.MetricStreamName)) { - metric.UpdateDouble(value, tags); + // 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"); + Debug.Assert(this.metricsCurrentBatch != null, "this.metricsCurrentBatch was null"); - internal List AddMetricsListWithViews(Instrument instrument, List metricStreamConfigs) + try { - var maxCountMetricsToBeCreated = metricStreamConfigs.Count; - - // Create list with initial capacity as the max metric count. - // Due to duplicate/max limit, we may not end up using them - // all, and that memory is wasted until Meter disposed. - // TODO: Revisit to see if we need to do metrics.TrimExcess() - var metrics = new List(maxCountMetricsToBeCreated); - lock (this.instrumentCreationLock) + var indexSnapshot = Math.Min(this.metricIndex, this.metricLimit - 1); + var target = indexSnapshot + 1; + int metricCountCurrentBatch = 0; + for (int i = 0; i < target; i++) { - for (int i = 0; i < maxCountMetricsToBeCreated; i++) + ref var metric = ref this.metrics![i]; + if (metric != null) { - var metricStreamConfig = metricStreamConfigs[i]; - var metricStreamIdentity = new MetricStreamIdentity(instrument, metricStreamConfig); - - if (!MeterProviderBuilderSdk.IsValidInstrumentName(metricStreamIdentity.InstrumentName)) - { - OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored( - metricStreamIdentity.InstrumentName, - metricStreamIdentity.MeterName, - "Metric name is invalid.", - "The name must comply with the OpenTelemetry specification."); + int metricPointSize = metric.Snapshot(); - continue; - } - - if (this.instrumentIdentityToMetric.TryGetValue(metricStreamIdentity, out var existingMetric)) - { - metrics.Add(existingMetric); - continue; - } - - 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, instrument type, or aggregation configuration (like histogram bounds, tag keys etc. ). 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."); - } - - if (metricStreamConfig == MetricStreamConfiguration.Drop) + if (metricPointSize > 0) { - OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricStreamIdentity.InstrumentName, metricStreamIdentity.MeterName, "View configuration asks to drop this instrument.", "Modify view configuration to allow this instrument, if desired."); - continue; + this.metricsCurrentBatch![metricCountCurrentBatch++] = metric; } - var index = ++this.metricIndex; - if (index >= this.maxMetricStreams) - { - OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricStreamIdentity.InstrumentName, metricStreamIdentity.MeterName, "Maximum allowed Metric streams for the provider exceeded.", "Use MeterProviderBuilder.AddView to drop unused instruments. Or use MeterProviderBuilder.SetMaxMetricStreams to configure MeterProvider to allow higher limit."); - } - else + if (!metric.Active) { - Metric metric = new(metricStreamIdentity, this.GetAggregationTemporality(metricStreamIdentity.InstrumentType), this.maxMetricPointsPerMetricStream, this.exemplarFilter); - - this.instrumentIdentityToMetric[metricStreamIdentity] = metric; - this.metrics[index] = metric; - metrics.Add(metric); - this.metricStreamNames.Add(metricStreamIdentity.MetricStreamName); + this.RemoveMetric(ref metric); } } - - return metrics; } - } - internal void RecordLongMeasurement(List metrics, long value, ReadOnlySpan> tags) - { - if (metrics.Count == 1) - { - // special casing the common path - // as this is faster than the - // foreach, when count is 1. - metrics[0].UpdateLong(value, tags); - } - else - { - foreach (var metric in metrics) - { - metric.UpdateLong(value, tags); - } - } + return (metricCountCurrentBatch > 0) ? new Batch(this.metricsCurrentBatch!, metricCountCurrentBatch) : default; } - - internal void RecordDoubleMeasurement(List metrics, double value, ReadOnlySpan> tags) + catch (Exception ex) { - if (metrics.Count == 1) - { - // special casing the common path - // as this is faster than the - // foreach, when count is 1. - metrics[0].UpdateDouble(value, tags); - } - else - { - foreach (var metric in metrics) - { - metric.UpdateDouble(value, tags); - } - } + OpenTelemetrySdkEventSource.Log.MetricReaderException(nameof(this.GetMetricsBatch), ex); + return default; } + } - internal void CompleteSingleStreamMeasurement(Metric metric) - { - metric.InstrumentDisposed = true; - } + private void RemoveMetric(ref Metric? metric) + { + Debug.Assert(metric != null, "metric was null"); - internal void CompleteMeasurement(List metrics) - { - foreach (var metric in metrics) - { - metric.InstrumentDisposed = true; - } - } + // 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? - internal void SetMaxMetricStreams(int maxMetricStreams) - { - this.maxMetricStreams = maxMetricStreams; - this.metrics = new Metric[maxMetricStreams]; - this.metricsCurrentBatch = new Metric[maxMetricStreams]; - } + OpenTelemetrySdkEventSource.Log.MetricInstrumentRemoved(metric!.Name, metric.MeterName); - internal void SetExemplarFilter(ExemplarFilter exemplarFilter) - { - this.exemplarFilter = exemplarFilter; - } + var result = this.instrumentIdentityToMetric.TryRemove(metric.InstrumentIdentity, out var _); + Debug.Assert(result, "result was false"); - internal void SetMaxMetricPointsPerMetricStream(int maxMetricPointsPerMetricStream) - { - this.maxMetricPointsPerMetricStream = maxMetricPointsPerMetricStream; - } - - private Batch GetMetricsBatch() - { - try - { - var indexSnapshot = Math.Min(this.metricIndex, this.maxMetricStreams - 1); - var target = indexSnapshot + 1; - int metricCountCurrentBatch = 0; - for (int i = 0; i < target; i++) - { - var metric = this.metrics[i]; - int metricPointSize = 0; - if (metric != null) - { - if (metric.InstrumentDisposed) - { - metricPointSize = metric.Snapshot(); - this.instrumentIdentityToMetric.TryRemove(metric.InstrumentIdentity, out var _); - this.metrics[i] = null; - } - else - { - metricPointSize = metric.Snapshot(); - } - - if (metricPointSize > 0) - { - this.metricsCurrentBatch[metricCountCurrentBatch++] = metric; - } - } - } - - return (metricCountCurrentBatch > 0) ? new Batch(this.metricsCurrentBatch, metricCountCurrentBatch) : default; - } - catch (Exception ex) - { - OpenTelemetrySdkEventSource.Log.MetricReaderException(nameof(this.GetMetricsBatch), ex); - return default; - } - } + // Note: metric is a reference to the array storage so + // this clears the metric out of the array. + metric = null; } } diff --git a/src/OpenTelemetry/Metrics/MetricReaderOptions.cs b/src/OpenTelemetry/Metrics/MetricReaderOptions.cs index 13de4f98b0a..6123dc2c2cd 100644 --- a/src/OpenTelemetry/Metrics/MetricReaderOptions.cs +++ b/src/OpenTelemetry/Metrics/MetricReaderOptions.cs @@ -1,20 +1,5 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.Configuration; using OpenTelemetry.Internal; diff --git a/src/OpenTelemetry/Metrics/MetricReaderTemporalityPreference.cs b/src/OpenTelemetry/Metrics/MetricReaderTemporalityPreference.cs index 4443c4bb4a1..019fdd69027 100644 --- a/src/OpenTelemetry/Metrics/MetricReaderTemporalityPreference.cs +++ b/src/OpenTelemetry/Metrics/MetricReaderTemporalityPreference.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 namespace OpenTelemetry.Metrics; diff --git a/src/OpenTelemetry/Metrics/MetricState.cs b/src/OpenTelemetry/Metrics/MetricState.cs new file mode 100644 index 00000000000..ae552861b6e --- /dev/null +++ b/src/OpenTelemetry/Metrics/MetricState.cs @@ -0,0 +1,70 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; + +namespace OpenTelemetry.Metrics; + +internal sealed class MetricState +{ + public readonly Action CompleteMeasurement; + + public readonly RecordMeasurementAction RecordMeasurementLong; + public readonly RecordMeasurementAction RecordMeasurementDouble; + + private MetricState( + Action completeMeasurement, + RecordMeasurementAction recordMeasurementLong, + RecordMeasurementAction recordMeasurementDouble) + { + this.CompleteMeasurement = completeMeasurement; + this.RecordMeasurementLong = recordMeasurementLong; + this.RecordMeasurementDouble = recordMeasurementDouble; + } + + internal delegate void RecordMeasurementAction(T value, ReadOnlySpan> tags); + + public static MetricState BuildForSingleMetric( + Metric metric) + { + Debug.Assert(metric != null, "metric was null"); + + return new( + completeMeasurement: () => MetricReader.DeactivateMetric(metric!), + recordMeasurementLong: metric!.UpdateLong, + recordMeasurementDouble: metric!.UpdateDouble); + } + + public static MetricState BuildForMetricList( + List metrics) + { + Debug.Assert(metrics != null, "metrics was null"); + Debug.Assert(!metrics.Any(m => m == null), "metrics contained null elements"); + + // Note: Use an array here to elide bounds checks. + var metricsArray = metrics!.ToArray(); + + return new( + completeMeasurement: () => + { + for (int i = 0; i < metricsArray.Length; i++) + { + MetricReader.DeactivateMetric(metricsArray[i]); + } + }, + recordMeasurementLong: (v, t) => + { + for (int i = 0; i < metricsArray.Length; i++) + { + metricsArray[i].UpdateLong(v, t); + } + }, + recordMeasurementDouble: (v, t) => + { + for (int i = 0; i < metricsArray.Length; i++) + { + metricsArray[i].UpdateDouble(v, t); + } + }); + } +} diff --git a/src/OpenTelemetry/Metrics/MetricStreamConfiguration.cs b/src/OpenTelemetry/Metrics/MetricStreamConfiguration.cs index fec1be228f1..cb7fb92a598 100644 --- a/src/OpenTelemetry/Metrics/MetricStreamConfiguration.cs +++ b/src/OpenTelemetry/Metrics/MetricStreamConfiguration.cs @@ -1,115 +1,143 @@ -// // 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. -// - -namespace OpenTelemetry.Metrics +// SPDX-License-Identifier: Apache-2.0 + +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Metrics; + +/// +/// Stores configuration for a MetricStream. +/// +public class MetricStreamConfiguration { + private string? name; + + private int? cardinalityLimit = null; + + /// + /// Gets the drop configuration. + /// + /// + /// Note: All metrics for the given instrument will be dropped (not + /// collected). + /// + public static MetricStreamConfiguration Drop { get; } = new MetricStreamConfiguration { ViewId = -1 }; + /// - /// Stores configuration for a MetricStream. + /// Gets or sets the optional name of the metric stream. /// - public class MetricStreamConfiguration + /// + /// Note: If not provided the instrument name will be used. + /// + public string? Name { - private string name; - - /// - /// Gets the drop configuration. - /// - /// - /// Note: All metrics for the given instrument will be dropped (not - /// collected). - /// - public static MetricStreamConfiguration Drop { get; } = new MetricStreamConfiguration { ViewId = -1 }; - - /// - /// Gets or sets the optional name of the metric stream. - /// - /// - /// Note: If not provided the instrument name will be used. - /// - public string Name + get => this.name; + set { - get => this.name; - set + if (value != null && !MeterProviderBuilderSdk.IsValidViewName(value)) { - if (value != null && !MeterProviderBuilderSdk.IsValidViewName(value)) - { - throw new ArgumentException($"Custom view name {value} is invalid.", nameof(value)); - } + throw new ArgumentException($"Custom view name {value} is invalid.", nameof(value)); + } + + this.name = value; + } + } - this.name = value; + /// + /// Gets or sets the optional description of the metric stream. + /// + /// + /// Note: If not provided the instrument description will be used. + /// + public string? Description { get; set; } + + /// + /// Gets or sets the optional tag keys to include in the metric stream. + /// + /// + /// Notes: + /// + /// If not provided, all the tags provided by the instrument + /// while reporting measurements will be used for aggregation. + /// If provided, only those tags in this list will be used + /// for aggregation. Providing an empty array will result + /// in a metric stream without any tags. + /// + /// A copy is made of the provided array. + /// + /// + public string[]? TagKeys + { + get + { + if (this.CopiedTagKeys != null) + { + string[] copy = new string[this.CopiedTagKeys.Length]; + this.CopiedTagKeys.AsSpan().CopyTo(copy); + return copy; } + + return null; } - /// - /// Gets or sets the optional description of the metric stream. - /// - /// - /// Note: If not provided the instrument description will be used. - /// - public string Description { get; set; } - - /// - /// Gets or sets the optional tag keys to include in the metric stream. - /// - /// - /// Notes: - /// - /// If not provided, all the tags provided by the instrument - /// while reporting measurements will be used for aggregation. - /// If provided, only those tags in this list will be used - /// for aggregation. Providing an empty array will result - /// in a metric stream without any tags. - /// - /// A copy is made of the provided array. - /// - /// - public string[] TagKeys + set { - get + if (value != null) + { + string[] copy = new string[value.Length]; + value.AsSpan().CopyTo(copy); + this.CopiedTagKeys = copy; + } + else { - if (this.CopiedTagKeys != null) - { - string[] copy = new string[this.CopiedTagKeys.Length]; - this.CopiedTagKeys.AsSpan().CopyTo(copy); - return copy; - } - - return null; + this.CopiedTagKeys = null; } + } + } - set +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Gets or sets a positive integer value defining the maximum number of + /// data points allowed for the metric managed by the view. + /// + /// + /// WARNING: This is an experimental API which might change or + /// be removed in the future. Use at your own risk. + /// Spec reference: Cardinality + /// limits. + /// Note: If not set the default MeterProvider cardinality limit of 2000 + /// will apply. + /// +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.CardinalityLimitExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + internal +#endif + int? CardinalityLimit + { + get => this.cardinalityLimit; + set + { + if (value != null) { - if (value != null) - { - string[] copy = new string[value.Length]; - value.AsSpan().CopyTo(copy); - this.CopiedTagKeys = copy; - } - else - { - this.CopiedTagKeys = null; - } + Guard.ThrowIfOutOfRange(value.Value, min: 1, max: int.MaxValue); } + + this.cardinalityLimit = value; } + } - internal string[] CopiedTagKeys { get; private set; } + // TODO: Expose this to be complaint with the spec: + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#stream-configuration + internal Func? ExemplarReservoirFactory { get; set; } - internal int? ViewId { get; set; } + internal string[]? CopiedTagKeys { get; private set; } - // TODO: MetricPoints caps can be configured here on - // a per stream basis, when we add such a capability - // in the future. - } + internal int? ViewId { get; set; } } diff --git a/src/OpenTelemetry/Metrics/MetricStreamIdentity.cs b/src/OpenTelemetry/Metrics/MetricStreamIdentity.cs index 6251b91d871..9ff3a251792 100644 --- a/src/OpenTelemetry/Metrics/MetricStreamIdentity.cs +++ b/src/OpenTelemetry/Metrics/MetricStreamIdentity.cs @@ -1,176 +1,174 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Metrics; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +internal readonly struct MetricStreamIdentity : IEquatable { - internal readonly struct MetricStreamIdentity : IEquatable - { - private static readonly StringArrayEqualityComparer StringArrayComparer = new StringArrayEqualityComparer(); - private readonly int hashCode; + private static readonly StringArrayEqualityComparer StringArrayComparer = new(); + private readonly int hashCode; - public MetricStreamIdentity(Instrument instrument, MetricStreamConfiguration metricStreamConfiguration) - { - this.MeterName = instrument.Meter.Name; - this.MeterVersion = instrument.Meter.Version ?? string.Empty; - this.InstrumentName = metricStreamConfiguration?.Name ?? instrument.Name; - this.Unit = instrument.Unit ?? string.Empty; - this.Description = metricStreamConfiguration?.Description ?? instrument.Description ?? string.Empty; - this.InstrumentType = instrument.GetType(); - this.ViewId = metricStreamConfiguration?.ViewId; - this.MetricStreamName = $"{this.MeterName}.{this.MeterVersion}.{this.InstrumentName}"; - this.TagKeys = metricStreamConfiguration?.CopiedTagKeys; - this.HistogramBucketBounds = (metricStreamConfiguration as ExplicitBucketHistogramConfiguration)?.CopiedBoundaries; - this.ExponentialHistogramMaxSize = (metricStreamConfiguration as Base2ExponentialBucketHistogramConfiguration)?.MaxSize ?? 0; - this.ExponentialHistogramMaxScale = (metricStreamConfiguration as Base2ExponentialBucketHistogramConfiguration)?.MaxScale ?? 0; - this.HistogramRecordMinMax = (metricStreamConfiguration as HistogramConfiguration)?.RecordMinMax ?? true; + public MetricStreamIdentity(Instrument instrument, MetricStreamConfiguration? metricStreamConfiguration) + { + this.MeterName = instrument.Meter.Name; + this.MeterVersion = instrument.Meter.Version ?? string.Empty; + this.MeterTags = instrument.Meter.Tags; + this.InstrumentName = metricStreamConfiguration?.Name ?? instrument.Name; + this.Unit = instrument.Unit ?? string.Empty; + this.Description = metricStreamConfiguration?.Description ?? instrument.Description ?? string.Empty; + this.InstrumentType = instrument.GetType(); + this.ViewId = metricStreamConfiguration?.ViewId; + this.MetricStreamName = $"{this.MeterName}.{this.MeterVersion}.{this.InstrumentName}"; + this.TagKeys = metricStreamConfiguration?.CopiedTagKeys; + this.HistogramBucketBounds = (metricStreamConfiguration as ExplicitBucketHistogramConfiguration)?.CopiedBoundaries; + this.ExponentialHistogramMaxSize = (metricStreamConfiguration as Base2ExponentialBucketHistogramConfiguration)?.MaxSize ?? 0; + this.ExponentialHistogramMaxScale = (metricStreamConfiguration as Base2ExponentialBucketHistogramConfiguration)?.MaxScale ?? 0; + this.HistogramRecordMinMax = (metricStreamConfiguration as HistogramConfiguration)?.RecordMinMax ?? true; #if NET6_0_OR_GREATER - HashCode hashCode = default; - hashCode.Add(this.InstrumentType); - hashCode.Add(this.MeterName); - hashCode.Add(this.MeterVersion); - hashCode.Add(this.InstrumentName); - hashCode.Add(this.HistogramRecordMinMax); - hashCode.Add(this.Unit); - hashCode.Add(this.Description); - hashCode.Add(this.ViewId); - hashCode.Add(this.TagKeys, StringArrayComparer); - hashCode.Add(this.ExponentialHistogramMaxSize); - hashCode.Add(this.ExponentialHistogramMaxScale); - if (this.HistogramBucketBounds != null) + HashCode hashCode = default; + hashCode.Add(this.InstrumentType); + hashCode.Add(this.MeterName); + hashCode.Add(this.MeterVersion); + hashCode.Add(this.InstrumentName); + hashCode.Add(this.HistogramRecordMinMax); + hashCode.Add(this.Unit); + hashCode.Add(this.Description); + hashCode.Add(this.ViewId); + + // Note: The this.TagKeys! here is strange but it is fine for the value + // to be null. HashCode.Add is coded to handle the value being null. We + // are essentially suppressing a false positive due to an issue/quirk + // with the annotations. See: + // https://github.com/dotnet/runtime/pull/91905. + hashCode.Add(this.TagKeys!, StringArrayComparer); + + hashCode.Add(this.ExponentialHistogramMaxSize); + hashCode.Add(this.ExponentialHistogramMaxScale); + if (this.HistogramBucketBounds != null) + { + for (var i = 0; i < this.HistogramBucketBounds.Length; ++i) { - for (var i = 0; i < this.HistogramBucketBounds.Length; ++i) - { - hashCode.Add(this.HistogramBucketBounds[i]); - } + hashCode.Add(this.HistogramBucketBounds[i]); } + } - var hash = hashCode.ToHashCode(); + var hash = hashCode.ToHashCode(); #else - var hash = 17; - unchecked + var hash = 17; + unchecked + { + hash = (hash * 31) + this.InstrumentType.GetHashCode(); + hash = (hash * 31) + this.MeterName.GetHashCode(); + hash = (hash * 31) + this.MeterVersion.GetHashCode(); + + // MeterTags is not part of identity, so not included here. + hash = (hash * 31) + this.InstrumentName.GetHashCode(); + hash = (hash * 31) + this.HistogramRecordMinMax.GetHashCode(); + hash = (hash * 31) + this.ExponentialHistogramMaxSize.GetHashCode(); + hash = (hash * 31) + this.ExponentialHistogramMaxScale.GetHashCode(); + hash = (hash * 31) + this.Unit.GetHashCode(); + hash = (hash * 31) + this.Description.GetHashCode(); + hash = (hash * 31) + (this.ViewId ?? 0); + hash = (hash * 31) + (this.TagKeys != null ? StringArrayComparer.GetHashCode(this.TagKeys) : 0); + if (this.HistogramBucketBounds != null) { - hash = (hash * 31) + this.InstrumentType.GetHashCode(); - hash = (hash * 31) + this.MeterName.GetHashCode(); - hash = (hash * 31) + this.MeterVersion.GetHashCode(); - hash = (hash * 31) + this.InstrumentName.GetHashCode(); - hash = (hash * 31) + this.HistogramRecordMinMax.GetHashCode(); - hash = (hash * 31) + this.ExponentialHistogramMaxSize.GetHashCode(); - hash = (hash * 31) + this.ExponentialHistogramMaxScale.GetHashCode(); - hash = (hash * 31) + (this.Unit?.GetHashCode() ?? 0); - hash = (hash * 31) + (this.Description?.GetHashCode() ?? 0); - hash = (hash * 31) + (this.ViewId ?? 0); - hash = (hash * 31) + (this.TagKeys != null ? StringArrayComparer.GetHashCode(this.TagKeys) : 0); - if (this.HistogramBucketBounds != null) + var len = this.HistogramBucketBounds.Length; + for (var i = 0; i < len; ++i) { - var len = this.HistogramBucketBounds.Length; - for (var i = 0; i < len; ++i) - { - hash = (hash * 31) + this.HistogramBucketBounds[i].GetHashCode(); - } + hash = (hash * 31) + this.HistogramBucketBounds[i].GetHashCode(); } } + } #endif - this.hashCode = hash; - } + this.hashCode = hash; + } + + public string MeterName { get; } + + public string MeterVersion { get; } - public string MeterName { get; } + public IEnumerable>? MeterTags { get; } - public string MeterVersion { get; } + public string InstrumentName { get; } - public string InstrumentName { get; } + public string Unit { get; } - public string Unit { get; } + public string Description { get; } - public string Description { get; } + public Type InstrumentType { get; } - public Type InstrumentType { get; } + public int? ViewId { get; } - public int? ViewId { get; } + public string MetricStreamName { get; } - public string MetricStreamName { get; } + public string[]? TagKeys { get; } - public string[] TagKeys { get; } + public double[]? HistogramBucketBounds { get; } - public double[] HistogramBucketBounds { get; } + public int ExponentialHistogramMaxSize { get; } - public int ExponentialHistogramMaxSize { get; } + public int ExponentialHistogramMaxScale { get; } - public int ExponentialHistogramMaxScale { get; } + public bool HistogramRecordMinMax { get; } - public bool HistogramRecordMinMax { get; } + public static bool operator ==(MetricStreamIdentity metricIdentity1, MetricStreamIdentity metricIdentity2) => metricIdentity1.Equals(metricIdentity2); - public static bool operator ==(MetricStreamIdentity metricIdentity1, MetricStreamIdentity metricIdentity2) => metricIdentity1.Equals(metricIdentity2); + public static bool operator !=(MetricStreamIdentity metricIdentity1, MetricStreamIdentity metricIdentity2) => !metricIdentity1.Equals(metricIdentity2); - public static bool operator !=(MetricStreamIdentity metricIdentity1, MetricStreamIdentity metricIdentity2) => !metricIdentity1.Equals(metricIdentity2); + public override readonly bool Equals(object? obj) + { + return obj is MetricStreamIdentity other && this.Equals(other); + } + + public bool Equals(MetricStreamIdentity other) + { + return this.InstrumentType == other.InstrumentType + && this.MeterName == other.MeterName + && this.MeterVersion == other.MeterVersion + && this.InstrumentName == other.InstrumentName + && this.Unit == other.Unit + && this.Description == other.Description + && this.ViewId == other.ViewId + && this.HistogramRecordMinMax == other.HistogramRecordMinMax + && this.ExponentialHistogramMaxSize == other.ExponentialHistogramMaxSize + && this.ExponentialHistogramMaxScale == other.ExponentialHistogramMaxScale + && StringArrayComparer.Equals(this.TagKeys, other.TagKeys) + && HistogramBoundsEqual(this.HistogramBucketBounds, other.HistogramBucketBounds); + } - public readonly override bool Equals(object obj) + public override readonly int GetHashCode() => this.hashCode; + + private static bool HistogramBoundsEqual(double[]? bounds1, double[]? bounds2) + { + if (ReferenceEquals(bounds1, bounds2)) { - return obj is MetricStreamIdentity other && this.Equals(other); + return true; } - public bool Equals(MetricStreamIdentity other) + if (ReferenceEquals(bounds1, null) || ReferenceEquals(bounds2, null)) { - return this.InstrumentType == other.InstrumentType - && this.MeterName == other.MeterName - && this.MeterVersion == other.MeterVersion - && this.InstrumentName == other.InstrumentName - && this.Unit == other.Unit - && this.Description == other.Description - && this.ViewId == other.ViewId - && this.HistogramRecordMinMax == other.HistogramRecordMinMax - && this.ExponentialHistogramMaxSize == other.ExponentialHistogramMaxSize - && this.ExponentialHistogramMaxScale == other.ExponentialHistogramMaxScale - && StringArrayComparer.Equals(this.TagKeys, other.TagKeys) - && HistogramBoundsEqual(this.HistogramBucketBounds, other.HistogramBucketBounds); + return false; } - public readonly override int GetHashCode() => this.hashCode; + var len1 = bounds1.Length; - private static bool HistogramBoundsEqual(double[] bounds1, double[] bounds2) + if (len1 != bounds2.Length) { - if (ReferenceEquals(bounds1, bounds2)) - { - return true; - } - - if (ReferenceEquals(bounds1, null) || ReferenceEquals(bounds2, null)) - { - return false; - } - - var len1 = bounds1.Length; + return false; + } - if (len1 != bounds2.Length) + for (int i = 0; i < len1; i++) + { + if (!bounds1[i].Equals(bounds2[i])) { return false; } - - for (int i = 0; i < len1; i++) - { - if (!bounds1[i].Equals(bounds2[i])) - { - return false; - } - } - - return true; } + + return true; } } diff --git a/src/OpenTelemetry/Metrics/MetricType.cs b/src/OpenTelemetry/Metrics/MetricType.cs index 2633ec68bb6..526bdbb466f 100644 --- a/src/OpenTelemetry/Metrics/MetricType.cs +++ b/src/OpenTelemetry/Metrics/MetricType.cs @@ -1,86 +1,75 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// Enumeration used to define the type of a . +/// +[Flags] +public enum MetricType : byte { - [Flags] - public enum MetricType : byte - { - /* - Type: - 0x10: Sum - 0x20: Gauge - 0x30: Summary (reserved) - 0x40: Histogram - 0x50: ExponentialHistogram - 0x60: (unused) - 0x70: (unused) - 0x80: SumNonMonotonic + /* + Type: + 0x10: Sum + 0x20: Gauge + 0x30: Summary (reserved) + 0x40: Histogram + 0x50: ExponentialHistogram + 0x60: (unused) + 0x70: (unused) + 0x80: SumNonMonotonic - Point kind: - 0x04: I1 (signed 1-byte integer) - 0x05: U1 (unsigned 1-byte integer) - 0x06: I2 (signed 2-byte integer) - 0x07: U2 (unsigned 2-byte integer) - 0x08: I4 (signed 4-byte integer) - 0x09: U4 (unsigned 4-byte integer) - 0x0a: I8 (signed 8-byte integer) - 0x0b: U8 (unsigned 8-byte integer) - 0x0c: R4 (4-byte floating point) - 0x0d: R8 (8-byte floating point) - */ + Point kind: + 0x04: I1 (signed 1-byte integer) + 0x05: U1 (unsigned 1-byte integer) + 0x06: I2 (signed 2-byte integer) + 0x07: U2 (unsigned 2-byte integer) + 0x08: I4 (signed 4-byte integer) + 0x09: U4 (unsigned 4-byte integer) + 0x0a: I8 (signed 8-byte integer) + 0x0b: U8 (unsigned 8-byte integer) + 0x0c: R4 (4-byte floating point) + 0x0d: R8 (8-byte floating point) + */ - /// - /// Sum of Long type. - /// - LongSum = 0x1a, + /// + /// Sum of Long type. + /// + LongSum = 0x1a, - /// - /// Sum of Double type. - /// - DoubleSum = 0x1d, + /// + /// Sum of Double type. + /// + DoubleSum = 0x1d, - /// - /// Gauge of Long type. - /// - LongGauge = 0x2a, + /// + /// Gauge of Long type. + /// + LongGauge = 0x2a, - /// - /// Gauge of Double type. - /// - DoubleGauge = 0x2d, + /// + /// Gauge of Double type. + /// + DoubleGauge = 0x2d, - /// - /// Histogram. - /// - Histogram = 0x40, + /// + /// Histogram. + /// + Histogram = 0x40, - /// - /// Exponential Histogram. - /// - ExponentialHistogram = 0x50, + /// + /// Exponential Histogram. + /// + ExponentialHistogram = 0x50, - /// - /// Non-monotonic Sum of Long type. - /// - LongSumNonMonotonic = 0x8a, + /// + /// Non-monotonic Sum of Long type. + /// + LongSumNonMonotonic = 0x8a, - /// - /// Non-monotonic Sum of Double type. - /// - DoubleSumNonMonotonic = 0x8d, - } + /// + /// Non-monotonic Sum of Double type. + /// + DoubleSumNonMonotonic = 0x8d, } diff --git a/src/OpenTelemetry/Metrics/MetricTypeExtensions.cs b/src/OpenTelemetry/Metrics/MetricTypeExtensions.cs index 595886e6cfd..c21b1be3dd2 100644 --- a/src/OpenTelemetry/Metrics/MetricTypeExtensions.cs +++ b/src/OpenTelemetry/Metrics/MetricTypeExtensions.cs @@ -1,79 +1,98 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Runtime.CompilerServices; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// Contains extension methods for performing common operations against the class. +/// +public static class MetricTypeExtensions { - public static class MetricTypeExtensions - { #pragma warning disable SA1310 // field should not contain an underscore - internal const MetricType METRIC_TYPE_MASK = (MetricType)0xf0; + internal const MetricType METRIC_TYPE_MASK = (MetricType)0xf0; - internal const MetricType METRIC_TYPE_MONOTONIC_SUM = (MetricType)0x10; - internal const MetricType METRIC_TYPE_GAUGE = (MetricType)0x20; - /* internal const byte METRIC_TYPE_SUMMARY = 0x30; // not used */ - internal const MetricType METRIC_TYPE_HISTOGRAM = (MetricType)0x40; - internal const MetricType METRIC_TYPE_NON_MONOTONIC_SUM = (MetricType)0x80; + internal const MetricType METRIC_TYPE_MONOTONIC_SUM = (MetricType)0x10; + internal const MetricType METRIC_TYPE_GAUGE = (MetricType)0x20; + /* internal const byte METRIC_TYPE_SUMMARY = 0x30; // not used */ + internal const MetricType METRIC_TYPE_HISTOGRAM = (MetricType)0x40; + internal const MetricType METRIC_TYPE_NON_MONOTONIC_SUM = (MetricType)0x80; - internal const MetricType POINT_KIND_MASK = (MetricType)0x0f; + internal const MetricType POINT_KIND_MASK = (MetricType)0x0f; - internal const MetricType POINT_KIND_I1 = (MetricType)0x04; // signed 1-byte integer - internal const MetricType POINT_KIND_U1 = (MetricType)0x05; // unsigned 1-byte integer - internal const MetricType POINT_KIND_I2 = (MetricType)0x06; // signed 2-byte integer - internal const MetricType POINT_KIND_U2 = (MetricType)0x07; // unsigned 2-byte integer - internal const MetricType POINT_KIND_I4 = (MetricType)0x08; // signed 4-byte integer - internal const MetricType POINT_KIND_U4 = (MetricType)0x09; // unsigned 4-byte integer - internal const MetricType POINT_KIND_I8 = (MetricType)0x0a; // signed 8-byte integer - internal const MetricType POINT_KIND_U8 = (MetricType)0x0b; // unsigned 8-byte integer - internal const MetricType POINT_KIND_R4 = (MetricType)0x0c; // 4-byte floating point - internal const MetricType POINT_KIND_R8 = (MetricType)0x0d; // 8-byte floating point + internal const MetricType POINT_KIND_I1 = (MetricType)0x04; // signed 1-byte integer + internal const MetricType POINT_KIND_U1 = (MetricType)0x05; // unsigned 1-byte integer + internal const MetricType POINT_KIND_I2 = (MetricType)0x06; // signed 2-byte integer + internal const MetricType POINT_KIND_U2 = (MetricType)0x07; // unsigned 2-byte integer + internal const MetricType POINT_KIND_I4 = (MetricType)0x08; // signed 4-byte integer + internal const MetricType POINT_KIND_U4 = (MetricType)0x09; // unsigned 4-byte integer + internal const MetricType POINT_KIND_I8 = (MetricType)0x0a; // signed 8-byte integer + internal const MetricType POINT_KIND_U8 = (MetricType)0x0b; // unsigned 8-byte integer + internal const MetricType POINT_KIND_R4 = (MetricType)0x0c; // 4-byte floating point + internal const MetricType POINT_KIND_R8 = (MetricType)0x0d; // 8-byte floating point #pragma warning restore SA1310 // field should not contain an underscore - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsSum(this MetricType self) - { - var type = self & METRIC_TYPE_MASK; - return type == METRIC_TYPE_MONOTONIC_SUM || type == METRIC_TYPE_NON_MONOTONIC_SUM; - } + /// + /// Determines if the supplied is a sum definition. + /// + /// . + /// if the supplied + /// is a sum definition. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsSum(this MetricType self) + { + var type = self & METRIC_TYPE_MASK; + return type == METRIC_TYPE_MONOTONIC_SUM || type == METRIC_TYPE_NON_MONOTONIC_SUM; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsGauge(this MetricType self) - { - return (self & METRIC_TYPE_MASK) == METRIC_TYPE_GAUGE; - } + /// + /// Determines if the supplied is a gauge definition. + /// + /// . + /// if the supplied + /// is a gauge definition. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsGauge(this MetricType self) + { + return (self & METRIC_TYPE_MASK) == METRIC_TYPE_GAUGE; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsHistogram(this MetricType self) - { - return self.HasFlag(METRIC_TYPE_HISTOGRAM); - } + /// + /// Determines if the supplied is a histogram definition. + /// + /// . + /// if the supplied + /// is a histogram definition. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsHistogram(this MetricType self) + { + return self.HasFlag(METRIC_TYPE_HISTOGRAM); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsDouble(this MetricType self) - { - return (self & POINT_KIND_MASK) == POINT_KIND_R8; - } + /// + /// Determines if the supplied is a double definition. + /// + /// . + /// if the supplied + /// is a double definition. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsDouble(this MetricType self) + { + return (self & POINT_KIND_MASK) == POINT_KIND_R8; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsLong(this MetricType self) - { - return (self & POINT_KIND_MASK) == POINT_KIND_I8; - } + /// + /// Determines if the supplied is a long definition. + /// + /// . + /// if the supplied + /// is a long definition. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsLong(this MetricType self) + { + return (self & POINT_KIND_MASK) == POINT_KIND_I8; } } diff --git a/src/OpenTelemetry/Metrics/PeriodicExportingMetricReader.cs b/src/OpenTelemetry/Metrics/PeriodicExportingMetricReader.cs index 69ad205c313..8f994bbc83d 100644 --- a/src/OpenTelemetry/Metrics/PeriodicExportingMetricReader.cs +++ b/src/OpenTelemetry/Metrics/PeriodicExportingMetricReader.cs @@ -1,155 +1,141 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Internal; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// MetricReader implementation which collects metrics based on +/// a user-configurable time interval and passes the metrics to +/// the configured MetricExporter. +/// +public class PeriodicExportingMetricReader : BaseExportingMetricReader { + internal const int DefaultExportIntervalMilliseconds = 60000; + internal const int DefaultExportTimeoutMilliseconds = 30000; + + internal readonly int ExportIntervalMilliseconds; + internal readonly int ExportTimeoutMilliseconds; + private readonly Thread exporterThread; + private readonly AutoResetEvent exportTrigger = new(false); + private readonly ManualResetEvent shutdownTrigger = new(false); + private bool disposed; + /// - /// MetricReader implementation which collects metrics based on - /// a user-configurable time interval and passes the metrics to - /// the configured MetricExporter. + /// Initializes a new instance of the class. /// - public class PeriodicExportingMetricReader : BaseExportingMetricReader + /// Exporter instance to export Metrics to. + /// The interval in milliseconds between two consecutive exports. The default value is 60000. + /// How long the export can run before it is cancelled. The default value is 30000. + public PeriodicExportingMetricReader( + BaseExporter exporter, + int exportIntervalMilliseconds = DefaultExportIntervalMilliseconds, + int exportTimeoutMilliseconds = DefaultExportTimeoutMilliseconds) + : base(exporter) { - internal const int DefaultExportIntervalMilliseconds = 60000; - internal const int DefaultExportTimeoutMilliseconds = 30000; - - internal readonly int ExportIntervalMilliseconds; - internal readonly int ExportTimeoutMilliseconds; - private readonly Thread exporterThread; - private readonly AutoResetEvent exportTrigger = new(false); - private readonly ManualResetEvent shutdownTrigger = new(false); - private bool disposed; - - /// - /// Initializes a new instance of the class. - /// - /// Exporter instance to export Metrics to. - /// The interval in milliseconds between two consecutive exports. The default value is 60000. - /// How long the export can run before it is cancelled. The default value is 30000. - public PeriodicExportingMetricReader( - BaseExporter exporter, - int exportIntervalMilliseconds = DefaultExportIntervalMilliseconds, - int exportTimeoutMilliseconds = DefaultExportTimeoutMilliseconds) - : base(exporter) + Guard.ThrowIfInvalidTimeout(exportIntervalMilliseconds); + Guard.ThrowIfZero(exportIntervalMilliseconds); + Guard.ThrowIfInvalidTimeout(exportTimeoutMilliseconds); + + if ((this.SupportedExportModes & ExportModes.Push) != ExportModes.Push) { - Guard.ThrowIfInvalidTimeout(exportIntervalMilliseconds); - Guard.ThrowIfZero(exportIntervalMilliseconds); - Guard.ThrowIfInvalidTimeout(exportTimeoutMilliseconds); + throw new InvalidOperationException($"The '{nameof(exporter)}' does not support '{nameof(ExportModes)}.{nameof(ExportModes.Push)}'"); + } - if ((this.SupportedExportModes & ExportModes.Push) != ExportModes.Push) - { - throw new InvalidOperationException($"The '{nameof(exporter)}' does not support '{nameof(ExportModes)}.{nameof(ExportModes.Push)}'"); - } + this.ExportIntervalMilliseconds = exportIntervalMilliseconds; + this.ExportTimeoutMilliseconds = exportTimeoutMilliseconds; + + this.exporterThread = new Thread(new ThreadStart(this.ExporterProc)) + { + IsBackground = true, + Name = $"OpenTelemetry-{nameof(PeriodicExportingMetricReader)}-{exporter.GetType().Name}", + }; + this.exporterThread.Start(); + } - this.ExportIntervalMilliseconds = exportIntervalMilliseconds; - this.ExportTimeoutMilliseconds = exportTimeoutMilliseconds; + /// + protected override bool OnShutdown(int timeoutMilliseconds) + { + var result = true; - this.exporterThread = new Thread(new ThreadStart(this.ExporterProc)) - { - IsBackground = true, - Name = $"OpenTelemetry-{nameof(PeriodicExportingMetricReader)}-{exporter.GetType().Name}", - }; - this.exporterThread.Start(); + try + { + this.shutdownTrigger.Set(); + } + catch (ObjectDisposedException) + { + return false; } - /// - protected override bool OnShutdown(int timeoutMilliseconds) + if (timeoutMilliseconds == Timeout.Infinite) { - var result = true; + this.exporterThread.Join(); + result = this.exporter.Shutdown() && result; + } + else + { + var sw = Stopwatch.StartNew(); + result = this.exporterThread.Join(timeoutMilliseconds) && result; + var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; + result = this.exporter.Shutdown((int)Math.Max(timeout, 0)) && result; + } - try - { - this.shutdownTrigger.Set(); - } - catch (ObjectDisposedException) - { - return false; - } + return result; + } - if (timeoutMilliseconds == Timeout.Infinite) - { - this.exporterThread.Join(); - result = this.exporter.Shutdown() && result; - } - else + /// + protected override void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing) { - var sw = Stopwatch.StartNew(); - result = this.exporterThread.Join(timeoutMilliseconds) && result; - var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; - result = this.exporter.Shutdown((int)Math.Max(timeout, 0)) && result; + this.exportTrigger.Dispose(); + this.shutdownTrigger.Dispose(); } - return result; + this.disposed = true; } - /// - protected override void Dispose(bool disposing) - { - if (!this.disposed) - { - if (disposing) - { - this.exportTrigger.Dispose(); - this.shutdownTrigger.Dispose(); - } - - this.disposed = true; - } + base.Dispose(disposing); + } - base.Dispose(disposing); - } + private void ExporterProc() + { + int index; + int timeout; + var triggers = new WaitHandle[] { this.exportTrigger, this.shutdownTrigger }; + var sw = Stopwatch.StartNew(); - private void ExporterProc() + while (true) { - int index; - int timeout; - var triggers = new WaitHandle[] { this.exportTrigger, this.shutdownTrigger }; - var sw = Stopwatch.StartNew(); + timeout = (int)(this.ExportIntervalMilliseconds - (sw.ElapsedMilliseconds % this.ExportIntervalMilliseconds)); + + try + { + index = WaitHandle.WaitAny(triggers, timeout); + } + catch (ObjectDisposedException) + { + return; + } - while (true) + switch (index) { - timeout = (int)(this.ExportIntervalMilliseconds - (sw.ElapsedMilliseconds % this.ExportIntervalMilliseconds)); - - try - { - index = WaitHandle.WaitAny(triggers, timeout); - } - catch (ObjectDisposedException) - { + case 0: // export + OpenTelemetrySdkEventSource.Log.MetricReaderEvent("PeriodicExportingMetricReader calling MetricReader.Collect because Export was triggered."); + this.Collect(this.ExportTimeoutMilliseconds); + break; + case 1: // shutdown + OpenTelemetrySdkEventSource.Log.MetricReaderEvent("PeriodicExportingMetricReader calling MetricReader.Collect because Shutdown was triggered."); + this.Collect(this.ExportTimeoutMilliseconds); // TODO: do we want to use the shutdown timeout here? return; - } - - switch (index) - { - case 0: // export - OpenTelemetrySdkEventSource.Log.MetricReaderEvent("PeriodicExportingMetricReader calling MetricReader.Collect because Export was triggered."); - this.Collect(this.ExportTimeoutMilliseconds); - break; - case 1: // shutdown - OpenTelemetrySdkEventSource.Log.MetricReaderEvent("PeriodicExportingMetricReader calling MetricReader.Collect because Shutdown was triggered."); - this.Collect(this.ExportTimeoutMilliseconds); // TODO: do we want to use the shutdown timeout here? - return; - case WaitHandle.WaitTimeout: // timer - OpenTelemetrySdkEventSource.Log.MetricReaderEvent("PeriodicExportingMetricReader calling MetricReader.Collect because the export interval has elapsed."); - this.Collect(this.ExportTimeoutMilliseconds); - break; - } + case WaitHandle.WaitTimeout: // timer + OpenTelemetrySdkEventSource.Log.MetricReaderEvent("PeriodicExportingMetricReader calling MetricReader.Collect because the export interval has elapsed."); + this.Collect(this.ExportTimeoutMilliseconds); + break; } } } diff --git a/src/OpenTelemetry/Metrics/PeriodicExportingMetricReaderOptions.cs b/src/OpenTelemetry/Metrics/PeriodicExportingMetricReaderOptions.cs index b3d62913220..a3dc1829801 100644 --- a/src/OpenTelemetry/Metrics/PeriodicExportingMetricReaderOptions.cs +++ b/src/OpenTelemetry/Metrics/PeriodicExportingMetricReaderOptions.cs @@ -1,62 +1,55 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.Configuration; using OpenTelemetry.Internal; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +/// +/// Contains periodic metric reader options. +/// +/// +/// Note: OTEL_METRIC_EXPORT_INTERVAL and OTEL_METRIC_EXPORT_TIMEOUT environment +/// variables are parsed during object construction. +/// +public class PeriodicExportingMetricReaderOptions { - public class PeriodicExportingMetricReaderOptions + internal const string OTelMetricExportIntervalEnvVarKey = "OTEL_METRIC_EXPORT_INTERVAL"; + internal const string OTelMetricExportTimeoutEnvVarKey = "OTEL_METRIC_EXPORT_TIMEOUT"; + + /// + /// Initializes a new instance of the class. + /// + public PeriodicExportingMetricReaderOptions() + : this(new ConfigurationBuilder().AddEnvironmentVariables().Build()) + { + } + + internal PeriodicExportingMetricReaderOptions(IConfiguration configuration) { - internal const string OTelMetricExportIntervalEnvVarKey = "OTEL_METRIC_EXPORT_INTERVAL"; - internal const string OTelMetricExportTimeoutEnvVarKey = "OTEL_METRIC_EXPORT_TIMEOUT"; - - /// - /// Initializes a new instance of the class. - /// - public PeriodicExportingMetricReaderOptions() - : this(new ConfigurationBuilder().AddEnvironmentVariables().Build()) + if (configuration.TryGetIntValue(OTelMetricExportIntervalEnvVarKey, out var interval)) { + this.ExportIntervalMilliseconds = interval; } - internal PeriodicExportingMetricReaderOptions(IConfiguration configuration) + if (configuration.TryGetIntValue(OTelMetricExportTimeoutEnvVarKey, out var timeout)) { - if (configuration.TryGetIntValue(OTelMetricExportIntervalEnvVarKey, out var interval)) - { - this.ExportIntervalMilliseconds = interval; - } - - if (configuration.TryGetIntValue(OTelMetricExportTimeoutEnvVarKey, out var timeout)) - { - this.ExportTimeoutMilliseconds = timeout; - } + this.ExportTimeoutMilliseconds = timeout; } - - /// - /// Gets or sets the metric export interval in milliseconds. - /// If not set, the default value depends on the type of metric exporter - /// associated with the metric reader. - /// - public int? ExportIntervalMilliseconds { get; set; } - - /// - /// Gets or sets the metric export timeout in milliseconds. - /// If not set, the default value depends on the type of metric exporter - /// associated with the metric reader. - /// - public int? ExportTimeoutMilliseconds { get; set; } } + + /// + /// Gets or sets the metric export interval in milliseconds. + /// If not set, the default value depends on the type of metric exporter + /// associated with the metric reader. + /// + public int? ExportIntervalMilliseconds { get; set; } + + /// + /// Gets or sets the metric export timeout in milliseconds. + /// If not set, the default value depends on the type of metric exporter + /// associated with the metric reader. + /// + public int? ExportTimeoutMilliseconds { get; set; } } diff --git a/src/OpenTelemetry/Metrics/PullMetricScope.cs b/src/OpenTelemetry/Metrics/PullMetricScope.cs index 631dbdf7145..89f624cca30 100644 --- a/src/OpenTelemetry/Metrics/PullMetricScope.cs +++ b/src/OpenTelemetry/Metrics/PullMetricScope.cs @@ -1,51 +1,37 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Context; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +internal sealed class PullMetricScope : IDisposable { - internal sealed class PullMetricScope : IDisposable - { - private static readonly RuntimeContextSlot Slot = RuntimeContext.RegisterSlot("otel.pull_metric"); + private static readonly RuntimeContextSlot Slot = RuntimeContext.RegisterSlot("otel.pull_metric"); - private readonly bool previousValue; - private bool disposed; + private readonly bool previousValue; + private bool disposed; - internal PullMetricScope(bool value = true) - { - this.previousValue = Slot.Get(); - Slot.Set(value); - } + internal PullMetricScope(bool value = true) + { + this.previousValue = Slot.Get(); + Slot.Set(value); + } - internal static bool IsPullAllowed => Slot.Get(); + internal static bool IsPullAllowed => Slot.Get(); - public static IDisposable Begin(bool value = true) - { - return new PullMetricScope(value); - } + public static IDisposable Begin(bool value = true) + { + return new PullMetricScope(value); + } - /// - public void Dispose() + /// + public void Dispose() + { + if (!this.disposed) { - if (!this.disposed) - { - Slot.Set(this.previousValue); - this.disposed = true; - } + Slot.Set(this.previousValue); + this.disposed = true; } } } diff --git a/src/OpenTelemetry/Metrics/StringArrayEqualityComparer.cs b/src/OpenTelemetry/Metrics/StringArrayEqualityComparer.cs index ef2800114bd..696cd40c23d 100644 --- a/src/OpenTelemetry/Metrics/StringArrayEqualityComparer.cs +++ b/src/OpenTelemetry/Metrics/StringArrayEqualityComparer.cs @@ -1,76 +1,67 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Metrics +using System.Diagnostics; + +namespace OpenTelemetry.Metrics; + +internal sealed class StringArrayEqualityComparer : IEqualityComparer { - internal sealed class StringArrayEqualityComparer : IEqualityComparer + public bool Equals(string[]? strings1, string[]? strings2) { - public bool Equals(string[] strings1, string[] strings2) + if (ReferenceEquals(strings1, strings2)) { - if (ReferenceEquals(strings1, strings2)) - { - return true; - } + return true; + } - if (ReferenceEquals(strings1, null) || ReferenceEquals(strings2, null)) - { - return false; - } + if (ReferenceEquals(strings1, null) || ReferenceEquals(strings2, null)) + { + return false; + } + + var len1 = strings1.Length; - var len1 = strings1.Length; + if (len1 != strings2.Length) + { + return false; + } - if (len1 != strings2.Length) + for (int i = 0; i < len1; i++) + { + if (!strings1[i].Equals(strings2[i], StringComparison.Ordinal)) { return false; } + } - for (int i = 0; i < len1; i++) - { - if (!strings1[i].Equals(strings2[i], StringComparison.Ordinal)) - { - return false; - } - } + return true; + } - return true; - } + public int GetHashCode(string[] strings) + { + Debug.Assert(strings != null, "strings was null"); - public int GetHashCode(string[] strings) - { #if NET6_0_OR_GREATER - HashCode hashCode = default; - for (int i = 0; i < strings.Length; i++) - { - hashCode.Add(strings[i]); - } + HashCode hashCode = default; + + for (int i = 0; i < strings.Length; i++) + { + hashCode.Add(strings[i]); + } - var hash = hashCode.ToHashCode(); + var hash = hashCode.ToHashCode(); #else - var hash = 17; + var hash = 17; - for (int i = 0; i < strings.Length; i++) + for (int i = 0; i < strings!.Length; i++) + { + unchecked { - unchecked - { - hash = (hash * 31) + (strings[i]?.GetHashCode() ?? 0); - } + hash = (hash * 31) + (strings[i]?.GetHashCode() ?? 0); } + } #endif - return hash; - } + return hash; } } diff --git a/src/OpenTelemetry/Metrics/Tags.cs b/src/OpenTelemetry/Metrics/Tags.cs index e51bb452bb3..4084808e8a9 100644 --- a/src/OpenTelemetry/Metrics/Tags.cs +++ b/src/OpenTelemetry/Metrics/Tags.cs @@ -1,96 +1,84 @@ -// // 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. -// - -namespace OpenTelemetry.Metrics +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Metrics; + +internal readonly struct Tags : IEquatable { - internal readonly struct Tags : IEquatable - { - private readonly int hashCode; + public static readonly Tags EmptyTags = new(Array.Empty>()); - public Tags(KeyValuePair[] keyValuePairs) - { - this.KeyValuePairs = keyValuePairs; + private readonly int hashCode; + + public Tags(KeyValuePair[] keyValuePairs) + { + this.KeyValuePairs = keyValuePairs; #if NET6_0_OR_GREATER - HashCode hashCode = default; - for (int i = 0; i < this.KeyValuePairs.Length; i++) - { - ref var item = ref this.KeyValuePairs[i]; - hashCode.Add(item.Key); - hashCode.Add(item.Value); - } + HashCode hashCode = default; + for (int i = 0; i < this.KeyValuePairs.Length; i++) + { + ref var item = ref this.KeyValuePairs[i]; + hashCode.Add(item.Key); + hashCode.Add(item.Value); + } - var hash = hashCode.ToHashCode(); + var hash = hashCode.ToHashCode(); #else - var hash = 17; - for (int i = 0; i < this.KeyValuePairs.Length; i++) + var hash = 17; + for (int i = 0; i < this.KeyValuePairs.Length; i++) + { + ref var item = ref this.KeyValuePairs[i]; + unchecked { - ref var item = ref this.KeyValuePairs[i]; - unchecked - { - hash = (hash * 31) + (item.Key?.GetHashCode() ?? 0); - hash = (hash * 31) + (item.Value?.GetHashCode() ?? 0); - } + hash = (hash * 31) + (item.Key?.GetHashCode() ?? 0); + hash = (hash * 31) + (item.Value?.GetHashCode() ?? 0); } + } #endif - this.hashCode = hash; - } + this.hashCode = hash; + } - public readonly KeyValuePair[] KeyValuePairs { get; } + public readonly KeyValuePair[] KeyValuePairs { get; } - public static bool operator ==(Tags tag1, Tags tag2) => tag1.Equals(tag2); + public static bool operator ==(Tags tag1, Tags tag2) => tag1.Equals(tag2); - public static bool operator !=(Tags tag1, Tags tag2) => !tag1.Equals(tag2); + public static bool operator !=(Tags tag1, Tags tag2) => !tag1.Equals(tag2); + + public override readonly bool Equals(object? obj) + { + return obj is Tags other && this.Equals(other); + } - public override readonly bool Equals(object obj) + public readonly bool Equals(Tags other) + { + var length = this.KeyValuePairs.Length; + + if (length != other.KeyValuePairs.Length) { - return obj is Tags other && this.Equals(other); + return false; } - public readonly bool Equals(Tags other) + for (int i = 0; i < length; i++) { - var length = this.KeyValuePairs.Length; + ref var left = ref this.KeyValuePairs[i]; + ref var right = ref other.KeyValuePairs[i]; - if (length != other.KeyValuePairs.Length) + // Equality check for Keys + if (!left.Key.Equals(right.Key, StringComparison.Ordinal)) { return false; } - for (int i = 0; i < length; i++) + // Equality check for Values + if (!left.Value?.Equals(right.Value) ?? right.Value != null) { - ref var left = ref this.KeyValuePairs[i]; - ref var right = ref other.KeyValuePairs[i]; - - // Equality check for Keys - if (!left.Key.Equals(right.Key, StringComparison.Ordinal)) - { - return false; - } - - // Equality check for Values - if (!left.Value?.Equals(right.Value) ?? right.Value != null) - { - return false; - } + return false; } - - return true; } - public override readonly int GetHashCode() => this.hashCode; + return true; } + + public override readonly int GetHashCode() => this.hashCode; } diff --git a/src/OpenTelemetry/Metrics/ThreadStaticStorage.cs b/src/OpenTelemetry/Metrics/ThreadStaticStorage.cs index 315d05766fa..35f9be5b5da 100644 --- a/src/OpenTelemetry/Metrics/ThreadStaticStorage.cs +++ b/src/OpenTelemetry/Metrics/ThreadStaticStorage.cs @@ -1,160 +1,163 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +#if NET8_0_OR_GREATER +using System.Collections.Frozen; +#endif +using System.Diagnostics; using System.Runtime.CompilerServices; using OpenTelemetry.Internal; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Metrics; + +internal sealed class ThreadStaticStorage { - internal sealed class ThreadStaticStorage - { - internal const int MaxTagCacheSize = 8; + internal const int MaxTagCacheSize = 8; - [ThreadStatic] - private static ThreadStaticStorage storage; + [ThreadStatic] + private static ThreadStaticStorage? storage; - private readonly TagStorage[] primaryTagStorage = new TagStorage[MaxTagCacheSize]; - private readonly TagStorage[] secondaryTagStorage = new TagStorage[MaxTagCacheSize]; + private readonly TagStorage[] primaryTagStorage = new TagStorage[MaxTagCacheSize]; + private readonly TagStorage[] secondaryTagStorage = new TagStorage[MaxTagCacheSize]; - private ThreadStaticStorage() + private ThreadStaticStorage() + { + for (int i = 0; i < MaxTagCacheSize; i++) { - for (int i = 0; i < MaxTagCacheSize; i++) - { - this.primaryTagStorage[i] = new TagStorage(i + 1); - this.secondaryTagStorage[i] = new TagStorage(i + 1); - } + this.primaryTagStorage[i] = new TagStorage(i + 1); + this.secondaryTagStorage[i] = new TagStorage(i + 1); } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static ThreadStaticStorage GetStorage() - { - if (storage == null) - { - storage = new ThreadStaticStorage(); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ThreadStaticStorage GetStorage() + => storage ??= new ThreadStaticStorage(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void SplitToKeysAndValues( + ReadOnlySpan> tags, + int tagLength, + out KeyValuePair[] tagKeysAndValues) + { + Guard.ThrowIfZero(tagLength, $"There must be at least one tag to use {nameof(ThreadStaticStorage)}"); - return storage; + if (tagLength <= MaxTagCacheSize) + { + tagKeysAndValues = this.primaryTagStorage[tagLength - 1].TagKeysAndValues; + } + else + { + tagKeysAndValues = new KeyValuePair[tagLength]; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void SplitToKeysAndValues(ReadOnlySpan> tags, int tagLength, out KeyValuePair[] tagKeysAndValues) + tags.CopyTo(tagKeysAndValues); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void SplitToKeysAndValues( + ReadOnlySpan> tags, + int tagLength, +#if NET8_0_OR_GREATER + FrozenSet tagKeysInteresting, +#else + HashSet tagKeysInteresting, +#endif + out KeyValuePair[]? tagKeysAndValues, + out int actualLength) + { + // We do not know ahead the actual length, so start with max possible length. + var maxLength = Math.Min(tagKeysInteresting.Count, tagLength); + if (maxLength == 0) + { + tagKeysAndValues = null; + } + else if (maxLength <= MaxTagCacheSize) + { + tagKeysAndValues = this.primaryTagStorage[maxLength - 1].TagKeysAndValues; + } + else { - Guard.ThrowIfZero(tagLength, $"There must be at least one tag to use {nameof(ThreadStaticStorage)}"); + tagKeysAndValues = new KeyValuePair[maxLength]; + } - if (tagLength <= MaxTagCacheSize) - { - tagKeysAndValues = this.primaryTagStorage[tagLength - 1].TagKeysAndValues; - } - else + actualLength = 0; + for (var n = 0; n < tagLength; n++) + { + // Copy only interesting tags, and keep count. + if (tagKeysInteresting.Contains(tags[n].Key)) { - tagKeysAndValues = new KeyValuePair[tagLength]; - } + Debug.Assert(tagKeysAndValues != null, "tagKeysAndValues was null"); - tags.CopyTo(tagKeysAndValues); + tagKeysAndValues![actualLength] = tags[n]; + actualLength++; + } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void SplitToKeysAndValues(ReadOnlySpan> tags, int tagLength, HashSet tagKeysInteresting, out KeyValuePair[] tagKeysAndValues, out int actualLength) + // If the actual length was equal to max, great! + // else, we need to pick the array of the actual length, + // and copy tags into it. + // This optimizes the common scenario: + // User is interested only in TagA and TagB + // and incoming measurement has TagA and TagB and many more. + // In this case, the actual length would be same as max length, + // and the following copy is avoided. + if (actualLength < maxLength) { - // We do not know ahead the actual length, so start with max possible length. - var maxLength = Math.Min(tagKeysInteresting.Count, tagLength); - if (maxLength == 0) + if (actualLength == 0) { tagKeysAndValues = null; + return; } - else if (maxLength <= MaxTagCacheSize) + + Debug.Assert(tagKeysAndValues != null, "tagKeysAndValues was null"); + + if (actualLength <= MaxTagCacheSize) { - tagKeysAndValues = this.primaryTagStorage[maxLength - 1].TagKeysAndValues; + var tmpTagKeysAndValues = this.primaryTagStorage[actualLength - 1].TagKeysAndValues; + + Array.Copy(tagKeysAndValues, 0, tmpTagKeysAndValues, 0, actualLength); + + tagKeysAndValues = tmpTagKeysAndValues; } else { - tagKeysAndValues = new KeyValuePair[maxLength]; - } + var tmpTagKeysAndValues = new KeyValuePair[actualLength]; - actualLength = 0; - for (var n = 0; n < tagLength; n++) - { - // Copy only interesting tags, and keep count. - if (tagKeysInteresting.Contains(tags[n].Key)) - { - tagKeysAndValues[actualLength] = tags[n]; - actualLength++; - } - } + Array.Copy(tagKeysAndValues, 0, tmpTagKeysAndValues, 0, actualLength); - // If the actual length was equal to max, great! - // else, we need to pick the array of the actual length, - // and copy tags into it. - // This optimizes the common scenario: - // User is interested only in TagA and TagB - // and incoming measurement has TagA and TagB and many more. - // In this case, the actual length would be same as max length, - // and the following copy is avoided. - if (actualLength < maxLength) - { - if (actualLength == 0) - { - tagKeysAndValues = null; - return; - } - else if (actualLength <= MaxTagCacheSize) - { - var tmpTagKeysAndValues = this.primaryTagStorage[actualLength - 1].TagKeysAndValues; - - Array.Copy(tagKeysAndValues, 0, tmpTagKeysAndValues, 0, actualLength); - - tagKeysAndValues = tmpTagKeysAndValues; - } - else - { - var tmpTagKeysAndValues = new KeyValuePair[actualLength]; - - Array.Copy(tagKeysAndValues, 0, tmpTagKeysAndValues, 0, actualLength); - - tagKeysAndValues = tmpTagKeysAndValues; - } + tagKeysAndValues = tmpTagKeysAndValues; } } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void CloneKeysAndValues(KeyValuePair[] inputTagKeysAndValues, int tagLength, out KeyValuePair[] clonedTagKeysAndValues) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void CloneKeysAndValues( + KeyValuePair[] inputTagKeysAndValues, + int tagLength, + out KeyValuePair[] clonedTagKeysAndValues) + { + Guard.ThrowIfZero(tagLength, $"There must be at least one tag to use {nameof(ThreadStaticStorage)}", $"{nameof(tagLength)}"); + + if (tagLength <= MaxTagCacheSize) + { + clonedTagKeysAndValues = this.secondaryTagStorage[tagLength - 1].TagKeysAndValues; + } + else { - Guard.ThrowIfZero(tagLength, $"There must be at least one tag to use {nameof(ThreadStaticStorage)}", $"{nameof(tagLength)}"); + clonedTagKeysAndValues = new KeyValuePair[tagLength]; + } - if (tagLength <= MaxTagCacheSize) - { - clonedTagKeysAndValues = this.secondaryTagStorage[tagLength - 1].TagKeysAndValues; - } - else - { - clonedTagKeysAndValues = new KeyValuePair[tagLength]; - } + Array.Copy(inputTagKeysAndValues, 0, clonedTagKeysAndValues, 0, tagLength); + } - Array.Copy(inputTagKeysAndValues, 0, clonedTagKeysAndValues, 0, tagLength); - } + internal sealed class TagStorage + { + // Used to split into Key sequence, Value sequence. + internal readonly KeyValuePair[] TagKeysAndValues; - internal sealed class TagStorage + internal TagStorage(int n) { - // Used to split into Key sequence, Value sequence. - internal readonly KeyValuePair[] TagKeysAndValues; - - internal TagStorage(int n) - { - this.TagKeysAndValues = new KeyValuePair[n]; - } + this.TagKeysAndValues = new KeyValuePair[n]; } } } diff --git a/src/OpenTelemetry/OpenTelemetry.csproj b/src/OpenTelemetry/OpenTelemetry.csproj index 3758002bca8..754627add78 100644 --- a/src/OpenTelemetry/OpenTelemetry.csproj +++ b/src/OpenTelemetry/OpenTelemetry.csproj @@ -1,19 +1,13 @@ + - - net6.0;netstandard2.1;netstandard2.0;net462 + $(TargetFrameworksForLibrariesExtended) OpenTelemetry .NET SDK - - $(NoWarn),1591,CS0618 core- - - - disable + @@ -22,7 +16,11 @@ - + + + + + diff --git a/src/OpenTelemetry/OpenTelemetryBuilderSdkExtensions.cs b/src/OpenTelemetry/OpenTelemetryBuilderSdkExtensions.cs new file mode 100644 index 00000000000..4ae87d34078 --- /dev/null +++ b/src/OpenTelemetry/OpenTelemetryBuilderSdkExtensions.cs @@ -0,0 +1,254 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Internal; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +namespace OpenTelemetry; + +/// +/// Contains methods for extending the interface. +/// +public static class OpenTelemetryBuilderSdkExtensions +{ + /// + /// Registers an action to configure the s used + /// by tracing, metrics, and logging. + /// + /// . + /// + /// Note: This is safe to be called multiple times and by library authors. + /// Each registered configuration action will be applied sequentially. + /// + /// configuration + /// action. + /// The supplied for chaining + /// calls. + public static IOpenTelemetryBuilder ConfigureResource( + this IOpenTelemetryBuilder builder, + Action configure) + { + Guard.ThrowIfNull(builder); + Guard.ThrowIfNull(configure); + + builder.Services.ConfigureOpenTelemetryMeterProvider( + builder => builder.ConfigureResource(configure)); + + builder.Services.ConfigureOpenTelemetryTracerProvider( + builder => builder.ConfigureResource(configure)); + + builder.Services.ConfigureOpenTelemetryLoggerProvider( + builder => builder.ConfigureResource(configure)); + + return builder; + } + + /// + /// Adds metric services into the builder. + /// + /// . + /// + /// 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. + public static IOpenTelemetryBuilder WithMetrics( + this IOpenTelemetryBuilder builder) + => WithMetrics(builder, b => { }); + + /// + /// Adds metric services into the builder. + /// + /// + /// . + /// + /// configuration callback. + /// The supplied for chaining + /// calls. + public static IOpenTelemetryBuilder WithMetrics( + this IOpenTelemetryBuilder builder, + Action configure) + { + OpenTelemetryMetricsBuilderExtensions.RegisterMetricsListener( + builder.Services, + configure); + + return builder; + } + + /// + /// Adds tracing services into the builder. + /// + /// . + /// + /// 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 IOpenTelemetryBuilder WithTracing(this IOpenTelemetryBuilder builder) + => WithTracing(builder, b => { }); + + /// + /// Adds tracing services into the builder. + /// + /// + /// . + /// + /// configuration callback. + /// The supplied for chaining + /// calls. + public static IOpenTelemetryBuilder WithTracing( + this IOpenTelemetryBuilder builder, + Action configure) + { + Guard.ThrowIfNull(configure); + + var tracerProviderBuilder = new TracerProviderBuilderBase(builder.Services); + + configure(tracerProviderBuilder); + + return builder; + } + +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds logging services into the builder. + /// + /// . + /// + /// WARNING: This is an experimental API which might change or + /// be removed in the future. Use at your own risk. + /// 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. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + /// + /// Adds logging services into the builder. + /// + /// . + /// + /// 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. + internal +#endif + static IOpenTelemetryBuilder WithLogging(this IOpenTelemetryBuilder builder) + => WithLogging(builder, configureBuilder: null, configureOptions: null); + +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds logging services into the builder. + /// + /// + /// . + /// + /// configuration callback. + /// The supplied for chaining + /// calls. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + /// + /// Adds logging services into the builder. + /// + /// + /// . + /// + /// configuration callback. + /// The supplied for chaining + /// calls. + internal +#endif + static IOpenTelemetryBuilder WithLogging( + this IOpenTelemetryBuilder builder, + Action configure) + { + Guard.ThrowIfNull(configure); + + return WithLogging(builder, configureBuilder: configure, configureOptions: null); + } + +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Adds logging services into the builder. + /// + /// + /// . + /// Optional configuration callback. + /// Optional configuration callback. + /// The supplied for chaining + /// calls. +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + /// + /// Adds logging services into the builder. + /// + /// + /// . + /// Optional configuration callback. + /// Optional configuration callback. + /// The supplied for chaining + /// calls. + internal +#endif + static IOpenTelemetryBuilder WithLogging( + this IOpenTelemetryBuilder builder, + Action? configureBuilder, + Action? configureOptions) + { + builder.Services.AddLogging( + logging => logging.UseOpenTelemetry(configureBuilder, configureOptions)); + + return builder; + } +} diff --git a/src/OpenTelemetry/ProviderExtensions.cs b/src/OpenTelemetry/ProviderExtensions.cs index 2a78b2f580c..8b79b1f998e 100644 --- a/src/OpenTelemetry/ProviderExtensions.cs +++ b/src/OpenTelemetry/ProviderExtensions.cs @@ -1,21 +1,7 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics.CodeAnalysis; using OpenTelemetry.Logs; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; @@ -33,7 +19,7 @@ public static class ProviderExtensions /// /// . /// if found otherwise . - public static Resource GetResource(this BaseProvider baseProvider) + public static Resource GetResource([AllowNull] this BaseProvider baseProvider) { if (baseProvider is TracerProviderSdk tracerProviderSdk) { @@ -60,14 +46,14 @@ public static Resource GetResource(this BaseProvider baseProvider) /// /// . /// if found otherwise . - public static Resource GetDefaultResource(this BaseProvider baseProvider) + public static Resource GetDefaultResource([AllowNull] this BaseProvider baseProvider) { var builder = ResourceBuilder.CreateDefault(); builder.ServiceProvider = GetServiceProvider(baseProvider); return builder.Build(); } - internal static IServiceProvider? GetServiceProvider(this BaseProvider baseProvider) + internal static IServiceProvider? GetServiceProvider(this BaseProvider? baseProvider) { if (baseProvider is TracerProviderSdk tracerProviderSdk) { diff --git a/src/OpenTelemetry/README.md b/src/OpenTelemetry/README.md index ed3fccbfcf1..8e444a6f167 100644 --- a/src/OpenTelemetry/README.md +++ b/src/OpenTelemetry/README.md @@ -35,11 +35,12 @@ and enable the SDK, when they install a particular exporter. ## Getting started with Logging If you are new to logging, it is recommended to first follow the [getting -started in 5 minutes](../../docs/logs/getting-started/README.md) guide to get up +started in 5 minutes - Console +Application](../../docs/logs/getting-started-console/README.md) guide to get up and running. While [OpenTelemetry -logging](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/overview.md) +logging](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/README.md) specification is an experimental signal, `ILogger` is the de-facto logging API provided by the .NET runtime and is a stable API recommended for production use. This repo ships an OpenTelemetry @@ -173,4 +174,4 @@ start from beginning and overwrite existing text. * [OpenTelemetry Project](https://opentelemetry.io/) * [OpenTelemetry Tracing SDK specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md) -* [OpenTelemetry Logging specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/overview.md) +* [OpenTelemetry Logging specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/README.md) diff --git a/src/OpenTelemetry/ReadOnlyFilteredTagCollection.cs b/src/OpenTelemetry/ReadOnlyFilteredTagCollection.cs new file mode 100644 index 00000000000..2c41020c767 --- /dev/null +++ b/src/OpenTelemetry/ReadOnlyFilteredTagCollection.cs @@ -0,0 +1,131 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NET8_0_OR_GREATER +using System.Collections.Frozen; +#endif +using System.Diagnostics; +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +using OpenTelemetry.Internal; +#endif + +namespace OpenTelemetry; + +#if EXPOSE_EXPERIMENTAL_FEATURES +/// +/// A read-only collection of tag key/value pairs which returns a filtered +/// subset of tags when enumerated. +/// +// Note: Does not implement IReadOnlyCollection<> or IEnumerable<> to +// prevent accidental boxing. +#if NET8_0_OR_GREATER +[Experimental(DiagnosticDefinitions.ExemplarExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif +public +#else +internal +#endif + readonly struct ReadOnlyFilteredTagCollection +{ +#if NET8_0_OR_GREATER + private readonly FrozenSet? excludedKeys; +#else + private readonly HashSet? excludedKeys; +#endif + private readonly KeyValuePair[] tags; + private readonly int count; + + internal ReadOnlyFilteredTagCollection( +#if NET8_0_OR_GREATER + FrozenSet? excludedKeys, +#else + HashSet? excludedKeys, +#endif + KeyValuePair[] tags, + int count) + { + Debug.Assert(tags != null, "tags was null"); + Debug.Assert(count <= tags!.Length, "count was invalid"); + + this.excludedKeys = excludedKeys; + this.tags = tags; + this.count = count; + } + + /// + /// Gets the maximum number of tags in the collection. + /// + /// + /// Note: Enumerating the collection may return fewer results depending on + /// the filter. + /// + internal int MaximumCount => this.count; + + /// + /// Returns an enumerator that iterates through the tags. + /// + /// . + public Enumerator GetEnumerator() => new(this); + + internal IReadOnlyList> ToReadOnlyList() + { + var list = new List>(this.MaximumCount); + + foreach (var item in this) + { + list.Add(item); + } + + return list; + } + + /// + /// Enumerates the elements of a . + /// + // Note: Does not implement IEnumerator<> to prevent accidental boxing. + public struct Enumerator + { + private readonly ReadOnlyFilteredTagCollection source; + private int index; + + internal Enumerator(ReadOnlyFilteredTagCollection source) + { + this.source = source; + this.index = -1; + } + + /// + /// Gets the tag at the current position of the enumerator. + /// + public readonly KeyValuePair Current + => this.source.tags[this.index]; + + /// + /// Advances the enumerator to the next element of the . + /// + /// if the enumerator was + /// successfully advanced to the next element; if the enumerator has passed the end of the + /// collection. + public bool MoveNext() + { + while (true) + { + int index = ++this.index; + if (index < this.source.MaximumCount) + { + if (this.source.excludedKeys?.Contains(this.source.tags[index].Key) ?? false) + { + continue; + } + + return true; + } + + return false; + } + } + } +} diff --git a/src/OpenTelemetry/ReadOnlyTagCollection.cs b/src/OpenTelemetry/ReadOnlyTagCollection.cs index 1ff8bbfd4c7..f8582e1af99 100644 --- a/src/OpenTelemetry/ReadOnlyTagCollection.cs +++ b/src/OpenTelemetry/ReadOnlyTagCollection.cs @@ -1,91 +1,62 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -#nullable enable +namespace OpenTelemetry; -namespace OpenTelemetry +/// +/// A read-only collection of tag key/value pairs. +/// +// Note: Does not implement IReadOnlyCollection<> or IEnumerable<> to +// prevent accidental boxing. +public readonly struct ReadOnlyTagCollection { + internal readonly KeyValuePair[] KeyAndValues; + + internal ReadOnlyTagCollection(KeyValuePair[]? keyAndValues) + { + this.KeyAndValues = keyAndValues ?? Array.Empty>(); + } + /// - /// A read-only collection of tag key/value pairs. + /// Gets the number of tags in the collection. /// - // Note: Does not implement IReadOnlyCollection<> or IEnumerable<> to - // prevent accidental boxing. - public readonly struct ReadOnlyTagCollection + public int Count => this.KeyAndValues.Length; + + /// + /// Returns an enumerator that iterates through the tags. + /// + /// . + public Enumerator GetEnumerator() => new(this); + + /// + /// Enumerates the elements of a . + /// + // Note: Does not implement IEnumerator<> to prevent accidental boxing. + public struct Enumerator { - internal readonly KeyValuePair[] KeyAndValues; + private readonly ReadOnlyTagCollection source; + private int index; - internal ReadOnlyTagCollection(KeyValuePair[]? keyAndValues) + internal Enumerator(ReadOnlyTagCollection source) { - this.KeyAndValues = keyAndValues ?? Array.Empty>(); + this.source = source; + this.index = -1; } /// - /// Gets the number of tags in the collection. + /// Gets the tag at the current position of the enumerator. /// - public int Count => this.KeyAndValues.Length; + public readonly KeyValuePair Current + => this.source.KeyAndValues[this.index]; /// - /// Returns an enumerator that iterates through the tags. + /// Advances the enumerator to the next element of the . /// - /// . - public Enumerator GetEnumerator() => new(this); - - /// - /// Enumerates the elements of a . - /// - // Note: Does not implement IEnumerator<> to prevent accidental boxing. - public struct Enumerator - { - private readonly ReadOnlyTagCollection source; - private int index; - - internal Enumerator(ReadOnlyTagCollection source) - { - this.source = source; - this.index = 0; - this.Current = default; - } - - /// - /// Gets the tag at the current position of the enumerator. - /// - public KeyValuePair Current { get; private set; } - - /// - /// Advances the enumerator to the next element of the . - /// - /// if the enumerator was - /// successfully advanced to the next element; if the enumerator has passed the end of the - /// collection. - public bool MoveNext() - { - int index = this.index; - - if (index < this.source.Count) - { - this.Current = this.source.KeyAndValues[index]; - - this.index++; - return true; - } - - return false; - } - } + /// if the enumerator was + /// successfully advanced to the next element; if the enumerator has passed the end of the + /// collection. + public bool MoveNext() => ++this.index < this.source.Count; } } diff --git a/src/OpenTelemetry/Resources/IResourceDetector.cs b/src/OpenTelemetry/Resources/IResourceDetector.cs index df3fdf427d5..a2ebc96bec4 100644 --- a/src/OpenTelemetry/Resources/IResourceDetector.cs +++ b/src/OpenTelemetry/Resources/IResourceDetector.cs @@ -1,32 +1,16 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -#nullable enable +namespace OpenTelemetry.Resources; -namespace OpenTelemetry.Resources +/// +/// An interface for Resource detectors. +/// +public interface IResourceDetector { /// - /// An interface for Resource detectors. + /// Called to get a resource with attributes from detector. /// - public interface IResourceDetector - { - /// - /// Called to get a resource with attributes from detector. - /// - /// An instance of . - Resource Detect(); - } + /// An instance of . + Resource Detect(); } diff --git a/src/OpenTelemetry/Resources/OtelEnvResourceDetector.cs b/src/OpenTelemetry/Resources/OtelEnvResourceDetector.cs index 59dbc8d08c8..a03aed36710 100644 --- a/src/OpenTelemetry/Resources/OtelEnvResourceDetector.cs +++ b/src/OpenTelemetry/Resources/OtelEnvResourceDetector.cs @@ -1,69 +1,53 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.Configuration; using OpenTelemetry.Internal; -namespace OpenTelemetry.Resources +namespace OpenTelemetry.Resources; + +internal sealed class OtelEnvResourceDetector : IResourceDetector { - internal sealed class OtelEnvResourceDetector : IResourceDetector + public const string EnvVarKey = "OTEL_RESOURCE_ATTRIBUTES"; + private const char AttributeListSplitter = ','; + private const char AttributeKeyValueSplitter = '='; + + private readonly IConfiguration configuration; + + public OtelEnvResourceDetector(IConfiguration configuration) { - public const string EnvVarKey = "OTEL_RESOURCE_ATTRIBUTES"; - private const char AttributeListSplitter = ','; - private const char AttributeKeyValueSplitter = '='; + this.configuration = configuration; + } - private readonly IConfiguration configuration; + public Resource Detect() + { + var resource = Resource.Empty; - public OtelEnvResourceDetector(IConfiguration configuration) + if (this.configuration.TryGetStringValue(EnvVarKey, out string? envResourceAttributeValue)) { - this.configuration = configuration; + var attributes = ParseResourceAttributes(envResourceAttributeValue!); + resource = new Resource(attributes); } - public Resource Detect() - { - var resource = Resource.Empty; - - if (this.configuration.TryGetStringValue(EnvVarKey, out string? envResourceAttributeValue)) - { - var attributes = ParseResourceAttributes(envResourceAttributeValue!); - resource = new Resource(attributes); - } + return resource; + } - return resource; - } + private static IEnumerable> ParseResourceAttributes(string resourceAttributes) + { + var attributes = new List>(); - private static IEnumerable> ParseResourceAttributes(string resourceAttributes) + string[] rawAttributes = resourceAttributes.Split(AttributeListSplitter); + foreach (string rawKeyValuePair in rawAttributes) { - var attributes = new List>(); - - string[] rawAttributes = resourceAttributes.Split(AttributeListSplitter); - foreach (string rawKeyValuePair in rawAttributes) + string[] keyValuePair = rawKeyValuePair.Split(AttributeKeyValueSplitter); + if (keyValuePair.Length != 2) { - string[] keyValuePair = rawKeyValuePair.Split(AttributeKeyValueSplitter); - if (keyValuePair.Length != 2) - { - continue; - } - - attributes.Add(new KeyValuePair(keyValuePair[0].Trim(), keyValuePair[1].Trim())); + continue; } - return attributes; + attributes.Add(new KeyValuePair(keyValuePair[0].Trim(), keyValuePair[1].Trim())); } + + return attributes; } } diff --git a/src/OpenTelemetry/Resources/OtelServiceNameEnvVarDetector.cs b/src/OpenTelemetry/Resources/OtelServiceNameEnvVarDetector.cs index 4319a8a9f65..1d1de001f94 100644 --- a/src/OpenTelemetry/Resources/OtelServiceNameEnvVarDetector.cs +++ b/src/OpenTelemetry/Resources/OtelServiceNameEnvVarDetector.cs @@ -1,50 +1,34 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.Configuration; using OpenTelemetry.Internal; -namespace OpenTelemetry.Resources +namespace OpenTelemetry.Resources; + +internal sealed class OtelServiceNameEnvVarDetector : IResourceDetector { - internal sealed class OtelServiceNameEnvVarDetector : IResourceDetector - { - public const string EnvVarKey = "OTEL_SERVICE_NAME"; + public const string EnvVarKey = "OTEL_SERVICE_NAME"; - private readonly IConfiguration configuration; + private readonly IConfiguration configuration; - public OtelServiceNameEnvVarDetector(IConfiguration configuration) - { - this.configuration = configuration; - } + public OtelServiceNameEnvVarDetector(IConfiguration configuration) + { + this.configuration = configuration; + } - public Resource Detect() - { - var resource = Resource.Empty; + public Resource Detect() + { + var resource = Resource.Empty; - if (this.configuration.TryGetStringValue(EnvVarKey, out string? envResourceAttributeValue)) + if (this.configuration.TryGetStringValue(EnvVarKey, out string? envResourceAttributeValue)) + { + resource = new Resource(new Dictionary { - resource = new Resource(new Dictionary - { - [ResourceSemanticConventions.AttributeServiceName] = envResourceAttributeValue!, - }); - } - - return resource; + [ResourceSemanticConventions.AttributeServiceName] = envResourceAttributeValue!, + }); } + + return resource; } } diff --git a/src/OpenTelemetry/Resources/Resource.cs b/src/OpenTelemetry/Resources/Resource.cs index ab5bc0733c8..a911deddf2e 100644 --- a/src/OpenTelemetry/Resources/Resource.cs +++ b/src/OpenTelemetry/Resources/Resource.cs @@ -1,133 +1,117 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Globalization; using OpenTelemetry.Internal; -namespace OpenTelemetry.Resources +namespace OpenTelemetry.Resources; + +/// +/// represents a resource, which captures identifying information about the entities +/// for which telemetry is reported. +/// Use to construct resource instances. +/// +public class Resource { + // This implementation follows https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md + /// - /// represents a resource, which captures identifying information about the entities - /// for which telemetry is reported. - /// Use to construct resource instances. + /// Initializes a new instance of the class. /// - public class Resource + /// An of attributes that describe the resource. + public Resource(IEnumerable> attributes) { - // This implementation follows https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md - - /// - /// Initializes a new instance of the class. - /// - /// An of attributes that describe the resource. - public Resource(IEnumerable> attributes) + if (attributes == null) { - if (attributes == null) - { - OpenTelemetrySdkEventSource.Log.InvalidArgument("Create resource", "attributes", "are null"); - this.Attributes = Enumerable.Empty>(); - return; - } - - // resource creation is expected to be done a few times during app startup i.e. not on the hot path, we can copy attributes. - this.Attributes = attributes.Select(SanitizeAttribute).ToList(); + OpenTelemetrySdkEventSource.Log.InvalidArgument("Create resource", "attributes", "are null"); + this.Attributes = Enumerable.Empty>(); + return; } - /// - /// Gets an empty Resource. - /// - public static Resource Empty { get; } = new Resource(Enumerable.Empty>()); + // resource creation is expected to be done a few times during app startup i.e. not on the hot path, we can copy attributes. + this.Attributes = attributes.Select(SanitizeAttribute).ToList(); + } - /// - /// Gets the collection of key-value pairs describing the resource. - /// - public IEnumerable> Attributes { get; } + /// + /// Gets an empty Resource. + /// + public static Resource Empty { get; } = new Resource(Enumerable.Empty>()); - /// - /// Returns a new, merged by merging the old with the - /// other . In case of a collision the other takes precedence. - /// - /// The that will be merged with this. - /// . - public Resource Merge(Resource other) - { - var newAttributes = new Dictionary(); + /// + /// Gets the collection of key-value pairs describing the resource. + /// + public IEnumerable> Attributes { get; } - if (other != null) - { - foreach (var attribute in other.Attributes) - { - if (!newAttributes.TryGetValue(attribute.Key, out _)) - { - newAttributes[attribute.Key] = attribute.Value; - } - } - } + /// + /// Returns a new, merged by merging the old with the + /// other . In case of a collision the other takes precedence. + /// + /// The that will be merged with this. + /// . + public Resource Merge(Resource other) + { + var newAttributes = new Dictionary(); - foreach (var attribute in this.Attributes) + if (other != null) + { + foreach (var attribute in other.Attributes) { if (!newAttributes.TryGetValue(attribute.Key, out _)) { newAttributes[attribute.Key] = attribute.Value; } } - - return new Resource(newAttributes); } - private static KeyValuePair SanitizeAttribute(KeyValuePair attribute) + foreach (var attribute in this.Attributes) { - string sanitizedKey; - if (attribute.Key == null) - { - OpenTelemetrySdkEventSource.Log.InvalidArgument("Create resource", "attribute key", "Attribute key should be non-null string."); - sanitizedKey = string.Empty; - } - else + if (!newAttributes.TryGetValue(attribute.Key, out _)) { - sanitizedKey = attribute.Key; + newAttributes[attribute.Key] = attribute.Value; } - - var sanitizedValue = SanitizeValue(attribute.Value, sanitizedKey); - return new KeyValuePair(sanitizedKey, sanitizedValue); } - private static object SanitizeValue(object value, string keyName) - { - Guard.ThrowIfNull(keyName); + return new Resource(newAttributes); + } - return value switch - { - string => value, - bool => value, - double => value, - long => value, - string[] => value, - bool[] => value, - double[] => value, - long[] => value, - int => Convert.ToInt64(value), - short => Convert.ToInt64(value), - float => Convert.ToDouble(value, CultureInfo.InvariantCulture), - int[] v => Array.ConvertAll(v, Convert.ToInt64), - short[] v => Array.ConvertAll(v, Convert.ToInt64), - float[] v => Array.ConvertAll(v, f => Convert.ToDouble(f, CultureInfo.InvariantCulture)), - _ => throw new ArgumentException("Attribute value type is not an accepted primitive", keyName), - }; + private static KeyValuePair SanitizeAttribute(KeyValuePair attribute) + { + string sanitizedKey; + if (attribute.Key == null) + { + OpenTelemetrySdkEventSource.Log.InvalidArgument("Create resource", "attribute key", "Attribute key should be non-null string."); + sanitizedKey = string.Empty; + } + else + { + sanitizedKey = attribute.Key; } + + var sanitizedValue = SanitizeValue(attribute.Value, sanitizedKey); + return new KeyValuePair(sanitizedKey, sanitizedValue); + } + + private static object SanitizeValue(object value, string keyName) + { + Guard.ThrowIfNull(keyName); + + return value switch + { + string => value, + bool => value, + double => value, + long => value, + string[] => value, + bool[] => value, + double[] => value, + long[] => value, + int => Convert.ToInt64(value), + short => Convert.ToInt64(value), + float => Convert.ToDouble(value, CultureInfo.InvariantCulture), + int[] v => Array.ConvertAll(v, Convert.ToInt64), + short[] v => Array.ConvertAll(v, Convert.ToInt64), + float[] v => Array.ConvertAll(v, f => Convert.ToDouble(f, CultureInfo.InvariantCulture)), + _ => throw new ArgumentException("Attribute value type is not an accepted primitive", keyName), + }; } } diff --git a/src/OpenTelemetry/Resources/ResourceBuilder.cs b/src/OpenTelemetry/Resources/ResourceBuilder.cs index 9b70509190b..f85755d7d02 100644 --- a/src/OpenTelemetry/Resources/ResourceBuilder.cs +++ b/src/OpenTelemetry/Resources/ResourceBuilder.cs @@ -1,211 +1,195 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Internal; -namespace OpenTelemetry.Resources +namespace OpenTelemetry.Resources; + +/// +/// Contains methods for building instances. +/// +public class ResourceBuilder { - /// - /// Contains methods for building instances. - /// - public class ResourceBuilder + internal readonly List ResourceDetectors = new(); + private static readonly Resource DefaultResource; + + static ResourceBuilder() { - internal readonly List ResourceDetectors = new(); - private static readonly Resource DefaultResource; + var defaultServiceName = "unknown_service"; - static ResourceBuilder() + try { - var defaultServiceName = "unknown_service"; - - try + var processName = Process.GetCurrentProcess().ProcessName; + if (!string.IsNullOrWhiteSpace(processName)) { - var processName = Process.GetCurrentProcess().ProcessName; - if (!string.IsNullOrWhiteSpace(processName)) - { - defaultServiceName = $"{defaultServiceName}:{processName}"; - } + defaultServiceName = $"{defaultServiceName}:{processName}"; } - catch - { - // GetCurrentProcess can throw PlatformNotSupportedException - } - - DefaultResource = new Resource(new Dictionary - { - [ResourceSemanticConventions.AttributeServiceName] = defaultServiceName, - }); } - - private ResourceBuilder() + catch { + // GetCurrentProcess can throw PlatformNotSupportedException } - internal IServiceProvider? ServiceProvider { get; set; } - - /// - /// Creates a instance with default attributes - /// added. See resource - /// semantic conventions for details. - /// Additionally it adds resource attributes parsed from OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME environment variables - /// to a following the Resource - /// SDK. - /// - /// Created . - public static ResourceBuilder CreateDefault() - => new ResourceBuilder() - .AddResource(DefaultResource) - .AddTelemetrySdk() - .AddEnvironmentVariableDetector(); - - /// - /// Creates an empty instance. - /// - /// Created . - public static ResourceBuilder CreateEmpty() - => new(); - - /// - /// Clears the s added to the builder. - /// - /// for chaining. - public ResourceBuilder Clear() + DefaultResource = new Resource(new Dictionary { - this.ResourceDetectors.Clear(); + [ResourceSemanticConventions.AttributeServiceName] = defaultServiceName, + }); + } - return this; - } + private ResourceBuilder() + { + } - /// - /// Build a merged from all the s added to the builder. - /// - /// . - public Resource Build() - { - Resource finalResource = Resource.Empty; + internal IServiceProvider? ServiceProvider { get; set; } - foreach (IResourceDetector resourceDetector in this.ResourceDetectors) + /// + /// Creates a instance with default attributes + /// added. See resource + /// semantic conventions for details. + /// Additionally it adds resource attributes parsed from OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME environment variables + /// to a following the Resource + /// SDK. + /// + /// Created . + public static ResourceBuilder CreateDefault() + => new ResourceBuilder() + .AddResource(DefaultResource) + .AddTelemetrySdk() + .AddEnvironmentVariableDetector(); + + /// + /// Creates an empty instance. + /// + /// Created . + public static ResourceBuilder CreateEmpty() + => new(); + + /// + /// Clears the s added to the builder. + /// + /// for chaining. + public ResourceBuilder Clear() + { + this.ResourceDetectors.Clear(); + + return this; + } + + /// + /// Build a merged from all the s added to the builder. + /// + /// . + public Resource Build() + { + Resource finalResource = Resource.Empty; + + foreach (IResourceDetector resourceDetector in this.ResourceDetectors) + { + if (resourceDetector is ResolvingResourceDetector resolvingResourceDetector) { - if (resourceDetector is ResolvingResourceDetector resolvingResourceDetector) - { - resolvingResourceDetector.Resolve(this.ServiceProvider); - } - - var resource = resourceDetector.Detect(); - if (resource != null) - { - finalResource = finalResource.Merge(resource); - } + resolvingResourceDetector.Resolve(this.ServiceProvider); } - return finalResource; + var resource = resourceDetector.Detect(); + if (resource != null) + { + finalResource = finalResource.Merge(resource); + } } - /// - /// Add a to the builder. - /// - /// . - /// Supplied for call chaining. - public ResourceBuilder AddDetector(IResourceDetector resourceDetector) - { - Guard.ThrowIfNull(resourceDetector); + return finalResource; + } - this.ResourceDetectors.Add(resourceDetector); + /// + /// Add a to the builder. + /// + /// . + /// Supplied for call chaining. + public ResourceBuilder AddDetector(IResourceDetector resourceDetector) + { + Guard.ThrowIfNull(resourceDetector); - return this; - } + this.ResourceDetectors.Add(resourceDetector); - /// - /// Add a to the builder which will be resolved using the application . - /// - /// Resource detector factory. - /// Supplied for call chaining. - public ResourceBuilder AddDetector(Func resourceDetectorFactory) - { - Guard.ThrowIfNull(resourceDetectorFactory); + return this; + } + + /// + /// Add a to the builder which will be resolved using the application . + /// + /// Resource detector factory. + /// Supplied for call chaining. + public ResourceBuilder AddDetector(Func resourceDetectorFactory) + { + Guard.ThrowIfNull(resourceDetectorFactory); - return this.AddDetectorInternal(sp => + return this.AddDetectorInternal(sp => + { + if (sp == null) { - if (sp == null) - { - throw new NotSupportedException("IResourceDetector factory pattern is not supported when calling ResourceBuilder.Build() directly."); - } + throw new NotSupportedException("IResourceDetector factory pattern is not supported when calling ResourceBuilder.Build() directly."); + } - return resourceDetectorFactory(sp); - }); - } + return resourceDetectorFactory(sp); + }); + } - internal ResourceBuilder AddDetectorInternal(Func resourceDetectorFactory) - { - Guard.ThrowIfNull(resourceDetectorFactory); + internal ResourceBuilder AddDetectorInternal(Func resourceDetectorFactory) + { + Guard.ThrowIfNull(resourceDetectorFactory); - this.ResourceDetectors.Add(new ResolvingResourceDetector(resourceDetectorFactory)); + this.ResourceDetectors.Add(new ResolvingResourceDetector(resourceDetectorFactory)); - return this; - } + return this; + } - internal ResourceBuilder AddResource(Resource resource) - { - Guard.ThrowIfNull(resource); + internal ResourceBuilder AddResource(Resource resource) + { + Guard.ThrowIfNull(resource); - this.ResourceDetectors.Add(new WrapperResourceDetector(resource)); + this.ResourceDetectors.Add(new WrapperResourceDetector(resource)); - return this; - } + return this; + } - internal sealed class WrapperResourceDetector : IResourceDetector + internal sealed class WrapperResourceDetector : IResourceDetector + { + private readonly Resource resource; + + public WrapperResourceDetector(Resource resource) { - private readonly Resource resource; + this.resource = resource; + } - public WrapperResourceDetector(Resource resource) - { - this.resource = resource; - } + public Resource Detect() => this.resource; + } - public Resource Detect() => this.resource; - } + private sealed class ResolvingResourceDetector : IResourceDetector + { + private readonly Func resourceDetectorFactory; + private IResourceDetector? resourceDetector; - private sealed class ResolvingResourceDetector : IResourceDetector + public ResolvingResourceDetector(Func resourceDetectorFactory) { - private readonly Func resourceDetectorFactory; - private IResourceDetector? resourceDetector; - - public ResolvingResourceDetector(Func resourceDetectorFactory) - { - this.resourceDetectorFactory = resourceDetectorFactory; - } + this.resourceDetectorFactory = resourceDetectorFactory; + } - public void Resolve(IServiceProvider? serviceProvider) - { - this.resourceDetector = this.resourceDetectorFactory(serviceProvider) - ?? throw new InvalidOperationException("ResourceDetector factory did not return a ResourceDetector instance."); - } + public void Resolve(IServiceProvider? serviceProvider) + { + this.resourceDetector = this.resourceDetectorFactory(serviceProvider) + ?? throw new InvalidOperationException("ResourceDetector factory did not return a ResourceDetector instance."); + } - public Resource Detect() - { - var detector = this.resourceDetector; + public Resource Detect() + { + var detector = this.resourceDetector; - Debug.Assert(detector != null, "detector was null"); + Debug.Assert(detector != null, "detector was null"); - return detector?.Detect() ?? Resource.Empty; - } + return detector?.Detect() ?? Resource.Empty; } } } diff --git a/src/OpenTelemetry/Resources/ResourceBuilderExtensions.cs b/src/OpenTelemetry/Resources/ResourceBuilderExtensions.cs index bc1d4c075bf..1f02b55381f 100644 --- a/src/OpenTelemetry/Resources/ResourceBuilderExtensions.cs +++ b/src/OpenTelemetry/Resources/ResourceBuilderExtensions.cs @@ -1,154 +1,114 @@ -// // 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. -// - -#nullable enable - -using System.Reflection; +// SPDX-License-Identifier: Apache-2.0 + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Internal; -namespace OpenTelemetry.Resources +namespace OpenTelemetry.Resources; + +/// +/// Contains extension methods for building s. +/// +public static class ResourceBuilderExtensions { + private static readonly string InstanceId = Guid.NewGuid().ToString(); + + private static Resource TelemetryResource { get; } = new Resource(new Dictionary + { + [ResourceSemanticConventions.AttributeTelemetrySdkName] = "opentelemetry", + [ResourceSemanticConventions.AttributeTelemetrySdkLanguage] = "dotnet", + [ResourceSemanticConventions.AttributeTelemetrySdkVersion] = Sdk.InformationalVersion, + }); + /// - /// Contains extension methods for building s. + /// Adds service information to a + /// following semantic + /// conventions. /// - public static class ResourceBuilderExtensions + /// . + /// Name of the service. + /// Optional namespace of the service. + /// Optional version of the service. + /// Specify to automatically generate a for if not supplied. + /// Optional unique identifier of the service instance. + /// Returns for chaining. + public static ResourceBuilder AddService( + this ResourceBuilder resourceBuilder, + string serviceName, + string? serviceNamespace = null, + string? serviceVersion = null, + bool autoGenerateServiceInstanceId = true, + string? serviceInstanceId = null) { - private static Resource TelemetryResource { get; } = new Resource(new Dictionary - { - [ResourceSemanticConventions.AttributeTelemetrySdkName] = "opentelemetry", - [ResourceSemanticConventions.AttributeTelemetrySdkLanguage] = "dotnet", - [ResourceSemanticConventions.AttributeTelemetrySdkVersion] = GetAssemblyInformationalVersion(), - }); - - /// - /// Adds service information to a - /// following semantic - /// conventions. - /// - /// . - /// Name of the service. - /// Optional namespace of the service. - /// Optional version of the service. - /// Specify to automatically generate a for if not supplied. - /// Optional unique identifier of the service instance. - /// Returns for chaining. - public static ResourceBuilder AddService( - this ResourceBuilder resourceBuilder, - string serviceName, - string? serviceNamespace = null, - string? serviceVersion = null, - bool autoGenerateServiceInstanceId = true, - string? serviceInstanceId = null) - { - Dictionary resourceAttributes = new Dictionary(); - - Guard.ThrowIfNullOrEmpty(serviceName); + Dictionary resourceAttributes = new Dictionary(); - resourceAttributes.Add(ResourceSemanticConventions.AttributeServiceName, serviceName); + Guard.ThrowIfNullOrEmpty(serviceName); - if (!string.IsNullOrEmpty(serviceNamespace)) - { - resourceAttributes.Add(ResourceSemanticConventions.AttributeServiceNamespace, serviceNamespace!); - } + resourceAttributes.Add(ResourceSemanticConventions.AttributeServiceName, serviceName); - if (!string.IsNullOrEmpty(serviceVersion)) - { - resourceAttributes.Add(ResourceSemanticConventions.AttributeServiceVersion, serviceVersion!); - } - - if (serviceInstanceId == null && autoGenerateServiceInstanceId) - { - serviceInstanceId = Guid.NewGuid().ToString(); - } - - if (serviceInstanceId != null) - { - resourceAttributes.Add(ResourceSemanticConventions.AttributeServiceInstance, serviceInstanceId); - } - - return resourceBuilder.AddResource(new Resource(resourceAttributes)); - } - - /// - /// Adds service information to a - /// following semantic - /// conventions. - /// - /// . - /// Returns for chaining. - public static ResourceBuilder AddTelemetrySdk(this ResourceBuilder resourceBuilder) + if (!string.IsNullOrEmpty(serviceNamespace)) { - return resourceBuilder.AddResource(TelemetryResource); + resourceAttributes.Add(ResourceSemanticConventions.AttributeServiceNamespace, serviceNamespace!); } - /// - /// Adds attributes to a . - /// - /// . - /// An of attributes that describe the resource. - /// Returns for chaining. - public static ResourceBuilder AddAttributes(this ResourceBuilder resourceBuilder, IEnumerable> attributes) + if (!string.IsNullOrEmpty(serviceVersion)) { - return resourceBuilder.AddResource(new Resource(attributes)); + resourceAttributes.Add(ResourceSemanticConventions.AttributeServiceVersion, serviceVersion!); } - /// - /// Adds resource attributes parsed from OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME environment variables - /// to a following the Resource - /// SDK. - /// - /// . - /// Returns for chaining. - public static ResourceBuilder AddEnvironmentVariableDetector(this ResourceBuilder resourceBuilder) + if (serviceInstanceId == null && autoGenerateServiceInstanceId) { - Lazy configuration = new Lazy(() => new ConfigurationBuilder().AddEnvironmentVariables().Build()); - - return resourceBuilder - .AddDetectorInternal(sp => new OtelEnvResourceDetector(sp?.GetService() ?? configuration.Value)) - .AddDetectorInternal(sp => new OtelServiceNameEnvVarDetector(sp?.GetService() ?? configuration.Value)); + serviceInstanceId = InstanceId; } - private static string GetAssemblyInformationalVersion() + if (serviceInstanceId != null) { - try - { - var informationalVersion = typeof(Resource).Assembly.GetCustomAttribute()?.InformationalVersion; - - if (informationalVersion == null) - { - return string.Empty; - } - - // informationalVersion could be in the following format: - // {majorVersion}.{minorVersion}.{patchVersion}.{pre-release label}.{pre-release version}.{gitHeight}.{Git SHA of current commit} - // The following parts are optional: pre-release label, pre-release version, git height, Git SHA of current commit - // for example: 1.5.0-alpha.1.40+807f703e1b4d9874a92bd86d9f2d4ebe5b5d52e4 - - var indexOfPlusSign = informationalVersion.IndexOf('+'); - return indexOfPlusSign > 0 ? informationalVersion.Substring(0, indexOfPlusSign) : informationalVersion; - } - catch (Exception) - { - return string.Empty; - } + resourceAttributes.Add(ResourceSemanticConventions.AttributeServiceInstance, serviceInstanceId); } + + return resourceBuilder.AddResource(new Resource(resourceAttributes)); + } + + /// + /// Adds service information to a + /// following semantic + /// conventions. + /// + /// . + /// Returns for chaining. + public static ResourceBuilder AddTelemetrySdk(this ResourceBuilder resourceBuilder) + { + return resourceBuilder.AddResource(TelemetryResource); + } + + /// + /// Adds attributes to a . + /// + /// . + /// An of attributes that describe the resource. + /// Returns for chaining. + public static ResourceBuilder AddAttributes(this ResourceBuilder resourceBuilder, IEnumerable> attributes) + { + return resourceBuilder.AddResource(new Resource(attributes)); + } + + /// + /// Adds resource attributes parsed from OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME environment variables + /// to a following the Resource + /// SDK. + /// + /// . + /// Returns for chaining. + public static ResourceBuilder AddEnvironmentVariableDetector(this ResourceBuilder resourceBuilder) + { + Lazy configuration = new Lazy(() => new ConfigurationBuilder().AddEnvironmentVariables().Build()); + + return resourceBuilder + .AddDetectorInternal(sp => new OtelEnvResourceDetector(sp?.GetService() ?? configuration.Value)) + .AddDetectorInternal(sp => new OtelServiceNameEnvVarDetector(sp?.GetService() ?? configuration.Value)); } } diff --git a/src/OpenTelemetry/Sdk.cs b/src/OpenTelemetry/Sdk.cs index 92d0b0a0bd4..15df83f2993 100644 --- a/src/OpenTelemetry/Sdk.cs +++ b/src/OpenTelemetry/Sdk.cs @@ -1,102 +1,138 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif +using System.Reflection; using OpenTelemetry.Context.Propagation; using OpenTelemetry.Internal; using OpenTelemetry.Logs; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; -namespace OpenTelemetry +namespace OpenTelemetry; + +/// +/// OpenTelemetry helper. +/// +public static class Sdk { - /// - /// OpenTelemetry helper. - /// - public static class Sdk + static Sdk() { - static Sdk() + Propagators.DefaultTextMapPropagator = new CompositeTextMapPropagator(new TextMapPropagator[] { - Propagators.DefaultTextMapPropagator = new CompositeTextMapPropagator(new TextMapPropagator[] - { - new TraceContextPropagator(), - new BaggagePropagator(), - }); - - Activity.DefaultIdFormat = ActivityIdFormat.W3C; - Activity.ForceDefaultIdFormat = true; - SelfDiagnostics.EnsureInitialized(); - } + new TraceContextPropagator(), + new BaggagePropagator(), + }); + + Activity.DefaultIdFormat = ActivityIdFormat.W3C; + Activity.ForceDefaultIdFormat = true; + SelfDiagnostics.EnsureInitialized(); - /// - /// Gets a value indicating whether instrumentation is suppressed (disabled). - /// - public static bool SuppressInstrumentation => SuppressInstrumentationScope.IsSuppressed; + var assemblyInformationalVersion = typeof(Sdk).Assembly.GetCustomAttribute()?.InformationalVersion; + InformationalVersion = ParseAssemblyInformationalVersion(assemblyInformationalVersion); - /// - /// Sets the Default TextMapPropagator. - /// - /// TextMapPropagator to be set as default. - public static void SetDefaultTextMapPropagator(TextMapPropagator textMapPropagator) + ConfigurationExtensions.LogInvalidEnvironmentVariable = (string key, string value) => { - Guard.ThrowIfNull(textMapPropagator); + OpenTelemetrySdkEventSource.Log.InvalidEnvironmentVariable(key, value); + }; + } - Propagators.DefaultTextMapPropagator = textMapPropagator; - } + /// + /// Gets a value indicating whether instrumentation is suppressed (disabled). + /// + public static bool SuppressInstrumentation => SuppressInstrumentationScope.IsSuppressed; - /// - /// Creates a which is used to build - /// a . In a typical application, a single - /// is created at application startup and - /// disposed at application shutdown. It is important to ensure that the - /// provider is not disposed too early. - /// - /// instance, which is used - /// to build a . - public static LoggerProviderBuilder CreateLoggerProviderBuilder() - { - return new LoggerProviderBuilderBase(); - } + internal static string InformationalVersion { get; } - /// - /// Creates a which is used to build - /// a . In a typical application, a single - /// is created at application startup and disposed - /// at application shutdown. It is important to ensure that the provider is not - /// disposed too early. - /// - /// instance, which is used to build a . - public static MeterProviderBuilder CreateMeterProviderBuilder() - { - return new MeterProviderBuilderBase(); - } + /// + /// Sets the Default TextMapPropagator. + /// + /// TextMapPropagator to be set as default. + public static void SetDefaultTextMapPropagator(TextMapPropagator textMapPropagator) + { + Guard.ThrowIfNull(textMapPropagator); + + Propagators.DefaultTextMapPropagator = textMapPropagator; + } + + /// + /// Creates a which is used to build + /// a . In a typical application, a single + /// is created at application startup and disposed + /// at application shutdown. It is important to ensure that the provider is not + /// disposed too early. + /// + /// instance, which is used to build a . + public static MeterProviderBuilder CreateMeterProviderBuilder() + { + return new MeterProviderBuilderBase(); + } + + /// + /// Creates a which is used to build + /// a . In a typical application, a single + /// is created at application startup and disposed + /// at application shutdown. It is important to ensure that the provider is not + /// disposed too early. + /// + /// instance, which is used to build a . + public static TracerProviderBuilder CreateTracerProviderBuilder() + { + return new TracerProviderBuilderBase(); + } - /// - /// Creates a which is used to build - /// a . In a typical application, a single - /// is created at application startup and disposed - /// at application shutdown. It is important to ensure that the provider is not - /// disposed too early. - /// - /// instance, which is used to build a . - public static TracerProviderBuilder CreateTracerProviderBuilder() +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + /// Creates a which is used to build + /// a . In a typical application, a single + /// is created at application startup and + /// disposed at application shutdown. It is important to ensure that the + /// provider is not disposed too early. + /// + /// WARNING: This is an experimental API which might change or be removed in the future. Use at your own risk. + /// instance, which is used + /// to build a . +#if NET8_0_OR_GREATER + [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#endif + public +#else + /// + /// Creates a which is used to build + /// a . In a typical application, a single + /// is created at application startup and + /// disposed at application shutdown. It is important to ensure that the + /// provider is not disposed too early. + /// + /// instance, which is used + /// to build a . + internal +#endif + static LoggerProviderBuilder CreateLoggerProviderBuilder() + { + return new LoggerProviderBuilderBase(); + } + + internal static string ParseAssemblyInformationalVersion(string? informationalVersion) + { + if (string.IsNullOrWhiteSpace(informationalVersion)) { - return new TracerProviderBuilderBase(); + informationalVersion = "1.0.0"; } + + /* + * InformationalVersion will be in the following format: + * {majorVersion}.{minorVersion}.{patchVersion}.{pre-release label}.{pre-release version}.{gitHeight}+{Git SHA of current commit} + * Ex: 1.5.0-alpha.1.40+807f703e1b4d9874a92bd86d9f2d4ebe5b5d52e4 + * The following parts are optional: pre-release label, pre-release version, git height, Git SHA of current commit + */ + + var indexOfPlusSign = informationalVersion!.IndexOf('+'); + return indexOfPlusSign > 0 + ? informationalVersion.Substring(0, indexOfPlusSign) + : informationalVersion; } } diff --git a/src/OpenTelemetry/SimpleExportProcessor.cs b/src/OpenTelemetry/SimpleExportProcessor.cs index 50dfb6679d1..10951cd8533 100644 --- a/src/OpenTelemetry/SimpleExportProcessor.cs +++ b/src/OpenTelemetry/SimpleExportProcessor.cs @@ -1,56 +1,40 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Internal; -namespace OpenTelemetry +namespace OpenTelemetry; + +/// +/// Implements processor that exports telemetry data at each OnEnd call. +/// +/// The type of telemetry object to be exported. +public abstract class SimpleExportProcessor : BaseExportProcessor + where T : class { + private readonly object syncObject = new(); + /// - /// Implements processor that exports telemetry data at each OnEnd call. + /// Initializes a new instance of the class. /// - /// The type of telemetry object to be exported. - public abstract class SimpleExportProcessor : BaseExportProcessor - where T : class + /// Exporter instance. + protected SimpleExportProcessor(BaseExporter exporter) + : base(exporter) { - private readonly object syncObject = new(); - - /// - /// Initializes a new instance of the class. - /// - /// Exporter instance. - protected SimpleExportProcessor(BaseExporter exporter) - : base(exporter) - { - } + } - /// - protected override void OnExport(T data) + /// + protected override void OnExport(T data) + { + lock (this.syncObject) { - lock (this.syncObject) + try + { + this.exporter.Export(new Batch(data)); + } + catch (Exception ex) { - try - { - this.exporter.Export(new Batch(data)); - } - catch (Exception ex) - { - OpenTelemetrySdkEventSource.Log.SpanProcessorException(nameof(this.OnExport), ex); - } + OpenTelemetrySdkEventSource.Log.SpanProcessorException(nameof(this.OnExport), ex); } } } diff --git a/src/OpenTelemetry/SuppressInstrumentationScope.cs b/src/OpenTelemetry/SuppressInstrumentationScope.cs index 8ee3a60bbde..68168bc79d1 100644 --- a/src/OpenTelemetry/SuppressInstrumentationScope.cs +++ b/src/OpenTelemetry/SuppressInstrumentationScope.cs @@ -1,163 +1,150 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Runtime.CompilerServices; using OpenTelemetry.Context; -namespace OpenTelemetry +namespace OpenTelemetry; + +/// +/// Contains methods managing instrumentation of internal operations. +/// +public sealed class SuppressInstrumentationScope : IDisposable { - public sealed class SuppressInstrumentationScope : IDisposable - { - // An integer value which controls whether instrumentation should be suppressed (disabled). - // * null: instrumentation is not suppressed - // * Depth = [int.MinValue, -1]: instrumentation is always suppressed - // * Depth = [1, int.MaxValue]: instrumentation is suppressed in a reference-counting mode - private static readonly RuntimeContextSlot Slot = RuntimeContext.RegisterSlot("otel.suppress_instrumentation"); + // An integer value which controls whether instrumentation should be suppressed (disabled). + // * null: instrumentation is not suppressed + // * Depth = [int.MinValue, -1]: instrumentation is always suppressed + // * Depth = [1, int.MaxValue]: instrumentation is suppressed in a reference-counting mode + private static readonly RuntimeContextSlot Slot = RuntimeContext.RegisterSlot("otel.suppress_instrumentation"); - private readonly SuppressInstrumentationScope? previousScope; - private bool disposed; + private readonly SuppressInstrumentationScope? previousScope; + private bool disposed; - internal SuppressInstrumentationScope(bool value = true) - { - this.previousScope = Slot.Get(); - this.Depth = value ? -1 : 0; - Slot.Set(this); - } + internal SuppressInstrumentationScope(bool value = true) + { + this.previousScope = Slot.Get(); + this.Depth = value ? -1 : 0; + Slot.Set(this); + } - internal static bool IsSuppressed => (Slot.Get()?.Depth ?? 0) != 0; - - internal int Depth { get; private set; } - - /// - /// Begins a new scope in which instrumentation is suppressed (disabled). - /// - /// Value indicating whether to suppress instrumentation. - /// Object to dispose to end the scope. - /// - /// This is typically used to prevent infinite loops created by - /// collection of internal operations, such as exporting traces over HTTP. - /// - /// public override async Task<ExportResult> ExportAsync( - /// IEnumerable<Activity> batch, - /// CancellationToken cancellationToken) - /// { - /// using (SuppressInstrumentationScope.Begin()) - /// { - /// // Instrumentation is suppressed (i.e., Sdk.SuppressInstrumentation == true) - /// } - /// - /// // Instrumentation is not suppressed (i.e., Sdk.SuppressInstrumentation == false) - /// } - /// - /// - public static IDisposable Begin(bool value = true) - { - return new SuppressInstrumentationScope(value); - } + internal static bool IsSuppressed => (Slot.Get()?.Depth ?? 0) != 0; + + internal int Depth { get; private set; } + + /// + /// Begins a new scope in which instrumentation is suppressed (disabled). + /// + /// Value indicating whether to suppress instrumentation. + /// Object to dispose to end the scope. + /// + /// This is typically used to prevent infinite loops created by + /// collection of internal operations, such as exporting traces over HTTP. + /// + /// public override async Task<ExportResult> ExportAsync( + /// IEnumerable<Activity> batch, + /// CancellationToken cancellationToken) + /// { + /// using (SuppressInstrumentationScope.Begin()) + /// { + /// // Instrumentation is suppressed (i.e., Sdk.SuppressInstrumentation == true) + /// } + /// + /// // Instrumentation is not suppressed (i.e., Sdk.SuppressInstrumentation == false) + /// } + /// + /// + public static IDisposable Begin(bool value = true) + { + return new SuppressInstrumentationScope(value); + } + + /// + /// Enters suppression mode. + /// If suppression mode is enabled (slot.Depth is a negative integer), do nothing. + /// If suppression mode is not enabled (slot is null), enter reference-counting suppression mode. + /// If suppression mode is enabled (slot.Depth is a positive integer), increment the ref count. + /// + /// The updated suppression slot value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Enter() + { + var currentScope = Slot.Get(); - /// - /// Enters suppression mode. - /// If suppression mode is enabled (slot.Depth is a negative integer), do nothing. - /// If suppression mode is not enabled (slot is null), enter reference-counting suppression mode. - /// If suppression mode is enabled (slot.Depth is a positive integer), increment the ref count. - /// - /// The updated suppression slot value. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int Enter() + if (currentScope == null) { - var currentScope = Slot.Get(); + Slot.Set( + new SuppressInstrumentationScope() + { + Depth = 1, + }); - if (currentScope == null) - { - Slot.Set( - new SuppressInstrumentationScope() - { - Depth = 1, - }); + return 1; + } - return 1; - } + var currentDepth = currentScope.Depth; - var currentDepth = currentScope.Depth; + if (currentDepth >= 0) + { + currentScope.Depth = ++currentDepth; + } - if (currentDepth >= 0) - { - currentScope.Depth = ++currentDepth; - } + return currentDepth; + } - return currentDepth; + /// + public void Dispose() + { + if (!this.disposed) + { + Slot.Set(this.previousScope); + this.disposed = true; } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int IncrementIfTriggered() + { + var currentScope = Slot.Get(); - /// - public void Dispose() + if (currentScope == null) { - if (!this.disposed) - { - Slot.Set(this.previousScope); - this.disposed = true; - } + return 0; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static int IncrementIfTriggered() - { - var currentScope = Slot.Get(); + var currentDepth = currentScope.Depth; - if (currentScope == null) - { - return 0; - } + if (currentScope.Depth > 0) + { + currentScope.Depth = ++currentDepth; + } - var currentDepth = currentScope.Depth; + return currentDepth; + } - if (currentScope.Depth > 0) - { - currentScope.Depth = ++currentDepth; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int DecrementIfTriggered() + { + var currentScope = Slot.Get(); - return currentDepth; + if (currentScope == null) + { + return 0; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static int DecrementIfTriggered() - { - var currentScope = Slot.Get(); + var currentDepth = currentScope.Depth; - if (currentScope == null) + if (currentScope.Depth > 0) + { + if (--currentDepth == 0) { - return 0; + Slot.Set(currentScope.previousScope); } - - var currentDepth = currentScope.Depth; - - if (currentScope.Depth > 0) + else { - if (--currentDepth == 0) - { - Slot.Set(currentScope.previousScope); - } - else - { - currentScope.Depth = currentDepth; - } + currentScope.Depth = currentDepth; } - - return currentDepth; } + + return currentDepth; } } diff --git a/src/OpenTelemetry/Trace/AlwaysOffSampler.cs b/src/OpenTelemetry/Trace/AlwaysOffSampler.cs index 30fd0768d00..68b68b81ac7 100644 --- a/src/OpenTelemetry/Trace/AlwaysOffSampler.cs +++ b/src/OpenTelemetry/Trace/AlwaysOffSampler.cs @@ -1,32 +1,16 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -#nullable enable +namespace OpenTelemetry.Trace; -namespace OpenTelemetry.Trace +/// +/// Sampler implementation which always returns SamplingDecision.Drop. +/// +public sealed class AlwaysOffSampler : Sampler { - /// - /// Sampler implementation which always returns SamplingDecision.Drop. - /// - public sealed class AlwaysOffSampler : Sampler + /// + public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) { - /// - public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) - { - return new SamplingResult(SamplingDecision.Drop); - } + return new SamplingResult(SamplingDecision.Drop); } } diff --git a/src/OpenTelemetry/Trace/AlwaysOnSampler.cs b/src/OpenTelemetry/Trace/AlwaysOnSampler.cs index bee15b682bd..3fce139f780 100644 --- a/src/OpenTelemetry/Trace/AlwaysOnSampler.cs +++ b/src/OpenTelemetry/Trace/AlwaysOnSampler.cs @@ -1,32 +1,16 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -#nullable enable +namespace OpenTelemetry.Trace; -namespace OpenTelemetry.Trace +/// +/// Sampler implementation which always returns SamplingDecision.RecordAndSample. +/// +public sealed class AlwaysOnSampler : Sampler { - /// - /// Sampler implementation which always returns SamplingDecision.RecordAndSample. - /// - public sealed class AlwaysOnSampler : Sampler + /// + public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) { - /// - public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) - { - return new SamplingResult(SamplingDecision.RecordAndSample); - } + return new SamplingResult(SamplingDecision.RecordAndSample); } } diff --git a/src/OpenTelemetry/Trace/BatchActivityExportProcessor.cs b/src/OpenTelemetry/Trace/BatchActivityExportProcessor.cs index 089b848815f..22b55f0e39f 100644 --- a/src/OpenTelemetry/Trace/BatchActivityExportProcessor.cs +++ b/src/OpenTelemetry/Trace/BatchActivityExportProcessor.cs @@ -1,51 +1,46 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -namespace OpenTelemetry +namespace OpenTelemetry; + +/// +/// Implements processor that batches objects before calling exporter. +/// +public class BatchActivityExportProcessor : BatchExportProcessor { - public class BatchActivityExportProcessor : BatchExportProcessor + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + /// + public BatchActivityExportProcessor( + BaseExporter exporter, + int maxQueueSize = DefaultMaxQueueSize, + int scheduledDelayMilliseconds = DefaultScheduledDelayMilliseconds, + int exporterTimeoutMilliseconds = DefaultExporterTimeoutMilliseconds, + int maxExportBatchSize = DefaultMaxExportBatchSize) + : base( + exporter, + maxQueueSize, + scheduledDelayMilliseconds, + exporterTimeoutMilliseconds, + maxExportBatchSize) { - public BatchActivityExportProcessor( - BaseExporter exporter, - int maxQueueSize = DefaultMaxQueueSize, - int scheduledDelayMilliseconds = DefaultScheduledDelayMilliseconds, - int exporterTimeoutMilliseconds = DefaultExporterTimeoutMilliseconds, - int maxExportBatchSize = DefaultMaxExportBatchSize) - : base( - exporter, - maxQueueSize, - scheduledDelayMilliseconds, - exporterTimeoutMilliseconds, - maxExportBatchSize) - { - } + } - /// - public override void OnEnd(Activity data) + /// + public override void OnEnd(Activity data) + { + if (!data.Recorded) { - if (!data.Recorded) - { - return; - } - - this.OnExport(data); + return; } + + this.OnExport(data); } } diff --git a/src/OpenTelemetry/Trace/BatchExportActivityProcessorOptions.cs b/src/OpenTelemetry/Trace/BatchExportActivityProcessorOptions.cs index 2091ac99a7c..cb4e7cec5f6 100644 --- a/src/OpenTelemetry/Trace/BatchExportActivityProcessorOptions.cs +++ b/src/OpenTelemetry/Trace/BatchExportActivityProcessorOptions.cs @@ -1,71 +1,55 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using Microsoft.Extensions.Configuration; using OpenTelemetry.Internal; -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +/// +/// Batch span processor options. +/// OTEL_BSP_MAX_QUEUE_SIZE, OTEL_BSP_MAX_EXPORT_BATCH_SIZE, OTEL_BSP_EXPORT_TIMEOUT, OTEL_BSP_SCHEDULE_DELAY +/// environment variables are parsed during object construction. +/// +public class BatchExportActivityProcessorOptions : BatchExportProcessorOptions { - /// - /// Batch span processor options. - /// OTEL_BSP_MAX_QUEUE_SIZE, OTEL_BSP_MAX_EXPORT_BATCH_SIZE, OTEL_BSP_EXPORT_TIMEOUT, OTEL_BSP_SCHEDULE_DELAY - /// environment variables are parsed during object construction. - /// - public class BatchExportActivityProcessorOptions : BatchExportProcessorOptions - { - internal const string MaxQueueSizeEnvVarKey = "OTEL_BSP_MAX_QUEUE_SIZE"; + internal const string MaxQueueSizeEnvVarKey = "OTEL_BSP_MAX_QUEUE_SIZE"; + + internal const string MaxExportBatchSizeEnvVarKey = "OTEL_BSP_MAX_EXPORT_BATCH_SIZE"; - internal const string MaxExportBatchSizeEnvVarKey = "OTEL_BSP_MAX_EXPORT_BATCH_SIZE"; + internal const string ExporterTimeoutEnvVarKey = "OTEL_BSP_EXPORT_TIMEOUT"; - internal const string ExporterTimeoutEnvVarKey = "OTEL_BSP_EXPORT_TIMEOUT"; + internal const string ScheduledDelayEnvVarKey = "OTEL_BSP_SCHEDULE_DELAY"; - internal const string ScheduledDelayEnvVarKey = "OTEL_BSP_SCHEDULE_DELAY"; + /// + /// Initializes a new instance of the class. + /// + public BatchExportActivityProcessorOptions() + : this(new ConfigurationBuilder().AddEnvironmentVariables().Build()) + { + } - /// - /// Initializes a new instance of the class. - /// - public BatchExportActivityProcessorOptions() - : this(new ConfigurationBuilder().AddEnvironmentVariables().Build()) + internal BatchExportActivityProcessorOptions(IConfiguration configuration) + { + if (configuration.TryGetIntValue(ExporterTimeoutEnvVarKey, out int value)) { + this.ExporterTimeoutMilliseconds = value; } - internal BatchExportActivityProcessorOptions(IConfiguration configuration) + if (configuration.TryGetIntValue(MaxExportBatchSizeEnvVarKey, out value)) { - if (configuration.TryGetIntValue(ExporterTimeoutEnvVarKey, out int value)) - { - this.ExporterTimeoutMilliseconds = value; - } - - if (configuration.TryGetIntValue(MaxExportBatchSizeEnvVarKey, out value)) - { - this.MaxExportBatchSize = value; - } + this.MaxExportBatchSize = value; + } - if (configuration.TryGetIntValue(MaxQueueSizeEnvVarKey, out value)) - { - this.MaxQueueSize = value; - } + if (configuration.TryGetIntValue(MaxQueueSizeEnvVarKey, out value)) + { + this.MaxQueueSize = value; + } - if (configuration.TryGetIntValue(ScheduledDelayEnvVarKey, out value)) - { - this.ScheduledDelayMilliseconds = value; - } + if (configuration.TryGetIntValue(ScheduledDelayEnvVarKey, out value)) + { + this.ScheduledDelayMilliseconds = value; } } } diff --git a/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderBase.cs b/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderBase.cs index 1b2315ef73a..062fea7faee 100644 --- a/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderBase.cs +++ b/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderBase.cs @@ -1,20 +1,5 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -117,7 +102,7 @@ internal TracerProvider InvokeBuild() protected TracerProviderBuilder AddInstrumentation( string instrumentationName, string instrumentationVersion, - Func instrumentationFactory) + Func instrumentationFactory) { Guard.ThrowIfNullOrWhitespace(instrumentationName); Guard.ThrowIfNullOrWhitespace(instrumentationVersion); @@ -130,7 +115,7 @@ protected TracerProviderBuilder AddInstrumentation( tracerProviderBuilderState.AddInstrumentation( instrumentationName, instrumentationVersion, - instrumentationFactory); + instrumentationFactory()); } }); diff --git a/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderExtensions.cs index a47bf9eb09e..a4c39becf16 100644 --- a/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderExtensions.cs @@ -1,253 +1,254 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using OpenTelemetry.Internal; using OpenTelemetry.Resources; -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +/// +/// Contains extension methods for the class. +/// +public static class TracerProviderBuilderExtensions { /// - /// Contains extension methods for the class. + /// Sets whether the status of + /// should be set to Status.Error when it ended abnormally due to an unhandled exception. /// - public static class TracerProviderBuilderExtensions + /// . + /// Enabled or not. Default value is true. + /// Returns for chaining. + /// + /// This method is not supported in native AOT or Mono Runtime as of .NET 8. + /// +#if NET7_0_OR_GREATER + [RequiresDynamicCode("The code for detecting exception and setting error status might not be available.")] +#endif + public static TracerProviderBuilder SetErrorStatusOnException(this TracerProviderBuilder tracerProviderBuilder, bool enabled = true) { - /// - /// Sets whether the status of - /// should be set to Status.Error when it ended abnormally due to an unhandled exception. - /// - /// . - /// Enabled or not. Default value is true. - /// Returns for chaining. - public static TracerProviderBuilder SetErrorStatusOnException(this TracerProviderBuilder tracerProviderBuilder, bool enabled = true) + tracerProviderBuilder.ConfigureBuilder((sp, builder) => { - tracerProviderBuilder.ConfigureBuilder((sp, builder) => + if (builder is TracerProviderBuilderSdk tracerProviderBuilderSdk) { - if (builder is TracerProviderBuilderSdk tracerProviderBuilderSdk) - { - tracerProviderBuilderSdk.SetErrorStatusOnException(enabled); - } - }); - - return tracerProviderBuilder; - } - - /// - /// Sets sampler. - /// - /// . - /// Sampler instance. - /// Returns for chaining. - public static TracerProviderBuilder SetSampler(this TracerProviderBuilder tracerProviderBuilder, Sampler sampler) - { - Guard.ThrowIfNull(sampler); + tracerProviderBuilderSdk.SetErrorStatusOnException(enabled); + } + }); - tracerProviderBuilder.ConfigureBuilder((sp, builder) => - { - if (builder is TracerProviderBuilderSdk tracerProviderBuilderSdk) - { - tracerProviderBuilderSdk.SetSampler(sampler); - } - }); + return tracerProviderBuilder; + } - return tracerProviderBuilder; - } + /// + /// Sets sampler. + /// + /// . + /// Sampler instance. + /// Returns for chaining. + public static TracerProviderBuilder SetSampler(this TracerProviderBuilder tracerProviderBuilder, Sampler sampler) + { + Guard.ThrowIfNull(sampler); - /// - /// Sets the sampler on the provider. - /// - /// - /// Note: The type specified by will be - /// registered as a singleton service into application services. - /// - /// Sampler type. - /// . - /// The supplied for chaining. - public static TracerProviderBuilder SetSampler(this TracerProviderBuilder tracerProviderBuilder) - where T : Sampler + tracerProviderBuilder.ConfigureBuilder((sp, builder) => { - tracerProviderBuilder.ConfigureServices(services => services.TryAddSingleton()); - - tracerProviderBuilder.ConfigureBuilder((sp, builder) => + if (builder is TracerProviderBuilderSdk tracerProviderBuilderSdk) { - if (builder is TracerProviderBuilderSdk tracerProviderBuilderSdk) - { - tracerProviderBuilderSdk.SetSampler(sp.GetRequiredService()); - } - }); + tracerProviderBuilderSdk.SetSampler(sampler); + } + }); - return tracerProviderBuilder; - } + return tracerProviderBuilder; + } - /// - /// Sets the sampler on the provider. - /// - /// . - /// The factory that creates the service. - /// The supplied for chaining. - public static TracerProviderBuilder SetSampler( - this TracerProviderBuilder tracerProviderBuilder, - Func implementationFactory) - { - Guard.ThrowIfNull(implementationFactory); + /// + /// Sets the sampler on the provider. + /// + /// + /// Note: The type specified by will be + /// registered as a singleton service into application services. + /// + /// Sampler type. + /// . + /// The supplied for chaining. + public static TracerProviderBuilder SetSampler< +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + T>(this TracerProviderBuilder tracerProviderBuilder) + where T : Sampler + { + tracerProviderBuilder.ConfigureServices(services => services.TryAddSingleton()); - tracerProviderBuilder.ConfigureBuilder((sp, builder) => + tracerProviderBuilder.ConfigureBuilder((sp, builder) => + { + if (builder is TracerProviderBuilderSdk tracerProviderBuilderSdk) { - if (builder is TracerProviderBuilderSdk tracerProviderBuilderSdk) - { - tracerProviderBuilderSdk.SetSampler(implementationFactory(sp)); - } - }); + tracerProviderBuilderSdk.SetSampler(sp.GetRequiredService()); + } + }); - return tracerProviderBuilder; - } + return tracerProviderBuilder; + } - /// - /// Sets the from which the Resource associated with - /// this provider is built from. Overwrites currently set ResourceBuilder. - /// You should usually use instead - /// (call if desired). - /// - /// . - /// from which Resource will be built. - /// Returns for chaining. - public static TracerProviderBuilder SetResourceBuilder(this TracerProviderBuilder tracerProviderBuilder, ResourceBuilder resourceBuilder) - { - Guard.ThrowIfNull(resourceBuilder); + /// + /// Sets the sampler on the provider. + /// + /// . + /// The factory that creates the service. + /// The supplied for chaining. + public static TracerProviderBuilder SetSampler( + this TracerProviderBuilder tracerProviderBuilder, + Func implementationFactory) + { + Guard.ThrowIfNull(implementationFactory); - tracerProviderBuilder.ConfigureBuilder((sp, builder) => + tracerProviderBuilder.ConfigureBuilder((sp, builder) => + { + if (builder is TracerProviderBuilderSdk tracerProviderBuilderSdk) { - if (builder is TracerProviderBuilderSdk tracerProviderBuilderSdk) - { - tracerProviderBuilderSdk.SetResourceBuilder(resourceBuilder); - } - }); + tracerProviderBuilderSdk.SetSampler(implementationFactory(sp)); + } + }); - return tracerProviderBuilder; - } + return tracerProviderBuilder; + } - /// - /// Modify the from which the Resource associated with - /// this provider is built from in-place. - /// - /// . - /// An action which modifies the provided in-place. - /// Returns for chaining. - public static TracerProviderBuilder ConfigureResource(this TracerProviderBuilder tracerProviderBuilder, Action configure) - { - Guard.ThrowIfNull(configure); + /// + /// Sets the from which the Resource associated with + /// this provider is built from. Overwrites currently set ResourceBuilder. + /// You should usually use instead + /// (call if desired). + /// + /// . + /// from which Resource will be built. + /// Returns for chaining. + public static TracerProviderBuilder SetResourceBuilder(this TracerProviderBuilder tracerProviderBuilder, ResourceBuilder resourceBuilder) + { + Guard.ThrowIfNull(resourceBuilder); - tracerProviderBuilder.ConfigureBuilder((sp, builder) => + tracerProviderBuilder.ConfigureBuilder((sp, builder) => + { + if (builder is TracerProviderBuilderSdk tracerProviderBuilderSdk) { - if (builder is TracerProviderBuilderSdk tracerProviderBuilderSdk) - { - tracerProviderBuilderSdk.ConfigureResource(configure); - } - }); + tracerProviderBuilderSdk.SetResourceBuilder(resourceBuilder); + } + }); - return tracerProviderBuilder; - } + return tracerProviderBuilder; + } - /// - /// Adds a processor to the provider. - /// - /// . - /// Activity processor to add. - /// Returns for chaining. - public static TracerProviderBuilder AddProcessor(this TracerProviderBuilder tracerProviderBuilder, BaseProcessor processor) - { - Guard.ThrowIfNull(processor); + /// + /// Modify the from which the Resource associated with + /// this provider is built from in-place. + /// + /// . + /// An action which modifies the provided in-place. + /// Returns for chaining. + public static TracerProviderBuilder ConfigureResource(this TracerProviderBuilder tracerProviderBuilder, Action configure) + { + Guard.ThrowIfNull(configure); - tracerProviderBuilder.ConfigureBuilder((sp, builder) => + tracerProviderBuilder.ConfigureBuilder((sp, builder) => + { + if (builder is TracerProviderBuilderSdk tracerProviderBuilderSdk) { - if (builder is TracerProviderBuilderSdk tracerProviderBuilderSdk) - { - tracerProviderBuilderSdk.AddProcessor(processor); - } - }); + tracerProviderBuilderSdk.ConfigureResource(configure); + } + }); - return tracerProviderBuilder; - } + return tracerProviderBuilder; + } - /// - /// Adds a processor to the provider which will be retrieved using dependency injection. - /// - /// - /// Note: The type specified by will be - /// registered as a singleton service into application services. - /// - /// Processor type. - /// . - /// The supplied for chaining. - public static TracerProviderBuilder AddProcessor(this TracerProviderBuilder tracerProviderBuilder) - where T : BaseProcessor - { - tracerProviderBuilder.ConfigureServices(services => services.TryAddSingleton()); + /// + /// Adds a processor to the provider. + /// + /// . + /// Activity processor to add. + /// Returns for chaining. + public static TracerProviderBuilder AddProcessor(this TracerProviderBuilder tracerProviderBuilder, BaseProcessor processor) + { + Guard.ThrowIfNull(processor); - tracerProviderBuilder.ConfigureBuilder((sp, builder) => + tracerProviderBuilder.ConfigureBuilder((sp, builder) => + { + if (builder is TracerProviderBuilderSdk tracerProviderBuilderSdk) { - if (builder is TracerProviderBuilderSdk tracerProviderBuilderSdk) - { - tracerProviderBuilderSdk.AddProcessor(sp.GetRequiredService()); - } - }); + tracerProviderBuilderSdk.AddProcessor(processor); + } + }); - return tracerProviderBuilder; - } + return tracerProviderBuilder; + } - /// - /// Adds a processor to the provider which will be retrieved using dependency injection. - /// - /// . - /// The factory that creates the service. - /// The supplied for chaining. - public static TracerProviderBuilder AddProcessor( - this TracerProviderBuilder tracerProviderBuilder, - Func> implementationFactory) - { - Guard.ThrowIfNull(implementationFactory); + /// + /// Adds a processor to the provider which will be retrieved using dependency injection. + /// + /// + /// Note: The type specified by will be + /// registered as a singleton service into application services. + /// + /// Processor type. + /// . + /// The supplied for chaining. + public static TracerProviderBuilder AddProcessor< +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + T>(this TracerProviderBuilder tracerProviderBuilder) + where T : BaseProcessor + { + tracerProviderBuilder.ConfigureServices(services => services.TryAddSingleton()); - tracerProviderBuilder.ConfigureBuilder((sp, builder) => + tracerProviderBuilder.ConfigureBuilder((sp, builder) => + { + if (builder is TracerProviderBuilderSdk tracerProviderBuilderSdk) { - if (builder is TracerProviderBuilderSdk tracerProviderBuilderSdk) - { - tracerProviderBuilderSdk.AddProcessor(implementationFactory(sp)); - } - }); + tracerProviderBuilderSdk.AddProcessor(sp.GetRequiredService()); + } + }); - return tracerProviderBuilder; - } + return tracerProviderBuilder; + } + + /// + /// Adds a processor to the provider which will be retrieved using dependency injection. + /// + /// . + /// The factory that creates the service. + /// The supplied for chaining. + public static TracerProviderBuilder AddProcessor( + this TracerProviderBuilder tracerProviderBuilder, + Func> implementationFactory) + { + Guard.ThrowIfNull(implementationFactory); - /// - /// Run the given actions to initialize the . - /// - /// . - /// . - public static TracerProvider? Build(this TracerProviderBuilder tracerProviderBuilder) + tracerProviderBuilder.ConfigureBuilder((sp, builder) => { - if (tracerProviderBuilder is TracerProviderBuilderBase tracerProviderBuilderBase) + if (builder is TracerProviderBuilderSdk tracerProviderBuilderSdk) { - return tracerProviderBuilderBase.InvokeBuild(); + tracerProviderBuilderSdk.AddProcessor(implementationFactory(sp)); } + }); - return null; + return tracerProviderBuilder; + } + + /// + /// Run the given actions to initialize the . + /// + /// . + /// . + public static TracerProvider Build(this TracerProviderBuilder tracerProviderBuilder) + { + if (tracerProviderBuilder is TracerProviderBuilderBase tracerProviderBuilderBase) + { + return tracerProviderBuilderBase.InvokeBuild(); } + + throw new NotSupportedException($"Build is not supported on '{tracerProviderBuilder?.GetType().FullName ?? "null"}' instances."); } } diff --git a/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderSdk.cs b/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderSdk.cs index c56044b884a..3e26b5fa27c 100644 --- a/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderSdk.cs +++ b/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderSdk.cs @@ -1,214 +1,196 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Internal; using OpenTelemetry.Resources; -namespace OpenTelemetry.Trace -{ - /// - /// Stores state used to build a . - /// - internal sealed class TracerProviderBuilderSdk : TracerProviderBuilder, ITracerProviderBuilder - { - private const string DefaultInstrumentationVersion = "1.0.0.0"; +namespace OpenTelemetry.Trace; - private readonly IServiceProvider serviceProvider; - private TracerProviderSdk? tracerProvider; +/// +/// Stores state used to build a . +/// +internal sealed class TracerProviderBuilderSdk : TracerProviderBuilder, ITracerProviderBuilder +{ + private const string DefaultInstrumentationVersion = "1.0.0.0"; - public TracerProviderBuilderSdk(IServiceProvider serviceProvider) - { - this.serviceProvider = serviceProvider; - } + private readonly IServiceProvider serviceProvider; + private TracerProviderSdk? tracerProvider; - public List Instrumentation { get; } = new(); + public TracerProviderBuilderSdk(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } - public ResourceBuilder? ResourceBuilder { get; private set; } + public List Instrumentation { get; } = new(); - public TracerProvider? Provider => this.tracerProvider; + public ResourceBuilder? ResourceBuilder { get; private set; } - public List> Processors { get; } = new(); + public TracerProvider? Provider => this.tracerProvider; - public List Sources { get; } = new(); + public List> Processors { get; } = new(); - public HashSet LegacyActivityOperationNames { get; } = new(StringComparer.OrdinalIgnoreCase); + public List Sources { get; } = new(); - public Sampler? Sampler { get; private set; } + public HashSet LegacyActivityOperationNames { get; } = new(StringComparer.OrdinalIgnoreCase); - public bool ExceptionProcessorEnabled { get; private set; } + public Sampler? Sampler { get; private set; } - public void RegisterProvider(TracerProviderSdk tracerProvider) - { - Debug.Assert(tracerProvider != null, "tracerProvider was null"); + public bool ExceptionProcessorEnabled { get; private set; } - if (this.tracerProvider != null) - { - throw new NotSupportedException("TracerProvider cannot be accessed while build is executing."); - } + public void RegisterProvider(TracerProviderSdk tracerProvider) + { + Debug.Assert(tracerProvider != null, "tracerProvider was null"); - this.tracerProvider = tracerProvider; + if (this.tracerProvider != null) + { + throw new NotSupportedException("TracerProvider cannot be accessed while build is executing."); } - public override TracerProviderBuilder AddInstrumentation( - Func instrumentationFactory) - { - Debug.Assert(instrumentationFactory != null, "instrumentationFactory was null"); + this.tracerProvider = tracerProvider; + } - return this.AddInstrumentation( - typeof(TInstrumentation).Name, - typeof(TInstrumentation).Assembly.GetName().Version?.ToString() ?? DefaultInstrumentationVersion, - instrumentationFactory!()); - } + public override TracerProviderBuilder AddInstrumentation(Func instrumentationFactory) + { + Debug.Assert(instrumentationFactory != null, "instrumentationFactory was null"); - public TracerProviderBuilder AddInstrumentation( - string instrumentationName, - string instrumentationVersion, - object instrumentation) - { - Debug.Assert(!string.IsNullOrWhiteSpace(instrumentationName), "instrumentationName was null or whitespace"); - Debug.Assert(!string.IsNullOrWhiteSpace(instrumentationVersion), "instrumentationVersion was null or whitespace"); - Debug.Assert(instrumentation != null, "instrumentation was null"); + return this.AddInstrumentation( + typeof(TInstrumentation).Name, + typeof(TInstrumentation).Assembly.GetName().Version?.ToString() ?? DefaultInstrumentationVersion, + instrumentationFactory!()); + } - this.Instrumentation.Add( - new InstrumentationRegistration( - instrumentationName, - instrumentationVersion, - instrumentation!)); + public TracerProviderBuilder AddInstrumentation( + string instrumentationName, + string instrumentationVersion, + object? instrumentation) + { + Debug.Assert(!string.IsNullOrWhiteSpace(instrumentationName), "instrumentationName was null or whitespace"); + Debug.Assert(!string.IsNullOrWhiteSpace(instrumentationVersion), "instrumentationVersion was null or whitespace"); - return this; - } + this.Instrumentation.Add( + new InstrumentationRegistration( + instrumentationName, + instrumentationVersion, + instrumentation)); - public TracerProviderBuilder ConfigureResource(Action configure) - { - Debug.Assert(configure != null, "configure was null"); + return this; + } - var resourceBuilder = this.ResourceBuilder ??= ResourceBuilder.CreateDefault(); + public TracerProviderBuilder ConfigureResource(Action configure) + { + Debug.Assert(configure != null, "configure was null"); - configure!(resourceBuilder); + var resourceBuilder = this.ResourceBuilder ??= ResourceBuilder.CreateDefault(); - return this; - } + configure!(resourceBuilder); - public TracerProviderBuilder SetResourceBuilder(ResourceBuilder resourceBuilder) - { - Debug.Assert(resourceBuilder != null, "resourceBuilder was null"); + return this; + } - this.ResourceBuilder = resourceBuilder; + public TracerProviderBuilder SetResourceBuilder(ResourceBuilder resourceBuilder) + { + Debug.Assert(resourceBuilder != null, "resourceBuilder was null"); - return this; - } + this.ResourceBuilder = resourceBuilder; - public override TracerProviderBuilder AddLegacySource(string operationName) - { - Debug.Assert(!string.IsNullOrWhiteSpace(operationName), "operationName was null or whitespace"); + return this; + } - this.LegacyActivityOperationNames.Add(operationName); + public override TracerProviderBuilder AddLegacySource(string operationName) + { + Debug.Assert(!string.IsNullOrWhiteSpace(operationName), "operationName was null or whitespace"); - return this; - } + this.LegacyActivityOperationNames.Add(operationName); - public override TracerProviderBuilder AddSource(params string[] names) - { - Debug.Assert(names != null, "names was null"); + return this; + } - foreach (var name in names!) - { - Guard.ThrowIfNullOrWhitespace(name); + public override TracerProviderBuilder AddSource(params string[] names) + { + Debug.Assert(names != null, "names was null"); - // TODO: We need to fix the listening model. - // Today it ignores version. - this.Sources.Add(name); - } + foreach (var name in names!) + { + Guard.ThrowIfNullOrWhitespace(name); - return this; + // TODO: We need to fix the listening model. + // Today it ignores version. + this.Sources.Add(name); } - public TracerProviderBuilder AddProcessor(BaseProcessor processor) - { - Debug.Assert(processor != null, "processor was null"); + return this; + } - this.Processors.Add(processor!); + public TracerProviderBuilder AddProcessor(BaseProcessor processor) + { + Debug.Assert(processor != null, "processor was null"); - return this; - } + this.Processors.Add(processor!); - public TracerProviderBuilder SetSampler(Sampler sampler) - { - Debug.Assert(sampler != null, "sampler was null"); + return this; + } - this.Sampler = sampler; + public TracerProviderBuilder SetSampler(Sampler sampler) + { + Debug.Assert(sampler != null, "sampler was null"); - return this; - } + this.Sampler = sampler; - public TracerProviderBuilder SetErrorStatusOnException(bool enabled) - { - this.ExceptionProcessorEnabled = enabled; + return this; + } - return this; - } + public TracerProviderBuilder SetErrorStatusOnException(bool enabled) + { + this.ExceptionProcessorEnabled = enabled; - public TracerProviderBuilder ConfigureBuilder(Action configure) - { - Debug.Assert(configure != null, "configure was null"); + return this; + } - configure!(this.serviceProvider, this); + public TracerProviderBuilder ConfigureBuilder(Action configure) + { + Debug.Assert(configure != null, "configure was null"); - return this; - } + configure!(this.serviceProvider, this); - public TracerProviderBuilder ConfigureServices(Action configure) - { - throw new NotSupportedException("Services cannot be configured after ServiceProvider has been created."); - } + return this; + } - public void AddExceptionProcessorIfEnabled() + public TracerProviderBuilder ConfigureServices(Action configure) + { + throw new NotSupportedException("Services cannot be configured after ServiceProvider has been created."); + } + + public void AddExceptionProcessorIfEnabled() + { + if (this.ExceptionProcessorEnabled) { - if (this.ExceptionProcessorEnabled) + try { - try - { - this.Processors.Insert(0, new ExceptionProcessor()); - } - catch (Exception ex) - { - throw new NotSupportedException($"'{nameof(TracerProviderBuilderExtensions.SetErrorStatusOnException)}' is not supported on this platform", ex); - } + this.Processors.Insert(0, new ExceptionProcessor()); + } + catch (Exception ex) + { + throw new NotSupportedException($"'{nameof(TracerProviderBuilderExtensions.SetErrorStatusOnException)}' is not supported on this platform", ex); } } + } - TracerProviderBuilder IDeferredTracerProviderBuilder.Configure(Action configure) - => this.ConfigureBuilder(configure); + TracerProviderBuilder IDeferredTracerProviderBuilder.Configure(Action configure) + => this.ConfigureBuilder(configure); - internal readonly struct InstrumentationRegistration - { - public readonly string Name; - public readonly string Version; - public readonly object Instance; + internal readonly struct InstrumentationRegistration + { + public readonly string Name; + public readonly string Version; + public readonly object? Instance; - internal InstrumentationRegistration(string name, string version, object instance) - { - this.Name = name; - this.Version = version; - this.Instance = instance; - } + internal InstrumentationRegistration(string name, string version, object? instance) + { + this.Name = name; + this.Version = version; + this.Instance = instance; } } } diff --git a/src/OpenTelemetry/Trace/ExceptionProcessor.cs b/src/OpenTelemetry/Trace/ExceptionProcessor.cs index 7472aa9649e..f0084d36cfb 100644 --- a/src/OpenTelemetry/Trace/ExceptionProcessor.cs +++ b/src/OpenTelemetry/Trace/ExceptionProcessor.cs @@ -1,86 +1,72 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +using System.Runtime.InteropServices; +#if !NET6_0_OR_GREATER && !NETFRAMEWORK using System.Linq.Expressions; using System.Reflection; -using System.Runtime.InteropServices; +#endif -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +internal sealed class ExceptionProcessor : BaseProcessor { - internal sealed class ExceptionProcessor : BaseProcessor + private const string ExceptionPointersKey = "otel.exception_pointers"; + + private readonly Func fnGetExceptionPointers; + + public ExceptionProcessor() { - private const string ExceptionPointersKey = "otel.exception_pointers"; +#if NET6_0_OR_GREATER || NETFRAMEWORK + this.fnGetExceptionPointers = Marshal.GetExceptionPointers; +#else + // When running on netstandard or similar the Marshal class is not a part of the netstandard API + // but it would still most likely be available in the underlying framework, so use reflection to retrieve it. + var flags = BindingFlags.Static | BindingFlags.Public; + var method = typeof(Marshal).GetMethod("GetExceptionPointers", flags, null, Type.EmptyTypes, null) + ?? throw new InvalidOperationException("Marshal.GetExceptionPointers method could not be resolved reflectively."); + var lambda = Expression.Lambda>(Expression.Call(method)); + this.fnGetExceptionPointers = lambda.Compile(); +#endif + this.fnGetExceptionPointers(); // attempt to access pointers to test for platform support + } - private readonly Func fnGetExceptionPointers; + /// + public override void OnStart(Activity activity) + { + var pointers = this.fnGetExceptionPointers(); - public ExceptionProcessor() + if (pointers != IntPtr.Zero) { - try - { - var flags = BindingFlags.Static | BindingFlags.Public; - var method = typeof(Marshal).GetMethod("GetExceptionPointers", flags, null, Type.EmptyTypes, null) - ?? throw new InvalidOperationException("Marshal.GetExceptionPointers method could not be resolved reflectively."); - var lambda = Expression.Lambda>(Expression.Call(method)); - this.fnGetExceptionPointers = lambda.Compile(); - } - catch (Exception ex) - { - throw new NotSupportedException($"'{typeof(Marshal).FullName}.GetExceptionPointers' is not supported", ex); - } + activity.SetTag(ExceptionPointersKey, pointers); } + } - /// - public override void OnStart(Activity activity) - { - var pointers = this.fnGetExceptionPointers(); - - if (pointers != IntPtr.Zero) - { - activity.SetTag(ExceptionPointersKey, pointers); - } - } + /// + public override void OnEnd(Activity activity) + { + var pointers = this.fnGetExceptionPointers(); - /// - public override void OnEnd(Activity activity) + if (pointers == IntPtr.Zero) { - var pointers = this.fnGetExceptionPointers(); - - if (pointers == IntPtr.Zero) - { - return; - } + return; + } - var snapshot = activity.GetTagValue(ExceptionPointersKey) as IntPtr?; + var snapshot = activity.GetTagValue(ExceptionPointersKey) as IntPtr?; - if (snapshot != null) - { - activity.SetTag(ExceptionPointersKey, null); - } + if (snapshot != null) + { + activity.SetTag(ExceptionPointersKey, null); + } - if (snapshot != pointers) - { - // TODO: Remove this when SetStatus is deprecated - activity.SetStatus(Status.Error); + if (snapshot != pointers) + { + // TODO: Remove this when SetStatus is deprecated + activity.SetStatus(Status.Error); - // For processors/exporters checking `Status` property. - activity.SetStatus(ActivityStatusCode.Error); - } + // For processors/exporters checking `Status` property. + activity.SetStatus(ActivityStatusCode.Error); } } } diff --git a/src/OpenTelemetry/Trace/ParentBasedSampler.cs b/src/OpenTelemetry/Trace/ParentBasedSampler.cs index 26c5c564173..c7665cc3f36 100644 --- a/src/OpenTelemetry/Trace/ParentBasedSampler.cs +++ b/src/OpenTelemetry/Trace/ParentBasedSampler.cs @@ -1,132 +1,116 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Internal; -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +/// +/// Sampler implementation which by default will take a sample if parent Activity is sampled. +/// Otherwise, samples root traces according to the specified root sampler. +/// +/// +/// The default behavior can be customized by providing additional samplers to be invoked for different +/// combinations of local/remote parent and its sampling decision. +/// See . +/// +public sealed class ParentBasedSampler : Sampler { + private readonly Sampler rootSampler; + + private readonly Sampler remoteParentSampled; + private readonly Sampler remoteParentNotSampled; + private readonly Sampler localParentSampled; + private readonly Sampler localParentNotSampled; + /// - /// Sampler implementation which by default will take a sample if parent Activity is sampled. - /// Otherwise, samples root traces according to the specified root sampler. + /// Initializes a new instance of the class. /// - /// - /// The default behavior can be customized by providing additional samplers to be invoked for different - /// combinations of local/remote parent and its sampling decision. - /// See . - /// - public sealed class ParentBasedSampler : Sampler + /// The to be called for root span/activity. + public ParentBasedSampler(Sampler rootSampler) { - private readonly Sampler rootSampler; - - private readonly Sampler remoteParentSampled; - private readonly Sampler remoteParentNotSampled; - private readonly Sampler localParentSampled; - private readonly Sampler localParentNotSampled; + Guard.ThrowIfNull(rootSampler); - /// - /// Initializes a new instance of the class. - /// - /// The to be called for root span/activity. - public ParentBasedSampler(Sampler rootSampler) - { - Guard.ThrowIfNull(rootSampler); + this.rootSampler = rootSampler; + this.Description = $"ParentBased{{{rootSampler.Description}}}"; - this.rootSampler = rootSampler; - this.Description = $"ParentBased{{{rootSampler.Description}}}"; + this.remoteParentSampled = new AlwaysOnSampler(); + this.remoteParentNotSampled = new AlwaysOffSampler(); + this.localParentSampled = new AlwaysOnSampler(); + this.localParentNotSampled = new AlwaysOffSampler(); + } - this.remoteParentSampled = new AlwaysOnSampler(); - this.remoteParentNotSampled = new AlwaysOffSampler(); - this.localParentSampled = new AlwaysOnSampler(); - this.localParentNotSampled = new AlwaysOffSampler(); - } + /// + /// Initializes a new instance of the class with ability to delegate + /// sampling decision to one of the inner samplers provided. + /// + /// The to be called for root span/activity. + /// + /// A to delegate sampling decision to in case of + /// remote parent ( == true) with flag == true. + /// Default: . + /// + /// + /// A to delegate sampling decision to in case of + /// remote parent ( == true) with flag == false. + /// Default: . + /// + /// + /// A to delegate sampling decision to in case of + /// local parent ( == false) with flag == true. + /// Default: . + /// + /// + /// A to delegate sampling decision to in case of + /// local parent ( == false) with flag == false. + /// Default: . + /// + public ParentBasedSampler( + Sampler rootSampler, + Sampler? remoteParentSampled = null, + Sampler? remoteParentNotSampled = null, + Sampler? localParentSampled = null, + Sampler? localParentNotSampled = null) + : this(rootSampler) + { + this.remoteParentSampled = remoteParentSampled ?? new AlwaysOnSampler(); + this.remoteParentNotSampled = remoteParentNotSampled ?? new AlwaysOffSampler(); + this.localParentSampled = localParentSampled ?? new AlwaysOnSampler(); + this.localParentNotSampled = localParentNotSampled ?? new AlwaysOffSampler(); + } - /// - /// Initializes a new instance of the class with ability to delegate - /// sampling decision to one of the inner samplers provided. - /// - /// The to be called for root span/activity. - /// - /// A to delegate sampling decision to in case of - /// remote parent ( == true) with flag == true. - /// Default: . - /// - /// - /// A to delegate sampling decision to in case of - /// remote parent ( == true) with flag == false. - /// Default: . - /// - /// - /// A to delegate sampling decision to in case of - /// local parent ( == false) with flag == true. - /// Default: . - /// - /// - /// A to delegate sampling decision to in case of - /// local parent ( == false) with flag == false. - /// Default: . - /// - public ParentBasedSampler( - Sampler rootSampler, - Sampler? remoteParentSampled = null, - Sampler? remoteParentNotSampled = null, - Sampler? localParentSampled = null, - Sampler? localParentNotSampled = null) - : this(rootSampler) + /// + public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) + { + var parentContext = samplingParameters.ParentContext; + if (parentContext.TraceId == default) { - this.remoteParentSampled = remoteParentSampled ?? new AlwaysOnSampler(); - this.remoteParentNotSampled = remoteParentNotSampled ?? new AlwaysOffSampler(); - this.localParentSampled = localParentSampled ?? new AlwaysOnSampler(); - this.localParentNotSampled = localParentNotSampled ?? new AlwaysOffSampler(); + // If no parent, use the rootSampler to determine sampling. + return this.rootSampler.ShouldSample(samplingParameters); } - /// - public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) + // Is parent sampled? + if ((parentContext.TraceFlags & ActivityTraceFlags.Recorded) != 0) { - var parentContext = samplingParameters.ParentContext; - if (parentContext.TraceId == default) - { - // If no parent, use the rootSampler to determine sampling. - return this.rootSampler.ShouldSample(samplingParameters); - } - - // Is parent sampled? - if ((parentContext.TraceFlags & ActivityTraceFlags.Recorded) != 0) - { - if (parentContext.IsRemote) - { - return this.remoteParentSampled.ShouldSample(samplingParameters); - } - else - { - return this.localParentSampled.ShouldSample(samplingParameters); - } - } - - // If parent is not sampled => delegate to the "not sampled" inner samplers. if (parentContext.IsRemote) { - return this.remoteParentNotSampled.ShouldSample(samplingParameters); + return this.remoteParentSampled.ShouldSample(samplingParameters); } else { - return this.localParentNotSampled.ShouldSample(samplingParameters); + return this.localParentSampled.ShouldSample(samplingParameters); } } + + // If parent is not sampled => delegate to the "not sampled" inner samplers. + if (parentContext.IsRemote) + { + return this.remoteParentNotSampled.ShouldSample(samplingParameters); + } + else + { + return this.localParentNotSampled.ShouldSample(samplingParameters); + } } } diff --git a/src/OpenTelemetry/Trace/Sampler.cs b/src/OpenTelemetry/Trace/Sampler.cs index 331a090fc6c..46d60c3734d 100644 --- a/src/OpenTelemetry/Trace/Sampler.cs +++ b/src/OpenTelemetry/Trace/Sampler.cs @@ -1,48 +1,35 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +/// +/// Controls the number of samples of traces collected and sent to the backend. +/// +public abstract class Sampler { /// - /// Controls the number of samples of traces collected and sent to the backend. + /// Initializes a new instance of the class. /// - public abstract class Sampler + protected Sampler() { - protected Sampler() - { - this.Description = this.GetType().Name; - } + this.Description = this.GetType().Name; + } - /// - /// Gets or sets the sampler description. - /// - public string Description { get; protected set; } + /// + /// Gets or sets the sampler description. + /// + public string Description { get; protected set; } - /// - /// Checks whether activity needs to be created and tracked. - /// - /// - /// The used by the - /// to decide if the to be created is going to be sampled or not. - /// - /// Sampling decision on whether activity needs to be sampled or not. - public abstract SamplingResult ShouldSample(in SamplingParameters samplingParameters); - } + /// + /// Checks whether activity needs to be created and tracked. + /// + /// + /// The used by the + /// to decide if the to be created is going to be sampled or not. + /// + /// Sampling decision on whether activity needs to be sampled or not. + public abstract SamplingResult ShouldSample(in SamplingParameters samplingParameters); } diff --git a/src/OpenTelemetry/Trace/SamplingDecision.cs b/src/OpenTelemetry/Trace/SamplingDecision.cs index 514afc909e7..272697af850 100644 --- a/src/OpenTelemetry/Trace/SamplingDecision.cs +++ b/src/OpenTelemetry/Trace/SamplingDecision.cs @@ -1,46 +1,31 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -#nullable enable +namespace OpenTelemetry.Trace; -namespace OpenTelemetry.Trace +/// +/// Enumeration to define sampling decision. +/// +public enum SamplingDecision { /// - /// Enumeration to define sampling decision. + /// The activity will be created but not recorded. + /// Activity.IsAllDataRequested will return false. + /// Activity.Recorded will return false. /// - public enum SamplingDecision - { - /// - /// The activity will be created but not recorded. - /// Activity.IsAllDataRequested will return false. - /// - Drop, + Drop, - /// - /// The activity will be created and recorded, but sampling flag will not be set. - /// Activity.IsAllDataRequested will return true. - /// Activity.Recorded will return false. - /// - RecordOnly, + /// + /// The activity will be created and recorded, but sampling flag will not be set. + /// Activity.IsAllDataRequested will return true. + /// Activity.Recorded will return false. + /// + RecordOnly, - /// - /// The activity will be created, recorded, and sampling flag will be set. - /// Activity.IsAllDataRequested will return true. - /// Activity.Recorded will return true. - /// - RecordAndSample, - } + /// + /// The activity will be created, recorded, and sampling flag will be set. + /// Activity.IsAllDataRequested will return true. + /// Activity.Recorded will return true. + /// + RecordAndSample, } diff --git a/src/OpenTelemetry/Trace/SamplingParameters.cs b/src/OpenTelemetry/Trace/SamplingParameters.cs index daad55c05c8..6fe2725a4ff 100644 --- a/src/OpenTelemetry/Trace/SamplingParameters.cs +++ b/src/OpenTelemetry/Trace/SamplingParameters.cs @@ -1,96 +1,80 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +/// +/// Sampling parameters passed to a for it to make a sampling decision. +/// +public readonly struct SamplingParameters { /// - /// Sampling parameters passed to a for it to make a sampling decision. + /// Initializes a new instance of the struct. /// - public readonly struct SamplingParameters + /// Parent activity context. Typically taken from the wire. + /// Trace ID of a activity to be created. + /// The name (DisplayName) of the activity to be created. Note, that the name of the activity is settable. + /// So this name can be changed later and Sampler implementation should assume that. + /// Typical example of a name change is when representing incoming http request + /// has a name of url path and then being updated with route name when routing complete. + /// + /// The kind of the Activity to be created. + /// Initial set of Tags for the Activity being constructed. + /// Links associated with the activity. + public SamplingParameters( + ActivityContext parentContext, + ActivityTraceId traceId, + string name, + ActivityKind kind, + IEnumerable>? tags = null, + IEnumerable? links = null) { - /// - /// Initializes a new instance of the struct. - /// - /// Parent activity context. Typically taken from the wire. - /// Trace ID of a activity to be created. - /// The name (DisplayName) of the activity to be created. Note, that the name of the activity is settable. - /// So this name can be changed later and Sampler implementation should assume that. - /// Typical example of a name change is when representing incoming http request - /// has a name of url path and then being updated with route name when routing complete. - /// - /// The kind of the Activity to be created. - /// Initial set of Tags for the Activity being constructed. - /// Links associated with the activity. - public SamplingParameters( - ActivityContext parentContext, - ActivityTraceId traceId, - string name, - ActivityKind kind, - IEnumerable>? tags = null, - IEnumerable? links = null) - { - this.ParentContext = parentContext; - this.TraceId = traceId; - this.Kind = kind; - this.Tags = tags; - this.Links = links; + this.ParentContext = parentContext; + this.TraceId = traceId; + this.Kind = kind; + this.Tags = tags; + this.Links = links; - // Note: myActivitySource.StartActivity(name: null) is currently - // allowed even though OTel spec says span name is required. See: - // https://github.com/open-telemetry/opentelemetry-dotnet/issues/3802 - this.Name = name ?? string.Empty; - } + // Note: myActivitySource.StartActivity(name: null) is currently + // allowed even though OTel spec says span name is required. See: + // https://github.com/open-telemetry/opentelemetry-dotnet/issues/3802 + this.Name = name ?? string.Empty; + } - /// - /// Gets the parent activity context. - /// - public ActivityContext ParentContext { get; } + /// + /// Gets the parent activity context. + /// + public ActivityContext ParentContext { get; } - /// - /// Gets the trace ID of parent activity or a new generated one for root span/activity. - /// - public ActivityTraceId TraceId { get; } + /// + /// Gets the trace ID of parent activity or a new generated one for root span/activity. + /// + public ActivityTraceId TraceId { get; } - /// - /// Gets the name to be given to the span/activity. - /// - public string Name { get; } + /// + /// Gets the name to be given to the span/activity. + /// + public string Name { get; } - /// - /// Gets the kind of span/activity to be created. - /// - /// - /// For Activities created outside of ActivitySource, - /// the Kind will be the default (Internal). - /// - public ActivityKind Kind { get; } + /// + /// Gets the kind of span/activity to be created. + /// + /// + /// For Activities created outside of ActivitySource, + /// the Kind will be the default (Internal). + /// + public ActivityKind Kind { get; } - /// - /// Gets the tags to be associated to the span/activity to be created. - /// These are the tags provided at the time of Activity creation. - /// - public IEnumerable>? Tags { get; } + /// + /// Gets the tags to be associated to the span/activity to be created. + /// These are the tags provided at the time of Activity creation. + /// + public IEnumerable>? Tags { get; } - /// - /// Gets the links to be added to the activity to be created. - /// - public IEnumerable? Links { get; } - } + /// + /// Gets the links to be added to the activity to be created. + /// + public IEnumerable? Links { get; } } diff --git a/src/OpenTelemetry/Trace/SamplingResult.cs b/src/OpenTelemetry/Trace/SamplingResult.cs index 55f09a385a6..25120d4ee9a 100644 --- a/src/OpenTelemetry/Trace/SamplingResult.cs +++ b/src/OpenTelemetry/Trace/SamplingResult.cs @@ -1,147 +1,131 @@ -// // 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. -// - -#nullable enable - -namespace OpenTelemetry.Trace +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Trace; + +/// +/// Sampling result. +/// +public readonly struct SamplingResult : IEquatable { /// - /// Sampling result. + /// Initializes a new instance of the struct. /// - public readonly struct SamplingResult : IEquatable + /// indicates whether an activity object is recorded and sampled. + public SamplingResult(SamplingDecision decision) + : this(decision, attributes: null, traceStateString: null) { - /// - /// Initializes a new instance of the struct. - /// - /// indicates whether an activity object is recorded and sampled. - public SamplingResult(SamplingDecision decision) - : this(decision, attributes: null, traceStateString: null) - { - } + } - /// - /// Initializes a new instance of the struct. - /// - /// True if sampled, false otherwise. - public SamplingResult(bool isSampled) - : this(decision: isSampled ? SamplingDecision.RecordAndSample : SamplingDecision.Drop, attributes: null, traceStateString: null) - { - } + /// + /// Initializes a new instance of the struct. + /// + /// True if sampled, false otherwise. + public SamplingResult(bool isSampled) + : this(decision: isSampled ? SamplingDecision.RecordAndSample : SamplingDecision.Drop, attributes: null, traceStateString: null) + { + } - /// - /// Initializes a new instance of the struct. - /// - /// indicates whether an activity object is recorded and sampled. - /// Attributes associated with the sampling decision. Attributes list passed to - /// this method must be immutable. Mutations of the collection and/or attribute values may lead to unexpected behavior. - public SamplingResult(SamplingDecision decision, IEnumerable> attributes) - : this(decision, attributes, traceStateString: null) - { - } + /// + /// Initializes a new instance of the struct. + /// + /// indicates whether an activity object is recorded and sampled. + /// Attributes associated with the sampling decision. Attributes list passed to + /// this method must be immutable. Mutations of the collection and/or attribute values may lead to unexpected behavior. + public SamplingResult(SamplingDecision decision, IEnumerable> attributes) + : this(decision, attributes, traceStateString: null) + { + } - /// - /// Initializes a new instance of the struct. - /// - /// indicates whether an activity object is recorded and sampled. - /// traceStateString associated with the created Activity. - public SamplingResult(SamplingDecision decision, string traceStateString) - : this(decision, attributes: null, traceStateString) - { - } + /// + /// Initializes a new instance of the struct. + /// + /// indicates whether an activity object is recorded and sampled. + /// traceStateString associated with the created Activity. + public SamplingResult(SamplingDecision decision, string traceStateString) + : this(decision, attributes: null, traceStateString) + { + } - /// - /// Initializes a new instance of the struct. - /// - /// indicates whether an activity object is recorded and sampled. - /// attributes associated with the sampling decision. Attributes list passed to - /// this method must be immutable. Mutations of the collection and/or attribute values may lead to unexpected behavior. - /// traceStateString associated with the created Activity. - public SamplingResult(SamplingDecision decision, IEnumerable>? attributes, string? traceStateString) - { - this.Decision = decision; + /// + /// Initializes a new instance of the struct. + /// + /// indicates whether an activity object is recorded and sampled. + /// attributes associated with the sampling decision. Attributes list passed to + /// this method must be immutable. Mutations of the collection and/or attribute values may lead to unexpected behavior. + /// traceStateString associated with the created Activity. + public SamplingResult(SamplingDecision decision, IEnumerable>? attributes, string? traceStateString) + { + this.Decision = decision; - // Note: Decision object takes ownership of the collection. - // Current implementation has no means to ensure the collection will not be modified by the caller. - // If this behavior will be abused we must switch to cloning of the collection. - this.Attributes = attributes ?? Enumerable.Empty>(); + // Note: Decision object takes ownership of the collection. + // Current implementation has no means to ensure the collection will not be modified by the caller. + // If this behavior will be abused we must switch to cloning of the collection. + this.Attributes = attributes ?? Enumerable.Empty>(); - this.TraceStateString = traceStateString; - } + this.TraceStateString = traceStateString; + } - /// - /// Gets a value indicating indicates whether an activity object is recorded and sampled. - /// - public SamplingDecision Decision { get; } - - /// - /// Gets a map of attributes associated with the sampling decision. - /// - public IEnumerable> Attributes { get; } - - /// - /// Gets the tracestate. - /// - public string? TraceStateString { get; } - - /// - /// Compare two for equality. - /// - /// First Decision to compare. - /// Second Decision to compare. - public static bool operator ==(SamplingResult decision1, SamplingResult decision2) => decision1.Equals(decision2); - - /// - /// Compare two for not equality. - /// - /// First Decision to compare. - /// Second Decision to compare. - public static bool operator !=(SamplingResult decision1, SamplingResult decision2) => !decision1.Equals(decision2); - - /// - public override bool Equals(object? obj) - => obj is SamplingResult other && this.Equals(other); - - /// - public override int GetHashCode() - { + /// + /// Gets a value indicating indicates whether an activity object is recorded and sampled. + /// + public SamplingDecision Decision { get; } + + /// + /// Gets a map of attributes associated with the sampling decision. + /// + public IEnumerable> Attributes { get; } + + /// + /// Gets the tracestate. + /// + public string? TraceStateString { get; } + + /// + /// Compare two for equality. + /// + /// First Decision to compare. + /// Second Decision to compare. + public static bool operator ==(SamplingResult decision1, SamplingResult decision2) => decision1.Equals(decision2); + + /// + /// Compare two for not equality. + /// + /// First Decision to compare. + /// Second Decision to compare. + public static bool operator !=(SamplingResult decision1, SamplingResult decision2) => !decision1.Equals(decision2); + + /// + public override bool Equals(object? obj) + => obj is SamplingResult other && this.Equals(other); + + /// + public override int GetHashCode() + { #if NET6_0_OR_GREATER - HashCode hashCode = default; - hashCode.Add(this.Decision); - hashCode.Add(this.Attributes); - hashCode.Add(this.TraceStateString); + HashCode hashCode = default; + hashCode.Add(this.Decision); + hashCode.Add(this.Attributes); + hashCode.Add(this.TraceStateString); - var hash = hashCode.ToHashCode(); + var hash = hashCode.ToHashCode(); #else - var hash = 17; - unchecked - { - hash = (31 * hash) + this.Decision.GetHashCode(); - hash = (31 * hash) + this.Attributes.GetHashCode(); - hash = (31 * hash) + (this.TraceStateString?.GetHashCode() ?? 0); - } -#endif - return hash; - } - - /// - public bool Equals(SamplingResult other) + var hash = 17; + unchecked { - return this.Decision == other.Decision - && this.Attributes.SequenceEqual(other.Attributes) - && this.TraceStateString == other.TraceStateString; + hash = (31 * hash) + this.Decision.GetHashCode(); + hash = (31 * hash) + this.Attributes.GetHashCode(); + hash = (31 * hash) + (this.TraceStateString?.GetHashCode() ?? 0); } +#endif + return hash; + } + + /// + public bool Equals(SamplingResult other) + { + return this.Decision == other.Decision + && this.Attributes.SequenceEqual(other.Attributes) + && this.TraceStateString == other.TraceStateString; } } diff --git a/src/OpenTelemetry/Trace/SimpleActivityExportProcessor.cs b/src/OpenTelemetry/Trace/SimpleActivityExportProcessor.cs index 7a9215f2156..463bc6d111f 100644 --- a/src/OpenTelemetry/Trace/SimpleActivityExportProcessor.cs +++ b/src/OpenTelemetry/Trace/SimpleActivityExportProcessor.cs @@ -1,41 +1,32 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -namespace OpenTelemetry +namespace OpenTelemetry; + +/// +/// Implements processor that exports objects at each OnEnd call. +/// +public class SimpleActivityExportProcessor : SimpleExportProcessor { - public class SimpleActivityExportProcessor : SimpleExportProcessor + /// + /// Initializes a new instance of the class. + /// + /// . + public SimpleActivityExportProcessor(BaseExporter exporter) + : base(exporter) { - public SimpleActivityExportProcessor(BaseExporter exporter) - : base(exporter) - { - } + } - /// - public override void OnEnd(Activity data) + /// + public override void OnEnd(Activity data) + { + if (!data.Recorded) { - if (!data.Recorded) - { - return; - } - - this.OnExport(data); + return; } + + this.OnExport(data); } } diff --git a/src/OpenTelemetry/Trace/TraceIdRatioBasedSampler.cs b/src/OpenTelemetry/Trace/TraceIdRatioBasedSampler.cs index a782c002506..24b4df8af81 100644 --- a/src/OpenTelemetry/Trace/TraceIdRatioBasedSampler.cs +++ b/src/OpenTelemetry/Trace/TraceIdRatioBasedSampler.cs @@ -1,95 +1,79 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Globalization; using OpenTelemetry.Internal; -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +/// +/// Samples traces according to the specified probability. +/// +public sealed class TraceIdRatioBasedSampler + : Sampler { + private readonly long idUpperBound; + private readonly double probability; + /// - /// Samples traces according to the specified probability. + /// Initializes a new instance of the class. /// - public sealed class TraceIdRatioBasedSampler - : Sampler + /// The desired probability of sampling. This must be between 0.0 and 1.0. + /// Higher the value, higher is the probability of a given Activity to be sampled in. + /// + public TraceIdRatioBasedSampler(double probability) { - private readonly long idUpperBound; - private readonly double probability; - - /// - /// Initializes a new instance of the class. - /// - /// The desired probability of sampling. This must be between 0.0 and 1.0. - /// Higher the value, higher is the probability of a given Activity to be sampled in. - /// - public TraceIdRatioBasedSampler(double probability) - { - Guard.ThrowIfOutOfRange(probability, min: 0.0, max: 1.0); + Guard.ThrowIfOutOfRange(probability, min: 0.0, max: 1.0); - this.probability = probability; + this.probability = probability; - // The expected description is like TraceIdRatioBasedSampler{0.000100} - this.Description = "TraceIdRatioBasedSampler{" + this.probability.ToString("F6", CultureInfo.InvariantCulture) + "}"; + // The expected description is like TraceIdRatioBasedSampler{0.000100} + this.Description = "TraceIdRatioBasedSampler{" + this.probability.ToString("F6", CultureInfo.InvariantCulture) + "}"; - // Special case the limits, to avoid any possible issues with lack of precision across - // double/long boundaries. For probability == 0.0, we use Long.MIN_VALUE as this guarantees - // that we will never sample a trace, even in the case where the id == Long.MIN_VALUE, since - // Math.Abs(Long.MIN_VALUE) == Long.MIN_VALUE. - if (this.probability == 0.0) - { - this.idUpperBound = long.MinValue; - } - else if (this.probability == 1.0) - { - this.idUpperBound = long.MaxValue; - } - else - { - this.idUpperBound = (long)(probability * long.MaxValue); - } + // Special case the limits, to avoid any possible issues with lack of precision across + // double/long boundaries. For probability == 0.0, we use Long.MIN_VALUE as this guarantees + // that we will never sample a trace, even in the case where the id == Long.MIN_VALUE, since + // Math.Abs(Long.MIN_VALUE) == Long.MIN_VALUE. + if (this.probability == 0.0) + { + this.idUpperBound = long.MinValue; } - - /// - public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) + else if (this.probability == 1.0) + { + this.idUpperBound = long.MaxValue; + } + else { - // Always sample if we are within probability range. This is true even for child activities (that - // may have had a different sampling decision made) to allow for different sampling policies, - // and dynamic increases to sampling probabilities for debugging purposes. - // Note use of '<' for comparison. This ensures that we never sample for probability == 0.0, - // while allowing for a (very) small chance of *not* sampling if the id == Long.MAX_VALUE. - // This is considered a reasonable trade-off for the simplicity/performance requirements (this - // code is executed in-line for every Activity creation). - Span traceIdBytes = stackalloc byte[16]; - samplingParameters.TraceId.CopyTo(traceIdBytes); - return new SamplingResult(Math.Abs(GetLowerLong(traceIdBytes)) < this.idUpperBound); + this.idUpperBound = (long)(probability * long.MaxValue); } + } + + /// + public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) + { + // Always sample if we are within probability range. This is true even for child activities (that + // may have had a different sampling decision made) to allow for different sampling policies, + // and dynamic increases to sampling probabilities for debugging purposes. + // Note use of '<' for comparison. This ensures that we never sample for probability == 0.0, + // while allowing for a (very) small chance of *not* sampling if the id == Long.MAX_VALUE. + // This is considered a reasonable trade-off for the simplicity/performance requirements (this + // code is executed in-line for every Activity creation). + Span traceIdBytes = stackalloc byte[16]; + samplingParameters.TraceId.CopyTo(traceIdBytes); + return new SamplingResult(Math.Abs(GetLowerLong(traceIdBytes)) < this.idUpperBound); + } - private static long GetLowerLong(ReadOnlySpan bytes) + private static long GetLowerLong(ReadOnlySpan bytes) + { + long result = 0; + for (var i = 0; i < 8; i++) { - long result = 0; - for (var i = 0; i < 8; i++) - { - result <<= 8; + result <<= 8; #pragma warning disable CS0675 // Bitwise-or operator used on a sign-extended operand - result |= bytes[i] & 0xff; + result |= bytes[i] & 0xff; #pragma warning restore CS0675 // Bitwise-or operator used on a sign-extended operand - } - - return result; } + + return result; } } diff --git a/src/OpenTelemetry/Trace/TracerProviderExtensions.cs b/src/OpenTelemetry/Trace/TracerProviderExtensions.cs index 91e4f610490..864e36959b1 100644 --- a/src/OpenTelemetry/Trace/TracerProviderExtensions.cs +++ b/src/OpenTelemetry/Trace/TracerProviderExtensions.cs @@ -1,132 +1,116 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Internal; -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +/// +/// Contains extension methods for the class. +/// +public static class TracerProviderExtensions { /// - /// Contains extension methods for the class. + /// Add a processor to the provider. /// - public static class TracerProviderExtensions + /// . + /// . + /// The supplied instance for call chaining. + public static TracerProvider AddProcessor(this TracerProvider provider, BaseProcessor processor) { - /// - /// Add a processor to the provider. - /// - /// . - /// . - /// The supplied instance for call chaining. - public static TracerProvider AddProcessor(this TracerProvider provider, BaseProcessor processor) + Guard.ThrowIfNull(provider); + Guard.ThrowIfNull(processor); + + if (provider is TracerProviderSdk tracerProviderSdk) { - Guard.ThrowIfNull(provider); - Guard.ThrowIfNull(processor); + tracerProviderSdk.AddProcessor(processor); + } - if (provider is TracerProviderSdk tracerProviderSdk) - { - tracerProviderSdk.AddProcessor(processor); - } + return provider; + } - return provider; - } + /// + /// Flushes all the processors registered under TracerProviderSdk, blocks the current thread + /// until flush completed, shutdown signaled or timed out. + /// + /// TracerProviderSdk instance on which ForceFlush will be called. + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when force flush succeeded; otherwise, false. + /// + /// + /// Thrown when the timeoutMilliseconds is smaller than -1. + /// + /// + /// This function guarantees thread-safety. + /// + public static bool ForceFlush(this TracerProvider provider, int timeoutMilliseconds = Timeout.Infinite) + { + Guard.ThrowIfNull(provider); + Guard.ThrowIfInvalidTimeout(timeoutMilliseconds); - /// - /// Flushes all the processors registered under TracerProviderSdk, blocks the current thread - /// until flush completed, shutdown signaled or timed out. - /// - /// TracerProviderSdk instance on which ForceFlush will be called. - /// - /// The number (non-negative) of milliseconds to wait, or - /// Timeout.Infinite to wait indefinitely. - /// - /// - /// Returns true when force flush succeeded; otherwise, false. - /// - /// - /// Thrown when the timeoutMilliseconds is smaller than -1. - /// - /// - /// This function guarantees thread-safety. - /// - public static bool ForceFlush(this TracerProvider provider, int timeoutMilliseconds = Timeout.Infinite) + if (provider is TracerProviderSdk tracerProviderSdk) { - Guard.ThrowIfNull(provider); - Guard.ThrowIfInvalidTimeout(timeoutMilliseconds); - - if (provider is TracerProviderSdk tracerProviderSdk) + try { - try - { - return tracerProviderSdk.OnForceFlush(timeoutMilliseconds); - } - catch (Exception ex) - { - OpenTelemetrySdkEventSource.Log.TracerProviderException(nameof(tracerProviderSdk.OnForceFlush), ex); - return false; - } + return tracerProviderSdk.OnForceFlush(timeoutMilliseconds); + } + catch (Exception ex) + { + OpenTelemetrySdkEventSource.Log.TracerProviderException(nameof(tracerProviderSdk.OnForceFlush), ex); + return false; } - - return true; } - /// - /// Attempts to shutdown the TracerProviderSdk, blocks the current thread until - /// shutdown completed or timed out. - /// - /// TracerProviderSdk instance on which Shutdown will be called. - /// - /// The number (non-negative) of milliseconds to wait, or - /// Timeout.Infinite to wait indefinitely. - /// - /// - /// Returns true when shutdown succeeded; otherwise, false. - /// - /// - /// Thrown when the timeoutMilliseconds is smaller than -1. - /// - /// - /// This function guarantees thread-safety. Only the first call will - /// win, subsequent calls will be no-op. - /// - public static bool Shutdown(this TracerProvider provider, int timeoutMilliseconds = Timeout.Infinite) - { - Guard.ThrowIfNull(provider); - Guard.ThrowIfInvalidTimeout(timeoutMilliseconds); + return true; + } - if (provider is TracerProviderSdk tracerProviderSdk) - { - if (Interlocked.Increment(ref tracerProviderSdk.ShutdownCount) > 1) - { - return false; // shutdown already called - } + /// + /// Attempts to shutdown the TracerProviderSdk, blocks the current thread until + /// shutdown completed or timed out. + /// + /// TracerProviderSdk instance on which Shutdown will be called. + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when shutdown succeeded; otherwise, false. + /// + /// + /// Thrown when the timeoutMilliseconds is smaller than -1. + /// + /// + /// This function guarantees thread-safety. Only the first call will + /// win, subsequent calls will be no-op. + /// + public static bool Shutdown(this TracerProvider provider, int timeoutMilliseconds = Timeout.Infinite) + { + Guard.ThrowIfNull(provider); + Guard.ThrowIfInvalidTimeout(timeoutMilliseconds); - try - { - return tracerProviderSdk.OnShutdown(timeoutMilliseconds); - } - catch (Exception ex) - { - OpenTelemetrySdkEventSource.Log.TracerProviderException(nameof(tracerProviderSdk.OnShutdown), ex); - return false; - } + if (provider is TracerProviderSdk tracerProviderSdk) + { + if (Interlocked.Increment(ref tracerProviderSdk.ShutdownCount) > 1) + { + return false; // shutdown already called } - return true; + try + { + return tracerProviderSdk.OnShutdown(timeoutMilliseconds); + } + catch (Exception ex) + { + OpenTelemetrySdkEventSource.Log.TracerProviderException(nameof(tracerProviderSdk.OnShutdown), ex); + return false; + } } + + return true; } } diff --git a/src/OpenTelemetry/Trace/TracerProviderSdk.cs b/src/OpenTelemetry/Trace/TracerProviderSdk.cs index 218bf4c7a9a..55507e87ff3 100644 --- a/src/OpenTelemetry/Trace/TracerProviderSdk.cs +++ b/src/OpenTelemetry/Trace/TracerProviderSdk.cs @@ -1,549 +1,538 @@ -// // 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. -// - -#nullable enable +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Runtime.CompilerServices; using System.Text; +using System.Text.RegularExpressions; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Internal; using OpenTelemetry.Resources; -namespace OpenTelemetry.Trace +namespace OpenTelemetry.Trace; + +internal sealed class TracerProviderSdk : TracerProvider { - internal sealed class TracerProviderSdk : TracerProvider + internal readonly IServiceProvider ServiceProvider; + internal readonly IDisposable? OwnedServiceProvider; + internal int ShutdownCount; + internal bool Disposed; + + private readonly List instrumentations = new(); + private readonly ActivityListener listener; + private readonly Sampler sampler; + private readonly Action getRequestedDataAction; + private readonly bool supportLegacyActivity; + private BaseProcessor? processor; + + internal TracerProviderSdk( + IServiceProvider serviceProvider, + bool ownsServiceProvider) { - internal readonly IServiceProvider ServiceProvider; - internal readonly IDisposable? OwnedServiceProvider; - internal int ShutdownCount; - internal bool Disposed; - - private readonly List instrumentations = new(); - private readonly ActivityListener listener; - private readonly Sampler sampler; - private readonly Action getRequestedDataAction; - private readonly bool supportLegacyActivity; - private BaseProcessor? processor; - - internal TracerProviderSdk( - IServiceProvider serviceProvider, - bool ownsServiceProvider) - { - Debug.Assert(serviceProvider != null, "serviceProvider was null"); + Debug.Assert(serviceProvider != null, "serviceProvider was null"); - var state = serviceProvider!.GetRequiredService(); - state.RegisterProvider(this); + var state = serviceProvider!.GetRequiredService(); + state.RegisterProvider(this); - this.ServiceProvider = serviceProvider!; + this.ServiceProvider = serviceProvider!; - if (ownsServiceProvider) - { - this.OwnedServiceProvider = serviceProvider as IDisposable; - Debug.Assert(this.OwnedServiceProvider != null, "serviceProvider was not IDisposable"); - } + if (ownsServiceProvider) + { + this.OwnedServiceProvider = serviceProvider as IDisposable; + Debug.Assert(this.OwnedServiceProvider != null, "serviceProvider was not IDisposable"); + } - OpenTelemetrySdkEventSource.Log.TracerProviderSdkEvent("Building TracerProvider."); + OpenTelemetrySdkEventSource.Log.TracerProviderSdkEvent("Building TracerProvider."); - var configureProviderBuilders = serviceProvider!.GetServices(); - foreach (var configureProviderBuilder in configureProviderBuilders) - { - configureProviderBuilder.ConfigureBuilder(serviceProvider!, state); - } + var configureProviderBuilders = serviceProvider!.GetServices(); + foreach (var configureProviderBuilder in configureProviderBuilders) + { + configureProviderBuilder.ConfigureBuilder(serviceProvider!, state); + } - StringBuilder processorsAdded = new StringBuilder(); - StringBuilder instrumentationFactoriesAdded = new StringBuilder(); + StringBuilder processorsAdded = new StringBuilder(); + StringBuilder instrumentationFactoriesAdded = new StringBuilder(); - state.AddExceptionProcessorIfEnabled(); + state.AddExceptionProcessorIfEnabled(); - var resourceBuilder = state.ResourceBuilder ?? ResourceBuilder.CreateDefault(); - resourceBuilder.ServiceProvider = serviceProvider; - this.Resource = resourceBuilder.Build(); + var resourceBuilder = state.ResourceBuilder ?? ResourceBuilder.CreateDefault(); + resourceBuilder.ServiceProvider = serviceProvider; + this.Resource = resourceBuilder.Build(); - this.sampler = state.Sampler ?? new ParentBasedSampler(new AlwaysOnSampler()); - this.supportLegacyActivity = state.LegacyActivityOperationNames.Count > 0; + this.sampler = state.Sampler ?? new ParentBasedSampler(new AlwaysOnSampler()); + OpenTelemetrySdkEventSource.Log.TracerProviderSdkEvent($"Sampler added = \"{this.sampler.GetType()}\"."); - bool legacyActivityWildcardMode = false; - var legacyActivityWildcardModeRegex = WildcardHelper.GetWildcardRegex(); - foreach (var legacyName in state.LegacyActivityOperationNames) - { - if (WildcardHelper.ContainsWildcard(legacyName)) - { - legacyActivityWildcardMode = true; - legacyActivityWildcardModeRegex = WildcardHelper.GetWildcardRegex(state.LegacyActivityOperationNames); - break; - } - } + this.supportLegacyActivity = state.LegacyActivityOperationNames.Count > 0; - foreach (var processor in state.Processors) + Regex? legacyActivityWildcardModeRegex = null; + foreach (var legacyName in state.LegacyActivityOperationNames) + { + if (WildcardHelper.ContainsWildcard(legacyName)) { - this.AddProcessor(processor); - processorsAdded.Append(processor.GetType()); - processorsAdded.Append(';'); + legacyActivityWildcardModeRegex = WildcardHelper.GetWildcardRegex(state.LegacyActivityOperationNames); + break; } + } + + foreach (var processor in state.Processors) + { + this.AddProcessor(processor); + processorsAdded.Append(processor.GetType()); + processorsAdded.Append(';'); + } - foreach (var instrumentation in state.Instrumentation) + foreach (var instrumentation in state.Instrumentation) + { + if (instrumentation.Instance is not null) { this.instrumentations.Add(instrumentation.Instance); - instrumentationFactoriesAdded.Append(instrumentation.Name); - instrumentationFactoriesAdded.Append(';'); } - if (processorsAdded.Length != 0) + instrumentationFactoriesAdded.Append(instrumentation.Name); + instrumentationFactoriesAdded.Append(';'); + } + + if (processorsAdded.Length != 0) + { + processorsAdded.Remove(processorsAdded.Length - 1, 1); + OpenTelemetrySdkEventSource.Log.TracerProviderSdkEvent($"Processors added = \"{processorsAdded}\"."); + } + + if (instrumentationFactoriesAdded.Length != 0) + { + instrumentationFactoriesAdded.Remove(instrumentationFactoriesAdded.Length - 1, 1); + OpenTelemetrySdkEventSource.Log.TracerProviderSdkEvent($"Instrumentations added = \"{instrumentationFactoriesAdded}\"."); + } + + var listener = new ActivityListener(); + + if (this.supportLegacyActivity) + { + Func? legacyActivityPredicate = null; + if (legacyActivityWildcardModeRegex != null) { - processorsAdded.Remove(processorsAdded.Length - 1, 1); - OpenTelemetrySdkEventSource.Log.TracerProviderSdkEvent($"Processors added = \"{processorsAdded}\"."); + legacyActivityPredicate = activity => legacyActivityWildcardModeRegex.IsMatch(activity.OperationName); } - - if (instrumentationFactoriesAdded.Length != 0) + else { - instrumentationFactoriesAdded.Remove(instrumentationFactoriesAdded.Length - 1, 1); - OpenTelemetrySdkEventSource.Log.TracerProviderSdkEvent($"Instrumentations added = \"{instrumentationFactoriesAdded}\"."); + legacyActivityPredicate = activity => state.LegacyActivityOperationNames.Contains(activity.OperationName); } - var listener = new ActivityListener(); - - if (this.supportLegacyActivity) + listener.ActivityStarted = activity => { - Func? legacyActivityPredicate = null; - if (legacyActivityWildcardMode) - { - legacyActivityPredicate = activity => legacyActivityWildcardModeRegex.IsMatch(activity.OperationName); - } - else - { - legacyActivityPredicate = activity => state.LegacyActivityOperationNames.Contains(activity.OperationName); - } + OpenTelemetrySdkEventSource.Log.ActivityStarted(activity); - listener.ActivityStarted = activity => + if (string.IsNullOrEmpty(activity.Source.Name)) { - OpenTelemetrySdkEventSource.Log.ActivityStarted(activity); - - if (string.IsNullOrEmpty(activity.Source.Name)) + if (legacyActivityPredicate(activity)) { - if (legacyActivityPredicate(activity)) + // Legacy activity matches the user configured list. + // Call sampler for the legacy activity + // unless suppressed. + if (!Sdk.SuppressInstrumentation) { - // Legacy activity matches the user configured list. - // Call sampler for the legacy activity - // unless suppressed. - if (!Sdk.SuppressInstrumentation) - { - this.getRequestedDataAction!(activity); - } - else - { - activity.IsAllDataRequested = false; - } + this.getRequestedDataAction!(activity); } else { - // Legacy activity doesn't match the user configured list. No need to proceed further. - return; + activity.IsAllDataRequested = false; } } - - if (!activity.IsAllDataRequested) + else { + // Legacy activity doesn't match the user configured list. No need to proceed further. return; } + } - if (SuppressInstrumentationScope.IncrementIfTriggered() == 0) - { - this.processor?.OnStart(activity); - } - }; + if (!activity.IsAllDataRequested) + { + return; + } - listener.ActivityStopped = activity => + if (SuppressInstrumentationScope.IncrementIfTriggered() == 0) { - OpenTelemetrySdkEventSource.Log.ActivityStopped(activity); + this.processor?.OnStart(activity); + } + }; - if (string.IsNullOrEmpty(activity.Source.Name) && !legacyActivityPredicate(activity)) - { - // Legacy activity doesn't match the user configured list. No need to proceed further. - return; - } + listener.ActivityStopped = activity => + { + OpenTelemetrySdkEventSource.Log.ActivityStopped(activity); - if (!activity.IsAllDataRequested) - { - return; - } + if (string.IsNullOrEmpty(activity.Source.Name) && !legacyActivityPredicate(activity)) + { + // Legacy activity doesn't match the user configured list. No need to proceed further. + return; + } - // Spec says IsRecording must be false once span ends. - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#isrecording - // However, Activity has slightly different semantic - // than Span and we don't have strong reason to do this - // now, as Activity anyway allows read/write always. - // Intentionally commenting the following line. - // activity.IsAllDataRequested = false; + if (!activity.IsAllDataRequested) + { + return; + } - if (SuppressInstrumentationScope.DecrementIfTriggered() == 0) - { - this.processor?.OnEnd(activity); - } - }; - } - else + // Spec says IsRecording must be false once span ends. + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#isrecording + // However, Activity has slightly different semantic + // than Span and we don't have strong reason to do this + // now, as Activity anyway allows read/write always. + // Intentionally commenting the following line. + // activity.IsAllDataRequested = false; + + if (SuppressInstrumentationScope.DecrementIfTriggered() == 0) + { + this.processor?.OnEnd(activity); + } + }; + } + else + { + listener.ActivityStarted = activity => { - listener.ActivityStarted = activity => + OpenTelemetrySdkEventSource.Log.ActivityStarted(activity); + + if (activity.IsAllDataRequested && SuppressInstrumentationScope.IncrementIfTriggered() == 0) { - OpenTelemetrySdkEventSource.Log.ActivityStarted(activity); + this.processor?.OnStart(activity); + } + }; - if (activity.IsAllDataRequested && SuppressInstrumentationScope.IncrementIfTriggered() == 0) - { - this.processor?.OnStart(activity); - } - }; + listener.ActivityStopped = activity => + { + OpenTelemetrySdkEventSource.Log.ActivityStopped(activity); - listener.ActivityStopped = activity => + if (!activity.IsAllDataRequested) { - OpenTelemetrySdkEventSource.Log.ActivityStopped(activity); + return; + } - if (!activity.IsAllDataRequested) - { - return; - } + // Spec says IsRecording must be false once span ends. + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#isrecording + // However, Activity has slightly different semantic + // than Span and we don't have strong reason to do this + // now, as Activity anyway allows read/write always. + // Intentionally commenting the following line. + // activity.IsAllDataRequested = false; - // Spec says IsRecording must be false once span ends. - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#isrecording - // However, Activity has slightly different semantic - // than Span and we don't have strong reason to do this - // now, as Activity anyway allows read/write always. - // Intentionally commenting the following line. - // activity.IsAllDataRequested = false; + if (SuppressInstrumentationScope.DecrementIfTriggered() == 0) + { + this.processor?.OnEnd(activity); + } + }; + } - if (SuppressInstrumentationScope.DecrementIfTriggered() == 0) - { - this.processor?.OnEnd(activity); - } - }; - } + if (this.sampler is AlwaysOnSampler) + { + listener.Sample = (ref ActivityCreationOptions options) => + !Sdk.SuppressInstrumentation ? ActivitySamplingResult.AllDataAndRecorded : ActivitySamplingResult.None; + this.getRequestedDataAction = this.RunGetRequestedDataAlwaysOnSampler; + } + else if (this.sampler is AlwaysOffSampler) + { + listener.Sample = (ref ActivityCreationOptions options) => + !Sdk.SuppressInstrumentation ? PropagateOrIgnoreData(options.Parent) : ActivitySamplingResult.None; + this.getRequestedDataAction = this.RunGetRequestedDataAlwaysOffSampler; + } + else + { + // This delegate informs ActivitySource about sampling decision when the parent context is an ActivityContext. + listener.Sample = (ref ActivityCreationOptions options) => + !Sdk.SuppressInstrumentation ? ComputeActivitySamplingResult(ref options, this.sampler) : ActivitySamplingResult.None; + this.getRequestedDataAction = this.RunGetRequestedDataOtherSampler; + } - if (this.sampler is AlwaysOnSampler) - { - listener.Sample = (ref ActivityCreationOptions options) => - !Sdk.SuppressInstrumentation ? ActivitySamplingResult.AllDataAndRecorded : ActivitySamplingResult.None; - this.getRequestedDataAction = this.RunGetRequestedDataAlwaysOnSampler; - } - else if (this.sampler is AlwaysOffSampler) + // Sources can be null. This happens when user + // is only interested in InstrumentationLibraries + // which do not depend on ActivitySources. + if (state.Sources.Any()) + { + // Validation of source name is already done in builder. + if (state.Sources.Any(s => WildcardHelper.ContainsWildcard(s))) { - listener.Sample = (ref ActivityCreationOptions options) => - !Sdk.SuppressInstrumentation ? PropagateOrIgnoreData(options.Parent) : ActivitySamplingResult.None; - this.getRequestedDataAction = this.RunGetRequestedDataAlwaysOffSampler; + var regex = WildcardHelper.GetWildcardRegex(state.Sources); + + // Function which takes ActivitySource and returns true/false to indicate if it should be subscribed to + // or not. + listener.ShouldListenTo = (activitySource) => + this.supportLegacyActivity ? + string.IsNullOrEmpty(activitySource.Name) || regex.IsMatch(activitySource.Name) : + regex.IsMatch(activitySource.Name); } else { - // This delegate informs ActivitySource about sampling decision when the parent context is an ActivityContext. - listener.Sample = (ref ActivityCreationOptions options) => - !Sdk.SuppressInstrumentation ? ComputeActivitySamplingResult(ref options, this.sampler) : ActivitySamplingResult.None; - this.getRequestedDataAction = this.RunGetRequestedDataOtherSampler; - } + var activitySources = new HashSet(state.Sources, StringComparer.OrdinalIgnoreCase); - // Sources can be null. This happens when user - // is only interested in InstrumentationLibraries - // which do not depend on ActivitySources. - if (state.Sources.Any()) - { - // Validation of source name is already done in builder. - if (state.Sources.Any(s => WildcardHelper.ContainsWildcard(s))) + if (this.supportLegacyActivity) { - var regex = WildcardHelper.GetWildcardRegex(state.Sources); - - // Function which takes ActivitySource and returns true/false to indicate if it should be subscribed to - // or not. - listener.ShouldListenTo = (activitySource) => - this.supportLegacyActivity ? - string.IsNullOrEmpty(activitySource.Name) || regex.IsMatch(activitySource.Name) : - regex.IsMatch(activitySource.Name); + activitySources.Add(string.Empty); } - else - { - var activitySources = new HashSet(state.Sources, StringComparer.OrdinalIgnoreCase); - if (this.supportLegacyActivity) - { - activitySources.Add(string.Empty); - } - - // Function which takes ActivitySource and returns true/false to indicate if it should be subscribed to - // or not. - listener.ShouldListenTo = (activitySource) => activitySources.Contains(activitySource.Name); - } + // Function which takes ActivitySource and returns true/false to indicate if it should be subscribed to + // or not. + listener.ShouldListenTo = (activitySource) => activitySources.Contains(activitySource.Name); } - else + } + else + { + if (this.supportLegacyActivity) { - if (this.supportLegacyActivity) - { - listener.ShouldListenTo = (activitySource) => string.IsNullOrEmpty(activitySource.Name); - } + listener.ShouldListenTo = (activitySource) => string.IsNullOrEmpty(activitySource.Name); } - - ActivitySource.AddActivityListener(listener); - this.listener = listener; - OpenTelemetrySdkEventSource.Log.TracerProviderSdkEvent("TracerProvider built successfully."); } - internal Resource Resource { get; } + ActivitySource.AddActivityListener(listener); + this.listener = listener; + OpenTelemetrySdkEventSource.Log.TracerProviderSdkEvent("TracerProvider built successfully."); + } - internal List Instrumentations => this.instrumentations; + internal Resource Resource { get; } - internal BaseProcessor? Processor => this.processor; + internal List Instrumentations => this.instrumentations; - internal Sampler Sampler => this.sampler; + internal BaseProcessor? Processor => this.processor; - internal TracerProviderSdk AddProcessor(BaseProcessor processor) - { - Guard.ThrowIfNull(processor); + internal Sampler Sampler => this.sampler; - processor.SetParentProvider(this); + internal TracerProviderSdk AddProcessor(BaseProcessor processor) + { + Guard.ThrowIfNull(processor); - if (this.processor == null) - { - this.processor = processor; - } - else if (this.processor is CompositeProcessor compositeProcessor) - { - compositeProcessor.AddProcessor(processor); - } - else - { - var newCompositeProcessor = new CompositeProcessor(new[] - { - this.processor, - }); - newCompositeProcessor.SetParentProvider(this); - newCompositeProcessor.AddProcessor(processor); - this.processor = newCompositeProcessor; - } + processor.SetParentProvider(this); - return this; + if (this.processor == null) + { + this.processor = processor; } - - internal bool OnForceFlush(int timeoutMilliseconds) + else if (this.processor is CompositeProcessor compositeProcessor) { - return this.processor?.ForceFlush(timeoutMilliseconds) ?? true; + compositeProcessor.AddProcessor(processor); } - - /// - /// Called by Shutdown. This function should block the current - /// thread until shutdown completed or timed out. - /// - /// - /// The number (non-negative) of milliseconds to wait, or - /// Timeout.Infinite to wait indefinitely. - /// - /// - /// Returns true when shutdown succeeded; otherwise, false. - /// - /// - /// This function is called synchronously on the thread which made the - /// first call to Shutdown. This function should not throw - /// exceptions. - /// - internal bool OnShutdown(int timeoutMilliseconds) + else { - // TO DO Put OnShutdown logic in a task to run within the user provider timeOutMilliseconds - bool? result; - if (this.instrumentations != null) + var newCompositeProcessor = new CompositeProcessor(new[] { - foreach (var item in this.instrumentations) - { - (item as IDisposable)?.Dispose(); - } + this.processor, + }); + newCompositeProcessor.SetParentProvider(this); + newCompositeProcessor.AddProcessor(processor); + this.processor = newCompositeProcessor; + } + + return this; + } + + internal bool OnForceFlush(int timeoutMilliseconds) + { + return this.processor?.ForceFlush(timeoutMilliseconds) ?? true; + } - this.instrumentations.Clear(); + /// + /// Called by Shutdown. This function should block the current + /// thread until shutdown completed or timed out. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when shutdown succeeded; otherwise, false. + /// + /// + /// This function is called synchronously on the thread which made the + /// first call to Shutdown. This function should not throw + /// exceptions. + /// + internal bool OnShutdown(int timeoutMilliseconds) + { + // TO DO Put OnShutdown logic in a task to run within the user provider timeOutMilliseconds + bool? result; + if (this.instrumentations != null) + { + foreach (var item in this.instrumentations) + { + (item as IDisposable)?.Dispose(); } - result = this.processor?.Shutdown(timeoutMilliseconds); - this.listener?.Dispose(); - return result ?? true; + this.instrumentations.Clear(); } - protected override void Dispose(bool disposing) + result = this.processor?.Shutdown(timeoutMilliseconds); + this.listener?.Dispose(); + return result ?? true; + } + + protected override void Dispose(bool disposing) + { + if (!this.Disposed) { - if (!this.Disposed) + if (disposing) { - if (disposing) + if (this.instrumentations != null) { - if (this.instrumentations != null) + foreach (var item in this.instrumentations) { - foreach (var item in this.instrumentations) - { - (item as IDisposable)?.Dispose(); - } - - this.instrumentations.Clear(); + (item as IDisposable)?.Dispose(); } - (this.sampler as IDisposable)?.Dispose(); + this.instrumentations.Clear(); + } - // Wait for up to 5 seconds grace period - this.processor?.Shutdown(5000); - this.processor?.Dispose(); + (this.sampler as IDisposable)?.Dispose(); - // Shutdown the listener last so that anything created while instrumentation cleans up will still be processed. - // Redis instrumentation, for example, flushes during dispose which creates Activity objects for any profiling - // sessions that were open. - this.listener?.Dispose(); + // Wait for up to 5 seconds grace period + this.processor?.Shutdown(5000); + this.processor?.Dispose(); - this.OwnedServiceProvider?.Dispose(); - } + // Shutdown the listener last so that anything created while instrumentation cleans up will still be processed. + // Redis instrumentation, for example, flushes during dispose which creates Activity objects for any profiling + // sessions that were open. + this.listener?.Dispose(); - this.Disposed = true; - OpenTelemetrySdkEventSource.Log.ProviderDisposed(nameof(TracerProvider)); + this.OwnedServiceProvider?.Dispose(); } - base.Dispose(disposing); + this.Disposed = true; + OpenTelemetrySdkEventSource.Log.ProviderDisposed(nameof(TracerProvider)); } - private static ActivitySamplingResult ComputeActivitySamplingResult( - ref ActivityCreationOptions options, - Sampler sampler) - { - var samplingParameters = new SamplingParameters( - options.Parent, - options.TraceId, - options.Name, - options.Kind, - options.Tags, - options.Links); + base.Dispose(disposing); + } - var samplingResult = sampler.ShouldSample(samplingParameters); + private static ActivitySamplingResult ComputeActivitySamplingResult( + ref ActivityCreationOptions options, + Sampler sampler) + { + var samplingParameters = new SamplingParameters( + options.Parent, + options.TraceId, + options.Name, + options.Kind, + options.Tags, + options.Links); - var activitySamplingResult = samplingResult.Decision switch - { - SamplingDecision.RecordAndSample => ActivitySamplingResult.AllDataAndRecorded, - SamplingDecision.RecordOnly => ActivitySamplingResult.AllData, - _ => ActivitySamplingResult.PropagationData, - }; + var samplingResult = sampler.ShouldSample(samplingParameters); - if (activitySamplingResult != ActivitySamplingResult.PropagationData) - { - foreach (var att in samplingResult.Attributes) - { - options.SamplingTags.Add(att.Key, att.Value); - } + var activitySamplingResult = samplingResult.Decision switch + { + SamplingDecision.RecordAndSample => ActivitySamplingResult.AllDataAndRecorded, + SamplingDecision.RecordOnly => ActivitySamplingResult.AllData, + _ => ActivitySamplingResult.PropagationData, + }; - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#sampler - // Spec requires clearing Tracestate if empty Tracestate is returned. - // Since .NET did not have this capability, it'll break - // existing samplers if we did that. So the following is - // adopted to remain spec-compliant and backward compat. - // The behavior is: - // if sampler returns null, its treated as if it has no intend - // to change Tracestate. Existing SamplingResult ctors will put null as default TraceStateString, - // so all existing samplers will get this behavior. - // if sampler returns non-null, then it'll be used as the - // new value for Tracestate - // A sampler can return string.Empty if it intends to clear the state. - if (samplingResult.TraceStateString != null) - { - options = options with { TraceState = samplingResult.TraceStateString }; - } + if (activitySamplingResult != ActivitySamplingResult.PropagationData) + { + foreach (var att in samplingResult.Attributes) + { + options.SamplingTags.Add(att.Key, att.Value); + } - return activitySamplingResult; + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#sampler + // Spec requires clearing Tracestate if empty Tracestate is returned. + // Since .NET did not have this capability, it'll break + // existing samplers if we did that. So the following is + // adopted to remain spec-compliant and backward compat. + // The behavior is: + // if sampler returns null, its treated as if it has no intend + // to change Tracestate. Existing SamplingResult ctors will put null as default TraceStateString, + // so all existing samplers will get this behavior. + // if sampler returns non-null, then it'll be used as the + // new value for Tracestate + // A sampler can return string.Empty if it intends to clear the state. + if (samplingResult.TraceStateString != null) + { + options = options with { TraceState = samplingResult.TraceStateString }; } - return PropagateOrIgnoreData(options.Parent); + return activitySamplingResult; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ActivitySamplingResult PropagateOrIgnoreData(in ActivityContext parentContext) - { - var isRootSpan = parentContext.TraceId == default; + return PropagateOrIgnoreData(options.Parent); + } - // If it is the root span or the parent is remote select PropagationData so the trace ID is preserved - // even if no activity of the trace is recorded (sampled per OpenTelemetry parlance). - return (isRootSpan || parentContext.IsRemote) - ? ActivitySamplingResult.PropagationData - : ActivitySamplingResult.None; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ActivitySamplingResult PropagateOrIgnoreData(in ActivityContext parentContext) + { + var isRootSpan = parentContext.TraceId == default; + + // If it is the root span or the parent is remote select PropagationData so the trace ID is preserved + // even if no activity of the trace is recorded (sampled per OpenTelemetry parlance). + return (isRootSpan || parentContext.IsRemote) + ? ActivitySamplingResult.PropagationData + : ActivitySamplingResult.None; + } + + private void RunGetRequestedDataAlwaysOnSampler(Activity activity) + { + activity.IsAllDataRequested = true; + activity.ActivityTraceFlags |= ActivityTraceFlags.Recorded; + } + + private void RunGetRequestedDataAlwaysOffSampler(Activity activity) + { + activity.IsAllDataRequested = false; + activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; + } + + private void RunGetRequestedDataOtherSampler(Activity activity) + { + ActivityContext parentContext; - private void RunGetRequestedDataAlwaysOnSampler(Activity activity) + // Check activity.ParentId alone is sufficient to normally determine if a activity is root or not. But if one uses activity.SetParentId to override the TraceId (without intending to set an actual parent), then additional check of parentspanid being empty is required to confirm if an activity is root or not. + // This checker can be removed, once Activity exposes an API to customize ID Generation (https://github.com/dotnet/runtime/issues/46704) or issue https://github.com/dotnet/runtime/issues/46706 is addressed. + if (string.IsNullOrEmpty(activity.ParentId) || activity.ParentSpanId.ToHexString() == "0000000000000000") { - activity.IsAllDataRequested = true; - activity.ActivityTraceFlags |= ActivityTraceFlags.Recorded; + parentContext = default; } - - private void RunGetRequestedDataAlwaysOffSampler(Activity activity) + else if (activity.Parent != null) { - activity.IsAllDataRequested = false; - activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; + parentContext = activity.Parent.Context; } - - private void RunGetRequestedDataOtherSampler(Activity activity) + else { - ActivityContext parentContext; + parentContext = new ActivityContext( + activity.TraceId, + activity.ParentSpanId, + activity.ActivityTraceFlags, + activity.TraceStateString, + isRemote: true); + } - // Check activity.ParentId alone is sufficient to normally determine if a activity is root or not. But if one uses activity.SetParentId to override the TraceId (without intending to set an actual parent), then additional check of parentspanid being empty is required to confirm if an activity is root or not. - // This checker can be removed, once Activity exposes an API to customize ID Generation (https://github.com/dotnet/runtime/issues/46704) or issue https://github.com/dotnet/runtime/issues/46706 is addressed. - if (string.IsNullOrEmpty(activity.ParentId) || activity.ParentSpanId.ToHexString() == "0000000000000000") - { - parentContext = default; - } - else if (activity.Parent != null) - { - parentContext = activity.Parent.Context; - } - else - { - parentContext = new ActivityContext( - activity.TraceId, - activity.ParentSpanId, - activity.ActivityTraceFlags, - activity.TraceStateString, - isRemote: true); - } + var samplingParameters = new SamplingParameters( + parentContext, + activity.TraceId, + activity.DisplayName, + activity.Kind, + activity.TagObjects, + activity.Links); - var samplingParameters = new SamplingParameters( - parentContext, - activity.TraceId, - activity.DisplayName, - activity.Kind, - activity.TagObjects, - activity.Links); + var samplingResult = this.sampler.ShouldSample(samplingParameters); - var samplingResult = this.sampler.ShouldSample(samplingParameters); + switch (samplingResult.Decision) + { + case SamplingDecision.Drop: + activity.IsAllDataRequested = false; + activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; + break; + case SamplingDecision.RecordOnly: + activity.IsAllDataRequested = true; + activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; + break; + case SamplingDecision.RecordAndSample: + activity.IsAllDataRequested = true; + activity.ActivityTraceFlags |= ActivityTraceFlags.Recorded; + break; + } - switch (samplingResult.Decision) + if (samplingResult.Decision != SamplingDecision.Drop) + { + foreach (var att in samplingResult.Attributes) { - case SamplingDecision.Drop: - activity.IsAllDataRequested = false; - activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; - break; - case SamplingDecision.RecordOnly: - activity.IsAllDataRequested = true; - activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; - break; - case SamplingDecision.RecordAndSample: - activity.IsAllDataRequested = true; - activity.ActivityTraceFlags |= ActivityTraceFlags.Recorded; - break; + activity.SetTag(att.Key, att.Value); } - if (samplingResult.Decision != SamplingDecision.Drop) + if (samplingResult.TraceStateString != null) { - foreach (var att in samplingResult.Attributes) - { - activity.SetTag(att.Key, att.Value); - } - - if (samplingResult.TraceStateString != null) - { - activity.TraceStateString = samplingResult.TraceStateString; - } + activity.TraceStateString = samplingResult.TraceStateString; } } } diff --git a/src/Shared/ActivityHelperExtensions.cs b/src/Shared/ActivityHelperExtensions.cs new file mode 100644 index 00000000000..b237ccd5457 --- /dev/null +++ b/src/Shared/ActivityHelperExtensions.cs @@ -0,0 +1,115 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Trace; + +/// +/// Extension methods on Activity. +/// +internal static class ActivityHelperExtensions +{ + /// + /// Gets the status of activity execution. + /// Activity class in .NET does not support 'Status'. + /// This extension provides a workaround to retrieve Status from special tags with key name otel.status_code and otel.status_description. + /// + /// Activity instance. + /// . + /// Status description. + /// if was found on the supplied Activity. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryGetStatus(this Activity activity, out StatusCode statusCode, out string? statusDescription) + { + Debug.Assert(activity != null, "Activity should not be null"); + + bool foundStatusCode = false; + statusCode = default; + statusDescription = null; + + foreach (ref readonly var tag in activity!.EnumerateTagObjects()) + { + switch (tag.Key) + { + case SpanAttributeConstants.StatusCodeKey: + foundStatusCode = StatusHelper.TryGetStatusCodeForTagValue(tag.Value as string, out statusCode); + if (!foundStatusCode) + { + // If status code was found but turned out to be invalid give up immediately. + return false; + } + + break; + case SpanAttributeConstants.StatusDescriptionKey: + statusDescription = tag.Value as string; + break; + default: + continue; + } + + if (foundStatusCode && statusDescription != null) + { + // If we found a status code and a description we break enumeration because our work is done. + break; + } + } + + return foundStatusCode; + } + + /// + /// Gets the value of a specific tag on an . + /// + /// Activity instance. + /// Case-sensitive tag name to retrieve. + /// Tag value or null if a match was not found. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static object? GetTagValue(this Activity activity, string? tagName) + { + Debug.Assert(activity != null, "Activity should not be null"); + + foreach (ref readonly var tag in activity!.EnumerateTagObjects()) + { + if (tag.Key == tagName) + { + return tag.Value; + } + } + + return null; + } + + /// + /// Checks if the user provided tag name is the first tag of the and retrieves the tag value. + /// + /// Activity instance. + /// Tag name. + /// Tag value. + /// if the first tag of the supplied Activity matches the user provide tag name. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryCheckFirstTag(this Activity activity, string tagName, out object? tagValue) + { + Debug.Assert(activity != null, "Activity should not be null"); + + var enumerator = activity!.EnumerateTagObjects(); + + if (enumerator.MoveNext()) + { + ref readonly var tag = ref enumerator.Current; + + if (tag.Key == tagName) + { + tagValue = tag.Value; + return true; + } + } + + tagValue = null; + return false; + } +} diff --git a/src/Shared/ActivityInstrumentationHelper.cs b/src/Shared/ActivityInstrumentationHelper.cs index d6124809b0b..b95c669bfb0 100644 --- a/src/Shared/ActivityInstrumentationHelper.cs +++ b/src/Shared/ActivityInstrumentationHelper.cs @@ -1,47 +1,25 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -using System.Linq.Expressions; -using System.Reflection; #pragma warning restore IDE0005 -namespace OpenTelemetry.Instrumentation +namespace OpenTelemetry.Instrumentation; + +internal static class ActivityInstrumentationHelper { - internal static class ActivityInstrumentationHelper - { - internal static readonly Action SetKindProperty = CreateActivityKindSetter(); - internal static readonly Action SetActivitySourceProperty = CreateActivitySourceSetter(); + internal static readonly Action SetKindProperty = CreateActivityKindSetter(); + internal static readonly Action SetActivitySourceProperty = CreateActivitySourceSetter(); - private static Action CreateActivitySourceSetter() - { - ParameterExpression instance = Expression.Parameter(typeof(Activity), "instance"); - ParameterExpression propertyValue = Expression.Parameter(typeof(ActivitySource), "propertyValue"); - PropertyInfo sourcePropertyInfo = typeof(Activity).GetProperty("Source"); - var body = Expression.Assign(Expression.Property(instance, sourcePropertyInfo), propertyValue); - return Expression.Lambda>(body, instance, propertyValue).Compile(); - } + private static Action CreateActivitySourceSetter() + { + return (Action)typeof(Activity).GetProperty("Source") + .SetMethod.CreateDelegate(typeof(Action)); + } - private static Action CreateActivityKindSetter() - { - ParameterExpression instance = Expression.Parameter(typeof(Activity), "instance"); - ParameterExpression propertyValue = Expression.Parameter(typeof(ActivityKind), "propertyValue"); - PropertyInfo kindPropertyInfo = typeof(Activity).GetProperty("Kind"); - var body = Expression.Assign(Expression.Property(instance, kindPropertyInfo), propertyValue); - return Expression.Lambda>(body, instance, propertyValue).Compile(); - } + private static Action CreateActivityKindSetter() + { + return (Action)typeof(Activity).GetProperty("Kind") + .SetMethod.CreateDelegate(typeof(Action)); } } diff --git a/src/Shared/DiagnosticDefinitions.cs b/src/Shared/DiagnosticDefinitions.cs new file mode 100644 index 00000000000..b772ea53652 --- /dev/null +++ b/src/Shared/DiagnosticDefinitions.cs @@ -0,0 +1,16 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +namespace OpenTelemetry.Internal; + +internal static class DiagnosticDefinitions +{ + public const string ExperimentalApiUrlFormat = "https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/diagnostics/experimental-apis/README.md#{0}"; + + public const string LoggerProviderExperimentalApi = "OTEL1000"; + public const string LogsBridgeExperimentalApi = "OTEL1001"; + public const string ExemplarExperimentalApi = "OTEL1002"; + public const string CardinalityLimitExperimentalApi = "OTEL1003"; +} diff --git a/src/Shared/DiagnosticSourceInstrumentation/DiagnosticSourceListener.cs b/src/Shared/DiagnosticSourceInstrumentation/DiagnosticSourceListener.cs index 6993b06805a..51838533a86 100644 --- a/src/Shared/DiagnosticSourceInstrumentation/DiagnosticSourceListener.cs +++ b/src/Shared/DiagnosticSourceInstrumentation/DiagnosticSourceListener.cs @@ -1,59 +1,47 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Internal; -#pragma warning restore IDE0005 -namespace OpenTelemetry.Instrumentation +namespace OpenTelemetry.Instrumentation; + +internal sealed class DiagnosticSourceListener : IObserver> { - internal sealed class DiagnosticSourceListener : IObserver> + private readonly ListenerHandler handler; + + private readonly Action logUnknownException; + + public DiagnosticSourceListener(ListenerHandler handler, Action logUnknownException) { - private readonly ListenerHandler handler; + Guard.ThrowIfNull(handler); - public DiagnosticSourceListener(ListenerHandler handler) - { - Guard.ThrowIfNull(handler); + this.handler = handler; + this.logUnknownException = logUnknownException; + } - this.handler = handler; - } + public void OnCompleted() + { + } + + public void OnError(Exception error) + { + } - public void OnCompleted() + public void OnNext(KeyValuePair value) + { + if (!this.handler.SupportsNullActivity && Activity.Current == null) { + return; } - public void OnError(Exception error) + try { + this.handler.OnEventWritten(value.Key, value.Value); } - - public void OnNext(KeyValuePair value) + catch (Exception ex) { - if (!this.handler.SupportsNullActivity && Activity.Current == null) - { - return; - } - - try - { - this.handler.OnEventWritten(value.Key, value.Value); - } - catch (Exception ex) - { - InstrumentationEventSource.Log.UnknownErrorProcessingEvent(this.handler?.SourceName, value.Key, ex); - } + this.logUnknownException?.Invoke(this.handler?.SourceName, value.Key, ex); } } } diff --git a/src/Shared/DiagnosticSourceInstrumentation/DiagnosticSourceSubscriber.cs b/src/Shared/DiagnosticSourceInstrumentation/DiagnosticSourceSubscriber.cs index 775fc8ae5a4..3071617f8a1 100644 --- a/src/Shared/DiagnosticSourceInstrumentation/DiagnosticSourceSubscriber.cs +++ b/src/Shared/DiagnosticSourceInstrumentation/DiagnosticSourceSubscriber.cs @@ -1,113 +1,103 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Internal; -namespace OpenTelemetry.Instrumentation +namespace OpenTelemetry.Instrumentation; + +internal sealed class DiagnosticSourceSubscriber : IDisposable, IObserver { - internal sealed class DiagnosticSourceSubscriber : IDisposable, IObserver + private readonly List listenerSubscriptions; + private readonly Func handlerFactory; + private readonly Func diagnosticSourceFilter; + private readonly Func isEnabledFilter; + private readonly Action logUnknownException; + private long disposed; + private IDisposable allSourcesSubscription; + + public DiagnosticSourceSubscriber( + ListenerHandler handler, + Func isEnabledFilter, + Action logUnknownException) + : this(_ => handler, value => handler.SourceName == value.Name, isEnabledFilter, logUnknownException) { - private readonly List listenerSubscriptions; - private readonly Func handlerFactory; - private readonly Func diagnosticSourceFilter; - private readonly Func isEnabledFilter; - private long disposed; - private IDisposable allSourcesSubscription; - - public DiagnosticSourceSubscriber( - ListenerHandler handler, - Func isEnabledFilter) - : this(_ => handler, value => handler.SourceName == value.Name, isEnabledFilter) - { - } + } - public DiagnosticSourceSubscriber( - Func handlerFactory, - Func diagnosticSourceFilter, - Func isEnabledFilter) - { - Guard.ThrowIfNull(handlerFactory); + public DiagnosticSourceSubscriber( + Func handlerFactory, + Func diagnosticSourceFilter, + Func isEnabledFilter, + Action logUnknownException) + { + Guard.ThrowIfNull(handlerFactory); - this.listenerSubscriptions = new List(); - this.handlerFactory = handlerFactory; - this.diagnosticSourceFilter = diagnosticSourceFilter; - this.isEnabledFilter = isEnabledFilter; - } + this.listenerSubscriptions = new List(); + this.handlerFactory = handlerFactory; + this.diagnosticSourceFilter = diagnosticSourceFilter; + this.isEnabledFilter = isEnabledFilter; + this.logUnknownException = logUnknownException; + } - public void Subscribe() + public void Subscribe() + { + if (this.allSourcesSubscription == null) { - if (this.allSourcesSubscription == null) - { - this.allSourcesSubscription = DiagnosticListener.AllListeners.Subscribe(this); - } + this.allSourcesSubscription = DiagnosticListener.AllListeners.Subscribe(this); } + } - public void OnNext(DiagnosticListener value) + public void OnNext(DiagnosticListener value) + { + if ((Interlocked.Read(ref this.disposed) == 0) && + this.diagnosticSourceFilter(value)) { - if ((Interlocked.Read(ref this.disposed) == 0) && - this.diagnosticSourceFilter(value)) + var handler = this.handlerFactory(value.Name); + var listener = new DiagnosticSourceListener(handler, this.logUnknownException); + var subscription = this.isEnabledFilter == null ? + value.Subscribe(listener) : + value.Subscribe(listener, this.isEnabledFilter); + + lock (this.listenerSubscriptions) { - var handler = this.handlerFactory(value.Name); - var listener = new DiagnosticSourceListener(handler); - var subscription = this.isEnabledFilter == null ? - value.Subscribe(listener) : - value.Subscribe(listener, this.isEnabledFilter); - - lock (this.listenerSubscriptions) - { - this.listenerSubscriptions.Add(subscription); - } + this.listenerSubscriptions.Add(subscription); } } + } - public void OnCompleted() - { - } + public void OnCompleted() + { + } - public void OnError(Exception error) - { - } + public void OnError(Exception error) + { + } - /// - public void Dispose() + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (Interlocked.CompareExchange(ref this.disposed, 1, 0) == 1) { - this.Dispose(true); - GC.SuppressFinalize(this); + return; } - private void Dispose(bool disposing) + lock (this.listenerSubscriptions) { - if (Interlocked.CompareExchange(ref this.disposed, 1, 0) == 1) - { - return; - } - - lock (this.listenerSubscriptions) + foreach (var listenerSubscription in this.listenerSubscriptions) { - foreach (var listenerSubscription in this.listenerSubscriptions) - { - listenerSubscription?.Dispose(); - } - - this.listenerSubscriptions.Clear(); + listenerSubscription?.Dispose(); } - this.allSourcesSubscription?.Dispose(); - this.allSourcesSubscription = null; + this.listenerSubscriptions.Clear(); } + + this.allSourcesSubscription?.Dispose(); + this.allSourcesSubscription = null; } } diff --git a/src/Shared/DiagnosticSourceInstrumentation/InstrumentationEventSource.cs b/src/Shared/DiagnosticSourceInstrumentation/InstrumentationEventSource.cs deleted file mode 100644 index ba44cab4dce..00000000000 --- a/src/Shared/DiagnosticSourceInstrumentation/InstrumentationEventSource.cs +++ /dev/null @@ -1,52 +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.Tracing; -using OpenTelemetry.Internal; -#pragma warning restore IDE0005 - -namespace OpenTelemetry.Instrumentation -{ - /// - /// EventSource events emitted from the project. - /// - [EventSource(Name = "OpenTelemetry-Instrumentation")] - internal sealed class InstrumentationEventSource : EventSource - { - public static InstrumentationEventSource Log = new(); - - [Event(1, Message = "Current Activity is NULL in the '{0}' callback. Activity will not be recorded.", Level = EventLevel.Warning)] - public void NullActivity(string eventName) - { - this.WriteEvent(1, eventName); - } - - [NonEvent] - public void UnknownErrorProcessingEvent(string handlerName, string eventName, Exception ex) - { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.UnknownErrorProcessingEvent(handlerName, eventName, ex.ToInvariantString()); - } - } - - [Event(2, Message = "Unknown error processing event '{1}' from handler '{0}', Exception: {2}", Level = EventLevel.Error)] - public void UnknownErrorProcessingEvent(string handlerName, string eventName, string ex) - { - this.WriteEvent(2, handlerName, eventName, ex); - } - } -} diff --git a/src/Shared/DiagnosticSourceInstrumentation/ListenerHandler.cs b/src/Shared/DiagnosticSourceInstrumentation/ListenerHandler.cs index 2c1678715b1..98c55107a67 100644 --- a/src/Shared/DiagnosticSourceInstrumentation/ListenerHandler.cs +++ b/src/Shared/DiagnosticSourceInstrumentation/ListenerHandler.cs @@ -1,53 +1,40 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + using System.Diagnostics; -namespace OpenTelemetry.Instrumentation +namespace OpenTelemetry.Instrumentation; + +/// +/// ListenerHandler base class. +/// +internal abstract class ListenerHandler { /// - /// ListenerHandler base class. + /// Initializes a new instance of the class. /// - internal abstract class ListenerHandler + /// The name of the . + public ListenerHandler(string sourceName) { - /// - /// Initializes a new instance of the class. - /// - /// The name of the . - public ListenerHandler(string sourceName) - { - this.SourceName = sourceName; - } + this.SourceName = sourceName; + } - /// - /// Gets the name of the . - /// - public string SourceName { get; } + /// + /// Gets the name of the . + /// + public string SourceName { get; } - /// - /// Gets a value indicating whether the supports NULL . - /// - public virtual bool SupportsNullActivity { get; } + /// + /// Gets a value indicating whether the supports NULL . + /// + public virtual bool SupportsNullActivity { get; } - /// - /// Method called for an event which does not have 'Start', 'Stop' or 'Exception' as suffix. - /// - /// Custom name. - /// An object that represent the value being passed as a payload for the event. - public virtual void OnEventWritten(string name, object payload) - { - } + /// + /// Method called for an event which does not have 'Start', 'Stop' or 'Exception' as suffix. + /// + /// Custom name. + /// An object that represent the value being passed as a payload for the event. + public virtual void OnEventWritten(string name, object payload) + { } } diff --git a/src/Shared/DiagnosticSourceInstrumentation/PropertyFetcher.cs b/src/Shared/DiagnosticSourceInstrumentation/PropertyFetcher.cs index f1e8b25ebeb..a2ac797d8a3 100644 --- a/src/Shared/DiagnosticSourceInstrumentation/PropertyFetcher.cs +++ b/src/Shared/DiagnosticSourceInstrumentation/PropertyFetcher.cs @@ -1,155 +1,217 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +#nullable enable + +#if NETSTANDARD2_1_0_OR_GREATER || NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif using System.Reflection; -using OpenTelemetry.Internal; -#pragma warning restore IDE0005 -namespace OpenTelemetry.Instrumentation +namespace OpenTelemetry.Instrumentation; + +/// +/// PropertyFetcher fetches a property from an object. +/// +/// The type of the property being fetched. +internal sealed class PropertyFetcher { +#if NET6_0_OR_GREATER + private const string TrimCompatibilityMessage = "PropertyFetcher is used to access properties on objects dynamically by design and cannot be made trim compatible."; +#endif + private readonly string propertyName; + private PropertyFetch? innerFetcher; + /// - /// PropertyFetcher fetches a property from an object. + /// Initializes a new instance of the class. /// - /// The type of the property being fetched. - internal sealed class PropertyFetcher + /// Property name to fetch. + public PropertyFetcher(string propertyName) { - private readonly string propertyName; - private PropertyFetch innerFetcher; - - /// - /// Initializes a new instance of the class. - /// - /// Property name to fetch. - public PropertyFetcher(string propertyName) - { - this.propertyName = propertyName; - } + this.propertyName = propertyName; + } - /// - /// Fetch the property from the object. - /// - /// Object to be fetched. - /// Property fetched. - public T Fetch(object obj) + public int NumberOfInnerFetchers => this.innerFetcher == null + ? 0 + : 1 + this.innerFetcher.NumberOfInnerFetchers; + + /// + /// Try to fetch the property from the object. + /// + /// Object to be fetched. + /// Fetched value. + /// if the property was fetched. +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode(TrimCompatibilityMessage)] +#endif + public bool TryFetch( +#if NETSTANDARD2_1_0_OR_GREATER || NET6_0_OR_GREATER + [NotNullWhen(true)] +#endif + object? obj, + out T? value) + { + var innerFetcher = this.innerFetcher; + if (innerFetcher is null) { - Guard.ThrowIfNull(obj); + return TryFetchRare(obj, this.propertyName, ref this.innerFetcher, out value); + } - if (!this.TryFetch(obj, out T value, true)) - { - throw new ArgumentException($"Unable to fetch property: '{nameof(obj)}'", nameof(obj)); - } + return innerFetcher.TryFetch(obj, out value); + } - return value; +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode(TrimCompatibilityMessage)] +#endif + private static bool TryFetchRare(object? obj, string propertyName, ref PropertyFetch? destination, out T? value) + { + if (obj is null) + { + value = default; + return false; } - /// - /// Try to fetch the property from the object. - /// - /// Object to be fetched. - /// Fetched value. - /// Set this to if we know is not . - /// if the property was fetched. - public bool TryFetch(object obj, out T value, bool skipObjNullCheck = false) + var fetcher = PropertyFetch.Create(obj.GetType().GetTypeInfo(), propertyName); + + if (fetcher is null) { - if (!skipObjNullCheck && obj == null) - { - value = default; - return false; - } + value = default; + return false; + } - if (this.innerFetcher == null) - { - this.innerFetcher = PropertyFetch.Create(obj.GetType().GetTypeInfo(), this.propertyName); - } + destination = fetcher; - if (this.innerFetcher == null) - { - value = default; - return false; - } + return fetcher.TryFetch(obj, out value); + } - return this.innerFetcher.TryFetch(obj, out value); - } + // see https://github.com/dotnet/corefx/blob/master/src/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/DiagnosticSourceEventSource.cs +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode(TrimCompatibilityMessage)] +#endif + private abstract class PropertyFetch + { + public abstract int NumberOfInnerFetchers { get; } - // see https://github.com/dotnet/corefx/blob/master/src/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/DiagnosticSourceEventSource.cs - private class PropertyFetch + public static PropertyFetch? Create(TypeInfo type, string propertyName) { - public static PropertyFetch Create(TypeInfo type, string propertyName) + var property = type.DeclaredProperties.FirstOrDefault(p => string.Equals(p.Name, propertyName, StringComparison.OrdinalIgnoreCase)) ?? type.GetProperty(propertyName); + return CreateFetcherForProperty(property); + + static PropertyFetch? CreateFetcherForProperty(PropertyInfo? propertyInfo) { - var property = type.DeclaredProperties.FirstOrDefault(p => string.Equals(p.Name, propertyName, StringComparison.OrdinalIgnoreCase)); - if (property == null) + if (propertyInfo == null || !typeof(T).IsAssignableFrom(propertyInfo.PropertyType)) { - property = type.GetProperty(propertyName); + // returns null and wait for a valid payload to arrive. + return null; } - return CreateFetcherForProperty(property); + var declaringType = propertyInfo.DeclaringType; + if (declaringType!.IsValueType) + { + throw new NotSupportedException( + $"Type: {declaringType.FullName} is a value type. PropertyFetcher can only operate on reference payload types."); + } - static PropertyFetch CreateFetcherForProperty(PropertyInfo propertyInfo) + if (declaringType == typeof(object)) { - if (propertyInfo == null || !typeof(T).IsAssignableFrom(propertyInfo.PropertyType)) - { - // returns null and wait for a valid payload to arrive. - return null; - } - - var typedPropertyFetcher = typeof(TypedPropertyFetch<,>); - var instantiatedTypedPropertyFetcher = typedPropertyFetcher.MakeGenericType( - typeof(T), propertyInfo.DeclaringType, propertyInfo.PropertyType); - return (PropertyFetch)Activator.CreateInstance(instantiatedTypedPropertyFetcher, propertyInfo); + // TODO: REMOVE this if branch when .NET 7 is out of support. + // This branch is never executed and is only needed for .NET 7 AOT-compiler at trimming stage; i.e., + // this is not needed in .NET 8, because the compiler is improved and call into MakeGenericMethod will be AOT-compatible. + // It is used to force the AOT compiler to create an instantiation of the method with a reference type. + // The code for that instantiation can then be reused at runtime to create instantiation over any other reference. + return CreateInstantiated(propertyInfo); + } + else + { + return DynamicInstantiationHelper(declaringType, propertyInfo); } - } - public virtual bool TryFetch(object obj, out T value) - { - value = default; - return false; + // Separated as a local function to be able to target the suppression to just this call. + // IL3050 was generated here because of the call to MakeGenericType, which is problematic in AOT if one of the type parameters is a value type; + // because the compiler might need to generate code specific to that type. + // If the type parameter is a reference type, there will be no problem; because the generated code can be shared among all reference type instantiations. +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "The code guarantees that all the generic parameters are reference types.")] +#endif + static PropertyFetch? DynamicInstantiationHelper(Type declaringType, PropertyInfo propertyInfo) + { + return (PropertyFetch?)typeof(PropertyFetch) + .GetMethod(nameof(CreateInstantiated), BindingFlags.NonPublic | BindingFlags.Static)! + .MakeGenericMethod(declaringType) // This is validated in the earlier call chain to be a reference type. + .Invoke(null, new object[] { propertyInfo })!; + } } + } - private sealed class TypedPropertyFetch : PropertyFetch - where TDeclaredProperty : T + public abstract bool TryFetch( +#if NETSTANDARD2_1_0_OR_GREATER || NET6_0_OR_GREATER + [NotNullWhen(true)] +#endif + object? obj, + out T? value); + + // Goal: make PropertyFetcher AOT-compatible. + // AOT compiler can't guarantee correctness when call into MakeGenericType or MakeGenericMethod + // if one of the generic parameters is a value type (reference types are OK.) + // For PropertyFetcher, the decision was made to only support reference type payloads, i.e.: + // the object from which to get the property value MUST be a reference type. + // Create generics with the declared object type as a generic parameter is OK, but we need the return type + // of the property to be a value type (on top of reference types.) + // Normally, we would have a helper class like `PropertyFetchInstantiated` that takes 2 generic parameters, + // the declared object type, and the type of the property value. + // But that would mean calling MakeGenericType, with value type parameters which AOT won't support. + // + // As a workaround, Generic instantiation was split into: + // 1. The object type comes from the PropertyFetcher generic parameter. + // Compiler supports it even if it is a value type; the type is known statically during compilation + // since PropertyFetcher is used with it. + // 2. Then, the declared object type is passed as a generic parameter to a generic method on PropertyFetcher (or nested type.) + // Therefore, calling into MakeGenericMethod will only require specifying one parameter - the declared object type. + // The declared object type is guaranteed to be a reference type (throw on value type.) Thus, MakeGenericMethod is AOT compatible. + private static PropertyFetch CreateInstantiated(PropertyInfo propertyInfo) + where TDeclaredObject : class + => new PropertyFetchInstantiated(propertyInfo); + +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode(TrimCompatibilityMessage)] +#endif + private sealed class PropertyFetchInstantiated : PropertyFetch + where TDeclaredObject : class + { + private readonly string propertyName; + private readonly Func propertyFetch; + private PropertyFetch? innerFetcher; + + public PropertyFetchInstantiated(PropertyInfo property) { - private readonly string propertyName; - private readonly Func propertyFetch; + this.propertyName = property.Name; + this.propertyFetch = (Func)property.GetMethod!.CreateDelegate(typeof(Func)); + } - private PropertyFetch innerFetcher; + public override int NumberOfInnerFetchers => this.innerFetcher == null + ? 0 + : 1 + this.innerFetcher.NumberOfInnerFetchers; - public TypedPropertyFetch(PropertyInfo property) + public override bool TryFetch( +#if NETSTANDARD2_1_0_OR_GREATER || NET6_0_OR_GREATER + [NotNullWhen(true)] +#endif + object? obj, + out T? value) + { + if (obj is TDeclaredObject o) { - this.propertyName = property.Name; - this.propertyFetch = (Func)property.GetMethod.CreateDelegate(typeof(Func)); + value = this.propertyFetch(o); + return true; } - public override bool TryFetch(object obj, out T value) + var innerFetcher = this.innerFetcher; + if (innerFetcher is null) { - if (obj is TDeclaredObject o) - { - value = this.propertyFetch(o); - return true; - } - - this.innerFetcher ??= Create(obj.GetType().GetTypeInfo(), this.propertyName); - - if (this.innerFetcher == null) - { - value = default; - return false; - } - - return this.innerFetcher.TryFetch(obj, out value); + return TryFetchRare(obj, this.propertyName, ref this.innerFetcher, out value); } + + return innerFetcher.TryFetch(obj, out value); } } } diff --git a/src/OpenTelemetry/Internal/EnvironmentVariables/EnvironmentVariablesConfigurationProvider.cs b/src/Shared/EnvironmentVariables/EnvironmentVariablesConfigurationProvider.cs similarity index 100% rename from src/OpenTelemetry/Internal/EnvironmentVariables/EnvironmentVariablesConfigurationProvider.cs rename to src/Shared/EnvironmentVariables/EnvironmentVariablesConfigurationProvider.cs diff --git a/src/OpenTelemetry/Internal/EnvironmentVariables/EnvironmentVariablesConfigurationSource.cs b/src/Shared/EnvironmentVariables/EnvironmentVariablesConfigurationSource.cs similarity index 100% rename from src/OpenTelemetry/Internal/EnvironmentVariables/EnvironmentVariablesConfigurationSource.cs rename to src/Shared/EnvironmentVariables/EnvironmentVariablesConfigurationSource.cs diff --git a/src/OpenTelemetry/Internal/EnvironmentVariables/EnvironmentVariablesExtensions.cs b/src/Shared/EnvironmentVariables/EnvironmentVariablesExtensions.cs similarity index 100% rename from src/OpenTelemetry/Internal/EnvironmentVariables/EnvironmentVariablesExtensions.cs rename to src/Shared/EnvironmentVariables/EnvironmentVariablesExtensions.cs diff --git a/src/Shared/ExceptionExtensions.cs b/src/Shared/ExceptionExtensions.cs new file mode 100644 index 00000000000..9070b59c206 --- /dev/null +++ b/src/Shared/ExceptionExtensions.cs @@ -0,0 +1,32 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Globalization; + +namespace OpenTelemetry.Internal; + +internal static class ExceptionExtensions +{ + /// + /// Returns a culture-independent string representation of the given object, + /// appropriate for diagnostics tracing. + /// + /// Exception to convert to string. + /// Exception as string with no culture. + public static string ToInvariantString(this Exception exception) + { + var originalUICulture = Thread.CurrentThread.CurrentUICulture; + + try + { + Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; + return exception.ToString(); + } + finally + { + Thread.CurrentThread.CurrentUICulture = originalUICulture; + } + } +} diff --git a/src/OpenTelemetry.Api/Internal/Guard.cs b/src/Shared/Guard.cs similarity index 93% rename from src/OpenTelemetry.Api/Internal/Guard.cs rename to src/Shared/Guard.cs index efaf5bc1e26..e768d676045 100644 --- a/src/OpenTelemetry.Api/Internal/Guard.cs +++ b/src/Shared/Guard.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #nullable enable @@ -55,6 +42,7 @@ internal sealed class NotNullAttribute : Attribute } #endif +#pragma warning disable IDE0161 // Convert to file-scoped namespace namespace OpenTelemetry.Internal { /// diff --git a/src/OpenTelemetry/Internal/MathHelper.cs b/src/Shared/MathHelper.cs similarity index 84% rename from src/OpenTelemetry/Internal/MathHelper.cs rename to src/Shared/MathHelper.cs index 91670f60daa..6bf5b3ac6c5 100644 --- a/src/OpenTelemetry/Internal/MathHelper.cs +++ b/src/Shared/MathHelper.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; #if NET6_0_OR_GREATER diff --git a/src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogram.LowerBoundary.cs b/src/Shared/Metrics/Base2ExponentialBucketHistogramHelper.cs similarity index 75% rename from src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogram.LowerBoundary.cs rename to src/Shared/Metrics/Base2ExponentialBucketHistogramHelper.cs index f8e7283c453..096eb51828d 100644 --- a/src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogram.LowerBoundary.cs +++ b/src/Shared/Metrics/Base2ExponentialBucketHistogramHelper.cs @@ -1,36 +1,25 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable namespace OpenTelemetry.Metrics; -/// -/// This file contains an implementation for LowerBoundary. -/// LowerBoundary returns the lower boundary of a bucket for -/// a Base2ExponentialBucketHistogram. -/// -/// The LowerBoundary implementation is intentionally placed -/// in its own file so that components like the Console exporter -/// can include it. -/// -internal sealed partial class Base2ExponentialBucketHistogram +/// +/// Contains helper methods for the Base2ExponentialBucketHistogram class. +/// +internal static class Base2ExponentialBucketHistogramHelper { private const double EpsilonTimes2 = double.Epsilon * 2; private static readonly double Ln2 = Math.Log(2); - public static double LowerBoundary(int index, int scale) + /// + /// Calculate the lower boundary for a Base2ExponentialBucketHistogram bucket. + /// + /// Index. + /// Scale. + /// Calculated lower boundary. + public static double CalculateLowerBoundary(int index, int scale) { if (scale > 0) { diff --git a/src/OpenTelemetry/Internal/Options/ConfigurationExtensions.cs b/src/Shared/Options/ConfigurationExtensions.cs similarity index 84% rename from src/OpenTelemetry/Internal/Options/ConfigurationExtensions.cs rename to src/Shared/Options/ConfigurationExtensions.cs index 7a00025a104..82e06158810 100644 --- a/src/OpenTelemetry/Internal/Options/ConfigurationExtensions.cs +++ b/src/Shared/Options/ConfigurationExtensions.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #nullable enable @@ -95,6 +82,26 @@ public static bool TryGetIntValue( return true; } + public static bool TryGetBoolValue( + this IConfiguration configuration, + string key, + out bool value) + { + if (!configuration.TryGetStringValue(key, out var stringValue)) + { + value = default; + return false; + } + + if (!bool.TryParse(stringValue, out value)) + { + LogInvalidEnvironmentVariable?.Invoke(key, stringValue!); + return false; + } + + return true; + } + public static bool TryGetValue( this IConfiguration configuration, string key, @@ -127,7 +134,7 @@ public static IServiceCollection RegisterOptionsFactory( Debug.Assert(services != null, "services was null"); Debug.Assert(optionsFactoryFunc != null, "optionsFactoryFunc was null"); - services.TryAddSingleton>(sp => + services!.TryAddSingleton>(sp => { return new DelegatingOptionsFactory( (c, n) => optionsFactoryFunc!(c), @@ -148,7 +155,7 @@ public static IServiceCollection RegisterOptionsFactory( Debug.Assert(services != null, "services was null"); Debug.Assert(optionsFactoryFunc != null, "optionsFactoryFunc was null"); - services.TryAddSingleton>(sp => + services!.TryAddSingleton>(sp => { return new DelegatingOptionsFactory( (c, n) => optionsFactoryFunc!(sp, c, n), diff --git a/src/OpenTelemetry/Internal/Options/DelegatingOptionsFactory.cs b/src/Shared/Options/DelegatingOptionsFactory.cs similarity index 96% rename from src/OpenTelemetry/Internal/Options/DelegatingOptionsFactory.cs rename to src/Shared/Options/DelegatingOptionsFactory.cs index 59fdd713434..6d987c3dd4d 100644 --- a/src/OpenTelemetry/Internal/Options/DelegatingOptionsFactory.cs +++ b/src/Shared/Options/DelegatingOptionsFactory.cs @@ -40,6 +40,8 @@ internal sealed class DelegatingOptionsFactory : /// /// Initializes a new instance with the specified options configurations. /// + /// Factory delegate used to create instances. + /// . /// The configuration actions to run. /// The initialization actions to run. /// The validations to run. diff --git a/src/Shared/PeerServiceResolver.cs b/src/Shared/PeerServiceResolver.cs new file mode 100644 index 00000000000..d4c0b8fcd13 --- /dev/null +++ b/src/Shared/PeerServiceResolver.cs @@ -0,0 +1,95 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Runtime.CompilerServices; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Exporter; + +internal static class PeerServiceResolver +{ + private static readonly Dictionary PeerServiceKeyResolutionDictionary = new(StringComparer.OrdinalIgnoreCase) + { + [SemanticConventions.AttributePeerService] = 0, // priority 0 (highest). + ["peer.hostname"] = 1, + ["peer.address"] = 1, + [SemanticConventions.AttributeHttpHost] = 2, // peer.service for Http. + [SemanticConventions.AttributeDbInstance] = 2, // peer.service for Redis. + }; + + public interface IPeerServiceState + { + string? PeerService { get; set; } + + int? PeerServicePriority { get; set; } + + string? HostName { get; set; } + + string? IpAddress { get; set; } + + long Port { get; set; } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void InspectTag(ref T state, string key, string? value) + where T : struct, IPeerServiceState + { + if (PeerServiceKeyResolutionDictionary.TryGetValue(key, out int priority) + && (state.PeerService == null || priority < state.PeerServicePriority)) + { + state.PeerService = value; + state.PeerServicePriority = priority; + } + else if (key == SemanticConventions.AttributeNetPeerName) + { + state.HostName = value; + } + else if (key == SemanticConventions.AttributeNetPeerIp) + { + state.IpAddress = value; + } + else if (key == SemanticConventions.AttributeNetPeerPort && long.TryParse(value, out var port)) + { + state.Port = port; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void InspectTag(ref T state, string key, long value) + where T : struct, IPeerServiceState + { + if (key == SemanticConventions.AttributeNetPeerPort) + { + state.Port = value; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Resolve(ref T state, out string? peerServiceName, out bool addAsTag) + where T : struct, IPeerServiceState + { + peerServiceName = state.PeerService; + + // If priority = 0 that means peer.service was included in tags + addAsTag = state.PeerServicePriority != 0; + + if (addAsTag) + { + var hostNameOrIpAddress = state.HostName ?? state.IpAddress; + + // peer.service has not already been included, but net.peer.name/ip and optionally net.peer.port are present + if (hostNameOrIpAddress != null) + { + peerServiceName = state.Port == default + ? hostNameOrIpAddress + : $"{hostNameOrIpAddress}:{state.Port}"; + } + else if (state.PeerService != null) + { + peerServiceName = state.PeerService; + } + } + } +} diff --git a/src/OpenTelemetry/Internal/PeriodicExportingMetricReaderHelper.cs b/src/Shared/PeriodicExportingMetricReaderHelper.cs similarity index 63% rename from src/OpenTelemetry/Internal/PeriodicExportingMetricReaderHelper.cs rename to src/Shared/PeriodicExportingMetricReaderHelper.cs index 6fcc823c748..d24c74c8f9c 100644 --- a/src/OpenTelemetry/Internal/PeriodicExportingMetricReaderHelper.cs +++ b/src/Shared/PeriodicExportingMetricReaderHelper.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #nullable enable diff --git a/src/Shared/PooledList.cs b/src/Shared/PooledList.cs new file mode 100644 index 00000000000..043cd0a4525 --- /dev/null +++ b/src/Shared/PooledList.cs @@ -0,0 +1,137 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Buffers; +using System.Collections; + +namespace OpenTelemetry.Internal; + +internal readonly struct PooledList : IEnumerable, ICollection +{ + private static int lastAllocatedSize = 64; + + private readonly T[] buffer; + + private PooledList(T[] buffer, int count) + { + this.buffer = buffer; + this.Count = count; + } + + public int Count { get; } + + public bool IsEmpty => this.Count == 0; + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot => this; + + public ref T this[int index] + { + get => ref this.buffer[index]; + } + + public static PooledList Create() + { + return new PooledList(ArrayPool.Shared.Rent(lastAllocatedSize), 0); + } + + public static void Add(ref PooledList list, T item) + { + Guard.ThrowIfNull(list.buffer); + + var buffer = list.buffer; + + if (list.Count >= buffer.Length) + { + lastAllocatedSize = buffer.Length * 2; + var previousBuffer = buffer; + + buffer = ArrayPool.Shared.Rent(lastAllocatedSize); + + var span = previousBuffer.AsSpan(); + span.CopyTo(buffer); + ArrayPool.Shared.Return(previousBuffer); + } + + buffer[list.Count] = item; + list = new PooledList(buffer, list.Count + 1); + } + + public static void Clear(ref PooledList list) + { + list = new PooledList(list.buffer, 0); + } + + public void Return() + { + var buffer = this.buffer; + if (buffer != null) + { + ArrayPool.Shared.Return(buffer); + } + } + + void ICollection.CopyTo(Array array, int index) + { + Array.Copy(this.buffer, 0, array, index, this.Count); + } + + public Enumerator GetEnumerator() + { + return new Enumerator(in this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return new Enumerator(in this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return new Enumerator(in this); + } + + public struct Enumerator : IEnumerator, IEnumerator + { + private readonly T[] buffer; + private readonly int count; + private int index; + private T current; + + public Enumerator(in PooledList list) + { + this.buffer = list.buffer; + this.count = list.Count; + this.index = 0; + this.current = default; + } + + public T Current { get => this.current; } + + object IEnumerator.Current { get => this.Current; } + + public void Dispose() + { + } + + public bool MoveNext() + { + if (this.index < this.count) + { + this.current = this.buffer[this.index++]; + return true; + } + + this.index = this.count + 1; + this.current = default; + return false; + } + + void IEnumerator.Reset() + { + this.index = 0; + this.current = default; + } + } +} diff --git a/src/Shared/RequestMethodHelper.cs b/src/Shared/RequestMethodHelper.cs new file mode 100644 index 00000000000..05a3f022511 --- /dev/null +++ b/src/Shared/RequestMethodHelper.cs @@ -0,0 +1,72 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#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 + internal static readonly Dictionary KnownMethods; +#endif + + static RequestMethodHelper() + { + var knownMethodSet = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "GET", "GET" }, + { "PUT", "PUT" }, + { "POST", "POST" }, + { "DELETE", "DELETE" }, + { "HEAD", "HEAD" }, + { "OPTIONS", "OPTIONS" }, + { "TRACE", "TRACE" }, + { "PATCH", "PATCH" }, + { "CONNECT", "CONNECT" }, + }; + + // KnownMethods ignores case. Use the value returned by the dictionary to have a consistent case. +#if NET8_0_OR_GREATER + KnownMethods = FrozenDictionary.ToFrozenDictionary(knownMethodSet, StringComparer.OrdinalIgnoreCase); +#else + 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); + } + } + + public static void SetHttpClientActivityDisplayName(Activity activity, string method) + { + // https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md#name + activity.DisplayName = KnownMethods.TryGetValue(method, out var httpMethod) ? httpMethod : "HTTP"; + } +} diff --git a/src/Shared/ResourceSemanticConventions.cs b/src/Shared/ResourceSemanticConventions.cs new file mode 100644 index 00000000000..e38218874c6 --- /dev/null +++ b/src/Shared/ResourceSemanticConventions.cs @@ -0,0 +1,53 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +namespace OpenTelemetry.Resources; + +internal static class ResourceSemanticConventions +{ + public const string AttributeServiceName = "service.name"; + public const string AttributeServiceNamespace = "service.namespace"; + public const string AttributeServiceInstance = "service.instance.id"; + public const string AttributeServiceVersion = "service.version"; + + public const string AttributeTelemetrySdkName = "telemetry.sdk.name"; + public const string AttributeTelemetrySdkLanguage = "telemetry.sdk.language"; + public const string AttributeTelemetrySdkVersion = "telemetry.sdk.version"; + + public const string AttributeContainerName = "container.name"; + public const string AttributeContainerImage = "container.image.name"; + public const string AttributeContainerTag = "container.image.tag"; + + public const string AttributeFaasName = "faas.name"; + public const string AttributeFaasId = "faas.id"; + public const string AttributeFaasVersion = "faas.version"; + public const string AttributeFaasInstance = "faas.instance"; + + public const string AttributeK8sCluster = "k8s.cluster.name"; + public const string AttributeK8sNamespace = "k8s.namespace.name"; + public const string AttributeK8sPod = "k8s.pod.name"; + public const string AttributeK8sDeployment = "k8s.deployment.name"; + + public const string AttributeHostHostname = "host.hostname"; + public const string AttributeHostId = "host.id"; + public const string AttributeHostName = "host.name"; + public const string AttributeHostType = "host.type"; + public const string AttributeHostImageName = "host.image.name"; + public const string AttributeHostImageId = "host.image.id"; + public const string AttributeHostImageVersion = "host.image.version"; + + public const string AttributeProcessId = "process.id"; + public const string AttributeProcessExecutableName = "process.executable.name"; + public const string AttributeProcessExecutablePath = "process.executable.path"; + public const string AttributeProcessCommand = "process.command"; + public const string AttributeProcessCommandLine = "process.command_line"; + public const string AttributeProcessUsername = "process.username"; + + public const string AttributeCloudProvider = "cloud.provider"; + public const string AttributeCloudAccount = "cloud.account.id"; + public const string AttributeCloudRegion = "cloud.region"; + public const string AttributeCloudZone = "cloud.zone"; + public const string AttributeComponent = "component"; +} diff --git a/src/Shared/SemanticConventions.cs b/src/Shared/SemanticConventions.cs new file mode 100644 index 00000000000..8628b79d237 --- /dev/null +++ b/src/Shared/SemanticConventions.cs @@ -0,0 +1,121 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +namespace OpenTelemetry.Trace; + +/// +/// Constants for semantic attribute names outlined by the OpenTelemetry specifications. +/// and +/// . +/// +internal static class SemanticConventions +{ + // The set of constants matches the specification as of this commit. + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/general/trace.md + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/exceptions/exceptions-spans.md + public const string AttributeNetTransport = "net.transport"; + public const string AttributeNetPeerIp = "net.peer.ip"; + public const string AttributeNetPeerPort = "net.peer.port"; + public const string AttributeNetPeerName = "net.peer.name"; + public const string AttributeNetHostIp = "net.host.ip"; + public const string AttributeNetHostPort = "net.host.port"; + public const string AttributeNetHostName = "net.host.name"; + + public const string AttributeEnduserId = "enduser.id"; + public const string AttributeEnduserRole = "enduser.role"; + public const string AttributeEnduserScope = "enduser.scope"; + + public const string AttributePeerService = "peer.service"; + + public const string AttributeHttpMethod = "http.method"; + public const string AttributeHttpUrl = "http.url"; + public const string AttributeHttpTarget = "http.target"; + public const string AttributeHttpHost = "http.host"; + public const string AttributeHttpScheme = "http.scheme"; + public const string AttributeHttpStatusCode = "http.status_code"; + public const string AttributeHttpStatusText = "http.status_text"; + public const string AttributeHttpFlavor = "http.flavor"; + public const string AttributeHttpServerName = "http.server_name"; + public const string AttributeHttpRoute = "http.route"; + public const string AttributeHttpClientIP = "http.client_ip"; + public const string AttributeHttpUserAgent = "http.user_agent"; + public const string AttributeHttpRequestContentLength = "http.request_content_length"; + public const string AttributeHttpRequestContentLengthUncompressed = "http.request_content_length_uncompressed"; + public const string AttributeHttpResponseContentLength = "http.response_content_length"; + public const string AttributeHttpResponseContentLengthUncompressed = "http.response_content_length_uncompressed"; + + public const string AttributeDbSystem = "db.system"; + public const string AttributeDbConnectionString = "db.connection_string"; + public const string AttributeDbUser = "db.user"; + public const string AttributeDbMsSqlInstanceName = "db.mssql.instance_name"; + public const string AttributeDbJdbcDriverClassName = "db.jdbc.driver_classname"; + public const string AttributeDbName = "db.name"; + public const string AttributeDbStatement = "db.statement"; + public const string AttributeDbOperation = "db.operation"; + public const string AttributeDbInstance = "db.instance"; + public const string AttributeDbUrl = "db.url"; + public const string AttributeDbCassandraKeyspace = "db.cassandra.keyspace"; + public const string AttributeDbHBaseNamespace = "db.hbase.namespace"; + public const string AttributeDbRedisDatabaseIndex = "db.redis.database_index"; + public const string AttributeDbMongoDbCollection = "db.mongodb.collection"; + + public const string AttributeRpcSystem = "rpc.system"; + public const string AttributeRpcService = "rpc.service"; + public const string AttributeRpcMethod = "rpc.method"; + public const string AttributeRpcGrpcStatusCode = "rpc.grpc.status_code"; + + public const string AttributeMessageType = "message.type"; + public const string AttributeMessageId = "message.id"; + public const string AttributeMessageCompressedSize = "message.compressed_size"; + public const string AttributeMessageUncompressedSize = "message.uncompressed_size"; + + public const string AttributeFaasTrigger = "faas.trigger"; + public const string AttributeFaasExecution = "faas.execution"; + public const string AttributeFaasDocumentCollection = "faas.document.collection"; + public const string AttributeFaasDocumentOperation = "faas.document.operation"; + public const string AttributeFaasDocumentTime = "faas.document.time"; + public const string AttributeFaasDocumentName = "faas.document.name"; + public const string AttributeFaasTime = "faas.time"; + public const string AttributeFaasCron = "faas.cron"; + + public const string AttributeMessagingSystem = "messaging.system"; + public const string AttributeMessagingDestination = "messaging.destination"; + public const string AttributeMessagingDestinationKind = "messaging.destination_kind"; + public const string AttributeMessagingTempDestination = "messaging.temp_destination"; + public const string AttributeMessagingProtocol = "messaging.protocol"; + public const string AttributeMessagingProtocolVersion = "messaging.protocol_version"; + public const string AttributeMessagingUrl = "messaging.url"; + public const string AttributeMessagingMessageId = "messaging.message_id"; + public const string AttributeMessagingConversationId = "messaging.conversation_id"; + public const string AttributeMessagingPayloadSize = "messaging.message_payload_size_bytes"; + public const string AttributeMessagingPayloadCompressedSize = "messaging.message_payload_compressed_size_bytes"; + public const string AttributeMessagingOperation = "messaging.operation"; + + public const string AttributeExceptionEventName = "exception"; + public const string AttributeExceptionType = "exception.type"; + public const string AttributeExceptionMessage = "exception.message"; + public const string AttributeExceptionStacktrace = "exception.stacktrace"; + public const string AttributeErrorType = "error.type"; + + // v1.21.0 + // https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md + // https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/database/database-spans.md + // https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/rpc/rpc-spans.md + public const string AttributeClientAddress = "client.address"; + public const string AttributeClientPort = "client.port"; + public const string AttributeHttpRequestMethod = "http.request.method"; // replaces: "http.method" (AttributeHttpMethod) + public const string AttributeHttpResponseStatusCode = "http.response.status_code"; // replaces: "http.status_code" (AttributeHttpStatusCode) + public const string AttributeNetworkProtocolVersion = "network.protocol.version"; // replaces: "http.flavor" (AttributeHttpFlavor) + public const string AttributeNetworkProtocolName = "network.protocol.name"; + public const string AttributeServerAddress = "server.address"; // replaces: "net.host.name" (AttributeNetHostName) and "net.peer.name" (AttributeNetPeerName) + public const string AttributeServerPort = "server.port"; // replaces: "net.host.port" (AttributeNetHostPort) and "net.peer.port" (AttributeNetPeerPort) + public const string AttributeServerSocketAddress = "server.socket.address"; // replaces: "net.peer.ip" (AttributeNetPeerIp) + public const string AttributeUrlFull = "url.full"; // replaces: "http.url" (AttributeHttpUrl) + public const string AttributeUrlPath = "url.path"; // replaces: "http.target" (AttributeHttpTarget) + public const string AttributeUrlScheme = "url.scheme"; // replaces: "http.scheme" (AttributeHttpScheme) + public const string AttributeUrlQuery = "url.query"; + public const string AttributeUserAgentOriginal = "user_agent.original"; // replaces: "http.user_agent" (AttributeHttpUserAgent) + public const string AttributeHttpRequestMethodOriginal = "http.request.method_original"; +} diff --git a/src/Shared/Shims/IsExternalInit.cs b/src/Shared/Shims/IsExternalInit.cs new file mode 100644 index 00000000000..ec91c2662af --- /dev/null +++ b/src/Shared/Shims/IsExternalInit.cs @@ -0,0 +1,11 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NETFRAMEWORK || NETSTANDARD2_0_OR_GREATER +namespace System.Runtime.CompilerServices; + +// This enabled "init" keyword in net462 + netstandard2.0 targets. +internal sealed class IsExternalInit +{ +} +#endif diff --git a/src/OpenTelemetry/Internal/Shims/NullableAttributes.cs b/src/Shared/Shims/NullableAttributes.cs similarity index 73% rename from src/OpenTelemetry/Internal/Shims/NullableAttributes.cs rename to src/Shared/Shims/NullableAttributes.cs index 78bcfeab9ae..c471bbe6134 100644 --- a/src/OpenTelemetry/Internal/Shims/NullableAttributes.cs +++ b/src/Shared/Shims/NullableAttributes.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 // Source: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/NullableAttributes.cs diff --git a/src/Shared/SpanAttributeConstants.cs b/src/Shared/SpanAttributeConstants.cs new file mode 100644 index 00000000000..dcd7e5b8ef4 --- /dev/null +++ b/src/Shared/SpanAttributeConstants.cs @@ -0,0 +1,15 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +namespace OpenTelemetry.Trace; + +/// +/// Defines well-known span attribute keys. +/// +internal static class SpanAttributeConstants +{ + public const string StatusCodeKey = "otel.status_code"; + public const string StatusDescriptionKey = "otel.status_description"; +} diff --git a/src/Shared/SpanHelper.cs b/src/Shared/SpanHelper.cs new file mode 100644 index 00000000000..8f5aabc4a31 --- /dev/null +++ b/src/Shared/SpanHelper.cs @@ -0,0 +1,31 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; + +namespace OpenTelemetry.Trace; + +/// +/// A collection of helper methods to be used when building spans. +/// +internal static class SpanHelper +{ + /// + /// Helper method that populates span properties from http status code according + /// to https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#common-attributes. + /// + /// The span kind. + /// Http status code. + /// Resolved span for the Http status code. + public static ActivityStatusCode ResolveSpanStatusForHttpStatusCode(ActivityKind kind, int httpStatusCode) + { + var lowerBound = kind == ActivityKind.Client ? 400 : 500; + var upperBound = 599; + if (httpStatusCode >= lowerBound && httpStatusCode <= upperBound) + { + return ActivityStatusCode.Error; + } + + return ActivityStatusCode.Unset; + } +} diff --git a/src/Shared/StatusHelper.cs b/src/Shared/StatusHelper.cs new file mode 100644 index 00000000000..5d9245010c9 --- /dev/null +++ b/src/Shared/StatusHelper.cs @@ -0,0 +1,60 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Runtime.CompilerServices; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Internal; + +internal static class StatusHelper +{ + public const string UnsetStatusCodeTagValue = "UNSET"; + public const string OkStatusCodeTagValue = "OK"; + public const string ErrorStatusCodeTagValue = "ERROR"; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string? GetTagValueForStatusCode(StatusCode statusCode) + { + return statusCode switch + { + /* + * Note: Order here does matter for perf. Unset is + * first because assumption is most spans will be + * Unset, then Error. Ok is not set by the SDK. + */ + StatusCode.Unset => UnsetStatusCodeTagValue, + StatusCode.Error => ErrorStatusCodeTagValue, + StatusCode.Ok => OkStatusCodeTagValue, + _ => null, + }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static StatusCode? GetStatusCodeForTagValue(string? statusCodeTagValue) + { + return statusCodeTagValue switch + { + /* + * Note: Order here does matter for perf. Unset is + * first because assumption is most spans will be + * Unset, then Error. Ok is not set by the SDK. + */ + not null when UnsetStatusCodeTagValue.Equals(statusCodeTagValue, StringComparison.OrdinalIgnoreCase) => StatusCode.Unset, + not null when ErrorStatusCodeTagValue.Equals(statusCodeTagValue, StringComparison.OrdinalIgnoreCase) => StatusCode.Error, + not null when OkStatusCodeTagValue.Equals(statusCodeTagValue, StringComparison.OrdinalIgnoreCase) => StatusCode.Ok, + _ => null, + }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryGetStatusCodeForTagValue(string? statusCodeTagValue, out StatusCode statusCode) + { + StatusCode? tempStatusCode = GetStatusCodeForTagValue(statusCodeTagValue); + + statusCode = tempStatusCode ?? default; + + return tempStatusCode.HasValue; + } +} diff --git a/src/OpenTelemetry/Internal/TagAndValueTransformer.cs b/src/Shared/TagAndValueTransformer.cs similarity index 68% rename from src/OpenTelemetry/Internal/TagAndValueTransformer.cs rename to src/Shared/TagAndValueTransformer.cs index fe1d1181215..32ac24da674 100644 --- a/src/OpenTelemetry/Internal/TagAndValueTransformer.cs +++ b/src/Shared/TagAndValueTransformer.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; diff --git a/src/OpenTelemetry/Internal/TagTransformer.cs b/src/Shared/TagTransformer.cs similarity index 88% rename from src/OpenTelemetry/Internal/TagTransformer.cs rename to src/Shared/TagTransformer.cs index fdf7acb88df..0c778691599 100644 --- a/src/OpenTelemetry/Internal/TagTransformer.cs +++ b/src/Shared/TagTransformer.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 namespace OpenTelemetry.Internal; diff --git a/src/Shared/TagTransformerJsonHelper.cs b/src/Shared/TagTransformerJsonHelper.cs new file mode 100644 index 00000000000..9934d33422e --- /dev/null +++ b/src/Shared/TagTransformerJsonHelper.cs @@ -0,0 +1,53 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Text.Json; +#if NET6_0_OR_GREATER +using System.Text.Json.Serialization; +#endif + +namespace OpenTelemetry.Internal; + +/// +/// This class has to be partial so that JSON source generator can provide code for the JsonSerializerContext. +/// +internal static partial class TagTransformerJsonHelper +{ +#if NET6_0_OR_GREATER + // In net6.0 or higher ships System.Text.Json "in box" as part of the base class libraries; + // meaning the consumer automatically got upgraded to use v6.0 System.Text.Json + // which has support for using source generators for JSON serialization. + // The source generator makes the serialization faster and also AOT compatible. + + internal static string JsonSerializeArrayTag(Array array) + { + return JsonSerializer.Serialize(array, typeof(Array), ArrayTagJsonContext.Default); + } + + [JsonSerializable(typeof(Array))] + [JsonSerializable(typeof(char))] + [JsonSerializable(typeof(string))] + [JsonSerializable(typeof(bool))] + [JsonSerializable(typeof(byte))] + [JsonSerializable(typeof(sbyte))] + [JsonSerializable(typeof(short))] + [JsonSerializable(typeof(ushort))] + [JsonSerializable(typeof(int))] + [JsonSerializable(typeof(uint))] + [JsonSerializable(typeof(long))] + [JsonSerializable(typeof(ulong))] + [JsonSerializable(typeof(float))] + [JsonSerializable(typeof(double))] + private sealed partial class ArrayTagJsonContext : JsonSerializerContext + { + } + +#else + internal static string JsonSerializeArrayTag(Array array) + { + return JsonSerializer.Serialize(array); + } +#endif +} diff --git a/test/Benchmarks/Benchmarks.csproj b/test/Benchmarks/Benchmarks.csproj index da72459fe63..a8b4e74879a 100644 --- a/test/Benchmarks/Benchmarks.csproj +++ b/test/Benchmarks/Benchmarks.csproj @@ -2,8 +2,7 @@ Exe - - net7.0;net6.0;net462 + $(TargetFrameworksForTests) disable @@ -11,34 +10,26 @@ - + - - - + + - - - - - - - - + diff --git a/test/Benchmarks/EventSourceBenchmarks.cs b/test/Benchmarks/EventSourceBenchmarks.cs index b30cc90e094..f8304368632 100644 --- a/test/Benchmarks/EventSourceBenchmarks.cs +++ b/test/Benchmarks/EventSourceBenchmarks.cs @@ -1,47 +1,33 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using BenchmarkDotNet.Attributes; using OpenTelemetry.Internal; -namespace OpenTelemetry.Benchmarks +namespace OpenTelemetry.Benchmarks; + +public class EventSourceBenchmarks { - public class EventSourceBenchmarks + [Benchmark] + public void EventWithIdAllocation() { - [Benchmark] - public void EventWithIdAllocation() - { - using var activity = new Activity("TestActivity"); - activity.SetIdFormat(ActivityIdFormat.W3C); - activity.Start(); - activity.Stop(); + using var activity = new Activity("TestActivity"); + activity.SetIdFormat(ActivityIdFormat.W3C); + activity.Start(); + activity.Stop(); - OpenTelemetrySdkEventSource.Log.ActivityStarted(activity.OperationName, activity.Id); - } + OpenTelemetrySdkEventSource.Log.ActivityStarted(activity.OperationName, activity.Id); + } - [Benchmark] - public void EventWithCheck() - { - using var activity = new Activity("TestActivity"); - activity.SetIdFormat(ActivityIdFormat.W3C); - activity.Start(); - activity.Stop(); + [Benchmark] + public void EventWithCheck() + { + using var activity = new Activity("TestActivity"); + activity.SetIdFormat(ActivityIdFormat.W3C); + activity.Start(); + activity.Stop(); - OpenTelemetrySdkEventSource.Log.ActivityStarted(activity); - } + OpenTelemetrySdkEventSource.Log.ActivityStarted(activity); } } diff --git a/test/Benchmarks/Exporter/JaegerExporterBenchmarks.cs b/test/Benchmarks/Exporter/JaegerExporterBenchmarks.cs deleted file mode 100644 index b4a9d6d7855..00000000000 --- a/test/Benchmarks/Exporter/JaegerExporterBenchmarks.cs +++ /dev/null @@ -1,91 +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. -// - -extern alias Jaeger; - -using System.Diagnostics; -using BenchmarkDotNet.Attributes; -using Benchmarks.Helper; -using Jaeger::OpenTelemetry.Exporter; -using Jaeger::OpenTelemetry.Exporter.Jaeger.Implementation; -using Jaeger::Thrift.Protocol; -using OpenTelemetry; -using OpenTelemetry.Internal; - -namespace Benchmarks.Exporter -{ - public class JaegerExporterBenchmarks - { - private Activity activity; - private CircularBuffer activityBatch; - - [Params(1, 10, 100)] - public int NumberOfBatches { get; set; } - - [Params(10000)] - public int NumberOfSpans { get; set; } - - [GlobalSetup] - public void GlobalSetup() - { - this.activity = ActivityHelper.CreateTestActivity(); - this.activityBatch = new CircularBuffer(this.NumberOfSpans); - } - - [Benchmark] - public void JaegerExporter_Batching() - { - using JaegerExporter exporter = new JaegerExporter( - new JaegerExporterOptions(), - new TCompactProtocol.Factory(), - new NoopJaegerClient()); - - for (int i = 0; i < this.NumberOfBatches; i++) - { - for (int c = 0; c < this.NumberOfSpans; c++) - { - this.activityBatch.Add(this.activity); - } - - exporter.Export(new Batch(this.activityBatch, this.NumberOfSpans)); - } - - exporter.Shutdown(); - } - - private sealed class NoopJaegerClient : IJaegerClient - { - public bool Connected => true; - - public void Close() - { - } - - public void Connect() - { - } - - public void Dispose() - { - } - - public int Send(byte[] buffer, int offset, int count) - { - return count; - } - } - } -} diff --git a/test/Benchmarks/Exporter/OtlpGrpcExporterBenchmarks.cs b/test/Benchmarks/Exporter/OtlpGrpcExporterBenchmarks.cs index 13ca0a7d4e3..f80d59d2a14 100644 --- a/test/Benchmarks/Exporter/OtlpGrpcExporterBenchmarks.cs +++ b/test/Benchmarks/Exporter/OtlpGrpcExporterBenchmarks.cs @@ -1,88 +1,64 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 extern alias OpenTelemetryProtocol; using System.Diagnostics; using BenchmarkDotNet.Attributes; using Benchmarks.Helper; -using Grpc.Core; -using Moq; using OpenTelemetry; using OpenTelemetry.Internal; using OpenTelemetryProtocol::OpenTelemetry.Exporter; using OpenTelemetryProtocol::OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; using OpenTelemetryProtocol::OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; -using OtlpCollector = OpenTelemetryProtocol::OpenTelemetry.Proto.Collector.Trace.V1; +using OpenTelemetryProtocol::OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; +using OpenTelemetryProtocol::OpenTelemetry.Proto.Collector.Trace.V1; -namespace Benchmarks.Exporter -{ - public class OtlpGrpcExporterBenchmarks - { - private OtlpTraceExporter exporter; - private Activity activity; - private CircularBuffer activityBatch; +namespace Benchmarks.Exporter; - [Params(1, 10, 100)] - public int NumberOfBatches { get; set; } +public class OtlpGrpcExporterBenchmarks +{ + private OtlpTraceExporter exporter; + private Activity activity; + private CircularBuffer activityBatch; - [Params(10000)] - public int NumberOfSpans { get; set; } + [Params(1, 10, 100)] + public int NumberOfBatches { get; set; } - [GlobalSetup] - public void GlobalSetup() - { - var mockClient = new Mock(); - mockClient - .Setup(m => m.Export( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(new OtlpCollector.ExportTraceServiceResponse()); + [Params(10000)] + public int NumberOfSpans { get; set; } - var options = new OtlpExporterOptions(); - this.exporter = new OtlpTraceExporter( - options, - new SdkLimitOptions(), - new OtlpGrpcTraceExportClient(options, mockClient.Object)); + [GlobalSetup] + public void GlobalSetup() + { + var options = new OtlpExporterOptions(); + this.exporter = new OtlpTraceExporter( + options, + new SdkLimitOptions(), + new OtlpExporterTransmissionHandler(new OtlpGrpcTraceExportClient(options, new TestTraceServiceClient()))); - this.activity = ActivityHelper.CreateTestActivity(); - this.activityBatch = new CircularBuffer(this.NumberOfSpans); - } + this.activity = ActivityHelper.CreateTestActivity(); + this.activityBatch = new CircularBuffer(this.NumberOfSpans); + } - [GlobalCleanup] - public void GlobalCleanup() - { - this.exporter.Shutdown(); - this.exporter.Dispose(); - } + [GlobalCleanup] + public void GlobalCleanup() + { + this.exporter.Shutdown(); + this.exporter.Dispose(); + } - [Benchmark] - public void OtlpExporter_Batching() + [Benchmark] + public void OtlpExporter_Batching() + { + for (int i = 0; i < this.NumberOfBatches; i++) { - for (int i = 0; i < this.NumberOfBatches; i++) + for (int c = 0; c < this.NumberOfSpans; c++) { - for (int c = 0; c < this.NumberOfSpans; c++) - { - this.activityBatch.Add(this.activity); - } - - this.exporter.Export(new Batch(this.activityBatch, this.NumberOfSpans)); + this.activityBatch.Add(this.activity); } + + this.exporter.Export(new Batch(this.activityBatch, this.NumberOfSpans)); } } } diff --git a/test/Benchmarks/Exporter/OtlpHttpExporterBenchmarks.cs b/test/Benchmarks/Exporter/OtlpHttpExporterBenchmarks.cs index a216c3000b3..86e79812be0 100644 --- a/test/Benchmarks/Exporter/OtlpHttpExporterBenchmarks.cs +++ b/test/Benchmarks/Exporter/OtlpHttpExporterBenchmarks.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 extern alias OpenTelemetryProtocol; @@ -25,81 +12,82 @@ using OpenTelemetryProtocol::OpenTelemetry.Exporter; using OpenTelemetryProtocol::OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; using OpenTelemetryProtocol::OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; +using OpenTelemetryProtocol::OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; +using OpenTelemetryProtocol::OpenTelemetry.Proto.Collector.Trace.V1; -namespace Benchmarks.Exporter +namespace Benchmarks.Exporter; + +public class OtlpHttpExporterBenchmarks { - public class OtlpHttpExporterBenchmarks - { - private readonly byte[] buffer = new byte[1024 * 1024]; - private IDisposable server; - private string serverHost; - private int serverPort; - private OtlpTraceExporter exporter; - private Activity activity; - private CircularBuffer activityBatch; + private readonly byte[] buffer = new byte[1024 * 1024]; + private IDisposable server; + private string serverHost; + private int serverPort; + private OtlpTraceExporter exporter; + private Activity activity; + private CircularBuffer activityBatch; - [Params(1, 10, 100)] - public int NumberOfBatches { get; set; } + [Params(1, 10, 100)] + public int NumberOfBatches { get; set; } - [Params(10000)] - public int NumberOfSpans { get; set; } + [Params(10000)] + public int NumberOfSpans { get; set; } - [GlobalSetup] - public void GlobalSetup() - { - this.server = TestHttpServer.RunServer( - (ctx) => + [GlobalSetup] + public void GlobalSetup() + { + this.server = TestHttpServer.RunServer( + (ctx) => + { + using (Stream receiveStream = ctx.Request.InputStream) { - using (Stream receiveStream = ctx.Request.InputStream) + while (true) { - while (true) + if (receiveStream.Read(this.buffer, 0, this.buffer.Length) == 0) { - if (receiveStream.Read(this.buffer, 0, this.buffer.Length) == 0) - { - break; - } + break; } } + } - ctx.Response.StatusCode = 200; - ctx.Response.OutputStream.Close(); - }, - out this.serverHost, - out this.serverPort); + ctx.Response.StatusCode = 200; + ctx.Response.OutputStream.Close(); + }, + out this.serverHost, + out this.serverPort); - var options = new OtlpExporterOptions - { - Endpoint = new Uri($"http://{this.serverHost}:{this.serverPort}"), - }; - this.exporter = new OtlpTraceExporter( - options, - new SdkLimitOptions(), - new OtlpHttpTraceExportClient(options, options.HttpClientFactory())); + var options = new OtlpExporterOptions + { + Endpoint = new Uri($"http://{this.serverHost}:{this.serverPort}"), + }; + this.exporter = new OtlpTraceExporter( + options, + new SdkLimitOptions(), + new OtlpExporterTransmissionHandler(new OtlpHttpTraceExportClient(options, options.HttpClientFactory()))); - this.activity = ActivityHelper.CreateTestActivity(); - this.activityBatch = new CircularBuffer(this.NumberOfSpans); - } + this.activity = ActivityHelper.CreateTestActivity(); + this.activityBatch = new CircularBuffer(this.NumberOfSpans); + } - [GlobalCleanup] - public void GlobalCleanup() - { - this.exporter.Shutdown(); - this.exporter.Dispose(); - this.server.Dispose(); - } + [GlobalCleanup] + public void GlobalCleanup() + { + this.exporter.Shutdown(); + this.exporter.Dispose(); + this.server.Dispose(); + } - [Benchmark] - public void OtlpExporter_Batching() + [Benchmark] + public void OtlpExporter_Batching() + { + for (int i = 0; i < this.NumberOfBatches; i++) { - for (int i = 0; i < this.NumberOfBatches; i++) + for (int c = 0; c < this.NumberOfSpans; c++) { - for (int c = 0; c < this.NumberOfSpans; c++) - { - this.activityBatch.Add(this.activity); - } - - this.exporter.Export(new Batch(this.activityBatch, this.NumberOfSpans)); + this.activityBatch.Add(this.activity); } + + this.exporter.Export(new Batch(this.activityBatch, this.NumberOfSpans)); } } } diff --git a/test/Benchmarks/Exporter/OtlpLogExporterBenchmarks.cs b/test/Benchmarks/Exporter/OtlpLogExporterBenchmarks.cs new file mode 100644 index 00000000000..17bcf3dda49 --- /dev/null +++ b/test/Benchmarks/Exporter/OtlpLogExporterBenchmarks.cs @@ -0,0 +1,141 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if !NETFRAMEWORK +extern alias OpenTelemetryProtocol; + +using BenchmarkDotNet.Attributes; +using Benchmarks.Helper; +using Grpc.Core; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using OpenTelemetry; +using OpenTelemetry.Internal; +using OpenTelemetry.Logs; +using OpenTelemetry.Tests; +using OpenTelemetryProtocol::OpenTelemetry.Exporter; +using OtlpCollector = OpenTelemetryProtocol::OpenTelemetry.Proto.Collector.Logs.V1; + +/* +BenchmarkDotNet v0.13.6, Windows 11 (10.0.22621.2134/22H2/2022Update/SunValley2) (Hyper-V) +AMD EPYC 7763, 1 CPU, 16 logical and 8 physical cores +.NET SDK 7.0.400 + [Host] : .NET 7.0.10 (7.0.1023.36312), X64 RyuJIT AVX2 + DefaultJob : .NET 7.0.10 (7.0.1023.36312), X64 RyuJIT AVX2 + + +| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +|--------------------- |---------:|--------:|--------:|-------:|-------:|----------:| +| OtlpLogExporter_Http | 138.7 us | 2.08 us | 1.95 us | 0.4883 | 0.2441 | 9.85 KB | +| OtlpLogExporter_Grpc | 268.3 us | 2.57 us | 2.28 us | 0.4883 | - | 9.54 KB | +*/ + +namespace Benchmarks.Exporter; + +public class OtlpLogExporterBenchmarks +{ + private OtlpLogExporter exporter; + private LogRecord logRecord; + private CircularBuffer logRecordBatch; + + private IHost host; + private IDisposable server; + private string serverHost; + private int serverPort; + + [GlobalSetup(Target = nameof(OtlpLogExporter_Grpc))] + public void GlobalSetupGrpc() + { + this.host = new HostBuilder() + .ConfigureWebHostDefaults(webBuilder => webBuilder + .ConfigureKestrel(options => + { + options.ListenLocalhost(4317, listenOptions => listenOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http2); + }) + .ConfigureServices(services => + { + services.AddGrpc(); + }) + .Configure(app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapGrpcService(); + }); + })) + .Start(); + + var options = new OtlpExporterOptions(); + this.exporter = new OtlpLogExporter(options); + + this.logRecord = LogRecordHelper.CreateTestLogRecord(); + this.logRecordBatch = new CircularBuffer(1); + this.logRecordBatch.Add(this.logRecord); + } + + [GlobalSetup(Target = nameof(OtlpLogExporter_Http))] + public void GlobalSetupHttp() + { + this.server = TestHttpServer.RunServer( + (ctx) => + { + ctx.Response.StatusCode = 200; + ctx.Response.OutputStream.Close(); + }, + out this.serverHost, + out this.serverPort); + + var options = new OtlpExporterOptions + { + Endpoint = new Uri($"http://{this.serverHost}:{this.serverPort}"), + Protocol = OtlpExportProtocol.HttpProtobuf, + }; + this.exporter = new OtlpLogExporter(options); + + this.logRecord = LogRecordHelper.CreateTestLogRecord(); + this.logRecordBatch = new CircularBuffer(1); + this.logRecordBatch.Add(this.logRecord); + } + + [GlobalCleanup(Target = nameof(OtlpLogExporter_Grpc))] + public void GlobalCleanupGrpc() + { + this.exporter.Shutdown(); + this.exporter.Dispose(); + this.host.Dispose(); + } + + [GlobalCleanup(Target = nameof(OtlpLogExporter_Http))] + public void GlobalCleanupHttp() + { + this.exporter.Shutdown(); + this.exporter.Dispose(); + this.server.Dispose(); + } + + [Benchmark] + public void OtlpLogExporter_Http() + { + this.exporter.Export(new Batch(this.logRecordBatch, 1)); + } + + [Benchmark] + public void OtlpLogExporter_Grpc() + { + this.exporter.Export(new Batch(this.logRecordBatch, 1)); + } + + private sealed class MockLogService : OtlpCollector.LogsService.LogsServiceBase + { + private static OtlpCollector.ExportLogsServiceResponse response = new OtlpCollector.ExportLogsServiceResponse(); + + public override Task Export(OtlpCollector.ExportLogsServiceRequest request, ServerCallContext context) + { + return Task.FromResult(response); + } + } +} +#endif diff --git a/test/Benchmarks/Exporter/OtlpTraceExporterBenchmarks.cs b/test/Benchmarks/Exporter/OtlpTraceExporterBenchmarks.cs new file mode 100644 index 00000000000..392e0612c0e --- /dev/null +++ b/test/Benchmarks/Exporter/OtlpTraceExporterBenchmarks.cs @@ -0,0 +1,143 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if !NETFRAMEWORK +extern alias OpenTelemetryProtocol; + +using System.Diagnostics; +using BenchmarkDotNet.Attributes; +using Benchmarks.Helper; +using Grpc.Core; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using OpenTelemetry; +using OpenTelemetry.Internal; +using OpenTelemetry.Tests; +using OpenTelemetryProtocol::OpenTelemetry.Exporter; +using OtlpCollector = OpenTelemetryProtocol::OpenTelemetry.Proto.Collector.Trace.V1; + +/* +BenchmarkDotNet v0.13.6, Windows 11 (10.0.22621.2134/22H2/2022Update/SunValley2) (Hyper-V) +AMD EPYC 7763, 1 CPU, 16 logical and 8 physical cores +.NET SDK 7.0.400 + [Host] : .NET 7.0.10 (7.0.1023.36312), X64 RyuJIT AVX2 + DefaultJob : .NET 7.0.10 (7.0.1023.36312), X64 RyuJIT AVX2 + + +| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +|----------------------- |---------:|--------:|--------:|-------:|-------:|----------:| +| OtlpTraceExporter_Http | 139.4 us | 1.41 us | 1.32 us | 0.4883 | 0.2441 | 9.8 KB | +| OtlpTraceExporter_Grpc | 263.0 us | 3.47 us | 3.24 us | 0.4883 | - | 9.34 KB | +*/ + +namespace Benchmarks.Exporter; + +public class OtlpTraceExporterBenchmarks +{ + private OtlpTraceExporter exporter; + private Activity activity; + private CircularBuffer activityBatch; + + private IHost host; + private IDisposable server; + private string serverHost; + private int serverPort; + + [GlobalSetup(Target = nameof(OtlpTraceExporter_Grpc))] + public void GlobalSetupGrpc() + { + this.host = new HostBuilder() + .ConfigureWebHostDefaults(webBuilder => webBuilder + .ConfigureKestrel(options => + { + options.ListenLocalhost(4317, listenOptions => listenOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http2); + }) + .ConfigureServices(services => + { + services.AddGrpc(); + }) + .Configure(app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapGrpcService(); + }); + })) + .Start(); + + var options = new OtlpExporterOptions(); + this.exporter = new OtlpTraceExporter(options); + + this.activity = ActivityHelper.CreateTestActivity(); + this.activityBatch = new CircularBuffer(1); + this.activityBatch.Add(this.activity); + } + + [GlobalSetup(Target = nameof(OtlpTraceExporter_Http))] + public void GlobalSetupHttp() + { + this.server = TestHttpServer.RunServer( + (ctx) => + { + ctx.Response.StatusCode = 200; + ctx.Response.OutputStream.Close(); + }, + out this.serverHost, + out this.serverPort); + + var options = new OtlpExporterOptions + { + Endpoint = new Uri($"http://{this.serverHost}:{this.serverPort}"), + Protocol = OtlpExportProtocol.HttpProtobuf, + }; + this.exporter = new OtlpTraceExporter(options); + + this.activity = ActivityHelper.CreateTestActivity(); + this.activityBatch = new CircularBuffer(1); + this.activityBatch.Add(this.activity); + } + + [GlobalCleanup(Target = nameof(OtlpTraceExporter_Grpc))] + public void GlobalCleanupGrpc() + { + this.exporter.Shutdown(); + this.exporter.Dispose(); + this.activity.Dispose(); + this.host.Dispose(); + } + + [GlobalCleanup(Target = nameof(OtlpTraceExporter_Http))] + public void GlobalCleanupHttp() + { + this.exporter.Shutdown(); + this.exporter.Dispose(); + this.server.Dispose(); + this.activity.Dispose(); + } + + [Benchmark] + public void OtlpTraceExporter_Http() + { + this.exporter.Export(new Batch(this.activityBatch, 1)); + } + + [Benchmark] + public void OtlpTraceExporter_Grpc() + { + this.exporter.Export(new Batch(this.activityBatch, 1)); + } + + private sealed class MockTraceService : OtlpCollector.TraceService.TraceServiceBase + { + private static OtlpCollector.ExportTraceServiceResponse response = new OtlpCollector.ExportTraceServiceResponse(); + + public override Task Export(OtlpCollector.ExportTraceServiceRequest request, ServerCallContext context) + { + return Task.FromResult(response); + } + } +} +#endif diff --git a/test/Benchmarks/Exporter/PrometheusSerializerBenchmarks.cs b/test/Benchmarks/Exporter/PrometheusSerializerBenchmarks.cs index 1f94b555b91..caeda40f44c 100644 --- a/test/Benchmarks/Exporter/PrometheusSerializerBenchmarks.cs +++ b/test/Benchmarks/Exporter/PrometheusSerializerBenchmarks.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Metrics; using BenchmarkDotNet.Attributes; @@ -21,57 +8,68 @@ using OpenTelemetry.Metrics; using OpenTelemetry.Tests; -namespace Benchmarks.Exporter +namespace Benchmarks.Exporter; + +public class PrometheusSerializerBenchmarks { - public class PrometheusSerializerBenchmarks - { - private readonly List metrics = new(); - private readonly byte[] buffer = new byte[85000]; - private Meter meter; - private MeterProvider meterProvider; + private readonly List metrics = new(); + private readonly byte[] buffer = new byte[85000]; + private Meter meter; + private MeterProvider meterProvider; + private Dictionary cache = new Dictionary(); - [Params(1, 1000, 10000)] - public int NumberOfSerializeCalls { get; set; } + [Params(1, 1000, 10000)] + public int NumberOfSerializeCalls { get; set; } - [GlobalSetup] - public void GlobalSetup() - { - this.meter = new Meter(Utils.GetCurrentMethodName()); - this.meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(this.meter.Name) - .AddInMemoryExporter(this.metrics) - .Build(); + [GlobalSetup] + public void GlobalSetup() + { + this.meter = new Meter(Utils.GetCurrentMethodName()); + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(this.meter.Name) + .AddInMemoryExporter(this.metrics) + .Build(); - var counter = this.meter.CreateCounter("counter_name_1", "long", "counter_name_1_description"); - counter.Add(18, new("label1", "value1"), new("label2", "value2")); + var counter = this.meter.CreateCounter("counter_name_1", "long", "counter_name_1_description"); + counter.Add(18, new("label1", "value1"), new("label2", "value2")); - var gauge = this.meter.CreateObservableGauge("gauge_name_1", () => 18.0D, "long", "gauge_name_1_description"); + var gauge = this.meter.CreateObservableGauge("gauge_name_1", () => 18.0D, "long", "gauge_name_1_description"); - var histogram = this.meter.CreateHistogram("histogram_name_1", "long", "histogram_name_1_description"); - histogram.Record(100, new("label1", "value1"), new("label2", "value2")); + var histogram = this.meter.CreateHistogram("histogram_name_1", "long", "histogram_name_1_description"); + histogram.Record(100, new("label1", "value1"), new("label2", "value2")); - this.meterProvider.ForceFlush(); - } + this.meterProvider.ForceFlush(); + } - [GlobalCleanup] - public void GlobalCleanup() - { - this.meter?.Dispose(); - this.meterProvider?.Dispose(); - } + [GlobalCleanup] + public void GlobalCleanup() + { + this.meter?.Dispose(); + this.meterProvider.Dispose(); + } - // TODO: this has a dependency on https://github.com/open-telemetry/opentelemetry-dotnet/issues/2361 - [Benchmark] - public void WriteMetric() + // TODO: this has a dependency on https://github.com/open-telemetry/opentelemetry-dotnet/issues/2361 + [Benchmark] + public void WriteMetric() + { + for (int i = 0; i < this.NumberOfSerializeCalls; i++) { - for (int i = 0; i < this.NumberOfSerializeCalls; i++) + int cursor = 0; + foreach (var metric in this.metrics) { - int cursor = 0; - foreach (var metric in this.metrics) - { - cursor = PrometheusSerializer.WriteMetric(this.buffer, cursor, metric); - } + cursor = PrometheusSerializer.WriteMetric(this.buffer, cursor, metric, this.GetPrometheusMetric(metric)); } } } + + private PrometheusMetric GetPrometheusMetric(Metric metric) + { + if (!this.cache.TryGetValue(metric, out var prometheusMetric)) + { + prometheusMetric = PrometheusMetric.Create(metric, false); + this.cache[metric] = prometheusMetric; + } + + return prometheusMetric; + } } diff --git a/test/Benchmarks/Exporter/ZipkinExporterBenchmarks.cs b/test/Benchmarks/Exporter/ZipkinExporterBenchmarks.cs index caf6e5a1227..fb0f802b55a 100644 --- a/test/Benchmarks/Exporter/ZipkinExporterBenchmarks.cs +++ b/test/Benchmarks/Exporter/ZipkinExporterBenchmarks.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 extern alias Zipkin; @@ -24,78 +11,77 @@ using OpenTelemetry.Tests; using Zipkin::OpenTelemetry.Exporter; -namespace Benchmarks.Exporter -{ -#if !NET462 - [ThreadingDiagnoser] +namespace Benchmarks.Exporter; + +#if !NETFRAMEWORK +[ThreadingDiagnoser] #endif - public class ZipkinExporterBenchmarks - { - private readonly byte[] buffer = new byte[4096]; - private Activity activity; - private CircularBuffer activityBatch; - private IDisposable server; - private string serverHost; - private int serverPort; +public class ZipkinExporterBenchmarks +{ + private readonly byte[] buffer = new byte[4096]; + private Activity activity; + private CircularBuffer activityBatch; + private IDisposable server; + private string serverHost; + private int serverPort; - [Params(1, 10, 100)] - public int NumberOfBatches { get; set; } + [Params(1, 10, 100)] + public int NumberOfBatches { get; set; } - [Params(10000)] - public int NumberOfSpans { get; set; } + [Params(10000)] + public int NumberOfSpans { get; set; } - [GlobalSetup] - public void GlobalSetup() - { - this.activity = ActivityHelper.CreateTestActivity(); - this.activityBatch = new CircularBuffer(this.NumberOfSpans); - this.server = TestHttpServer.RunServer( - (ctx) => + [GlobalSetup] + public void GlobalSetup() + { + this.activity = ActivityHelper.CreateTestActivity(); + this.activityBatch = new CircularBuffer(this.NumberOfSpans); + this.server = TestHttpServer.RunServer( + (ctx) => + { + using (Stream receiveStream = ctx.Request.InputStream) { - using (Stream receiveStream = ctx.Request.InputStream) + while (true) { - while (true) + if (receiveStream.Read(this.buffer, 0, this.buffer.Length) == 0) { - if (receiveStream.Read(this.buffer, 0, this.buffer.Length) == 0) - { - break; - } + break; } } + } - ctx.Response.StatusCode = 200; - ctx.Response.OutputStream.Close(); - }, - out this.serverHost, - out this.serverPort); - } - - [GlobalCleanup] - public void GlobalCleanup() - { - this.server.Dispose(); - } + ctx.Response.StatusCode = 200; + ctx.Response.OutputStream.Close(); + }, + out this.serverHost, + out this.serverPort); + } - [Benchmark] - public void ZipkinExporter_Batching() - { - using var exporter = new ZipkinExporter( - new ZipkinExporterOptions - { - Endpoint = new Uri($"http://{this.serverHost}:{this.serverPort}"), - }); + [GlobalCleanup] + public void GlobalCleanup() + { + this.server.Dispose(); + } - for (int i = 0; i < this.NumberOfBatches; i++) + [Benchmark] + public void ZipkinExporter_Batching() + { + using var exporter = new ZipkinExporter( + new ZipkinExporterOptions { - for (int c = 0; c < this.NumberOfSpans; c++) - { - this.activityBatch.Add(this.activity); - } + Endpoint = new Uri($"http://{this.serverHost}:{this.serverPort}"), + }); - exporter.Export(new Batch(this.activityBatch, this.NumberOfSpans)); + for (int i = 0; i < this.NumberOfBatches; i++) + { + for (int c = 0; c < this.NumberOfSpans; c++) + { + this.activityBatch.Add(this.activity); } - exporter.Shutdown(); + exporter.Export(new Batch(this.activityBatch, this.NumberOfSpans)); } + + exporter.Shutdown(); } } diff --git a/test/Benchmarks/Helper/ActivityCreationScenarios.cs b/test/Benchmarks/Helper/ActivityCreationScenarios.cs index a9a90318197..48c67b4c3d4 100644 --- a/test/Benchmarks/Helper/ActivityCreationScenarios.cs +++ b/test/Benchmarks/Helper/ActivityCreationScenarios.cs @@ -1,56 +1,43 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -namespace Benchmarks.Helper +namespace Benchmarks.Helper; + +internal static class ActivityCreationScenarios { - internal static class ActivityCreationScenarios + public static void CreateActivity(ActivitySource source) { - public static void CreateActivity(ActivitySource source) - { - using var activity = source.StartActivity("name"); - activity?.Stop(); - } - - public static void CreateActivityWithKind(ActivitySource source) - { - using var activity = source.StartActivity("name", ActivityKind.Client); - activity?.Stop(); - } + using var activity = source.StartActivity("name"); + activity?.Stop(); + } - public static void CreateActivityFromParentContext(ActivitySource source, ActivityContext parentCtx) - { - using var activity = source.StartActivity("name", ActivityKind.Internal, parentCtx); - activity?.Stop(); - } + public static void CreateActivityFromParentContext(ActivitySource source, ActivityContext parentCtx) + { + using var activity = source.StartActivity("name", ActivityKind.Internal, parentCtx); + activity?.Stop(); + } - public static void CreateActivityFromParentId(ActivitySource source, string parentId) - { - using var activity = source.StartActivity("name", ActivityKind.Internal, parentId); - activity?.Stop(); - } + public static void CreateActivityWithSetTags(ActivitySource source) + { + using var activity = source.StartActivity("name"); + activity?.SetTag("tag1", "string"); + activity?.SetTag("tag2", 1); + activity?.SetTag("tag3", true); + activity?.SetTag("tag4", "string-again"); + activity?.SetTag("tag5", "string-more"); + activity?.Stop(); + } - public static void CreateActivityWithAttributes(ActivitySource source) - { - using var activity = source.StartActivity("name"); - activity?.SetTag("tag1", "string"); - activity?.SetTag("tag2", 1); - activity?.SetTag("tag3", true); - activity?.Stop(); - } + public static void CreateActivityWithAddTags(ActivitySource source) + { + using var activity = source.StartActivity("name"); + activity?.AddTag("tag1", "string"); + activity?.AddTag("tag2", 1); + activity?.AddTag("tag3", true); + activity?.AddTag("tag4", "string-again"); + activity?.AddTag("tag5", "string-more"); + activity?.Stop(); } } diff --git a/test/Benchmarks/Helper/ActivityHelper.cs b/test/Benchmarks/Helper/ActivityHelper.cs index 2d453ed3f60..33f55dafe57 100644 --- a/test/Benchmarks/Helper/ActivityHelper.cs +++ b/test/Benchmarks/Helper/ActivityHelper.cs @@ -1,100 +1,86 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -namespace Benchmarks.Helper +namespace Benchmarks.Helper; + +internal static class ActivityHelper { - internal static class ActivityHelper + public static Activity CreateTestActivity() { - public static Activity CreateTestActivity() - { - var startTimestamp = DateTime.UtcNow; - var endTimestamp = startTimestamp.AddSeconds(60); - var eventTimestamp = DateTime.UtcNow; + var startTimestamp = DateTime.UtcNow; + var endTimestamp = startTimestamp.AddSeconds(60); + var eventTimestamp = DateTime.UtcNow; - var traceId = ActivityTraceId.CreateFromString("e8ea7e9ac72de94e91fabc613f9686b2".AsSpan()); - var parentSpanId = ActivitySpanId.CreateFromBytes(new byte[] { 12, 23, 34, 45, 56, 67, 78, 89 }); + var traceId = ActivityTraceId.CreateFromString("e8ea7e9ac72de94e91fabc613f9686b2".AsSpan()); + var parentSpanId = ActivitySpanId.CreateFromBytes(new byte[] { 12, 23, 34, 45, 56, 67, 78, 89 }); - var attributes = new Dictionary - { - { "stringKey", "value" }, - { "longKey", 1L }, - { "longKey2", 1 }, - { "doubleKey", 1D }, - { "doubleKey2", 1F }, - { "boolKey", true }, - }; + var attributes = new Dictionary + { + { "stringKey", "value" }, + { "longKey", 1L }, + { "longKey2", 1 }, + { "doubleKey", 1D }, + { "doubleKey2", 1F }, + { "boolKey", true }, + }; - var events = new List - { - new ActivityEvent( - "Event1", - eventTimestamp, - new ActivityTagsCollection(new Dictionary - { - { "key", "value" }, - })), - new ActivityEvent( - "Event2", - eventTimestamp, - new ActivityTagsCollection(new Dictionary - { - { "key", "value" }, - })), - }; + var events = new List + { + new ActivityEvent( + "Event1", + eventTimestamp, + new ActivityTagsCollection(new Dictionary + { + { "key", "value" }, + })), + new ActivityEvent( + "Event2", + eventTimestamp, + new ActivityTagsCollection(new Dictionary + { + { "key", "value" }, + })), + }; - var linkedSpanId = ActivitySpanId.CreateFromString("888915b6286b9c41".AsSpan()); + var linkedSpanId = ActivitySpanId.CreateFromString("888915b6286b9c41".AsSpan()); - using var activitySource = new ActivitySource(nameof(CreateTestActivity)); + using var activitySource = new ActivitySource(nameof(CreateTestActivity)); - var tags = attributes.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.ToString())); - var links = new[] - { - new ActivityLink(new ActivityContext( - traceId, - linkedSpanId, - ActivityTraceFlags.Recorded)), - }; + var tags = attributes.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.ToString())); + var links = new[] + { + new ActivityLink(new ActivityContext( + traceId, + linkedSpanId, + ActivityTraceFlags.Recorded)), + }; - using var listener = new ActivityListener() - { - ShouldListenTo = a => a.Name == nameof(CreateTestActivity), - Sample = (ref ActivityCreationOptions a) => ActivitySamplingResult.AllDataAndRecorded, - }; + using var listener = new ActivityListener() + { + ShouldListenTo = a => a.Name == nameof(CreateTestActivity), + Sample = (ref ActivityCreationOptions a) => ActivitySamplingResult.AllDataAndRecorded, + }; - ActivitySource.AddActivityListener(listener); + ActivitySource.AddActivityListener(listener); - var activity = activitySource.StartActivity( - "Name", - ActivityKind.Client, - parentContext: new ActivityContext(traceId, parentSpanId, ActivityTraceFlags.Recorded), - tags, - links, - startTime: startTimestamp); + var activity = activitySource.StartActivity( + "Name", + ActivityKind.Client, + parentContext: new ActivityContext(traceId, parentSpanId, ActivityTraceFlags.Recorded), + tags, + links, + startTime: startTimestamp); - foreach (var evnt in events) - { - activity.AddEvent(evnt); - } + foreach (var evnt in events) + { + activity.AddEvent(evnt); + } - activity.SetEndTime(endTimestamp); - activity.Stop(); + activity.SetEndTime(endTimestamp); + activity.Stop(); - return activity; - } + return activity; } } diff --git a/test/Benchmarks/Helper/LogRecordHelper.cs b/test/Benchmarks/Helper/LogRecordHelper.cs new file mode 100644 index 00000000000..0cfd5efde25 --- /dev/null +++ b/test/Benchmarks/Helper/LogRecordHelper.cs @@ -0,0 +1,24 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.Logging; +using OpenTelemetry.Logs; + +namespace Benchmarks.Helper; + +internal class LogRecordHelper +{ + internal static LogRecord CreateTestLogRecord() + { + var items = new List(1); + using var factory = LoggerFactory.Create(builder => builder + .AddOpenTelemetry(loggerOptions => + { + loggerOptions.AddInMemoryExporter(items); + })); + + var logger = factory.CreateLogger("TestLogger"); + logger.LogInformation("Hello from {Food} {Price}.", "artichoke", 3.99); + return items[0]; + } +} diff --git a/test/Benchmarks/Helper/SpanCreationScenarios.cs b/test/Benchmarks/Helper/SpanCreationScenarios.cs index c9dbae42b7f..c92d60e6e35 100644 --- a/test/Benchmarks/Helper/SpanCreationScenarios.cs +++ b/test/Benchmarks/Helper/SpanCreationScenarios.cs @@ -1,72 +1,58 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Trace; -namespace Benchmarks.Helper +namespace Benchmarks.Helper; + +internal static class SpanCreationScenarios { - internal static class SpanCreationScenarios + public static void CreateSpan(Tracer tracer) { - public static void CreateSpan(Tracer tracer) - { - using var span = tracer.StartSpan("span"); - span.End(); - } + using var span = tracer.StartSpan("span"); + span.End(); + } - public static void CreateSpan_ParentContext(Tracer tracer) - { - var parentContext = new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded, true); - using var span = tracer.StartSpan("span", SpanKind.Client, parentContext); - span.End(); - } + public static void CreateSpan_ParentContext(Tracer tracer) + { + var parentContext = new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded, true); + using var span = tracer.StartSpan("span", SpanKind.Client, parentContext); + span.End(); + } + + public static void CreateSpan_Attributes(Tracer tracer) + { + using var span = tracer.StartSpan("span"); + span.SetAttribute("string", "string"); + span.SetAttribute("int", 1); + span.SetAttribute("long", 1L); + span.SetAttribute("bool", false); + span.End(); + } - public static void CreateSpan_Attributes(Tracer tracer) + public static void CreateSpan_Propagate(Tracer tracer) + { + using var span = tracer.StartSpan("span"); + using (Tracer.WithSpan(span)) { - using var span = tracer.StartSpan("span"); - span.SetAttribute("string", "string"); - span.SetAttribute("int", 1); - span.SetAttribute("long", 1L); - span.SetAttribute("bool", false); - span.End(); } - public static void CreateSpan_Propagate(Tracer tracer) - { - using var span = tracer.StartSpan("span"); - using (Tracer.WithSpan(span)) - { - } + span.End(); + } - span.End(); - } + public static void CreateSpan_Active(Tracer tracer) + { + using var span = tracer.StartSpan("span"); + } - public static void CreateSpan_Active(Tracer tracer) - { - using var span = tracer.StartSpan("span"); - } + public static void CreateSpan_Active_GetCurrent(Tracer tracer) + { + TelemetrySpan span; - public static void CreateSpan_Active_GetCurrent(Tracer tracer) + using (tracer.StartSpan("span")) { - TelemetrySpan span; - - using (tracer.StartSpan("span")) - { - span = Tracer.CurrentSpan; - } + span = Tracer.CurrentSpan; } } } diff --git a/test/Benchmarks/Helper/TestExporter.cs b/test/Benchmarks/Helper/TestExporter.cs new file mode 100644 index 00000000000..3d2b0038f8d --- /dev/null +++ b/test/Benchmarks/Helper/TestExporter.cs @@ -0,0 +1,22 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Tests; + +internal class TestExporter : BaseExporter + where T : class +{ + private readonly Action> processBatchAction; + + public TestExporter(Action> processBatchAction) + { + this.processBatchAction = processBatchAction ?? throw new ArgumentNullException(nameof(processBatchAction)); + } + + public override ExportResult Export(in Batch batch) + { + this.processBatchAction(batch); + + return ExportResult.Success; + } +} diff --git a/test/Benchmarks/Instrumentation/AspNetCoreInstrumentationBenchmarks.cs b/test/Benchmarks/Instrumentation/AspNetCoreInstrumentationBenchmarks.cs index 4cb306ab727..85c1f5fa2d3 100644 --- a/test/Benchmarks/Instrumentation/AspNetCoreInstrumentationBenchmarks.cs +++ b/test/Benchmarks/Instrumentation/AspNetCoreInstrumentationBenchmarks.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #if !NETFRAMEWORK using BenchmarkDotNet.Attributes; @@ -78,126 +65,125 @@ = 2.45 + (1138 + 64) / 1024 = 2.45 + 1.17 = ~3.63KB */ -namespace Benchmarks.Instrumentation +namespace Benchmarks.Instrumentation; + +public class AspNetCoreInstrumentationBenchmarks { - public class AspNetCoreInstrumentationBenchmarks + private HttpClient httpClient; + private WebApplication app; + private TracerProvider tracerProvider; + private MeterProvider meterProvider; + + [Flags] + public enum EnableInstrumentationOption { - private HttpClient httpClient; - private WebApplication app; - private TracerProvider tracerProvider; - private MeterProvider meterProvider; + /// + /// Instrumentation is not enabled for any signal. + /// + None = 0, + + /// + /// Instrumentation is enbled only for Traces. + /// + Traces = 1, + + /// + /// Instrumentation is enbled only for Metrics. + /// + Metrics = 2, + } + + [Params(0, 1, 2, 3)] + public EnableInstrumentationOption EnableInstrumentation { get; set; } - [Flags] - public enum EnableInstrumentationOption + [GlobalSetup(Target = nameof(GetRequestForAspNetCoreApp))] + public void GetRequestForAspNetCoreAppGlobalSetup() + { + if (this.EnableInstrumentation == EnableInstrumentationOption.None) { - /// - /// Instrumentation is not enabled for any signal. - /// - None = 0, - - /// - /// Instrumentation is enbled only for Traces. - /// - Traces = 1, - - /// - /// Instrumentation is enbled only for Metrics. - /// - Metrics = 2, + this.StartWebApplication(); + this.httpClient = new HttpClient(); } + else if (this.EnableInstrumentation == EnableInstrumentationOption.Traces) + { + this.StartWebApplication(); + this.httpClient = new HttpClient(); - [Params(0, 1, 2, 3)] - public EnableInstrumentationOption EnableInstrumentation { get; set; } + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .Build(); + } + else if (this.EnableInstrumentation == EnableInstrumentationOption.Metrics) + { + this.StartWebApplication(); + this.httpClient = new HttpClient(); - [GlobalSetup(Target = nameof(GetRequestForAspNetCoreApp))] - public void GetRequestForAspNetCoreAppGlobalSetup() + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddAspNetCoreInstrumentation() + .Build(); + } + else if (this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Traces) && + this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Metrics)) { - if (this.EnableInstrumentation == EnableInstrumentationOption.None) - { - this.StartWebApplication(); - this.httpClient = new HttpClient(); - } - else if (this.EnableInstrumentation == EnableInstrumentationOption.Traces) - { - this.StartWebApplication(); - this.httpClient = new HttpClient(); - - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation() - .Build(); - } - else if (this.EnableInstrumentation == EnableInstrumentationOption.Metrics) - { - this.StartWebApplication(); - this.httpClient = new HttpClient(); - - this.meterProvider = Sdk.CreateMeterProviderBuilder() - .AddAspNetCoreInstrumentation() - .Build(); - } - else if (this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Traces) && - this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Metrics)) - { - this.StartWebApplication(); - this.httpClient = new HttpClient(); - - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation() - .Build(); - - this.meterProvider = Sdk.CreateMeterProviderBuilder() - .AddAspNetCoreInstrumentation() - .Build(); - } + this.StartWebApplication(); + this.httpClient = new HttpClient(); + + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .Build(); + + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddAspNetCoreInstrumentation() + .Build(); } + } - [GlobalCleanup(Target = nameof(GetRequestForAspNetCoreApp))] - public void GetRequestForAspNetCoreAppGlobalCleanup() + [GlobalCleanup(Target = nameof(GetRequestForAspNetCoreApp))] + public void GetRequestForAspNetCoreAppGlobalCleanup() + { + if (this.EnableInstrumentation == EnableInstrumentationOption.None) { - if (this.EnableInstrumentation == EnableInstrumentationOption.None) - { - this.httpClient.Dispose(); - this.app.DisposeAsync().GetAwaiter().GetResult(); - } - else if (this.EnableInstrumentation == EnableInstrumentationOption.Traces) - { - this.httpClient.Dispose(); - this.app.DisposeAsync().GetAwaiter().GetResult(); - this.tracerProvider.Dispose(); - } - else if (this.EnableInstrumentation == EnableInstrumentationOption.Metrics) - { - this.httpClient.Dispose(); - this.app.DisposeAsync().GetAwaiter().GetResult(); - this.meterProvider.Dispose(); - } - else if (this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Traces) && - this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Metrics)) - { - this.httpClient.Dispose(); - this.app.DisposeAsync().GetAwaiter().GetResult(); - this.tracerProvider.Dispose(); - this.meterProvider.Dispose(); - } + this.httpClient.Dispose(); + this.app.DisposeAsync().GetAwaiter().GetResult(); } - - [Benchmark] - public async Task GetRequestForAspNetCoreApp() + else if (this.EnableInstrumentation == EnableInstrumentationOption.Traces) { - var httpResponse = await this.httpClient.GetAsync("http://localhost:5000").ConfigureAwait(false); - httpResponse.EnsureSuccessStatusCode(); + this.httpClient.Dispose(); + this.app.DisposeAsync().GetAwaiter().GetResult(); + this.tracerProvider.Dispose(); } - - private void StartWebApplication() + else if (this.EnableInstrumentation == EnableInstrumentationOption.Metrics) { - var builder = WebApplication.CreateBuilder(); - builder.Logging.ClearProviders(); - var app = builder.Build(); - app.MapGet("/", async context => await context.Response.WriteAsync($"Hello World!")); - app.RunAsync(); - - this.app = app; + this.httpClient.Dispose(); + this.app.DisposeAsync().GetAwaiter().GetResult(); + this.meterProvider.Dispose(); } + else if (this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Traces) && + this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Metrics)) + { + this.httpClient.Dispose(); + this.app.DisposeAsync().GetAwaiter().GetResult(); + this.tracerProvider.Dispose(); + this.meterProvider.Dispose(); + } + } + + [Benchmark] + public async Task GetRequestForAspNetCoreApp() + { + var httpResponse = await this.httpClient.GetAsync("http://localhost:5000").ConfigureAwait(false); + httpResponse.EnsureSuccessStatusCode(); + } + + private void StartWebApplication() + { + var builder = WebApplication.CreateBuilder(); + builder.Logging.ClearProviders(); + var app = builder.Build(); + app.MapGet("/", async context => await context.Response.WriteAsync($"Hello World!")); + app.RunAsync(); + + this.app = app; } } #endif diff --git a/test/Benchmarks/Instrumentation/AspNetCoreInstrumentationNewBenchmarks.cs b/test/Benchmarks/Instrumentation/AspNetCoreInstrumentationNewBenchmarks.cs new file mode 100644 index 00000000000..4ddc7b56018 --- /dev/null +++ b/test/Benchmarks/Instrumentation/AspNetCoreInstrumentationNewBenchmarks.cs @@ -0,0 +1,210 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if !NETFRAMEWORK +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +/* +// * Summary * + +BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1992/22H2/2022Update/SunValley2), VM=Hyper-V +AMD EPYC 7763, 1 CPU, 16 logical and 8 physical cores +.NET SDK=7.0.306 + [Host] : .NET 7.0.9 (7.0.923.32018), X64 RyuJIT AVX2 + DefaultJob : .NET 7.0.9 (7.0.923.32018), X64 RyuJIT AVX2 + + +| Method | EnableInstrumentation | Mean | Error | StdDev | Allocated | +|--------------------------- |---------------------- |---------:|--------:|--------:|----------:| +| GetRequestForAspNetCoreApp | None | 150.7 us | 1.68 us | 1.57 us | 2.45 KB | +| GetRequestForAspNetCoreApp | Traces | 156.6 us | 3.12 us | 6.37 us | 3.46 KB | +| GetRequestForAspNetCoreApp | Metrics | 148.8 us | 2.87 us | 2.69 us | 2.92 KB | +| GetRequestForAspNetCoreApp | Traces, Metrics | 164.0 us | 3.19 us | 6.22 us | 3.52 KB | + +Allocation details for .NET 7: + +// Traces +* Activity creation + `Activity.Start()` = 416 B +* Casting of the struct `Microsoft.Extensions.Primitives.StringValues` to `IEnumerable` by `HttpRequestHeaderValuesGetter` + - `TraceContextPropagator.Extract` = 24 B + - `BaggageContextPropagator.Extract` = 24 B +* String creation for `HttpRequest.HostString.Host` = 40 B +* `Activity.TagsLinkedList` (this is allocated on the first Activity.SetTag call) = 40 B +* Boxing of `Port` number when adding it as a tag = 24 B +* Setting `Baggage` (Setting AsyncLocal values causes allocation) + - `BaggageHolder` creation = 24 B + - `System.Threading.AsyncLocalValueMap.TwoElementAsyncLocalValueMap` = 48 B + - `System.Threading.ExecutionContext` = 40 B +* `DiagNode>` + - This is allocated seven times for the seven (eight if query string is available) tags that are added = 7 * 40 = 280 B +* `Activity.Stop()` trying to set `Activity.Current` (This happens because of setting another AsyncLocal variable which is `Baggage` + - System.Threading.AsyncLocalValueMap.OneElementAsyncLocalValueMap = 32 B + - System.Threading.ExecutionContext = 40 B + +Baseline = 2.45 KB +With Traces = 2.45 + (1032 / 1024) = 2.45 + 1.01 = 3.46 KB + + +// Metrics +* Activity creation + `Activity.Start()` = 416 B +* Boxing of `Port` number when adding it as a tag = 24 B +* String creation for `HttpRequest.HostString.Host` = 40 B + +Baseline = 2.45 KB +With Metrics = 2.45 + (416 + 40 + 24) / 1024 = 2.45 + 0.47 = 2.92 KB + +// With Traces and Metrics + +Baseline = 2.45 KB +With Traces and Metrics = Baseline + With Traces + (With Metrics - (Activity creation + `Acitivity.Stop()`)) (they use the same activity) + = 2.45 + (1032 + 64) / 1024 = 2.45 + 1.07 = ~3.52KB +*/ +namespace Benchmarks.Instrumentation; + +public class AspNetCoreInstrumentationNewBenchmarks +{ + private HttpClient httpClient; + private WebApplication app; + private TracerProvider tracerProvider; + private MeterProvider meterProvider; + + [Flags] + public enum EnableInstrumentationOption + { + /// + /// Instrumentation is not enabled for any signal. + /// + None = 0, + + /// + /// Instrumentation is enbled only for Traces. + /// + Traces = 1, + + /// + /// Instrumentation is enbled only for Metrics. + /// + Metrics = 2, + } + + [Params(0, 1, 2, 3)] + public EnableInstrumentationOption EnableInstrumentation { get; set; } + + [GlobalSetup(Target = nameof(GetRequestForAspNetCoreApp))] + public void GetRequestForAspNetCoreAppGlobalSetup() + { + KeyValuePair[] config = new KeyValuePair[] { new KeyValuePair("OTEL_SEMCONV_STABILITY_OPT_IN", "http") }; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(config) + .Build(); + + if (this.EnableInstrumentation == EnableInstrumentationOption.None) + { + this.StartWebApplication(); + this.httpClient = new HttpClient(); + } + else if (this.EnableInstrumentation == EnableInstrumentationOption.Traces) + { + this.StartWebApplication(); + this.httpClient = new HttpClient(); + + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => services.AddSingleton(configuration)) + .AddAspNetCoreInstrumentation() + .Build(); + } + else if (this.EnableInstrumentation == EnableInstrumentationOption.Metrics) + { + this.StartWebApplication(); + this.httpClient = new HttpClient(); + + var exportedItems = new List(); + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => services.AddSingleton(configuration)) + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 1000; + }) + .Build(); + } + else if (this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Traces) && + this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Metrics)) + { + this.StartWebApplication(); + this.httpClient = new HttpClient(); + + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => services.AddSingleton(configuration)) + .AddAspNetCoreInstrumentation() + .Build(); + + var exportedItems = new List(); + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => services.AddSingleton(configuration)) + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 1000; + }) + .Build(); + } + } + + [GlobalCleanup(Target = nameof(GetRequestForAspNetCoreApp))] + public void GetRequestForAspNetCoreAppGlobalCleanup() + { + if (this.EnableInstrumentation == EnableInstrumentationOption.None) + { + this.httpClient.Dispose(); + this.app.DisposeAsync().GetAwaiter().GetResult(); + } + else if (this.EnableInstrumentation == EnableInstrumentationOption.Traces) + { + this.httpClient.Dispose(); + this.app.DisposeAsync().GetAwaiter().GetResult(); + this.tracerProvider.Dispose(); + } + else if (this.EnableInstrumentation == EnableInstrumentationOption.Metrics) + { + this.httpClient.Dispose(); + this.app.DisposeAsync().GetAwaiter().GetResult(); + this.meterProvider.Dispose(); + } + else if (this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Traces) && + this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Metrics)) + { + this.httpClient.Dispose(); + this.app.DisposeAsync().GetAwaiter().GetResult(); + this.tracerProvider.Dispose(); + this.meterProvider.Dispose(); + } + } + + [Benchmark] + public async Task GetRequestForAspNetCoreApp() + { + var httpResponse = await this.httpClient.GetAsync("http://localhost:5000").ConfigureAwait(false); + httpResponse.EnsureSuccessStatusCode(); + } + + private void StartWebApplication() + { + var builder = WebApplication.CreateBuilder(); + builder.Logging.ClearProviders(); + var app = builder.Build(); + app.MapGet("/", async context => await context.Response.WriteAsync($"Hello World!")); + app.RunAsync(); + + this.app = app; + } +} +#endif diff --git a/test/Benchmarks/Instrumentation/DiagnosticSourceSubscriberBenchmark.cs b/test/Benchmarks/Instrumentation/DiagnosticSourceSubscriberBenchmark.cs deleted file mode 100644 index ab63af387f6..00000000000 --- a/test/Benchmarks/Instrumentation/DiagnosticSourceSubscriberBenchmark.cs +++ /dev/null @@ -1,88 +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 BenchmarkDotNet.Attributes; -using OpenTelemetry.Instrumentation; - -namespace Benchmarks.Instrumentation -{ - [InProcess] - public class DiagnosticSourceSubscriberBenchmark - { - [Params(1, 2)] - public int SubscriberCount; - - [Params(false, true)] - public bool UseIsEnabledFilter; - - private const string SourceName = "MySource"; - - private readonly DiagnosticListener listener = new(SourceName); - private readonly List subscribers = new(); - private readonly Func isEnabledFilter = (name, arg1, arg2) => ((EventPayload)arg1).Data == "Data"; - - [GlobalSetup] - public void GlobalSetup() - { - for (var i = 0; i < this.SubscriberCount; ++i) - { - var subscriber = new DiagnosticSourceSubscriber( - new TestListener(), - this.UseIsEnabledFilter ? this.isEnabledFilter : null); - - this.subscribers.Add(subscriber); - subscriber.Subscribe(); - } - } - - [GlobalCleanup] - public void GlobalCleanup() - { - foreach (var subscriber in this.subscribers) - { - subscriber.Dispose(); - } - } - - [Benchmark] - public void WriteDiagnosticSourceEvent() - { - var payload = new EventPayload("Data"); - this.listener.Write("SomeEvent", payload); - } - - private struct EventPayload - { - public EventPayload(string data) - { - this.Data = data; - } - - public string Data { get; } - } - - private class TestListener : ListenerHandler - { - public TestListener() - : base(DiagnosticSourceSubscriberBenchmark.SourceName) - { - } - - public override bool SupportsNullActivity => true; - } - } -} diff --git a/test/Benchmarks/Instrumentation/HttpClientInstrumentationBenchmarks.cs b/test/Benchmarks/Instrumentation/HttpClientInstrumentationBenchmarks.cs index 2583b32cf0d..2db79fa9d1f 100644 --- a/test/Benchmarks/Instrumentation/HttpClientInstrumentationBenchmarks.cs +++ b/test/Benchmarks/Instrumentation/HttpClientInstrumentationBenchmarks.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #if !NETFRAMEWORK using BenchmarkDotNet.Attributes; @@ -39,126 +26,135 @@ | HttpClientRequest | Traces, Metrics | 175.6 us | 1.96 us | 1.83 us | 0.4883 | 4.28 KB | */ -namespace Benchmarks.Instrumentation +namespace Benchmarks.Instrumentation; + +public class HttpClientInstrumentationBenchmarks { - public class HttpClientInstrumentationBenchmarks + private HttpClient httpClient; + private WebApplication app; + private TracerProvider tracerProvider; + private MeterProvider meterProvider; + + [Flags] + public enum EnableInstrumentationOption { - private HttpClient httpClient; - private WebApplication app; - private TracerProvider tracerProvider; - private MeterProvider meterProvider; + /// + /// Instrumentation is not enabled for any signal. + /// + None = 0, + + /// + /// Instrumentation is enbled only for Traces. + /// + Traces = 1, + + /// + /// Instrumentation is enbled only for Metrics. + /// + Metrics = 2, + } - [Flags] - public enum EnableInstrumentationOption + [Params(0, 1, 2, 3)] + public EnableInstrumentationOption EnableInstrumentation { get; set; } + + [GlobalSetup(Target = nameof(HttpClientRequest))] + public void HttpClientRequestGlobalSetup() + { + if (this.EnableInstrumentation == EnableInstrumentationOption.None) { - /// - /// Instrumentation is not enabled for any signal. - /// - None = 0, - - /// - /// Instrumentation is enbled only for Traces. - /// - Traces = 1, - - /// - /// Instrumentation is enbled only for Metrics. - /// - Metrics = 2, + this.StartWebApplication(); + this.httpClient = new HttpClient(); } + else if (this.EnableInstrumentation == EnableInstrumentationOption.Traces) + { + this.StartWebApplication(); + this.httpClient = new HttpClient(); - [Params(0, 1, 2, 3)] - public EnableInstrumentationOption EnableInstrumentation { get; set; } - - [GlobalSetup(Target = nameof(HttpClientRequest))] - public void HttpClientRequestGlobalSetup() + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddHttpClientInstrumentation() + .Build(); + } + else if (this.EnableInstrumentation == EnableInstrumentationOption.Metrics) { - if (this.EnableInstrumentation == EnableInstrumentationOption.None) - { - this.StartWebApplication(); - this.httpClient = new HttpClient(); - } - else if (this.EnableInstrumentation == EnableInstrumentationOption.Traces) - { - this.StartWebApplication(); - this.httpClient = new HttpClient(); - - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation() - .Build(); - } - else if (this.EnableInstrumentation == EnableInstrumentationOption.Metrics) - { - this.StartWebApplication(); - this.httpClient = new HttpClient(); - - this.meterProvider = Sdk.CreateMeterProviderBuilder() - .AddHttpClientInstrumentation() - .Build(); - } - else if (this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Traces) && - this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Metrics)) - { - this.StartWebApplication(); - this.httpClient = new HttpClient(); - - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation() - .Build(); - - this.meterProvider = Sdk.CreateMeterProviderBuilder() - .AddHttpClientInstrumentation() - .Build(); - } + this.StartWebApplication(); + this.httpClient = new HttpClient(); + + var exportedItems = new List(); + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddHttpClientInstrumentation() + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 1000; + }) + .Build(); } - - [GlobalCleanup(Target = nameof(HttpClientRequest))] - public void HttpClientRequestGlobalCleanup() + else if (this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Traces) && + this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Metrics)) { - if (this.EnableInstrumentation == EnableInstrumentationOption.None) - { - this.httpClient.Dispose(); - this.app.DisposeAsync().GetAwaiter().GetResult(); - } - else if (this.EnableInstrumentation == EnableInstrumentationOption.Traces) - { - this.httpClient.Dispose(); - this.app.DisposeAsync().GetAwaiter().GetResult(); - this.tracerProvider.Dispose(); - } - else if (this.EnableInstrumentation == EnableInstrumentationOption.Metrics) - { - this.httpClient.Dispose(); - this.app.DisposeAsync().GetAwaiter().GetResult(); - this.meterProvider.Dispose(); - } - else if (this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Traces) && - this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Metrics)) - { - this.httpClient.Dispose(); - this.app.DisposeAsync().GetAwaiter().GetResult(); - this.tracerProvider.Dispose(); - this.meterProvider.Dispose(); - } + this.StartWebApplication(); + this.httpClient = new HttpClient(); + + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddHttpClientInstrumentation() + .Build(); + + var exportedItems = new List(); + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddHttpClientInstrumentation() + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 1000; + }) + .Build(); } + } - [Benchmark] - public async Task HttpClientRequest() + [GlobalCleanup(Target = nameof(HttpClientRequest))] + public void HttpClientRequestGlobalCleanup() + { + if (this.EnableInstrumentation == EnableInstrumentationOption.None) { - var httpResponse = await this.httpClient.GetAsync("http://localhost:5000").ConfigureAwait(false); - httpResponse.EnsureSuccessStatusCode(); + this.httpClient.Dispose(); + this.app.DisposeAsync().GetAwaiter().GetResult(); } - - private void StartWebApplication() + else if (this.EnableInstrumentation == EnableInstrumentationOption.Traces) { - var builder = WebApplication.CreateBuilder(); - builder.Logging.ClearProviders(); - var app = builder.Build(); - app.MapGet("/", async context => await context.Response.WriteAsync($"Hello World!")); - app.RunAsync(); - - this.app = app; + this.httpClient.Dispose(); + this.app.DisposeAsync().GetAwaiter().GetResult(); + this.tracerProvider.Dispose(); + } + else if (this.EnableInstrumentation == EnableInstrumentationOption.Metrics) + { + this.httpClient.Dispose(); + this.app.DisposeAsync().GetAwaiter().GetResult(); + this.meterProvider.Dispose(); } + else if (this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Traces) && + this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Metrics)) + { + this.httpClient.Dispose(); + this.app.DisposeAsync().GetAwaiter().GetResult(); + this.tracerProvider.Dispose(); + this.meterProvider.Dispose(); + } + } + + [Benchmark] + public async Task HttpClientRequest() + { + var httpResponse = await this.httpClient.GetAsync("http://localhost:5000").ConfigureAwait(false); + httpResponse.EnsureSuccessStatusCode(); + } + + private void StartWebApplication() + { + var builder = WebApplication.CreateBuilder(); + builder.Logging.ClearProviders(); + var app = builder.Build(); + app.MapGet("/", async context => await context.Response.WriteAsync($"Hello World!")); + app.RunAsync(); + + this.app = app; } } #endif diff --git a/test/Benchmarks/Logs/Food.cs b/test/Benchmarks/Logs/Food.cs index f1fabbe06f4..4b5288f4e5a 100644 --- a/test/Benchmarks/Logs/Food.cs +++ b/test/Benchmarks/Logs/Food.cs @@ -1,30 +1,12 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.Logging; -namespace Benchmarks.Logs +namespace Benchmarks.Logs; + +public static partial class Food { - public static partial class Food - { - [LoggerMessage( - EventId = 0, - Level = LogLevel.Information, - Message = "Hello from {food} {price}.")] - public static partial void SayHello( - ILogger logger, string food, double price); - } + [LoggerMessage(Level = LogLevel.Information, Message = "Hello from {food} {price}.")] + public static partial void SayHello(ILogger logger, string food, double price); } diff --git a/test/Benchmarks/Logs/LogBenchmarks.cs b/test/Benchmarks/Logs/LogBenchmarks.cs index 0175adcec11..5c0f4ad28a5 100644 --- a/test/Benchmarks/Logs/LogBenchmarks.cs +++ b/test/Benchmarks/Logs/LogBenchmarks.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using BenchmarkDotNet.Attributes; using Microsoft.Extensions.Logging; @@ -20,102 +7,124 @@ using OpenTelemetry.Logs; /* -// * Summary * - -BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.1466 (21H2) -Intel Core i7-4790 CPU 3.60GHz (Haswell), 1 CPU, 8 logical and 4 physical cores -.NET SDK=6.0.101 - [Host] : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT - DefaultJob : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT - - -| Method | Mean | Error | StdDev | Gen 0 | Allocated | -|--------------------------------------- |-----------:|----------:|----------:|-------:|----------:| -| NoListener | 72.365 ns | 0.9425 ns | 0.8817 ns | 0.0153 | 64 B | -| NoListenerWithLoggerMessageGenerator | 4.769 ns | 0.0161 ns | 0.0142 ns | - | - | -| OneProcessor | 168.330 ns | 0.6198 ns | 0.5494 ns | 0.0553 | 232 B | -| OneProcessorWithLoggerMessageGenerator | 142.898 ns | 0.5233 ns | 0.4086 ns | 0.0401 | 168 B | -| TwoProcessors | 173.727 ns | 0.5978 ns | 0.4992 ns | 0.0553 | 232 B | -| ThreeProcessors | 174.295 ns | 0.7697 ns | 0.7200 ns | 0.0553 | 232 B | +BenchmarkDotNet v0.13.10, Windows 11 (10.0.22621.3007/22H2/2022Update/SunValley2) +11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores +.NET SDK 8.0.101 + [Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 + DefaultJob : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 + + +| Method | Mean | Error | StdDev | Gen0 | Allocated | +|------------------------------ |-----------:|----------:|----------:|-------:|----------:| +| NoListenerStringInterpolation | 124.458 ns | 2.5188 ns | 2.2329 ns | 0.0114 | 72 B | +| NoListenerExtensionMethod | 36.326 ns | 0.2916 ns | 0.2435 ns | 0.0102 | 64 B | +| NoListener | 1.375 ns | 0.0586 ns | 0.0896 ns | - | - | +| CreateLoggerRepeatedly | 48.295 ns | 0.5951 ns | 0.4970 ns | 0.0038 | 24 B | +| OneProcessor | 98.133 ns | 1.8805 ns | 1.5703 ns | 0.0063 | 40 B | +| TwoProcessors | 105.414 ns | 0.4610 ns | 0.3850 ns | 0.0063 | 40 B | +| ThreeProcessors | 102.023 ns | 1.4187 ns | 1.1847 ns | 0.0063 | 40 B | */ -namespace Benchmarks.Logs +namespace Benchmarks.Logs; + +public class LogBenchmarks { - public class LogBenchmarks + private const double FoodPrice = 2.99; + private static readonly string FoodName = "tomato"; + + private readonly ILogger loggerWithNoListener; + private readonly ILogger loggerWithOneProcessor; + private readonly ILogger loggerWithTwoProcessors; + private readonly ILogger loggerWithThreeProcessors; + + private readonly ILoggerFactory loggerFactoryWithNoListener; + private readonly ILoggerFactory loggerFactoryWithOneProcessor; + private readonly ILoggerFactory loggerFactoryWithTwoProcessor; + private readonly ILoggerFactory loggerFactoryWithThreeProcessor; + + public LogBenchmarks() { - private readonly ILogger loggerWithNoListener; - private readonly ILogger loggerWithOneProcessor; - private readonly ILogger loggerWithTwoProcessors; - private readonly ILogger loggerWithThreeProcessors; + this.loggerFactoryWithNoListener = LoggerFactory.Create(builder => { }); + this.loggerWithNoListener = this.loggerFactoryWithNoListener.CreateLogger(); - public LogBenchmarks() + this.loggerFactoryWithOneProcessor = LoggerFactory.Create(builder => { - using var loggerFactoryWithNoListener = LoggerFactory.Create(builder => { }); - this.loggerWithNoListener = loggerFactoryWithNoListener.CreateLogger(); - - using var loggerFactoryWithOneProcessor = LoggerFactory.Create(builder => - { - builder.AddOpenTelemetry(options => options - .AddProcessor(new DummyLogProcessor())); - }); - this.loggerWithOneProcessor = loggerFactoryWithOneProcessor.CreateLogger(); - - using var loggerFactoryWithTwoProcessor = LoggerFactory.Create(builder => - { - builder.AddOpenTelemetry(options => options - .AddProcessor(new DummyLogProcessor()) - .AddProcessor(new DummyLogProcessor())); - }); - this.loggerWithTwoProcessors = loggerFactoryWithTwoProcessor.CreateLogger(); - - using var loggerFactoryWithThreeProcessor = LoggerFactory.Create(builder => - { - builder.AddOpenTelemetry(options => options - .AddProcessor(new DummyLogProcessor()) - .AddProcessor(new DummyLogProcessor()) - .AddProcessor(new DummyLogProcessor())); - }); - this.loggerWithThreeProcessors = loggerFactoryWithThreeProcessor.CreateLogger(); - } - - [Benchmark] - public void NoListener() - { - this.loggerWithNoListener.LogInformation("Hello from {name} {price}.", "tomato", 2.99); - } + builder.AddOpenTelemetry(options => options + .AddProcessor(new DummyLogProcessor())); + }); + this.loggerWithOneProcessor = this.loggerFactoryWithOneProcessor.CreateLogger(); - [Benchmark] - public void NoListenerWithLoggerMessageGenerator() + this.loggerFactoryWithTwoProcessor = LoggerFactory.Create(builder => { - Food.SayHello(this.loggerWithNoListener, "tomato", 2.99); - } + builder.AddOpenTelemetry(options => options + .AddProcessor(new DummyLogProcessor()) + .AddProcessor(new DummyLogProcessor())); + }); + this.loggerWithTwoProcessors = this.loggerFactoryWithTwoProcessor.CreateLogger(); - [Benchmark] - public void OneProcessor() + this.loggerFactoryWithThreeProcessor = LoggerFactory.Create(builder => { - this.loggerWithOneProcessor.LogInformation("Hello from {name} {price}.", "tomato", 2.99); - } + builder.AddOpenTelemetry(options => options + .AddProcessor(new DummyLogProcessor()) + .AddProcessor(new DummyLogProcessor()) + .AddProcessor(new DummyLogProcessor())); + }); + this.loggerWithThreeProcessors = this.loggerFactoryWithThreeProcessor.CreateLogger(); + } - [Benchmark] - public void OneProcessorWithLoggerMessageGenerator() - { - Food.SayHello(this.loggerWithOneProcessor, "tomato", 2.99); - } + [GlobalCleanup] + public void GlobalCleanup() + { + this.loggerFactoryWithNoListener.Dispose(); + this.loggerFactoryWithOneProcessor.Dispose(); + this.loggerFactoryWithTwoProcessor.Dispose(); + this.loggerFactoryWithThreeProcessor.Dispose(); + } - [Benchmark] - public void TwoProcessors() - { - this.loggerWithTwoProcessors.LogInformation("Hello from {name} {price}.", "tomato", 2.99); - } + [Benchmark] + public void NoListenerStringInterpolation() + { + this.loggerWithNoListener.LogInformation($"Hello from {FoodName} {FoodPrice}."); + } - [Benchmark] - public void ThreeProcessors() - { - this.loggerWithThreeProcessors.LogInformation("Hello from {name} {price}.", "tomato", 2.99); - } + [Benchmark] + public void NoListenerExtensionMethod() + { + this.loggerWithNoListener.LogInformation("Hello from {name} {price}.", FoodName, FoodPrice); + } - internal class DummyLogProcessor : BaseProcessor - { - } + [Benchmark] + public void NoListener() + { + Food.SayHello(this.loggerWithNoListener, FoodName, FoodPrice); + } + + [Benchmark] + public void CreateLoggerRepeatedly() + { + var logger = this.loggerFactoryWithNoListener.CreateLogger(); + Food.SayHello(logger, FoodName, FoodPrice); + } + + [Benchmark] + public void OneProcessor() + { + Food.SayHello(this.loggerWithOneProcessor, FoodName, FoodPrice); + } + + [Benchmark] + public void TwoProcessors() + { + Food.SayHello(this.loggerWithTwoProcessors, FoodName, FoodPrice); + } + + [Benchmark] + public void ThreeProcessors() + { + Food.SayHello(this.loggerWithThreeProcessors, FoodName, FoodPrice); + } + + internal class DummyLogProcessor : BaseProcessor + { } } diff --git a/test/Benchmarks/Logs/LogScopeBenchmarks.cs b/test/Benchmarks/Logs/LogScopeBenchmarks.cs index c33b2e0b8ee..affa9f5f66c 100644 --- a/test/Benchmarks/Logs/LogScopeBenchmarks.cs +++ b/test/Benchmarks/Logs/LogScopeBenchmarks.cs @@ -1,77 +1,80 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Collections.ObjectModel; using BenchmarkDotNet.Attributes; using Microsoft.Extensions.Logging; using OpenTelemetry.Logs; -namespace Benchmarks.Logs +/* +BenchmarkDotNet v0.13.10, Windows 11 (10.0.23424.1000) +Intel Core i7-9700 CPU 3.00GHz, 1 CPU, 8 logical and 8 physical cores +.NET SDK 8.0.100 + [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + + +| Method | Mean | Error | StdDev | Allocated | +|------------- |---------:|---------:|---------:|----------:| +| ForEachScope | 74.29 ns | 0.605 ns | 0.536 ns | - | +*/ + +namespace Benchmarks.Logs; + +public class LogScopeBenchmarks { - public class LogScopeBenchmarks - { - private readonly LoggerExternalScopeProvider scopeProvider = new(); + private readonly LoggerExternalScopeProvider scopeProvider = new(); - private readonly Action callback = (LogRecordScope scope, object state) => + private readonly Action callback = (LogRecordScope scope, object state) => + { + foreach (var scopeItem in scope) { - foreach (KeyValuePair scopeItem in scope) + _ = scopeItem.Key; + _ = scopeItem.Value; + } + }; + + private readonly LogRecord logRecord; + + public LogScopeBenchmarks() + { + this.scopeProvider.Push(new ReadOnlyCollection>( + new List> { - } - }; + new("item1", "value1"), + new("item2", "value2"), + })); - private readonly LogRecord logRecord; + this.scopeProvider.Push(new ReadOnlyCollection>( + new List> + { + new("item3", "value3"), + })); - public LogScopeBenchmarks() - { - this.scopeProvider.Push(new ReadOnlyCollection>( - new List> - { - new KeyValuePair("item1", "value1"), - new KeyValuePair("item2", "value2"), - })); - this.scopeProvider.Push(new ReadOnlyCollection>( - new List> - { - new KeyValuePair("item3", "value3"), - })); - this.scopeProvider.Push(new ReadOnlyCollection>( - new List> - { - new KeyValuePair("item4", "value4"), - new KeyValuePair("item5", "value5"), - })); + this.scopeProvider.Push(new ReadOnlyCollection>( + new List> + { + new("item4", "value4"), + new("item5", "value5"), + })); #pragma warning disable CS0618 // Type or member is obsolete - this.logRecord = new LogRecord( - this.scopeProvider, - DateTime.UtcNow, - "Benchmark", - LogLevel.Information, - 0, - "Message", - null, - null, - null); + this.logRecord = new LogRecord( + this.scopeProvider, + DateTime.UtcNow, + "Benchmark", + LogLevel.Information, + 0, + "Message", + null, + null, + null); #pragma warning restore CS0618 // Type or member is obsolete - } + } - [Benchmark] - public void ForEachScope() - { - this.logRecord.ForEachScope(this.callback, null); - } + [Benchmark] + public void ForEachScope() + { + this.logRecord.ForEachScope(this.callback, null); } } diff --git a/test/Benchmarks/Metrics/Base2ExponentialHistogramBenchmarks.cs b/test/Benchmarks/Metrics/Base2ExponentialHistogramBenchmarks.cs index 23c5acec757..aec8ba6fce5 100644 --- a/test/Benchmarks/Metrics/Base2ExponentialHistogramBenchmarks.cs +++ b/test/Benchmarks/Metrics/Base2ExponentialHistogramBenchmarks.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Diagnostics.Metrics; @@ -22,20 +9,20 @@ using OpenTelemetry.Tests; /* -BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.23424.1000) +BenchmarkDotNet v0.13.10, Windows 11 (10.0.23424.1000) Intel Core i7-9700 CPU 3.00GHz, 1 CPU, 8 logical and 8 physical cores -.NET SDK=7.0.203 - [Host] : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2 - DefaultJob : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2 +.NET SDK 8.0.100 + [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 -| Method | Mean | Error | StdDev | Allocated | +| Method | Mean | Error | StdDev | Allocated | |---------------------------- |----------:|---------:|---------:|----------:| -| HistogramHotPath | 54.78 ns | 0.907 ns | 0.848 ns | - | -| HistogramWith1LabelHotPath | 115.37 ns | 0.388 ns | 0.363 ns | - | -| HistogramWith3LabelsHotPath | 228.03 ns | 3.767 ns | 3.146 ns | - | -| HistogramWith5LabelsHotPath | 316.60 ns | 5.980 ns | 9.311 ns | - | -| HistogramWith7LabelsHotPath | 366.86 ns | 2.694 ns | 3.596 ns | - | +| HistogramHotPath | 42.27 ns | 0.298 ns | 0.279 ns | - | +| HistogramWith1LabelHotPath | 88.90 ns | 1.103 ns | 1.032 ns | - | +| HistogramWith3LabelsHotPath | 166.95 ns | 1.759 ns | 1.559 ns | - | +| HistogramWith5LabelsHotPath | 248.19 ns | 2.523 ns | 2.237 ns | - | +| HistogramWith7LabelsHotPath | 310.95 ns | 5.627 ns | 4.988 ns | - | */ namespace Benchmarks.Metrics; @@ -46,7 +33,7 @@ public class Base2ExponentialHistogramBenchmarks private readonly Random random = new(); private readonly string[] dimensionValues = new string[] { "DimVal1", "DimVal2", "DimVal3", "DimVal4", "DimVal5", "DimVal6", "DimVal7", "DimVal8", "DimVal9", "DimVal10" }; private Histogram histogram; - private MeterProvider provider; + private MeterProvider meterProvider; private Meter meter; [GlobalSetup] @@ -57,7 +44,7 @@ public void Setup() var exportedItems = new List(); - this.provider = Sdk.CreateMeterProviderBuilder() + this.meterProvider = Sdk.CreateMeterProviderBuilder() .AddMeter(this.meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { @@ -71,7 +58,7 @@ public void Setup() public void Cleanup() { this.meter?.Dispose(); - this.provider?.Dispose(); + this.meterProvider.Dispose(); } [Benchmark] diff --git a/test/Benchmarks/Metrics/Base2ExponentialHistogramMapToIndexBenchmarks.cs b/test/Benchmarks/Metrics/Base2ExponentialHistogramMapToIndexBenchmarks.cs index f334b3c4898..a7cc5edb5ca 100644 --- a/test/Benchmarks/Metrics/Base2ExponentialHistogramMapToIndexBenchmarks.cs +++ b/test/Benchmarks/Metrics/Base2ExponentialHistogramMapToIndexBenchmarks.cs @@ -1,35 +1,22 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using BenchmarkDotNet.Attributes; using OpenTelemetry.Metrics; /* -BenchmarkDotNet=v0.13.5, OS=macOS Ventura 13.4 (22F66) [Darwin 22.5.0] -Apple M1 Max, 1 CPU, 10 logical and 10 physical cores -.NET SDK=7.0.101 - [Host] : .NET 7.0.1 (7.0.122.56804), Arm64 RyuJIT AdvSIMD - DefaultJob : .NET 7.0.1 (7.0.122.56804), Arm64 RyuJIT AdvSIMD - - -| Method | Scale | Mean | Error | StdDev | Allocated | -|----------- |------ |---------:|---------:|---------:|----------:| -| MapToIndex | -11 | 11.60 ns | 0.057 ns | 0.053 ns | - | -| MapToIndex | 3 | 14.63 ns | 0.135 ns | 0.126 ns | - | -| MapToIndex | 20 | 14.40 ns | 0.026 ns | 0.024 ns | - | +BenchmarkDotNet v0.13.10, Windows 11 (10.0.23424.1000) +Intel Core i7-9700 CPU 3.00GHz, 1 CPU, 8 logical and 8 physical cores +.NET SDK 8.0.100 + [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + + +| Method | Scale | Mean | Error | StdDev | Allocated | +|----------- |------ |----------:|----------:|----------:|----------:| +| MapToIndex | -11 | 4.003 ns | 0.0288 ns | 0.0240 ns | - | +| MapToIndex | 3 | 11.081 ns | 0.1222 ns | 0.1143 ns | - | +| MapToIndex | 20 | 11.077 ns | 0.1103 ns | 0.1032 ns | - | */ namespace Benchmarks.Metrics; diff --git a/test/Benchmarks/Metrics/Base2ExponentialHistogramScaleBenchmarks.cs b/test/Benchmarks/Metrics/Base2ExponentialHistogramScaleBenchmarks.cs index 4a004fe49ed..d11d8743ebf 100644 --- a/test/Benchmarks/Metrics/Base2ExponentialHistogramScaleBenchmarks.cs +++ b/test/Benchmarks/Metrics/Base2ExponentialHistogramScaleBenchmarks.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Metrics; using BenchmarkDotNet.Attributes; @@ -21,18 +8,18 @@ using OpenTelemetry.Tests; /* -BenchmarkDotNet=v0.13.5, OS=macOS Ventura 13.4 (22F66) [Darwin 22.5.0] -Apple M1 Max, 1 CPU, 10 logical and 10 physical cores -.NET SDK=7.0.101 - [Host] : .NET 7.0.1 (7.0.122.56804), Arm64 RyuJIT AdvSIMD - DefaultJob : .NET 7.0.1 (7.0.122.56804), Arm64 RyuJIT AdvSIMD +BenchmarkDotNet v0.13.10, Windows 11 (10.0.23424.1000) +Intel Core i7-9700 CPU 3.00GHz, 1 CPU, 8 logical and 8 physical cores +.NET SDK 8.0.100 + [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 -| Method | Scale | Mean | Error | StdDev | Allocated | +| Method | Scale | Mean | Error | StdDev | Allocated | |----------------- |------ |---------:|---------:|---------:|----------:| -| HistogramHotPath | -11 | 29.79 ns | 0.054 ns | 0.042 ns | - | -| HistogramHotPath | 3 | 32.10 ns | 0.086 ns | 0.080 ns | - | -| HistogramHotPath | 20 | 32.08 ns | 0.076 ns | 0.063 ns | - | +| HistogramHotPath | -11 | 32.44 ns | 0.196 ns | 0.174 ns | - | +| HistogramHotPath | 3 | 42.33 ns | 0.158 ns | 0.124 ns | - | +| HistogramHotPath | 20 | 40.57 ns | 0.363 ns | 0.322 ns | - | */ namespace Benchmarks.Metrics; @@ -42,7 +29,7 @@ public class Base2ExponentialHistogramScaleBenchmarks private const int MaxValue = 10000; private readonly Random random = new(); private Histogram histogram; - private MeterProvider provider; + private MeterProvider meterProvider; private Meter meter; // This is a simple benchmark that records values in the range [0, 10000]. @@ -65,7 +52,7 @@ public void Setup() var exportedItems = new List(); - this.provider = Sdk.CreateMeterProviderBuilder() + this.meterProvider = Sdk.CreateMeterProviderBuilder() .AddMeter(this.meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { @@ -79,7 +66,7 @@ public void Setup() public void Cleanup() { this.meter?.Dispose(); - this.provider?.Dispose(); + this.meterProvider.Dispose(); } [Benchmark] diff --git a/test/Benchmarks/Metrics/ExemplarBenchmarks.cs b/test/Benchmarks/Metrics/ExemplarBenchmarks.cs index ec25fa5e3d3..ac1d347fba7 100644 --- a/test/Benchmarks/Metrics/ExemplarBenchmarks.cs +++ b/test/Benchmarks/Metrics/ExemplarBenchmarks.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Diagnostics.Metrics; @@ -22,126 +9,210 @@ using OpenTelemetry.Tests; /* -BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.23424.1000) -Intel Core i7-9700 CPU 3.00GHz, 1 CPU, 8 logical and 8 physical cores -.NET SDK=7.0.203 - [Host] : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2 - DefaultJob : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2 - - -| Method | ExemplarFilter | Mean | Error | StdDev | Allocated | -|-------------------------- |--------------- |---------:|--------:|--------:|----------:| -| HistogramNoTagReduction | AlwaysOff | 315.5 ns | 5.93 ns | 5.55 ns | - | -| HistogramWithTagReduction | AlwaysOff | 296.4 ns | 0.95 ns | 0.89 ns | - | -| HistogramNoTagReduction | AlwaysOn | 366.5 ns | 6.96 ns | 7.74 ns | - | -| HistogramWithTagReduction | AlwaysOn | 397.1 ns | 4.09 ns | 3.82 ns | - | -| HistogramNoTagReduction | HighValueOnly | 364.8 ns | 2.73 ns | 2.28 ns | - | -| HistogramWithTagReduction | HighValueOnly | 391.9 ns | 4.38 ns | 4.10 ns | - | +BenchmarkDotNet v0.13.10, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3) +12th Gen Intel Core i9-12900HK, 1 CPU, 20 logical and 14 physical cores +.NET SDK 8.0.200 + [Host] : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2 + DefaultJob : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2 + + +| Method | ExemplarConfiguration | Mean | Error | StdDev | Allocated | +|-------------------------- |---------------------- |---------:|--------:|--------:|----------:| +| HistogramNoTagReduction | AlwaysOff | 174.6 ns | 1.32 ns | 1.24 ns | - | +| HistogramWithTagReduction | AlwaysOff | 161.8 ns | 2.63 ns | 2.46 ns | - | +| CounterNoTagReduction | AlwaysOff | 141.6 ns | 2.12 ns | 1.77 ns | - | +| CounterWithTagReduction | AlwaysOff | 141.7 ns | 2.11 ns | 1.87 ns | - | +| HistogramNoTagReduction | AlwaysOn | 201.1 ns | 3.05 ns | 2.86 ns | - | +| HistogramWithTagReduction | AlwaysOn | 196.5 ns | 1.91 ns | 1.78 ns | - | +| CounterNoTagReduction | AlwaysOn | 149.7 ns | 1.42 ns | 1.33 ns | - | +| CounterWithTagReduction | AlwaysOn | 143.5 ns | 2.09 ns | 1.95 ns | - | +| HistogramNoTagReduction | TraceBased | 171.9 ns | 2.33 ns | 2.18 ns | - | +| HistogramWithTagReduction | TraceBased | 164.9 ns | 2.70 ns | 2.52 ns | - | +| CounterNoTagReduction | TraceBased | 148.1 ns | 2.76 ns | 2.58 ns | - | +| CounterWithTagReduction | TraceBased | 141.2 ns | 1.43 ns | 1.34 ns | - | +| HistogramNoTagReduction | Alway(...)pling [29] | 183.9 ns | 1.49 ns | 1.39 ns | - | +| HistogramWithTagReduction | Alway(...)pling [29] | 176.1 ns | 3.35 ns | 3.29 ns | - | +| CounterNoTagReduction | Alway(...)pling [29] | 159.3 ns | 3.12 ns | 4.38 ns | - | +| CounterWithTagReduction | Alway(...)pling [29] | 158.7 ns | 3.06 ns | 3.65 ns | - | */ -namespace Benchmarks.Metrics +namespace Benchmarks.Metrics; + +public class ExemplarBenchmarks { - public class ExemplarBenchmarks + private static readonly ThreadLocal ThreadLocalRandom = new(() => new Random()); + private readonly string[] dimensionValues = new string[] { "DimVal1", "DimVal2", "DimVal3", "DimVal4", "DimVal5", "DimVal6", "DimVal7", "DimVal8", "DimVal9", "DimVal10" }; + private Histogram histogramWithoutTagReduction; + private Histogram histogramWithTagReduction; + private Counter counterWithoutTagReduction; + private Counter counterWithTagReduction; + private MeterProvider meterProvider; + private Meter meter; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1602:Enumeration items should be documented", Justification = "Test only.")] + public enum ExemplarConfigurationType { - private static readonly ThreadLocal ThreadLocalRandom = new(() => new Random()); - private readonly string[] dimensionValues = new string[] { "DimVal1", "DimVal2", "DimVal3", "DimVal4", "DimVal5", "DimVal6", "DimVal7", "DimVal8", "DimVal9", "DimVal10" }; - private Histogram histogramWithoutTagReduction; + AlwaysOff, + AlwaysOn, + TraceBased, + AlwaysOnWithHighValueSampling, + } - private Histogram histogramWithTagReduction; + [Params(ExemplarConfigurationType.AlwaysOn, ExemplarConfigurationType.AlwaysOff, ExemplarConfigurationType.TraceBased, ExemplarConfigurationType.AlwaysOnWithHighValueSampling)] + public ExemplarConfigurationType ExemplarConfiguration { get; set; } - private MeterProvider provider; - private Meter meter; + [GlobalSetup] + public void Setup() + { + this.meter = new Meter(Utils.GetCurrentMethodName()); + this.histogramWithoutTagReduction = this.meter.CreateHistogram("HistogramWithoutTagReduction"); + this.histogramWithTagReduction = this.meter.CreateHistogram("HistogramWithTagReduction"); + this.counterWithoutTagReduction = this.meter.CreateCounter("CounterWithoutTagReduction"); + this.counterWithTagReduction = this.meter.CreateCounter("CounterWithTagReduction"); + var exportedItems = new List(); + + var exemplarFilter = this.ExemplarConfiguration == ExemplarConfigurationType.TraceBased + ? ExemplarFilterType.TraceBased + : this.ExemplarConfiguration != ExemplarConfigurationType.AlwaysOff + ? ExemplarFilterType.AlwaysOn + : ExemplarFilterType.AlwaysOff; + + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(this.meter.Name) + .SetExemplarFilter(exemplarFilter) + .AddView(i => + { + if (i.Name.Contains("WithTagReduction")) + { + return new MetricStreamConfiguration() + { + TagKeys = new string[] { "DimName1", "DimName2", "DimName3" }, + ExemplarReservoirFactory = CreateExemplarReservoir, + }; + } + else + { + return new MetricStreamConfiguration() + { + ExemplarReservoirFactory = CreateExemplarReservoir, + }; + } + }) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 1000; + }) + .Build(); - [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1602:Enumeration items should be documented", Justification = "Test only.")] - public enum ExemplarFilterTouse + ExemplarReservoir CreateExemplarReservoir() { - AlwaysOff, - AlwaysOn, - HighValueOnly, + return this.ExemplarConfiguration == ExemplarConfigurationType.AlwaysOnWithHighValueSampling + ? new HighValueExemplarReservoir(800D) + : null; } + } - [Params(ExemplarFilterTouse.AlwaysOn, ExemplarFilterTouse.AlwaysOff, ExemplarFilterTouse.HighValueOnly)] - public ExemplarFilterTouse ExemplarFilter { get; set; } + [GlobalCleanup] + public void Cleanup() + { + this.meter?.Dispose(); + this.meterProvider.Dispose(); + } - [GlobalSetup] - public void Setup() + [Benchmark] + public void HistogramNoTagReduction() + { + var random = ThreadLocalRandom.Value; + var tags = new TagList { - this.meter = new Meter(Utils.GetCurrentMethodName()); - this.histogramWithoutTagReduction = this.meter.CreateHistogram("HistogramWithoutTagReduction"); - this.histogramWithTagReduction = this.meter.CreateHistogram("HistogramWithTagReduction"); - var exportedItems = new List(); + { "DimName1", this.dimensionValues[random.Next(0, 2)] }, + { "DimName2", this.dimensionValues[random.Next(0, 2)] }, + { "DimName3", this.dimensionValues[random.Next(0, 5)] }, + { "DimName4", this.dimensionValues[random.Next(0, 5)] }, + { "DimName5", this.dimensionValues[random.Next(0, 10)] }, + }; + + this.histogramWithoutTagReduction.Record(random.NextDouble() * 1000D, tags); + } - ExemplarFilter exemplarFilter = new AlwaysOffExemplarFilter(); - if (this.ExemplarFilter == ExemplarFilterTouse.AlwaysOn) - { - exemplarFilter = new AlwaysOnExemplarFilter(); - } - else if (this.ExemplarFilter == ExemplarFilterTouse.HighValueOnly) - { - exemplarFilter = new HighValueExemplarFilter(); - } + [Benchmark] + public void HistogramWithTagReduction() + { + var random = ThreadLocalRandom.Value; + var tags = new TagList + { + { "DimName1", this.dimensionValues[random.Next(0, 2)] }, + { "DimName2", this.dimensionValues[random.Next(0, 2)] }, + { "DimName3", this.dimensionValues[random.Next(0, 5)] }, + { "DimName4", this.dimensionValues[random.Next(0, 5)] }, + { "DimName5", this.dimensionValues[random.Next(0, 10)] }, + }; + + this.histogramWithTagReduction.Record(random.NextDouble() * 1000D, tags); + } - this.provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(this.meter.Name) - .SetExemplarFilter(exemplarFilter) - .AddView("HistogramWithTagReduction", new MetricStreamConfiguration() { TagKeys = new string[] { "DimName1", "DimName2", "DimName3" } }) - .AddInMemoryExporter(exportedItems, metricReaderOptions => - { - metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 1000; - }) - .Build(); - } + [Benchmark] + public void CounterNoTagReduction() + { + var random = ThreadLocalRandom.Value; + var tags = new TagList + { + { "DimName1", this.dimensionValues[random.Next(0, 2)] }, + { "DimName2", this.dimensionValues[random.Next(0, 2)] }, + { "DimName3", this.dimensionValues[random.Next(0, 5)] }, + { "DimName4", this.dimensionValues[random.Next(0, 5)] }, + { "DimName5", this.dimensionValues[random.Next(0, 10)] }, + }; + + this.counterWithoutTagReduction.Add(random.Next(1000), tags); + } - [GlobalCleanup] - public void Cleanup() + [Benchmark] + public void CounterWithTagReduction() + { + var random = ThreadLocalRandom.Value; + var tags = new TagList { - this.meter?.Dispose(); - this.provider?.Dispose(); - } + { "DimName1", this.dimensionValues[random.Next(0, 2)] }, + { "DimName2", this.dimensionValues[random.Next(0, 2)] }, + { "DimName3", this.dimensionValues[random.Next(0, 5)] }, + { "DimName4", this.dimensionValues[random.Next(0, 5)] }, + { "DimName5", this.dimensionValues[random.Next(0, 10)] }, + }; + + this.counterWithTagReduction.Add(random.Next(1000), tags); + } - [Benchmark] - public void HistogramNoTagReduction() + private sealed class HighValueExemplarReservoir : FixedSizeExemplarReservoir + { + private readonly double threshold; + private int measurementCount; + + public HighValueExemplarReservoir(double threshold) + : base(10) { - var random = ThreadLocalRandom.Value; - var tags = new TagList - { - { "DimName1", this.dimensionValues[random.Next(0, 2)] }, - { "DimName2", this.dimensionValues[random.Next(0, 2)] }, - { "DimName3", this.dimensionValues[random.Next(0, 5)] }, - { "DimName4", this.dimensionValues[random.Next(0, 5)] }, - { "DimName5", this.dimensionValues[random.Next(0, 10)] }, - }; - - this.histogramWithoutTagReduction.Record(random.Next(1000), tags); + this.threshold = threshold; } - [Benchmark] - public void HistogramWithTagReduction() + public override void Offer(in ExemplarMeasurement measurement) { - var random = ThreadLocalRandom.Value; - var tags = new TagList + if (measurement.Value >= this.threshold) { - { "DimName1", this.dimensionValues[random.Next(0, 2)] }, - { "DimName2", this.dimensionValues[random.Next(0, 2)] }, - { "DimName3", this.dimensionValues[random.Next(0, 5)] }, - { "DimName4", this.dimensionValues[random.Next(0, 5)] }, - { "DimName5", this.dimensionValues[random.Next(0, 10)] }, - }; - - this.histogramWithTagReduction.Record(random.Next(1000), tags); + this.UpdateExemplar(this.measurementCount++ % this.Capacity, in measurement); + } } - internal class HighValueExemplarFilter : ExemplarFilter + public override void Offer(in ExemplarMeasurement measurement) { - public override bool ShouldSample(long value, ReadOnlySpan> tags) + if (measurement.Value >= this.threshold) { - return value > 800; + this.UpdateExemplar(this.measurementCount++ % this.Capacity, in measurement); } + } - public override bool ShouldSample(double value, ReadOnlySpan> tags) - { - return value > 800; - } + protected override void OnCollected() + { + this.measurementCount = 0; } } } diff --git a/test/Benchmarks/Metrics/HistogramBenchmarks.cs b/test/Benchmarks/Metrics/HistogramBenchmarks.cs index 321ad855026..17bbad18c67 100644 --- a/test/Benchmarks/Metrics/HistogramBenchmarks.cs +++ b/test/Benchmarks/Metrics/HistogramBenchmarks.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Diagnostics.Metrics; @@ -22,135 +9,134 @@ using OpenTelemetry.Tests; /* -BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.23424.1000) +BenchmarkDotNet v0.13.10, Windows 11 (10.0.23424.1000) Intel Core i7-9700 CPU 3.00GHz, 1 CPU, 8 logical and 8 physical cores -.NET SDK=7.0.203 - [Host] : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2 - DefaultJob : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2 +.NET SDK 8.0.100 + [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 -| Method | BoundCount | Mean | Error | StdDev | Median | Allocated | +| Method | BoundCount | Mean | Error | StdDev | Median | Allocated | |---------------------------- |----------- |----------:|----------:|----------:|----------:|----------:| -| HistogramHotPath | 10 | 50.11 ns | 0.219 ns | 0.204 ns | 50.06 ns | - | -| HistogramWith1LabelHotPath | 10 | 108.52 ns | 0.559 ns | 0.523 ns | 108.44 ns | - | -| HistogramWith3LabelsHotPath | 10 | 212.31 ns | 1.873 ns | 1.661 ns | 212.32 ns | - | -| HistogramWith5LabelsHotPath | 10 | 299.97 ns | 5.162 ns | 5.737 ns | 298.60 ns | - | -| HistogramWith7LabelsHotPath | 10 | 349.53 ns | 2.115 ns | 1.875 ns | 349.62 ns | - | -| HistogramHotPath | 49 | 61.34 ns | 0.171 ns | 0.160 ns | 61.35 ns | - | -| HistogramWith1LabelHotPath | 49 | 118.64 ns | 1.539 ns | 1.285 ns | 118.09 ns | - | -| HistogramWith3LabelsHotPath | 49 | 226.70 ns | 1.653 ns | 1.465 ns | 226.62 ns | - | -| HistogramWith5LabelsHotPath | 49 | 314.40 ns | 5.185 ns | 4.850 ns | 313.96 ns | - | -| HistogramWith7LabelsHotPath | 49 | 375.37 ns | 5.796 ns | 5.138 ns | 373.76 ns | - | -| HistogramHotPath | 50 | 60.08 ns | 0.062 ns | 0.049 ns | 60.08 ns | - | -| HistogramWith1LabelHotPath | 50 | 118.16 ns | 0.640 ns | 0.568 ns | 118.03 ns | - | -| HistogramWith3LabelsHotPath | 50 | 258.96 ns | 3.710 ns | 3.098 ns | 259.70 ns | - | -| HistogramWith5LabelsHotPath | 50 | 353.81 ns | 5.646 ns | 5.281 ns | 351.81 ns | - | -| HistogramWith7LabelsHotPath | 50 | 406.75 ns | 6.491 ns | 6.072 ns | 406.01 ns | - | -| HistogramHotPath | 1000 | 86.82 ns | 0.543 ns | 0.481 ns | 86.68 ns | - | -| HistogramWith1LabelHotPath | 1000 | 147.04 ns | 0.535 ns | 0.447 ns | 146.88 ns | - | -| HistogramWith3LabelsHotPath | 1000 | 619.11 ns | 10.943 ns | 14.608 ns | 617.10 ns | - | -| HistogramWith5LabelsHotPath | 1000 | 759.64 ns | 22.509 ns | 63.855 ns | 737.58 ns | - | -| HistogramWith7LabelsHotPath | 1000 | 760.85 ns | 6.220 ns | 5.514 ns | 761.68 ns | - | +| HistogramHotPath | 10 | 38.36 ns | 0.401 ns | 0.375 ns | 38.28 ns | - | +| HistogramWith1LabelHotPath | 10 | 78.66 ns | 0.258 ns | 0.241 ns | 78.67 ns | - | +| HistogramWith3LabelsHotPath | 10 | 162.22 ns | 0.946 ns | 0.839 ns | 162.03 ns | - | +| HistogramWith5LabelsHotPath | 10 | 230.90 ns | 1.262 ns | 1.181 ns | 231.12 ns | - | +| HistogramWith7LabelsHotPath | 10 | 288.06 ns | 1.362 ns | 1.208 ns | 288.01 ns | - | +| HistogramHotPath | 49 | 48.23 ns | 0.137 ns | 0.128 ns | 48.22 ns | - | +| HistogramWith1LabelHotPath | 49 | 90.52 ns | 0.404 ns | 0.358 ns | 90.47 ns | - | +| HistogramWith3LabelsHotPath | 49 | 170.17 ns | 0.801 ns | 0.710 ns | 170.07 ns | - | +| HistogramWith5LabelsHotPath | 49 | 244.93 ns | 3.935 ns | 3.681 ns | 244.85 ns | - | +| HistogramWith7LabelsHotPath | 49 | 308.28 ns | 5.927 ns | 5.544 ns | 306.44 ns | - | +| HistogramHotPath | 50 | 49.22 ns | 0.280 ns | 0.249 ns | 49.25 ns | - | +| HistogramWith1LabelHotPath | 50 | 91.70 ns | 0.589 ns | 0.492 ns | 91.68 ns | - | +| HistogramWith3LabelsHotPath | 50 | 213.74 ns | 4.258 ns | 5.537 ns | 212.26 ns | - | +| HistogramWith5LabelsHotPath | 50 | 299.59 ns | 5.940 ns | 13.408 ns | 296.23 ns | - | +| HistogramWith7LabelsHotPath | 50 | 342.75 ns | 5.066 ns | 4.491 ns | 341.84 ns | - | +| HistogramHotPath | 1000 | 72.04 ns | 0.723 ns | 0.603 ns | 71.94 ns | - | +| HistogramWith1LabelHotPath | 1000 | 118.73 ns | 2.130 ns | 1.992 ns | 117.98 ns | - | +| HistogramWith3LabelsHotPath | 1000 | 545.71 ns | 10.644 ns | 12.258 ns | 543.55 ns | - | +| HistogramWith5LabelsHotPath | 1000 | 661.81 ns | 14.837 ns | 42.091 ns | 651.99 ns | - | +| HistogramWith7LabelsHotPath | 1000 | 709.50 ns | 14.123 ns | 39.368 ns | 698.27 ns | - | */ -namespace Benchmarks.Metrics +namespace Benchmarks.Metrics; + +public class HistogramBenchmarks { - public class HistogramBenchmarks + private const int MaxValue = 10000; + private readonly Random random = new(); + private readonly string[] dimensionValues = new string[] { "DimVal1", "DimVal2", "DimVal3", "DimVal4", "DimVal5", "DimVal6", "DimVal7", "DimVal8", "DimVal9", "DimVal10" }; + private Histogram histogram; + private MeterProvider meterProvider; + private Meter meter; + private double[] bounds; + + // Note: Values related to `HistogramBuckets.DefaultHistogramCountForBinarySearch` + [Params(10, 49, 50, 1000)] + public int BoundCount { get; set; } + + [GlobalSetup] + public void Setup() { - private const int MaxValue = 10000; - private readonly Random random = new(); - private readonly string[] dimensionValues = new string[] { "DimVal1", "DimVal2", "DimVal3", "DimVal4", "DimVal5", "DimVal6", "DimVal7", "DimVal8", "DimVal9", "DimVal10" }; - private Histogram histogram; - private MeterProvider provider; - private Meter meter; - private double[] bounds; - - // Note: Values related to `HistogramBuckets.DefaultHistogramCountForBinarySearch` - [Params(10, 49, 50, 1000)] - public int BoundCount { get; set; } - - [GlobalSetup] - public void Setup() + this.meter = new Meter(Utils.GetCurrentMethodName()); + this.histogram = this.meter.CreateHistogram("histogram"); + + // Evenly distribute the bound values over the range [0, MaxValue) + this.bounds = new double[this.BoundCount]; + for (int i = 0; i < this.bounds.Length; i++) { - this.meter = new Meter(Utils.GetCurrentMethodName()); - this.histogram = this.meter.CreateHistogram("histogram"); + this.bounds[i] = i * MaxValue / this.bounds.Length; + } + + var exportedItems = new List(); - // Evenly distribute the bound values over the range [0, MaxValue) - this.bounds = new double[this.BoundCount]; - for (int i = 0; i < this.bounds.Length; i++) + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(this.meter.Name) + .AddInMemoryExporter(exportedItems, metricReaderOptions => { - this.bounds[i] = i * MaxValue / this.bounds.Length; - } - - var exportedItems = new List(); - - this.provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(this.meter.Name) - .AddInMemoryExporter(exportedItems, metricReaderOptions => - { - metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 1000; - }) - .AddView(this.histogram.Name, new ExplicitBucketHistogramConfiguration() { Boundaries = this.bounds }) - .Build(); - } + metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 1000; + }) + .AddView(this.histogram.Name, new ExplicitBucketHistogramConfiguration() { Boundaries = this.bounds }) + .Build(); + } - [GlobalCleanup] - public void Cleanup() - { - this.meter?.Dispose(); - this.provider?.Dispose(); - } + [GlobalCleanup] + public void Cleanup() + { + this.meter?.Dispose(); + this.meterProvider.Dispose(); + } - [Benchmark] - public void HistogramHotPath() - { - this.histogram.Record(this.random.Next(MaxValue)); - } + [Benchmark] + public void HistogramHotPath() + { + this.histogram.Record(this.random.Next(MaxValue)); + } - [Benchmark] - public void HistogramWith1LabelHotPath() - { - var tag1 = new KeyValuePair("DimName1", this.dimensionValues[this.random.Next(0, 2)]); - this.histogram.Record(this.random.Next(MaxValue), tag1); - } + [Benchmark] + public void HistogramWith1LabelHotPath() + { + var tag1 = new KeyValuePair("DimName1", this.dimensionValues[this.random.Next(0, 2)]); + this.histogram.Record(this.random.Next(MaxValue), tag1); + } - [Benchmark] - public void HistogramWith3LabelsHotPath() - { - var tag1 = new KeyValuePair("DimName1", this.dimensionValues[this.random.Next(0, 10)]); - var tag2 = new KeyValuePair("DimName2", this.dimensionValues[this.random.Next(0, 10)]); - var tag3 = new KeyValuePair("DimName3", this.dimensionValues[this.random.Next(0, 10)]); - this.histogram.Record(this.random.Next(MaxValue), tag1, tag2, tag3); - } + [Benchmark] + public void HistogramWith3LabelsHotPath() + { + var tag1 = new KeyValuePair("DimName1", this.dimensionValues[this.random.Next(0, 10)]); + var tag2 = new KeyValuePair("DimName2", this.dimensionValues[this.random.Next(0, 10)]); + var tag3 = new KeyValuePair("DimName3", this.dimensionValues[this.random.Next(0, 10)]); + this.histogram.Record(this.random.Next(MaxValue), tag1, tag2, tag3); + } - [Benchmark] - public void HistogramWith5LabelsHotPath() + [Benchmark] + public void HistogramWith5LabelsHotPath() + { + var tags = new TagList { - var tags = new TagList - { - { "DimName1", this.dimensionValues[this.random.Next(0, 2)] }, - { "DimName2", this.dimensionValues[this.random.Next(0, 2)] }, - { "DimName3", this.dimensionValues[this.random.Next(0, 5)] }, - { "DimName4", this.dimensionValues[this.random.Next(0, 5)] }, - { "DimName5", this.dimensionValues[this.random.Next(0, 10)] }, - }; - this.histogram.Record(this.random.Next(MaxValue), tags); - } + { "DimName1", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName2", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName3", this.dimensionValues[this.random.Next(0, 5)] }, + { "DimName4", this.dimensionValues[this.random.Next(0, 5)] }, + { "DimName5", this.dimensionValues[this.random.Next(0, 10)] }, + }; + this.histogram.Record(this.random.Next(MaxValue), tags); + } - [Benchmark] - public void HistogramWith7LabelsHotPath() + [Benchmark] + public void HistogramWith7LabelsHotPath() + { + var tags = new TagList { - var tags = new TagList - { - { "DimName1", this.dimensionValues[this.random.Next(0, 2)] }, - { "DimName2", this.dimensionValues[this.random.Next(0, 2)] }, - { "DimName3", this.dimensionValues[this.random.Next(0, 5)] }, - { "DimName4", this.dimensionValues[this.random.Next(0, 5)] }, - { "DimName5", this.dimensionValues[this.random.Next(0, 5)] }, - { "DimName6", this.dimensionValues[this.random.Next(0, 2)] }, - { "DimName7", this.dimensionValues[this.random.Next(0, 1)] }, - }; - this.histogram.Record(this.random.Next(MaxValue), tags); - } + { "DimName1", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName2", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName3", this.dimensionValues[this.random.Next(0, 5)] }, + { "DimName4", this.dimensionValues[this.random.Next(0, 5)] }, + { "DimName5", this.dimensionValues[this.random.Next(0, 5)] }, + { "DimName6", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName7", this.dimensionValues[this.random.Next(0, 1)] }, + }; + this.histogram.Record(this.random.Next(MaxValue), tags); } } diff --git a/test/Benchmarks/Metrics/MetricCollectBenchmarks.cs b/test/Benchmarks/Metrics/MetricCollectBenchmarks.cs index cc3bf7773e8..c0e0bdebb42 100644 --- a/test/Benchmarks/Metrics/MetricCollectBenchmarks.cs +++ b/test/Benchmarks/Metrics/MetricCollectBenchmarks.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Metrics; using BenchmarkDotNet.Attributes; @@ -21,107 +8,106 @@ using OpenTelemetry.Tests; /* -BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.23424.1000) +BenchmarkDotNet v0.13.10, Windows 11 (10.0.23424.1000) Intel Core i7-9700 CPU 3.00GHz, 1 CPU, 8 logical and 8 physical cores -.NET SDK=7.0.203 - [Host] : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2 - DefaultJob : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2 +.NET SDK 8.0.100 + [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 -| Method | UseWithRef | Mean | Error | StdDev | Allocated | +| Method | UseWithRef | Mean | Error | StdDev | Allocated | |-------- |----------- |---------:|---------:|---------:|----------:| -| Collect | False | 18.45 us | 0.161 us | 0.151 us | 96 B | -| Collect | True | 17.71 us | 0.347 us | 0.644 us | 96 B | +| Collect | False | 21.03 us | 0.148 us | 0.361 us | 96 B | +| Collect | True | 20.37 us | 0.399 us | 0.559 us | 96 B | */ -namespace Benchmarks.Metrics +namespace Benchmarks.Metrics; + +public class MetricCollectBenchmarks { - public class MetricCollectBenchmarks - { - private readonly string[] dimensionValues = new string[] { "DimVal1", "DimVal2", "DimVal3", "DimVal4", "DimVal5", "DimVal6", "DimVal7", "DimVal8", "DimVal9", "DimVal10" }; + private readonly string[] dimensionValues = new string[] { "DimVal1", "DimVal2", "DimVal3", "DimVal4", "DimVal5", "DimVal6", "DimVal7", "DimVal8", "DimVal9", "DimVal10" }; - // TODO: Confirm if this needs to be thread-safe - private readonly Random random = new(); - private Counter counter; - private MeterProvider provider; - private Meter meter; - private CancellationTokenSource token; - private BaseExportingMetricReader reader; - private Task writeMetricTask; + // TODO: Confirm if this needs to be thread-safe + private readonly Random random = new(); + private Counter counter; + private MeterProvider provider; + private Meter meter; + private CancellationTokenSource token; + private BaseExportingMetricReader reader; + private Task writeMetricTask; - [Params(false, true)] - public bool UseWithRef { get; set; } + [Params(false, true)] + public bool UseWithRef { get; set; } - [GlobalSetup] - public void Setup() + [GlobalSetup] + public void Setup() + { + var metricExporter = new TestExporter(ProcessExport); + void ProcessExport(Batch batch) { - var metricExporter = new TestExporter(ProcessExport); - void ProcessExport(Batch batch) + double sum = 0; + foreach (var metric in batch) { - double sum = 0; - foreach (var metric in batch) + if (this.UseWithRef) { - if (this.UseWithRef) + // The performant way of iterating. + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) { - // The performant way of iterating. - foreach (ref readonly var metricPoint in metric.GetMetricPoints()) - { - sum += metricPoint.GetSumDouble(); - } + sum += metricPoint.GetSumDouble(); } - else + } + else + { + // The non-performant way of iterating. + // This is still "correct", but less performant. + foreach (var metricPoint in metric.GetMetricPoints()) { - // The non-performant way of iterating. - // This is still "correct", but less performant. - foreach (var metricPoint in metric.GetMetricPoints()) - { - sum += metricPoint.GetSumDouble(); - } + sum += metricPoint.GetSumDouble(); } } } + } - this.reader = new BaseExportingMetricReader(metricExporter) - { - TemporalityPreference = MetricReaderTemporalityPreference.Cumulative, - }; + this.reader = new BaseExportingMetricReader(metricExporter) + { + TemporalityPreference = MetricReaderTemporalityPreference.Cumulative, + }; - this.meter = new Meter(Utils.GetCurrentMethodName()); + this.meter = new Meter(Utils.GetCurrentMethodName()); - this.provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(this.meter.Name) - .AddReader(this.reader) - .Build(); + this.provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(this.meter.Name) + .AddReader(this.reader) + .Build(); - this.counter = this.meter.CreateCounter("counter"); - this.token = new CancellationTokenSource(); - this.writeMetricTask = new Task(() => + this.counter = this.meter.CreateCounter("counter"); + this.token = new CancellationTokenSource(); + this.writeMetricTask = new Task(() => + { + while (!this.token.IsCancellationRequested) { - while (!this.token.IsCancellationRequested) - { - var tag1 = new KeyValuePair("DimName1", this.dimensionValues[this.random.Next(0, 10)]); - var tag2 = new KeyValuePair("DimName2", this.dimensionValues[this.random.Next(0, 10)]); - var tag3 = new KeyValuePair("DimName3", this.dimensionValues[this.random.Next(0, 10)]); - this.counter.Add(100.00, tag1, tag2, tag3); - } - }); - this.writeMetricTask.Start(); - } + var tag1 = new KeyValuePair("DimName1", this.dimensionValues[this.random.Next(0, 10)]); + var tag2 = new KeyValuePair("DimName2", this.dimensionValues[this.random.Next(0, 10)]); + var tag3 = new KeyValuePair("DimName3", this.dimensionValues[this.random.Next(0, 10)]); + this.counter.Add(100.00, tag1, tag2, tag3); + } + }); + this.writeMetricTask.Start(); + } - [GlobalCleanup] - public void Cleanup() - { - this.token.Cancel(); - this.token.Dispose(); - this.writeMetricTask.Wait(); - this.meter.Dispose(); - this.provider.Dispose(); - } + [GlobalCleanup] + public void Cleanup() + { + this.token.Cancel(); + this.token.Dispose(); + this.writeMetricTask.Wait(); + this.meter.Dispose(); + this.provider.Dispose(); + } - [Benchmark] - public void Collect() - { - this.reader.Collect(); - } + [Benchmark] + public void Collect() + { + this.reader.Collect(); } } diff --git a/test/Benchmarks/Metrics/MetricsBenchmarks.cs b/test/Benchmarks/Metrics/MetricsBenchmarks.cs index ca800268e12..e63aeb87576 100644 --- a/test/Benchmarks/Metrics/MetricsBenchmarks.cs +++ b/test/Benchmarks/Metrics/MetricsBenchmarks.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Diagnostics.Metrics; @@ -22,132 +9,288 @@ using OpenTelemetry.Tests; /* -BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.23424.1000) -Intel Core i7-9700 CPU 3.00GHz, 1 CPU, 8 logical and 8 physical cores -.NET SDK=7.0.203 - [Host] : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2 - DefaultJob : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2 - - -| Method | AggregationTemporality | Mean | Error | StdDev | Allocated | -|-------------------------- |----------------------- |----------:|---------:|---------:|----------:| -| CounterHotPath | Cumulative | 17.06 ns | 0.113 ns | 0.094 ns | - | -| CounterWith1LabelsHotPath | Cumulative | 71.47 ns | 1.464 ns | 2.100 ns | - | -| CounterWith3LabelsHotPath | Cumulative | 162.04 ns | 2.469 ns | 2.188 ns | - | -| CounterWith5LabelsHotPath | Cumulative | 237.30 ns | 2.884 ns | 2.698 ns | - | -| CounterWith6LabelsHotPath | Cumulative | 269.41 ns | 4.087 ns | 3.623 ns | - | -| CounterWith7LabelsHotPath | Cumulative | 303.01 ns | 5.313 ns | 4.970 ns | - | -| CounterHotPath | Delta | 17.30 ns | 0.350 ns | 0.310 ns | - | -| CounterWith1LabelsHotPath | Delta | 70.96 ns | 0.608 ns | 0.539 ns | - | -| CounterWith3LabelsHotPath | Delta | 156.55 ns | 3.139 ns | 3.358 ns | - | -| CounterWith5LabelsHotPath | Delta | 247.14 ns | 4.703 ns | 5.598 ns | - | -| CounterWith6LabelsHotPath | Delta | 271.30 ns | 5.310 ns | 5.215 ns | - | -| CounterWith7LabelsHotPath | Delta | 309.02 ns | 5.934 ns | 5.828 ns | - | +BenchmarkDotNet v0.13.10, Windows 11 (10.0.22621.3007/22H2/2022Update/SunValley2) +11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores +.NET SDK 8.0.101 + [Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 + DefaultJob : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 + + +| Method | AggregationTemporality | Mean | Error | StdDev | Gen0 | Allocated | +|-------------------------------------- |----------------------- |----------:|---------:|----------:|-------:|----------:| +| CounterHotPath | Cumulative | 11.27 ns | 0.173 ns | 0.145 ns | - | - | +| CounterWith1LabelsHotPath | Cumulative | 43.72 ns | 0.266 ns | 0.248 ns | - | - | +| CounterWith2LabelsHotPath | Cumulative | 65.90 ns | 1.334 ns | 1.248 ns | - | - | +| CounterWith3LabelsHotPath | Cumulative | 88.97 ns | 1.785 ns | 1.984 ns | - | - | +| CounterWith4LabelsHotPath | Cumulative | 122.00 ns | 2.131 ns | 1.994 ns | 0.0138 | 88 B | +| CounterWith5LabelsHotPath | Cumulative | 146.73 ns | 2.557 ns | 2.392 ns | 0.0165 | 104 B | +| CounterWith6LabelsHotPath | Cumulative | 163.22 ns | 2.136 ns | 1.783 ns | 0.0191 | 120 B | +| CounterWith7LabelsHotPath | Cumulative | 184.53 ns | 1.324 ns | 1.238 ns | 0.0215 | 136 B | +| CounterWith1LabelsHotPathUsingTagList | Cumulative | 58.27 ns | 1.157 ns | 1.504 ns | - | - | +| CounterWith2LabelsHotPathUsingTagList | Cumulative | 92.87 ns | 0.550 ns | 0.488 ns | - | - | +| CounterWith3LabelsHotPathUsingTagList | Cumulative | 120.31 ns | 0.739 ns | 0.617 ns | - | - | +| CounterWith4LabelsHotPathUsingTagList | Cumulative | 132.98 ns | 2.181 ns | 2.240 ns | - | - | +| CounterWith5LabelsHotPathUsingTagList | Cumulative | 154.56 ns | 2.685 ns | 2.380 ns | - | - | +| CounterWith6LabelsHotPathUsingTagList | Cumulative | 171.36 ns | 2.738 ns | 2.286 ns | - | - | +| CounterWith7LabelsHotPathUsingTagList | Cumulative | 194.81 ns | 1.894 ns | 1.582 ns | - | - | +| CounterWith8LabelsHotPathUsingTagList | Cumulative | 214.39 ns | 1.339 ns | 1.187 ns | - | - | +| CounterWith9LabelsHotPathUsingTagList | Cumulative | 300.38 ns | 3.945 ns | 3.690 ns | 0.0710 | 448 B | +| CounterHotPath | Delta | 14.11 ns | 0.257 ns | 0.228 ns | - | - | +| CounterWith1LabelsHotPath | Delta | 49.15 ns | 0.295 ns | 0.246 ns | - | - | +| CounterWith2LabelsHotPath | Delta | 68.99 ns | 0.477 ns | 0.398 ns | - | - | +| CounterWith3LabelsHotPath | Delta | 93.35 ns | 1.294 ns | 1.080 ns | - | - | +| CounterWith4LabelsHotPath | Delta | 141.40 ns | 2.846 ns | 6.539 ns | 0.0138 | 88 B | +| CounterWith5LabelsHotPath | Delta | 163.34 ns | 3.189 ns | 3.917 ns | 0.0165 | 104 B | +| CounterWith6LabelsHotPath | Delta | 181.62 ns | 3.582 ns | 4.125 ns | 0.0191 | 120 B | +| CounterWith7LabelsHotPath | Delta | 201.33 ns | 2.700 ns | 2.108 ns | 0.0215 | 136 B | +| CounterWith1LabelsHotPathUsingTagList | Delta | 75.56 ns | 1.457 ns | 1.496 ns | - | - | +| CounterWith2LabelsHotPathUsingTagList | Delta | 91.48 ns | 1.852 ns | 2.714 ns | - | - | +| CounterWith3LabelsHotPathUsingTagList | Delta | 129.23 ns | 2.608 ns | 3.298 ns | - | - | +| CounterWith4LabelsHotPathUsingTagList | Delta | 150.55 ns | 2.433 ns | 2.498 ns | - | - | +| CounterWith5LabelsHotPathUsingTagList | Delta | 191.60 ns | 3.119 ns | 2.918 ns | - | - | +| CounterWith6LabelsHotPathUsingTagList | Delta | 196.49 ns | 2.874 ns | 2.400 ns | - | - | +| CounterWith7LabelsHotPathUsingTagList | Delta | 224.42 ns | 4.482 ns | 8.196 ns | - | - | +| CounterWith8LabelsHotPathUsingTagList | Delta | 243.75 ns | 4.861 ns | 9.482 ns | - | - | +| CounterWith9LabelsHotPathUsingTagList | Delta | 331.22 ns | 6.493 ns | 11.373 ns | 0.0710 | 448 B | */ -namespace Benchmarks.Metrics +namespace Benchmarks.Metrics; + +public class MetricsBenchmarks { - public class MetricsBenchmarks + private readonly Random random = new(); + private readonly string[] dimensionValues = new string[] { "DimVal1", "DimVal2", "DimVal3", "DimVal4", "DimVal5", "DimVal6", "DimVal7", "DimVal8", "DimVal9", "DimVal10" }; + private Counter counter; + private MeterProvider meterProvider; + private Meter meter; + + [Params(MetricReaderTemporalityPreference.Cumulative, MetricReaderTemporalityPreference.Delta)] + public MetricReaderTemporalityPreference AggregationTemporality { get; set; } + + [GlobalSetup] + public void Setup() + { + this.meter = new Meter(Utils.GetCurrentMethodName()); + + var exportedItems = new List(); + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(this.meter.Name) // All instruments from this meter are enabled. + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 1000; + metricReaderOptions.TemporalityPreference = this.AggregationTemporality; + }) + .Build(); + + this.counter = this.meter.CreateCounter("counter"); + } + + [GlobalCleanup] + public void Cleanup() { - private readonly Random random = new(); - private readonly string[] dimensionValues = new string[] { "DimVal1", "DimVal2", "DimVal3", "DimVal4", "DimVal5", "DimVal6", "DimVal7", "DimVal8", "DimVal9", "DimVal10" }; - private Counter counter; - private MeterProvider provider; - private Meter meter; + this.meter?.Dispose(); + this.meterProvider.Dispose(); + } - [Params(MetricReaderTemporalityPreference.Cumulative, MetricReaderTemporalityPreference.Delta)] - public MetricReaderTemporalityPreference AggregationTemporality { get; set; } + [Benchmark] + public void CounterHotPath() + { + this.counter.Add(100); + } - [GlobalSetup] - public void Setup() + [Benchmark] + public void CounterWith1LabelsHotPath() + { + var tag1 = new KeyValuePair("DimName1", this.dimensionValues[this.random.Next(0, 10)]); + this.counter.Add(100, tag1); + } + + [Benchmark] + public void CounterWith2LabelsHotPath() + { + var tag1 = new KeyValuePair("DimName1", this.dimensionValues[this.random.Next(0, 10)]); + var tag2 = new KeyValuePair("DimName2", this.dimensionValues[this.random.Next(0, 10)]); + this.counter.Add(100, tag1, tag2); + } + + [Benchmark] + public void CounterWith3LabelsHotPath() + { + var tag1 = new KeyValuePair("DimName1", this.dimensionValues[this.random.Next(0, 10)]); + var tag2 = new KeyValuePair("DimName2", this.dimensionValues[this.random.Next(0, 10)]); + var tag3 = new KeyValuePair("DimName3", this.dimensionValues[this.random.Next(0, 10)]); + this.counter.Add(100, tag1, tag2, tag3); + } + + [Benchmark] + public void CounterWith4LabelsHotPath() + { + var tag1 = new KeyValuePair("DimName1", this.dimensionValues[this.random.Next(0, 2)]); + var tag2 = new KeyValuePair("DimName2", this.dimensionValues[this.random.Next(0, 5)]); + var tag3 = new KeyValuePair("DimName3", this.dimensionValues[this.random.Next(0, 10)]); + var tag4 = new KeyValuePair("DimName4", this.dimensionValues[this.random.Next(0, 10)]); + this.counter.Add(100, tag1, tag2, tag3, tag4); + } + + [Benchmark] + public void CounterWith5LabelsHotPath() + { + var tag1 = new KeyValuePair("DimName1", this.dimensionValues[this.random.Next(0, 2)]); + var tag2 = new KeyValuePair("DimName2", this.dimensionValues[this.random.Next(0, 2)]); + var tag3 = new KeyValuePair("DimName3", this.dimensionValues[this.random.Next(0, 5)]); + var tag4 = new KeyValuePair("DimName4", this.dimensionValues[this.random.Next(0, 5)]); + var tag5 = new KeyValuePair("DimName5", this.dimensionValues[this.random.Next(0, 10)]); + this.counter.Add(100, tag1, tag2, tag3, tag4, tag5); + } + + [Benchmark] + public void CounterWith6LabelsHotPath() + { + var tag1 = new KeyValuePair("DimName1", this.dimensionValues[this.random.Next(0, 2)]); + var tag2 = new KeyValuePair("DimName2", this.dimensionValues[this.random.Next(0, 2)]); + var tag3 = new KeyValuePair("DimName3", this.dimensionValues[this.random.Next(0, 2)]); + var tag4 = new KeyValuePair("DimName4", this.dimensionValues[this.random.Next(0, 5)]); + var tag5 = new KeyValuePair("DimName5", this.dimensionValues[this.random.Next(0, 5)]); + var tag6 = new KeyValuePair("DimName6", this.dimensionValues[this.random.Next(0, 5)]); + this.counter.Add(100, tag1, tag2, tag3, tag4, tag5, tag6); + } + + [Benchmark] + public void CounterWith7LabelsHotPath() + { + var tag1 = new KeyValuePair("DimName1", this.dimensionValues[this.random.Next(0, 1)]); + var tag2 = new KeyValuePair("DimName2", this.dimensionValues[this.random.Next(0, 2)]); + var tag3 = new KeyValuePair("DimName3", this.dimensionValues[this.random.Next(0, 2)]); + var tag4 = new KeyValuePair("DimName4", this.dimensionValues[this.random.Next(0, 2)]); + var tag5 = new KeyValuePair("DimName5", this.dimensionValues[this.random.Next(0, 5)]); + var tag6 = new KeyValuePair("DimName6", this.dimensionValues[this.random.Next(0, 5)]); + var tag7 = new KeyValuePair("DimName7", this.dimensionValues[this.random.Next(0, 5)]); + this.counter.Add(100, tag1, tag2, tag3, tag4, tag5, tag6, tag7); + } + + [Benchmark] + public void CounterWith1LabelsHotPathUsingTagList() + { + var tags = new TagList { - this.meter = new Meter(Utils.GetCurrentMethodName()); - - var exportedItems = new List(); - this.provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(this.meter.Name) // All instruments from this meter are enabled. - .AddInMemoryExporter(exportedItems, metricReaderOptions => - { - metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 1000; - metricReaderOptions.TemporalityPreference = this.AggregationTemporality; - }) - .Build(); - - this.counter = this.meter.CreateCounter("counter"); - } - - [GlobalCleanup] - public void Cleanup() + { "DimName1", this.dimensionValues[this.random.Next(0, 10)] }, + }; + this.counter.Add(100, tags); + } + + [Benchmark] + public void CounterWith2LabelsHotPathUsingTagList() + { + var tags = new TagList { - this.meter?.Dispose(); - this.provider?.Dispose(); - } + { "DimName1", this.dimensionValues[this.random.Next(0, 10)] }, + { "DimName2", this.dimensionValues[this.random.Next(0, 10)] }, + }; + this.counter.Add(100, tags); + } - [Benchmark] - public void CounterHotPath() + [Benchmark] + public void CounterWith3LabelsHotPathUsingTagList() + { + var tags = new TagList { - this.counter.Add(100); - } + { "DimName1", this.dimensionValues[this.random.Next(0, 10)] }, + { "DimName2", this.dimensionValues[this.random.Next(0, 10)] }, + { "DimName3", this.dimensionValues[this.random.Next(0, 10)] }, + }; + this.counter.Add(100, tags); + } + + [Benchmark] + public void CounterWith4LabelsHotPathUsingTagList() + { + var tags = new TagList + { + { "DimName1", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName2", this.dimensionValues[this.random.Next(0, 5)] }, + { "DimName3", this.dimensionValues[this.random.Next(0, 10)] }, + { "DimName4", this.dimensionValues[this.random.Next(0, 10)] }, + }; + this.counter.Add(100, tags); + } - [Benchmark] - public void CounterWith1LabelsHotPath() + [Benchmark] + public void CounterWith5LabelsHotPathUsingTagList() + { + var tags = new TagList { - var tag1 = new KeyValuePair("DimName1", this.dimensionValues[this.random.Next(0, 2)]); - this.counter.Add(100, tag1); - } + { "DimName1", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName2", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName3", this.dimensionValues[this.random.Next(0, 5)] }, + { "DimName4", this.dimensionValues[this.random.Next(0, 5)] }, + { "DimName5", this.dimensionValues[this.random.Next(0, 10)] }, + }; + this.counter.Add(100, tags); + } - [Benchmark] - public void CounterWith3LabelsHotPath() + [Benchmark] + public void CounterWith6LabelsHotPathUsingTagList() + { + var tags = new TagList { - var tag1 = new KeyValuePair("DimName1", this.dimensionValues[this.random.Next(0, 10)]); - var tag2 = new KeyValuePair("DimName2", this.dimensionValues[this.random.Next(0, 10)]); - var tag3 = new KeyValuePair("DimName3", this.dimensionValues[this.random.Next(0, 10)]); - this.counter.Add(100, tag1, tag2, tag3); - } - - [Benchmark] - public void CounterWith5LabelsHotPath() + { "DimName1", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName2", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName3", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName4", this.dimensionValues[this.random.Next(0, 5)] }, + { "DimName5", this.dimensionValues[this.random.Next(0, 5)] }, + { "DimName6", this.dimensionValues[this.random.Next(0, 5)] }, + }; + this.counter.Add(100, tags); + } + + [Benchmark] + public void CounterWith7LabelsHotPathUsingTagList() + { + var tags = new TagList { - var tags = new TagList - { - { "DimName1", this.dimensionValues[this.random.Next(0, 2)] }, - { "DimName2", this.dimensionValues[this.random.Next(0, 2)] }, - { "DimName3", this.dimensionValues[this.random.Next(0, 5)] }, - { "DimName4", this.dimensionValues[this.random.Next(0, 5)] }, - { "DimName5", this.dimensionValues[this.random.Next(0, 10)] }, - }; - this.counter.Add(100, tags); - } - - [Benchmark] - public void CounterWith6LabelsHotPath() + { "DimName1", this.dimensionValues[this.random.Next(0, 1)] }, + { "DimName2", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName3", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName4", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName5", this.dimensionValues[this.random.Next(0, 5)] }, + { "DimName6", this.dimensionValues[this.random.Next(0, 5)] }, + { "DimName7", this.dimensionValues[this.random.Next(0, 5)] }, + }; + this.counter.Add(100, tags); + } + + [Benchmark] + public void CounterWith8LabelsHotPathUsingTagList() + { + var tags = new TagList { - var tags = new TagList - { - { "DimName1", this.dimensionValues[this.random.Next(0, 2)] }, - { "DimName2", this.dimensionValues[this.random.Next(0, 2)] }, - { "DimName3", this.dimensionValues[this.random.Next(0, 5)] }, - { "DimName4", this.dimensionValues[this.random.Next(0, 5)] }, - { "DimName5", this.dimensionValues[this.random.Next(0, 5)] }, - { "DimName6", this.dimensionValues[this.random.Next(0, 2)] }, - }; - this.counter.Add(100, tags); - } - - [Benchmark] - public void CounterWith7LabelsHotPath() + { "DimName1", this.dimensionValues[this.random.Next(0, 1)] }, + { "DimName2", this.dimensionValues[this.random.Next(0, 1)] }, + { "DimName3", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName4", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName5", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName6", this.dimensionValues[this.random.Next(0, 5)] }, + { "DimName7", this.dimensionValues[this.random.Next(0, 5)] }, + { "DimName8", this.dimensionValues[this.random.Next(0, 5)] }, + }; + this.counter.Add(100, tags); + } + + [Benchmark] + public void CounterWith9LabelsHotPathUsingTagList() + { + var tags = new TagList { - var tags = new TagList - { - { "DimName1", this.dimensionValues[this.random.Next(0, 2)] }, - { "DimName2", this.dimensionValues[this.random.Next(0, 2)] }, - { "DimName3", this.dimensionValues[this.random.Next(0, 5)] }, - { "DimName4", this.dimensionValues[this.random.Next(0, 5)] }, - { "DimName5", this.dimensionValues[this.random.Next(0, 5)] }, - { "DimName6", this.dimensionValues[this.random.Next(0, 2)] }, - { "DimName7", this.dimensionValues[this.random.Next(0, 1)] }, - }; - this.counter.Add(100, tags); - } + { "DimName1", this.dimensionValues[this.random.Next(0, 1)] }, + { "DimName2", this.dimensionValues[this.random.Next(0, 1)] }, + { "DimName3", this.dimensionValues[this.random.Next(0, 1)] }, + { "DimName4", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName5", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName6", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName7", this.dimensionValues[this.random.Next(0, 5)] }, + { "DimName8", this.dimensionValues[this.random.Next(0, 5)] }, + { "DimName9", this.dimensionValues[this.random.Next(0, 5)] }, + }; + this.counter.Add(100, tags); } } diff --git a/test/Benchmarks/Metrics/MetricsViewBenchmarks.cs b/test/Benchmarks/Metrics/MetricsViewBenchmarks.cs index 6b762defb2e..359313ac9bf 100644 --- a/test/Benchmarks/Metrics/MetricsViewBenchmarks.cs +++ b/test/Benchmarks/Metrics/MetricsViewBenchmarks.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Diagnostics.Metrics; @@ -22,146 +9,145 @@ using OpenTelemetry.Tests; /* -BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.23424.1000) +BenchmarkDotNet v0.13.10, Windows 11 (10.0.23424.1000) Intel Core i7-9700 CPU 3.00GHz, 1 CPU, 8 logical and 8 physical cores -.NET SDK=7.0.203 - [Host] : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2 - DefaultJob : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2 - - -| Method | ViewConfig | Mean | Error | StdDev | Allocated | -|--------------- |------------- |---------:|--------:|--------:|----------:| -| CounterHotPath | NoView | 254.6 ns | 1.62 ns | 1.27 ns | - | -| CounterHotPath | ViewNA | 257.9 ns | 3.00 ns | 2.80 ns | - | -| CounterHotPath | ViewApplied | 282.7 ns | 5.60 ns | 6.45 ns | - | -| CounterHotPath | ViewToRename | 256.8 ns | 0.83 ns | 0.70 ns | - | -| CounterHotPath | ViewZeroTag | 112.1 ns | 1.87 ns | 1.75 ns | - | +.NET SDK 8.0.100 + [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + + +| Method | ViewConfig | Mean | Error | StdDev | Allocated | +|--------------- |------------- |----------:|---------:|---------:|----------:| +| CounterHotPath | NoView | 217.94 ns | 3.950 ns | 3.502 ns | - | +| CounterHotPath | ViewNA | 206.09 ns | 1.634 ns | 1.364 ns | - | +| CounterHotPath | ViewApplied | 210.63 ns | 4.116 ns | 5.904 ns | - | +| CounterHotPath | ViewToRename | 207.05 ns | 1.592 ns | 1.329 ns | - | +| CounterHotPath | ViewZeroTag | 68.67 ns | 0.613 ns | 0.573 ns | - | */ -namespace Benchmarks.Metrics +namespace Benchmarks.Metrics; + +public class MetricsViewBenchmarks { - public class MetricsViewBenchmarks + private static readonly ThreadLocal ThreadLocalRandom = new(() => new Random()); + private static readonly string[] DimensionValues = new string[] { "DimVal1", "DimVal2", "DimVal3", "DimVal4", "DimVal5", "DimVal6", "DimVal7", "DimVal8", "DimVal9", "DimVal10" }; + private static readonly int DimensionsValuesLength = DimensionValues.Length; + private List metrics; + private Counter counter; + private MeterProvider meterProvider; + private Meter meter; + + public enum ViewConfiguration { - private static readonly ThreadLocal ThreadLocalRandom = new(() => new Random()); - private static readonly string[] DimensionValues = new string[] { "DimVal1", "DimVal2", "DimVal3", "DimVal4", "DimVal5", "DimVal6", "DimVal7", "DimVal8", "DimVal9", "DimVal10" }; - private static readonly int DimensionsValuesLength = DimensionValues.Length; - private List metrics; - private Counter counter; - private MeterProvider provider; - private Meter meter; - - public enum ViewConfiguration - { - /// - /// No views registered in the provider. - /// - NoView, - - /// - /// Provider has view registered, but it doesn't select the instrument. - /// This tests the perf impact View has on hot path, for those - /// instruments not participating in View feature. - /// - ViewNA, - - /// - /// Provider has view registered and it does select the instrument - /// and keeps the subset of tags. - /// - ViewApplied, - - /// - /// Provider has view registered and it does select the instrument - /// and renames. - /// - ViewToRename, - - /// - /// Provider has view registered and it does select the instrument - /// and drops every tag. - /// - ViewZeroTag, - } + /// + /// No views registered in the provider. + /// + NoView, + + /// + /// Provider has view registered, but it doesn't select the instrument. + /// This tests the perf impact View has on hot path, for those + /// instruments not participating in View feature. + /// + ViewNA, + + /// + /// Provider has view registered and it does select the instrument + /// and keeps the subset of tags. + /// + ViewApplied, + + /// + /// Provider has view registered and it does select the instrument + /// and renames. + /// + ViewToRename, + + /// + /// Provider has view registered and it does select the instrument + /// and drops every tag. + /// + ViewZeroTag, + } + + [Params( + ViewConfiguration.NoView, + ViewConfiguration.ViewNA, + ViewConfiguration.ViewApplied, + ViewConfiguration.ViewToRename, + ViewConfiguration.ViewZeroTag)] + public ViewConfiguration ViewConfig { get; set; } - [Params( - ViewConfiguration.NoView, - ViewConfiguration.ViewNA, - ViewConfiguration.ViewApplied, - ViewConfiguration.ViewToRename, - ViewConfiguration.ViewZeroTag)] - public ViewConfiguration ViewConfig { get; set; } + [GlobalSetup] + public void Setup() + { + this.meter = new Meter(Utils.GetCurrentMethodName()); + this.counter = this.meter.CreateCounter("counter"); + this.metrics = new List(); - [GlobalSetup] - public void Setup() + if (this.ViewConfig == ViewConfiguration.NoView) { - this.meter = new Meter(Utils.GetCurrentMethodName()); - this.counter = this.meter.CreateCounter("counter"); - this.metrics = new List(); - - if (this.ViewConfig == ViewConfiguration.NoView) - { - this.provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(this.meter.Name) - .AddInMemoryExporter(this.metrics) - .Build(); - } - else if (this.ViewConfig == ViewConfiguration.ViewNA) - { - this.provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(this.meter.Name) - .AddView("nomatch", new MetricStreamConfiguration() { TagKeys = new string[] { "DimName1", "DimName2", "DimName3" } }) - .AddInMemoryExporter(this.metrics) - .Build(); - } - else if (this.ViewConfig == ViewConfiguration.ViewApplied) - { - this.provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(this.meter.Name) - .AddView(this.counter.Name, new MetricStreamConfiguration() { TagKeys = new string[] { "DimName1", "DimName2", "DimName3" } }) - .AddInMemoryExporter(this.metrics) - .Build(); - } - else if (this.ViewConfig == ViewConfiguration.ViewToRename) - { - this.provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(this.meter.Name) - .AddView(this.counter.Name, "newname") - .AddInMemoryExporter(this.metrics) - .Build(); - } - else if (this.ViewConfig == ViewConfiguration.ViewZeroTag) - { - this.provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(this.meter.Name) - .AddView(this.counter.Name, new MetricStreamConfiguration() { TagKeys = Array.Empty() }) - .AddInMemoryExporter(this.metrics) - .Build(); - } + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(this.meter.Name) + .AddInMemoryExporter(this.metrics) + .Build(); } - - [GlobalCleanup] - public void Cleanup() + else if (this.ViewConfig == ViewConfiguration.ViewNA) { - this.meter?.Dispose(); - this.provider?.Dispose(); + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(this.meter.Name) + .AddView("nomatch", new MetricStreamConfiguration() { TagKeys = new string[] { "DimName1", "DimName2", "DimName3" } }) + .AddInMemoryExporter(this.metrics) + .Build(); } - - [Benchmark] - public void CounterHotPath() + else if (this.ViewConfig == ViewConfiguration.ViewApplied) + { + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(this.meter.Name) + .AddView(this.counter.Name, new MetricStreamConfiguration() { TagKeys = new string[] { "DimName1", "DimName2", "DimName3" } }) + .AddInMemoryExporter(this.metrics) + .Build(); + } + else if (this.ViewConfig == ViewConfiguration.ViewToRename) { - var random = ThreadLocalRandom.Value; - var tags = new TagList - { - { "DimName1", DimensionValues[random.Next(0, 2)] }, - { "DimName2", DimensionValues[random.Next(0, 2)] }, - { "DimName3", DimensionValues[random.Next(0, 5)] }, - { "DimName4", DimensionValues[random.Next(0, 5)] }, - { "DimName5", DimensionValues[random.Next(0, 10)] }, - }; - - this.counter?.Add( - 100, - tags); + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(this.meter.Name) + .AddView(this.counter.Name, "newname") + .AddInMemoryExporter(this.metrics) + .Build(); } + else if (this.ViewConfig == ViewConfiguration.ViewZeroTag) + { + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(this.meter.Name) + .AddView(this.counter.Name, new MetricStreamConfiguration() { TagKeys = Array.Empty() }) + .AddInMemoryExporter(this.metrics) + .Build(); + } + } + + [GlobalCleanup] + public void Cleanup() + { + this.meter?.Dispose(); + this.meterProvider.Dispose(); + } + + [Benchmark] + public void CounterHotPath() + { + var random = ThreadLocalRandom.Value; + var tags = new TagList + { + { "DimName1", DimensionValues[random.Next(0, 2)] }, + { "DimName2", DimensionValues[random.Next(0, 2)] }, + { "DimName3", DimensionValues[random.Next(0, 5)] }, + { "DimName4", DimensionValues[random.Next(0, 5)] }, + { "DimName5", DimensionValues[random.Next(0, 10)] }, + }; + + this.counter?.Add( + 100, + tags); } } diff --git a/test/Benchmarks/Program.cs b/test/Benchmarks/Program.cs index bc6cd0d3b4f..91a947733ca 100644 --- a/test/Benchmarks/Program.cs +++ b/test/Benchmarks/Program.cs @@ -1,28 +1,14 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using BenchmarkDotNet.Running; -namespace OpenTelemetry.Benchmarks +namespace OpenTelemetry.Benchmarks; + +internal static class Program { - internal static class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); - } + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); } } 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/Benchmarks/SuppressInstrumentationScopeBenchmarks.cs b/test/Benchmarks/SuppressInstrumentationScopeBenchmarks.cs index e5a89c8ac48..b99b20c23d5 100644 --- a/test/Benchmarks/SuppressInstrumentationScopeBenchmarks.cs +++ b/test/Benchmarks/SuppressInstrumentationScopeBenchmarks.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using BenchmarkDotNet.Attributes; diff --git a/test/Benchmarks/TestExporter.cs b/test/Benchmarks/TestExporter.cs deleted file mode 100644 index ae2336cad5f..00000000000 --- a/test/Benchmarks/TestExporter.cs +++ /dev/null @@ -1,36 +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. -// - -namespace OpenTelemetry.Tests -{ - internal class TestExporter : BaseExporter - where T : class - { - private readonly Action> processBatchAction; - - public TestExporter(Action> processBatchAction) - { - this.processBatchAction = processBatchAction ?? throw new ArgumentNullException(nameof(processBatchAction)); - } - - public override ExportResult Export(in Batch batch) - { - this.processBatchAction(batch); - - return ExportResult.Success; - } - } -} diff --git a/test/Benchmarks/TestTraceServiceClient.cs b/test/Benchmarks/TestTraceServiceClient.cs new file mode 100644 index 00000000000..968ddd75715 --- /dev/null +++ b/test/Benchmarks/TestTraceServiceClient.cs @@ -0,0 +1,17 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +extern alias OpenTelemetryProtocol; + +using Grpc.Core; +using OpenTelemetryProtocol::OpenTelemetry.Proto.Collector.Trace.V1; + +namespace Benchmarks; + +internal class TestTraceServiceClient : TraceService.TraceServiceClient +{ + public override ExportTraceServiceResponse Export(ExportTraceServiceRequest request, Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default) + { + return new ExportTraceServiceResponse(); + } +} diff --git a/test/Benchmarks/Trace/ActivityCreationBenchmarks.cs b/test/Benchmarks/Trace/ActivityCreationBenchmarks.cs new file mode 100644 index 00000000000..447e550bd87 --- /dev/null +++ b/test/Benchmarks/Trace/ActivityCreationBenchmarks.cs @@ -0,0 +1,65 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using BenchmarkDotNet.Attributes; +using Benchmarks.Helper; +using OpenTelemetry; +using OpenTelemetry.Trace; + +/* +BenchmarkDotNet v0.13.10, Windows 11 (10.0.22621.2861) +11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores +.NET SDK 8.0.100 + [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + + +| Method | Mean | Error | StdDev | Median | +|----------------------------------------------- |----------:|---------:|----------:|----------:| +| CreateActivity_NoopProcessor | 247.22 ns | 4.977 ns | 13.198 ns | 240.34 ns | +| CreateActivity_WithParentContext_NoopProcessor | 55.17 ns | 1.131 ns | 1.111 ns | 54.98 ns | +| CreateActivity_WithSetTags_NoopProcessor | 375.2 ns | 7.52 ns | 18.44 ns | 370.4 ns | +| CreateActivity_WithAddTags_NoopProcessor | 340.9 ns | 6.27 ns | 12.81 ns | 336.1 ns | +*/ + +namespace Benchmarks.Trace; + +public class ActivityCreationBenchmarks +{ + private readonly ActivitySource benchmarkSource = new("Benchmark"); + private readonly ActivityContext parentCtx = new(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None); + private TracerProvider tracerProvider; + + [GlobalSetup] + public void GlobalSetup() + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("BenchMark") + .AddProcessor(new NoopActivityProcessor()) + .Build(); + } + + [GlobalCleanup] + public void GlobalCleanup() + { + this.tracerProvider.Dispose(); + this.benchmarkSource.Dispose(); + } + + [Benchmark] + public void CreateActivity_NoopProcessor() => ActivityCreationScenarios.CreateActivity(this.benchmarkSource); + + [Benchmark] + public void CreateActivity_WithParentContext_NoopProcessor() => ActivityCreationScenarios.CreateActivityFromParentContext(this.benchmarkSource, this.parentCtx); + + [Benchmark] + public void CreateActivity_WithSetTags_NoopProcessor() => ActivityCreationScenarios.CreateActivityWithSetTags(this.benchmarkSource); + + [Benchmark] + public void CreateActivity_WithAddTags_NoopProcessor() => ActivityCreationScenarios.CreateActivityWithAddTags(this.benchmarkSource); + + internal class NoopActivityProcessor : BaseProcessor + { + } +} diff --git a/test/Benchmarks/Trace/OpenTelemetrySdkBenchmarks.cs b/test/Benchmarks/Trace/OpenTelemetrySdkBenchmarks.cs deleted file mode 100644 index 9fc039e3f08..00000000000 --- a/test/Benchmarks/Trace/OpenTelemetrySdkBenchmarks.cs +++ /dev/null @@ -1,89 +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 BenchmarkDotNet.Attributes; -using Benchmarks.Helper; -using OpenTelemetry; -using OpenTelemetry.Trace; - -namespace Benchmarks.Trace -{ - public class OpenTelemetrySdkBenchmarks - { - private Tracer alwaysSampleTracer; - private Tracer neverSampleTracer; - private Tracer noopTracer; - private TracerProvider tracerProviderAlwaysOnSample; - private TracerProvider tracerProviderAlwaysOffSample; - - [GlobalSetup] - public void GlobalSetup() - { - this.tracerProviderAlwaysOnSample = Sdk.CreateTracerProviderBuilder() - .AddSource("AlwaysOnSample") - .SetSampler(new AlwaysOnSampler()) - .Build(); - - this.tracerProviderAlwaysOffSample = Sdk.CreateTracerProviderBuilder() - .AddSource("AlwaysOffSample") - .SetSampler(new AlwaysOffSampler()) - .Build(); - - using var traceProviderNoop = Sdk.CreateTracerProviderBuilder().Build(); - - this.alwaysSampleTracer = TracerProvider.Default.GetTracer("AlwaysOnSample"); - this.neverSampleTracer = TracerProvider.Default.GetTracer("AlwaysOffSample"); - this.noopTracer = TracerProvider.Default.GetTracer("Noop"); - } - - [GlobalCleanup] - public void GlobalCleanup() - { - this.tracerProviderAlwaysOffSample.Dispose(); - this.tracerProviderAlwaysOnSample.Dispose(); - } - - [Benchmark] - public void CreateSpan_Sampled() => SpanCreationScenarios.CreateSpan(this.alwaysSampleTracer); - - [Benchmark] - public void CreateSpan_ParentContext() => SpanCreationScenarios.CreateSpan_ParentContext(this.alwaysSampleTracer); - - [Benchmark] - public void CreateSpan_Attributes_Sampled() => SpanCreationScenarios.CreateSpan_Attributes(this.alwaysSampleTracer); - - [Benchmark] - public void CreateSpan_WithSpan() => SpanCreationScenarios.CreateSpan_Propagate(this.alwaysSampleTracer); - - [Benchmark] - public void CreateSpan_Active() => SpanCreationScenarios.CreateSpan_Active(this.alwaysSampleTracer); - - [Benchmark] - public void CreateSpan_Active_GetCurrent() => SpanCreationScenarios.CreateSpan_Active_GetCurrent(this.alwaysSampleTracer); - - [Benchmark] - public void CreateSpan_Attributes_NotSampled() => SpanCreationScenarios.CreateSpan_Attributes(this.neverSampleTracer); - - [Benchmark(Baseline = true)] - public void CreateSpan_Noop() => SpanCreationScenarios.CreateSpan(this.noopTracer); - - [Benchmark] - public void CreateSpan_Attributes_Noop() => SpanCreationScenarios.CreateSpan_Attributes(this.noopTracer); - - [Benchmark] - public void CreateSpan_Propagate_Noop() => SpanCreationScenarios.CreateSpan_Propagate(this.noopTracer); - } -} diff --git a/test/Benchmarks/Trace/OpenTelemetrySdkBenchmarksActivity.cs b/test/Benchmarks/Trace/OpenTelemetrySdkBenchmarksActivity.cs deleted file mode 100644 index c0f2e11a338..00000000000 --- a/test/Benchmarks/Trace/OpenTelemetrySdkBenchmarksActivity.cs +++ /dev/null @@ -1,81 +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 BenchmarkDotNet.Attributes; -using Benchmarks.Helper; -using OpenTelemetry; -using OpenTelemetry.Trace; - -/* -// * Summary * - -BenchmarkDotNet=v0.13.2, OS=Windows 10 (10.0.19044.2130/21H2/November2021Update) -Intel Core i7-4790 CPU 3.60GHz (Haswell), 1 CPU, 8 logical and 4 physical cores -.NET SDK=7.0.100-preview.7.22377.5 - [Host] : .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2 - DefaultJob : .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2 - - -| Method | Mean | Error | StdDev | Gen0 | Allocated | -|--------------------------------------------------------- |---------:|--------:|--------:|-------:|----------:| -| CreateActivity_NoopProcessor | 457.9 ns | 1.39 ns | 1.23 ns | 0.0992 | 416 B | -| CreateActivity_WithParentContext_NoopProcessor | 104.8 ns | 0.43 ns | 0.36 ns | - | - | -| CreateActivity_WithParentId_NoopProcessor | 221.9 ns | 0.44 ns | 0.39 ns | 0.0343 | 144 B | -| CreateActivity_WithAttributes_NoopProcessor | 541.4 ns | 3.32 ns | 2.94 ns | 0.1488 | 624 B | -| CreateActiviti_WithKind_NoopProcessor | 437.5 ns | 2.05 ns | 1.92 ns | 0.0992 | 416 B | -*/ - -namespace Benchmarks.Trace -{ - public class OpenTelemetrySdkBenchmarksActivity - { - private readonly ActivitySource benchmarkSource = new("Benchmark"); - private readonly ActivityContext parentCtx = new(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None); - private readonly string parentId = $"00-{ActivityTraceId.CreateRandom()}.{ActivitySpanId.CreateRandom()}.00"; - private TracerProvider tracerProvider; - - [GlobalSetup] - public void GlobalSetup() - { - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("BenchMark") - .Build(); - } - - [GlobalCleanup] - public void GlobalCleanup() - { - this.tracerProvider.Dispose(); - this.benchmarkSource.Dispose(); - } - - [Benchmark] - public void CreateActivity_NoopProcessor() => ActivityCreationScenarios.CreateActivity(this.benchmarkSource); - - [Benchmark] - public void CreateActivity_WithParentContext_NoopProcessor() => ActivityCreationScenarios.CreateActivityFromParentContext(this.benchmarkSource, this.parentCtx); - - [Benchmark] - public void CreateActivity_WithParentId_NoopProcessor() => ActivityCreationScenarios.CreateActivityFromParentId(this.benchmarkSource, this.parentId); - - [Benchmark] - public void CreateActivity_WithAttributes_NoopProcessor() => ActivityCreationScenarios.CreateActivityWithAttributes(this.benchmarkSource); - - [Benchmark] - public void CreateActiviti_WithKind_NoopProcessor() => ActivityCreationScenarios.CreateActivityWithKind(this.benchmarkSource); - } -} diff --git a/test/Benchmarks/Trace/SamplerBenchmarks.cs b/test/Benchmarks/Trace/SamplerBenchmarks.cs index 53b011b07bb..440536f281e 100644 --- a/test/Benchmarks/Trace/SamplerBenchmarks.cs +++ b/test/Benchmarks/Trace/SamplerBenchmarks.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using BenchmarkDotNet.Attributes; @@ -20,100 +7,96 @@ using OpenTelemetry.Trace; /* -// * Summary * +BenchmarkDotNet v0.13.10, Windows 11 (10.0.23424.1000) +Intel Core i7-9700 CPU 3.00GHz, 1 CPU, 8 logical and 8 physical cores +.NET SDK 8.0.100 + [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 -BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.1889 (21H2) -Intel Core i7-4790 CPU 3.60GHz (Haswell), 1 CPU, 8 logical and 4 physical cores -.NET SDK=7.0.100-preview.7.22377.5 - [Host] : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT - DefaultJob : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT - -| Method | Mean | Error | StdDev | Gen 0 | Allocated | +| Method | Mean | Error | StdDev | Gen0 | Allocated | |------------------------------ |---------:|--------:|--------:|-------:|----------:| -| SamplerNotModifyingTraceState | 398.6 ns | 7.48 ns | 7.68 ns | 0.0782 | 328 B | -| SamplerModifyingTraceState | 411.8 ns | 2.38 ns | 2.11 ns | 0.0782 | 328 B | -| SamplerAppendingTraceState | 428.5 ns | 2.54 ns | 2.25 ns | 0.0916 | 384 B | - +| SamplerNotModifyingTraceState | 293.3 ns | 3.55 ns | 3.15 ns | 0.0520 | 328 B | +| SamplerModifyingTraceState | 289.4 ns | 5.64 ns | 6.27 ns | 0.0520 | 328 B | +| SamplerAppendingTraceState | 312.7 ns | 6.07 ns | 8.10 ns | 0.0610 | 384 B | */ -namespace Benchmarks.Trace +namespace Benchmarks.Trace; + +public class SamplerBenchmarks { - public class SamplerBenchmarks - { - private readonly ActivitySource sourceNotModifyTracestate = new("SamplerNotModifyingTraceState"); - private readonly ActivitySource sourceModifyTracestate = new("SamplerModifyingTraceState"); - private readonly ActivitySource sourceAppendTracestate = new("SamplerAppendingTraceState"); - private readonly ActivityContext parentContext = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded, "a=b", true); + private readonly ActivitySource sourceNotModifyTracestate = new("SamplerNotModifyingTraceState"); + private readonly ActivitySource sourceModifyTracestate = new("SamplerModifyingTraceState"); + private readonly ActivitySource sourceAppendTracestate = new("SamplerAppendingTraceState"); + private readonly ActivityContext parentContext = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded, "a=b", true); - public SamplerBenchmarks() + public SamplerBenchmarks() + { + var testSamplerNotModifyTracestate = new TestSampler { - var testSamplerNotModifyTracestate = new TestSampler + SamplingAction = (samplingParams) => { - SamplingAction = (samplingParams) => - { - return new SamplingResult(SamplingDecision.RecordAndSample); - }, - }; + return new SamplingResult(SamplingDecision.RecordAndSample); + }, + }; - var testSamplerModifyTracestate = new TestSampler + var testSamplerModifyTracestate = new TestSampler + { + SamplingAction = (samplingParams) => { - SamplingAction = (samplingParams) => - { - return new SamplingResult(SamplingDecision.RecordAndSample, "a=b"); - }, - }; + return new SamplingResult(SamplingDecision.RecordAndSample, "a=b"); + }, + }; - var testSamplerAppendTracestate = new TestSampler + var testSamplerAppendTracestate = new TestSampler + { + SamplingAction = (samplingParams) => { - SamplingAction = (samplingParams) => - { - return new SamplingResult(SamplingDecision.RecordAndSample, samplingParams.ParentContext.TraceState + ",addedkey=bar"); - }, - }; - - Sdk.CreateTracerProviderBuilder() - .SetSampler(testSamplerNotModifyTracestate) - .AddSource(this.sourceNotModifyTracestate.Name) - .Build(); - - Sdk.CreateTracerProviderBuilder() - .SetSampler(testSamplerModifyTracestate) - .AddSource(this.sourceModifyTracestate.Name) - .Build(); - - Sdk.CreateTracerProviderBuilder() - .SetSampler(testSamplerAppendTracestate) - .AddSource(this.sourceAppendTracestate.Name) - .Build(); - } + return new SamplingResult(SamplingDecision.RecordAndSample, samplingParams.ParentContext.TraceState + ",addedkey=bar"); + }, + }; + + Sdk.CreateTracerProviderBuilder() + .SetSampler(testSamplerNotModifyTracestate) + .AddSource(this.sourceNotModifyTracestate.Name) + .Build(); + + Sdk.CreateTracerProviderBuilder() + .SetSampler(testSamplerModifyTracestate) + .AddSource(this.sourceModifyTracestate.Name) + .Build(); + + Sdk.CreateTracerProviderBuilder() + .SetSampler(testSamplerAppendTracestate) + .AddSource(this.sourceAppendTracestate.Name) + .Build(); + } - [Benchmark] - public void SamplerNotModifyingTraceState() - { - using var activity = this.sourceNotModifyTracestate.StartActivity("Benchmark", ActivityKind.Server, this.parentContext); - } + [Benchmark] + public void SamplerNotModifyingTraceState() + { + using var activity = this.sourceNotModifyTracestate.StartActivity("Benchmark", ActivityKind.Server, this.parentContext); + } - [Benchmark] - public void SamplerModifyingTraceState() - { - using var activity = this.sourceModifyTracestate.StartActivity("Benchmark", ActivityKind.Server, this.parentContext); - } + [Benchmark] + public void SamplerModifyingTraceState() + { + using var activity = this.sourceModifyTracestate.StartActivity("Benchmark", ActivityKind.Server, this.parentContext); + } - [Benchmark] - public void SamplerAppendingTraceState() - { - using var activity = this.sourceAppendTracestate.StartActivity("Benchmark", ActivityKind.Server, this.parentContext); - } + [Benchmark] + public void SamplerAppendingTraceState() + { + using var activity = this.sourceAppendTracestate.StartActivity("Benchmark", ActivityKind.Server, this.parentContext); + } - internal class TestSampler : Sampler - { - public Func SamplingAction { get; set; } + internal class TestSampler : Sampler + { + public Func SamplingAction { get; set; } - public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) - { - return this.SamplingAction?.Invoke(samplingParameters) ?? new SamplingResult(SamplingDecision.RecordAndSample); - } + public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) + { + return this.SamplingAction?.Invoke(samplingParameters) ?? new SamplingResult(SamplingDecision.RecordAndSample); } } } diff --git a/test/Benchmarks/Trace/SpanCreationBenchmarks.cs b/test/Benchmarks/Trace/SpanCreationBenchmarks.cs new file mode 100644 index 00000000000..9ca218f2dc8 --- /dev/null +++ b/test/Benchmarks/Trace/SpanCreationBenchmarks.cs @@ -0,0 +1,97 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using BenchmarkDotNet.Attributes; +using Benchmarks.Helper; +using OpenTelemetry; +using OpenTelemetry.Trace; + +/* +BenchmarkDotNet v0.13.10, Windows 11 (10.0.23424.1000) +Intel Core i7-9700 CPU 3.00GHz, 1 CPU, 8 logical and 8 physical cores +.NET SDK 8.0.100 + [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + + +| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | +|--------------------------------- |-----------:|----------:|-----------:|------:|--------:|-------:|----------:| +| CreateSpan_Sampled | 357.256 ns | 2.1430 ns | 1.7895 ns | 61.41 | 0.40 | 0.0701 | 440 B | +| CreateSpan_ParentContext | 354.797 ns | 2.4225 ns | 2.2660 ns | 60.93 | 0.46 | 0.0787 | 496 B | +| CreateSpan_Attributes_Sampled | 460.082 ns | 8.7219 ns | 16.3818 ns | 81.52 | 2.88 | 0.1135 | 712 B | +| CreateSpan_WithSpan | 439.489 ns | 8.7722 ns | 21.3526 ns | 79.36 | 2.81 | 0.1030 | 648 B | +| CreateSpan_Active | 348.698 ns | 4.3437 ns | 3.8506 ns | 59.98 | 0.64 | 0.0701 | 440 B | +| CreateSpan_Active_GetCurrent | 357.866 ns | 7.1779 ns | 9.0777 ns | 62.41 | 1.51 | 0.0701 | 440 B | +| CreateSpan_Attributes_NotSampled | 360.546 ns | 3.6948 ns | 3.2753 ns | 61.96 | 0.63 | 0.0815 | 512 B | +| CreateSpan_Noop | 5.818 ns | 0.0248 ns | 0.0207 ns | 1.00 | 0.00 | - | - | +| CreateSpan_Attributes_Noop | 15.953 ns | 0.3446 ns | 0.3539 ns | 2.75 | 0.07 | 0.0115 | 72 B | +| CreateSpan_Propagate_Noop | 12.320 ns | 0.2486 ns | 0.2326 ns | 2.12 | 0.04 | - | - | +*/ + +namespace Benchmarks.Trace; + +public class SpanCreationBenchmarks +{ + private Tracer alwaysSampleTracer; + private Tracer neverSampleTracer; + private Tracer noopTracer; + private TracerProvider tracerProviderAlwaysOnSample; + private TracerProvider tracerProviderAlwaysOffSample; + + [GlobalSetup] + public void GlobalSetup() + { + this.tracerProviderAlwaysOnSample = Sdk.CreateTracerProviderBuilder() + .AddSource("AlwaysOnSample") + .SetSampler(new AlwaysOnSampler()) + .Build(); + + this.tracerProviderAlwaysOffSample = Sdk.CreateTracerProviderBuilder() + .AddSource("AlwaysOffSample") + .SetSampler(new AlwaysOffSampler()) + .Build(); + + using var traceProviderNoop = Sdk.CreateTracerProviderBuilder().Build(); + + this.alwaysSampleTracer = TracerProvider.Default.GetTracer("AlwaysOnSample"); + this.neverSampleTracer = TracerProvider.Default.GetTracer("AlwaysOffSample"); + this.noopTracer = TracerProvider.Default.GetTracer("Noop"); + } + + [GlobalCleanup] + public void GlobalCleanup() + { + this.tracerProviderAlwaysOffSample.Dispose(); + this.tracerProviderAlwaysOnSample.Dispose(); + } + + [Benchmark] + public void CreateSpan_Sampled() => SpanCreationScenarios.CreateSpan(this.alwaysSampleTracer); + + [Benchmark] + public void CreateSpan_ParentContext() => SpanCreationScenarios.CreateSpan_ParentContext(this.alwaysSampleTracer); + + [Benchmark] + public void CreateSpan_Attributes_Sampled() => SpanCreationScenarios.CreateSpan_Attributes(this.alwaysSampleTracer); + + [Benchmark] + public void CreateSpan_WithSpan() => SpanCreationScenarios.CreateSpan_Propagate(this.alwaysSampleTracer); + + [Benchmark] + public void CreateSpan_Active() => SpanCreationScenarios.CreateSpan_Active(this.alwaysSampleTracer); + + [Benchmark] + public void CreateSpan_Active_GetCurrent() => SpanCreationScenarios.CreateSpan_Active_GetCurrent(this.alwaysSampleTracer); + + [Benchmark] + public void CreateSpan_Attributes_NotSampled() => SpanCreationScenarios.CreateSpan_Attributes(this.neverSampleTracer); + + [Benchmark(Baseline = true)] + public void CreateSpan_Noop() => SpanCreationScenarios.CreateSpan(this.noopTracer); + + [Benchmark] + public void CreateSpan_Attributes_Noop() => SpanCreationScenarios.CreateSpan_Attributes(this.noopTracer); + + [Benchmark] + public void CreateSpan_Propagate_Noop() => SpanCreationScenarios.CreateSpan_Propagate(this.noopTracer); +} diff --git a/test/Benchmarks/Trace/TraceBenchmarks.cs b/test/Benchmarks/Trace/TraceBenchmarks.cs index 8a7b82b5dd4..e85e52cbd5a 100644 --- a/test/Benchmarks/Trace/TraceBenchmarks.cs +++ b/test/Benchmarks/Trace/TraceBenchmarks.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using BenchmarkDotNet.Attributes; @@ -20,194 +7,190 @@ using OpenTelemetry.Trace; /* -// * Summary * +BenchmarkDotNet v0.13.10, Windows 11 (10.0.23424.1000) +Intel Core i7-9700 CPU 3.00GHz, 1 CPU, 8 logical and 8 physical cores +.NET SDK 8.0.100 + [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 -BenchmarkDotNet=v0.13.2, OS=Windows 10 (10.0.19044.2130/21H2/November2021Update) -Intel Core i7-4790 CPU 3.60GHz(Haswell), 1 CPU, 8 logical and 4 physical cores -.NET SDK= 7.0.100-preview.7.22377.5 - [Host] : .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2 - DefaultJob : .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2 - -| Method | Mean | Error | StdDev | Gen0 | Allocated | +| Method | Mean | Error | StdDev | Gen0 | Allocated | |--------------------------------- |----------:|---------:|---------:|-------:|----------:| -| NoListener | 20.86 ns | 0.379 ns | 0.336 ns | - | - | -| PropagationDataListner | 376.51 ns | 1.361 ns | 1.273 ns | 0.0992 | 416 B | -| AllDataListner | 377.38 ns | 2.715 ns | 2.407 ns | 0.0992 | 416 B | -| AllDataAndRecordedListner | 375.79 ns | 3.393 ns | 3.008 ns | 0.0992 | 416 B | -| OneProcessor | 432.98 ns | 1.562 ns | 1.461 ns | 0.0992 | 416 B | -| TwoProcessors | 430.16 ns | 2.538 ns | 2.250 ns | 0.0992 | 416 B | -| ThreeProcessors | 427.39 ns | 3.243 ns | 2.875 ns | 0.0992 | 416 B | -| OneInstrumentation | 411.56 ns | 2.310 ns | 2.161 ns | 0.0992 | 416 B | -| TwoInstrumentations | 422.27 ns | 3.304 ns | 2.929 ns | 0.0992 | 416 B | -| LegacyActivity_ExactMatchMode | 726.59 ns | 4.852 ns | 4.301 ns | 0.0992 | 416 B | -| LegacyActivity_WildcardMatchMode | 825.79 ns | 7.846 ns | 6.955 ns | 0.0992 | 416 B | - +| NoListener | 14.00 ns | 0.173 ns | 0.162 ns | - | - | +| PropagationDataListner | 265.96 ns | 4.022 ns | 3.762 ns | 0.0663 | 416 B | +| AllDataListner | 255.14 ns | 1.819 ns | 1.702 ns | 0.0663 | 416 B | +| AllDataAndRecordedListner | 258.32 ns | 2.387 ns | 2.116 ns | 0.0663 | 416 B | +| OneProcessor | 277.12 ns | 2.059 ns | 1.926 ns | 0.0663 | 416 B | +| TwoProcessors | 276.82 ns | 4.442 ns | 4.155 ns | 0.0663 | 416 B | +| ThreeProcessors | 283.12 ns | 1.970 ns | 1.645 ns | 0.0663 | 416 B | +| OneInstrumentation | 281.13 ns | 2.199 ns | 2.057 ns | 0.0663 | 416 B | +| TwoInstrumentations | 273.99 ns | 2.792 ns | 2.475 ns | 0.0663 | 416 B | +| LegacyActivity_ExactMatchMode | 471.38 ns | 2.211 ns | 1.960 ns | 0.0658 | 416 B | +| LegacyActivity_WildcardMatchMode | 496.84 ns | 2.138 ns | 2.000 ns | 0.0658 | 416 B | */ -namespace Benchmarks.Trace +namespace Benchmarks.Trace; + +public class TraceBenchmarks { - public class TraceBenchmarks + private readonly ActivitySource sourceWithNoListener = new("Benchmark.NoListener"); + private readonly ActivitySource sourceWithPropagationDataListner = new("Benchmark.PropagationDataListner"); + private readonly ActivitySource sourceWithAllDataListner = new("Benchmark.AllDataListner"); + private readonly ActivitySource sourceWithAllDataAndRecordedListner = new("Benchmark.AllDataAndRecordedListner"); + private readonly ActivitySource sourceWithOneProcessor = new("Benchmark.OneProcessor"); + private readonly ActivitySource sourceWithTwoProcessors = new("Benchmark.TwoProcessors"); + private readonly ActivitySource sourceWithThreeProcessors = new("Benchmark.ThreeProcessors"); + private readonly ActivitySource sourceWithOneLegacyActivityOperationNameSubscription = new("Benchmark.OneInstrumentation"); + private readonly ActivitySource sourceWithTwoLegacyActivityOperationNameSubscriptions = new("Benchmark.TwoInstrumentations"); + + public TraceBenchmarks() { - private readonly ActivitySource sourceWithNoListener = new("Benchmark.NoListener"); - private readonly ActivitySource sourceWithPropagationDataListner = new("Benchmark.PropagationDataListner"); - private readonly ActivitySource sourceWithAllDataListner = new("Benchmark.AllDataListner"); - private readonly ActivitySource sourceWithAllDataAndRecordedListner = new("Benchmark.AllDataAndRecordedListner"); - private readonly ActivitySource sourceWithOneProcessor = new("Benchmark.OneProcessor"); - private readonly ActivitySource sourceWithTwoProcessors = new("Benchmark.TwoProcessors"); - private readonly ActivitySource sourceWithThreeProcessors = new("Benchmark.ThreeProcessors"); - private readonly ActivitySource sourceWithOneLegacyActivityOperationNameSubscription = new("Benchmark.OneInstrumentation"); - private readonly ActivitySource sourceWithTwoLegacyActivityOperationNameSubscriptions = new("Benchmark.TwoInstrumentations"); - - public TraceBenchmarks() - { - Activity.DefaultIdFormat = ActivityIdFormat.W3C; - - ActivitySource.AddActivityListener(new ActivityListener - { - ActivityStarted = null, - ActivityStopped = null, - ShouldListenTo = (activitySource) => activitySource.Name == this.sourceWithPropagationDataListner.Name, - Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.PropagationData, - }); - - ActivitySource.AddActivityListener(new ActivityListener - { - ActivityStarted = null, - ActivityStopped = null, - ShouldListenTo = (activitySource) => activitySource.Name == this.sourceWithAllDataListner.Name, - Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, - }); - - ActivitySource.AddActivityListener(new ActivityListener - { - ActivityStarted = null, - ActivityStopped = null, - ShouldListenTo = (activitySource) => activitySource.Name == this.sourceWithAllDataAndRecordedListner.Name, - Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllDataAndRecorded, - }); - - Sdk.CreateTracerProviderBuilder() - .SetSampler(new AlwaysOnSampler()) - .AddSource(this.sourceWithOneProcessor.Name) - .AddProcessor(new DummyActivityProcessor()) - .Build(); - - Sdk.CreateTracerProviderBuilder() - .SetSampler(new AlwaysOnSampler()) - .AddSource(this.sourceWithTwoProcessors.Name) - .AddProcessor(new DummyActivityProcessor()) - .AddProcessor(new DummyActivityProcessor()) - .Build(); - - Sdk.CreateTracerProviderBuilder() - .SetSampler(new AlwaysOnSampler()) - .AddSource(this.sourceWithThreeProcessors.Name) - .AddProcessor(new DummyActivityProcessor()) - .AddProcessor(new DummyActivityProcessor()) - .AddProcessor(new DummyActivityProcessor()) - .Build(); - - Sdk.CreateTracerProviderBuilder() - .SetSampler(new AlwaysOnSampler()) - .AddSource(this.sourceWithOneLegacyActivityOperationNameSubscription.Name) - .AddLegacySource("TestOperationName") - .AddProcessor(new DummyActivityProcessor()) - .Build(); - - Sdk.CreateTracerProviderBuilder() - .SetSampler(new AlwaysOnSampler()) - .AddSource(this.sourceWithTwoLegacyActivityOperationNameSubscriptions.Name) - .AddLegacySource("TestOperationName1") - .AddLegacySource("TestOperationName2") - .AddProcessor(new DummyActivityProcessor()) - .Build(); - - Sdk.CreateTracerProviderBuilder() - .SetSampler(new AlwaysOnSampler()) - .AddLegacySource("ExactMatch.OperationName1") - .AddProcessor(new DummyActivityProcessor()) - .Build(); - - Sdk.CreateTracerProviderBuilder() - .SetSampler(new AlwaysOnSampler()) - .AddLegacySource("WildcardMatch.*") - .AddProcessor(new DummyActivityProcessor()) - .Build(); - } - - [Benchmark] - public void NoListener() - { - // this activity won't be created as there is no listener - using var activity = this.sourceWithNoListener.StartActivity("Benchmark"); - } + Activity.DefaultIdFormat = ActivityIdFormat.W3C; - [Benchmark] - public void PropagationDataListner() + ActivitySource.AddActivityListener(new ActivityListener { - // this activity will be created and feed into an ActivityListener that simply drops everything on the floor - using var activity = this.sourceWithPropagationDataListner.StartActivity("Benchmark"); - } + ActivityStarted = null, + ActivityStopped = null, + ShouldListenTo = (activitySource) => activitySource.Name == this.sourceWithPropagationDataListner.Name, + Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.PropagationData, + }); - [Benchmark] - public void AllDataListner() + ActivitySource.AddActivityListener(new ActivityListener { - // this activity will be created and feed into an ActivityListener that simply drops everything on the floor - using var activity = this.sourceWithAllDataListner.StartActivity("Benchmark"); - } + ActivityStarted = null, + ActivityStopped = null, + ShouldListenTo = (activitySource) => activitySource.Name == this.sourceWithAllDataListner.Name, + Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, + }); - [Benchmark] - public void AllDataAndRecordedListner() + ActivitySource.AddActivityListener(new ActivityListener { - // this activity will be created and feed into an ActivityListener that simply drops everything on the floor - using var activity = this.sourceWithAllDataAndRecordedListner.StartActivity("Benchmark"); - } + ActivityStarted = null, + ActivityStopped = null, + ShouldListenTo = (activitySource) => activitySource.Name == this.sourceWithAllDataAndRecordedListner.Name, + Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllDataAndRecorded, + }); + + Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddSource(this.sourceWithOneProcessor.Name) + .AddProcessor(new DummyActivityProcessor()) + .Build(); + + Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddSource(this.sourceWithTwoProcessors.Name) + .AddProcessor(new DummyActivityProcessor()) + .AddProcessor(new DummyActivityProcessor()) + .Build(); + + Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddSource(this.sourceWithThreeProcessors.Name) + .AddProcessor(new DummyActivityProcessor()) + .AddProcessor(new DummyActivityProcessor()) + .AddProcessor(new DummyActivityProcessor()) + .Build(); + + Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddSource(this.sourceWithOneLegacyActivityOperationNameSubscription.Name) + .AddLegacySource("TestOperationName") + .AddProcessor(new DummyActivityProcessor()) + .Build(); + + Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddSource(this.sourceWithTwoLegacyActivityOperationNameSubscriptions.Name) + .AddLegacySource("TestOperationName1") + .AddLegacySource("TestOperationName2") + .AddProcessor(new DummyActivityProcessor()) + .Build(); + + Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddLegacySource("ExactMatch.OperationName1") + .AddProcessor(new DummyActivityProcessor()) + .Build(); + + Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddLegacySource("WildcardMatch.*") + .AddProcessor(new DummyActivityProcessor()) + .Build(); + } - [Benchmark] - public void OneProcessor() - { - using var activity = this.sourceWithOneProcessor.StartActivity("Benchmark"); - } + [Benchmark] + public void NoListener() + { + // this activity won't be created as there is no listener + using var activity = this.sourceWithNoListener.StartActivity("Benchmark"); + } - [Benchmark] - public void TwoProcessors() - { - using var activity = this.sourceWithTwoProcessors.StartActivity("Benchmark"); - } + [Benchmark] + public void PropagationDataListner() + { + // this activity will be created and feed into an ActivityListener that simply drops everything on the floor + using var activity = this.sourceWithPropagationDataListner.StartActivity("Benchmark"); + } - [Benchmark] - public void ThreeProcessors() - { - using var activity = this.sourceWithThreeProcessors.StartActivity("Benchmark"); - } + [Benchmark] + public void AllDataListner() + { + // this activity will be created and feed into an ActivityListener that simply drops everything on the floor + using var activity = this.sourceWithAllDataListner.StartActivity("Benchmark"); + } - [Benchmark] - public void OneInstrumentation() - { - using var activity = this.sourceWithOneLegacyActivityOperationNameSubscription.StartActivity("Benchmark"); - } + [Benchmark] + public void AllDataAndRecordedListner() + { + // this activity will be created and feed into an ActivityListener that simply drops everything on the floor + using var activity = this.sourceWithAllDataAndRecordedListner.StartActivity("Benchmark"); + } - [Benchmark] - public void TwoInstrumentations() - { - using var activity = this.sourceWithTwoLegacyActivityOperationNameSubscriptions.StartActivity("Benchmark"); - } + [Benchmark] + public void OneProcessor() + { + using var activity = this.sourceWithOneProcessor.StartActivity("Benchmark"); + } - [Benchmark] - public void LegacyActivity_ExactMatchMode() - { - using var activity = new Activity("ExactMatch.OperationName1").Start(); - } + [Benchmark] + public void TwoProcessors() + { + using var activity = this.sourceWithTwoProcessors.StartActivity("Benchmark"); + } - [Benchmark] - public void LegacyActivity_WildcardMatchMode() - { - using var activity = new Activity("WildcardMatch.OperationName1").Start(); - } + [Benchmark] + public void ThreeProcessors() + { + using var activity = this.sourceWithThreeProcessors.StartActivity("Benchmark"); + } - internal class DummyActivityProcessor : BaseProcessor - { - } + [Benchmark] + public void OneInstrumentation() + { + using var activity = this.sourceWithOneLegacyActivityOperationNameSubscription.StartActivity("Benchmark"); + } + + [Benchmark] + public void TwoInstrumentations() + { + using var activity = this.sourceWithTwoLegacyActivityOperationNameSubscriptions.StartActivity("Benchmark"); + } + + [Benchmark] + public void LegacyActivity_ExactMatchMode() + { + using var activity = new Activity("ExactMatch.OperationName1").Start(); + } + + [Benchmark] + public void LegacyActivity_WildcardMatchMode() + { + using var activity = new Activity("WildcardMatch.OperationName1").Start(); + } + + internal class DummyActivityProcessor : BaseProcessor + { } } diff --git a/test/Benchmarks/Trace/TraceShimBenchmarks.cs b/test/Benchmarks/Trace/TraceShimBenchmarks.cs index be9cabe8264..b9d11939936 100644 --- a/test/Benchmarks/Trace/TraceShimBenchmarks.cs +++ b/test/Benchmarks/Trace/TraceShimBenchmarks.cs @@ -1,86 +1,88 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using BenchmarkDotNet.Attributes; using OpenTelemetry; using OpenTelemetry.Trace; -namespace Benchmarks.Trace +/* +BenchmarkDotNet v0.13.10, Windows 11 (10.0.23424.1000) +Intel Core i7-9700 CPU 3.00GHz, 1 CPU, 8 logical and 8 physical cores +.NET SDK 8.0.100 + [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + + +| Method | Mean | Error | StdDev | Gen0 | Allocated | +|---------------- |-----------:|----------:|----------:|-------:|----------:| +| NoListener | 5.322 ns | 0.0314 ns | 0.0262 ns | - | - | +| OneProcessor | 326.566 ns | 3.4034 ns | 3.0170 ns | 0.0701 | 440 B | +| TwoProcessors | 335.646 ns | 3.8341 ns | 3.3988 ns | 0.0701 | 440 B | +| ThreeProcessors | 336.069 ns | 6.5628 ns | 8.5335 ns | 0.0701 | 440 B | +*/ + +namespace Benchmarks.Trace; + +public class TraceShimBenchmarks { - public class TraceShimBenchmarks - { - private readonly Tracer tracerWithNoListener = TracerProvider.Default.GetTracer("Benchmark.NoListener"); - private readonly Tracer tracerWithOneProcessor = TracerProvider.Default.GetTracer("Benchmark.OneProcessor"); - private readonly Tracer tracerWithTwoProcessors = TracerProvider.Default.GetTracer("Benchmark.TwoProcessors"); - private readonly Tracer tracerWithThreeProcessors = TracerProvider.Default.GetTracer("Benchmark.ThreeProcessors"); + private readonly Tracer tracerWithNoListener = TracerProvider.Default.GetTracer("Benchmark.NoListener"); + private readonly Tracer tracerWithOneProcessor = TracerProvider.Default.GetTracer("Benchmark.OneProcessor"); + private readonly Tracer tracerWithTwoProcessors = TracerProvider.Default.GetTracer("Benchmark.TwoProcessors"); + private readonly Tracer tracerWithThreeProcessors = TracerProvider.Default.GetTracer("Benchmark.ThreeProcessors"); - public TraceShimBenchmarks() - { - Activity.DefaultIdFormat = ActivityIdFormat.W3C; + public TraceShimBenchmarks() + { + Activity.DefaultIdFormat = ActivityIdFormat.W3C; - Sdk.CreateTracerProviderBuilder() - .SetSampler(new AlwaysOnSampler()) - .AddSource("Benchmark.OneProcessor") - .AddProcessor(new DummyActivityProcessor()) - .Build(); + Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddSource("Benchmark.OneProcessor") + .AddProcessor(new DummyActivityProcessor()) + .Build(); - Sdk.CreateTracerProviderBuilder() - .SetSampler(new AlwaysOnSampler()) - .AddSource("Benchmark.TwoProcessors") - .AddProcessor(new DummyActivityProcessor()) - .AddProcessor(new DummyActivityProcessor()) - .Build(); + Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddSource("Benchmark.TwoProcessors") + .AddProcessor(new DummyActivityProcessor()) + .AddProcessor(new DummyActivityProcessor()) + .Build(); - Sdk.CreateTracerProviderBuilder() - .SetSampler(new AlwaysOnSampler()) - .AddSource("Benchmark.ThreeProcessors") - .AddProcessor(new DummyActivityProcessor()) - .AddProcessor(new DummyActivityProcessor()) - .AddProcessor(new DummyActivityProcessor()) - .Build(); - } + Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddSource("Benchmark.ThreeProcessors") + .AddProcessor(new DummyActivityProcessor()) + .AddProcessor(new DummyActivityProcessor()) + .AddProcessor(new DummyActivityProcessor()) + .Build(); + } - [Benchmark] - public void NoListener() - { - // this activity won't be created as there is no listener - using var activity = this.tracerWithNoListener.StartActiveSpan("Benchmark"); - } + [Benchmark] + public void NoListener() + { + // this activity won't be created as there is no listener + using var activity = this.tracerWithNoListener.StartActiveSpan("Benchmark"); + } - [Benchmark] - public void OneProcessor() - { - using var activity = this.tracerWithOneProcessor.StartActiveSpan("Benchmark"); - } + [Benchmark] + public void OneProcessor() + { + using var activity = this.tracerWithOneProcessor.StartActiveSpan("Benchmark"); + } - [Benchmark] - public void TwoProcessors() - { - using var activity = this.tracerWithTwoProcessors.StartActiveSpan("Benchmark"); - } + [Benchmark] + public void TwoProcessors() + { + using var activity = this.tracerWithTwoProcessors.StartActiveSpan("Benchmark"); + } - [Benchmark] - public void ThreeProcessors() - { - using var activity = this.tracerWithThreeProcessors.StartActiveSpan("Benchmark"); - } + [Benchmark] + public void ThreeProcessors() + { + using var activity = this.tracerWithThreeProcessors.StartActiveSpan("Benchmark"); + } - internal class DummyActivityProcessor : BaseProcessor - { - } + internal class DummyActivityProcessor : BaseProcessor + { } } diff --git a/test/Directory.Packages.props b/test/Directory.Packages.props index 9a73f74f978..575224321a8 100644 --- a/test/Directory.Packages.props +++ b/test/Directory.Packages.props @@ -1,8 +1,8 @@ - - - + + + diff --git a/test/OpenTelemetry.AotCompatibility.TestApp/OpenTelemetry.AotCompatibility.TestApp.csproj b/test/OpenTelemetry.AotCompatibility.TestApp/OpenTelemetry.AotCompatibility.TestApp.csproj index 8bdc01e944f..bf5db5a04c1 100644 --- a/test/OpenTelemetry.AotCompatibility.TestApp/OpenTelemetry.AotCompatibility.TestApp.csproj +++ b/test/OpenTelemetry.AotCompatibility.TestApp/OpenTelemetry.AotCompatibility.TestApp.csproj @@ -2,17 +2,22 @@ Exe - net7.0 - true + $(TargetFrameworksForAotCompatibilityTests) + true false + true + + + + - + diff --git a/test/OpenTelemetry.AotCompatibility.TestApp/Program.cs b/test/OpenTelemetry.AotCompatibility.TestApp/Program.cs index 254ee117fbc..121d205046b 100644 --- a/test/OpenTelemetry.AotCompatibility.TestApp/Program.cs +++ b/test/OpenTelemetry.AotCompatibility.TestApp/Program.cs @@ -1,17 +1,17 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -Console.WriteLine("Hello, World!"); +using OpenTelemetry.AotCompatibility.TestApp; + +try +{ + PropertyFetcherAotTest.Test(); +} +catch (Exception ex) +{ + Console.WriteLine(ex); + return -1; +} + +Console.WriteLine("Passed."); +return 0; diff --git a/test/OpenTelemetry.AotCompatibility.TestApp/PropertyFetcherAotTest.cs b/test/OpenTelemetry.AotCompatibility.TestApp/PropertyFetcherAotTest.cs new file mode 100644 index 00000000000..d79a00c9471 --- /dev/null +++ b/test/OpenTelemetry.AotCompatibility.TestApp/PropertyFetcherAotTest.cs @@ -0,0 +1,56 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics.CodeAnalysis; +using OpenTelemetry.Instrumentation; + +namespace OpenTelemetry.AotCompatibility.TestApp; + +internal class PropertyFetcherAotTest +{ + [UnconditionalSuppressMessage("", "IL2026", Justification = "Property presence guaranteed by explicit hints.")] + public static void Test() + { + var fetcher = new PropertyFetcher("Property"); + + GuaranteeProperties(); + var r = fetcher.TryFetch(new PayloadTypeWithBaseType(), out var value); + Assert(r, "TryFetch base did not return true."); + Assert(value!.GetType() == typeof(DerivedType), "TryFetch base value is not a derived type."); + + GuaranteeProperties(); + r = fetcher.TryFetch(new PayloadTypeWithDerivedType(), out value); + Assert(r, "TryFetch derived did not return true."); + Assert(value!.GetType() == typeof(DerivedType), "TryFetch derived value is not a derived type."); + } + + private static void GuaranteeProperties<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>() + { + } + + private static void Assert(bool condition, string message) + { + if (!condition) + { + throw new InvalidOperationException(message); + } + } + + private class BaseType + { + } + + private class DerivedType : BaseType + { + } + + private class PayloadTypeWithBaseType + { + public BaseType Property { get; set; } = new DerivedType(); + } + + private class PayloadTypeWithDerivedType + { + public DerivedType Property { get; set; } = new DerivedType(); + } +} diff --git a/test/OpenTelemetry.AotCompatibility.Tests/AotCompatibilityTests.cs b/test/OpenTelemetry.AotCompatibility.Tests/AotCompatibilityTests.cs deleted file mode 100644 index 248494307bb..00000000000 --- a/test/OpenTelemetry.AotCompatibility.Tests/AotCompatibilityTests.cs +++ /dev/null @@ -1,91 +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 Xunit; -using Xunit.Abstractions; - -namespace OpenTelemetry.AotCompatibility.Tests -{ - public class AotCompatibilityTests - { - private readonly ITestOutputHelper testOutputHelper; - - public AotCompatibilityTests(ITestOutputHelper testOutputHelper) - { - this.testOutputHelper = testOutputHelper; - } - - /// - /// This test ensures that the intended APIs of the OpenTelemetry.AotCompatibility.TestApp libraries are - /// trimming and NativeAOT compatible. - /// - /// This test follows the instructions in https://learn.microsoft.com/dotnet/core/deploying/trimming/prepare-libraries-for-trimming#show-all-warnings-with-sample-application - /// - /// If this test fails, it is due to adding trimming and/or AOT incompatible changes - /// to code that is supposed to be compatible. - /// - /// To diagnose the problem, inspect the test output which will contain the trimming and AOT errors. For example: - /// - /// error IL2091: 'T' generic argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicConstructors'. - /// - [Fact] - public void EnsureAotCompatibility() - { - string[] paths = { @"..", "..", "..", "..", "OpenTelemetry.AotCompatibility.TestApp" }; - string testAppPath = Path.Combine(paths); - string testAppProject = "OpenTelemetry.AotCompatibility.TestApp.csproj"; - - // ensure we run a clean publish every time - DirectoryInfo testObjDir = new DirectoryInfo(Path.Combine(testAppPath, "obj")); - if (testObjDir.Exists) - { - testObjDir.Delete(recursive: true); - } - - var process = new Process - { - // set '-nodereuse:false /p:UseSharedCompilation=false' so the MSBuild and Roslyn server processes don't hang around, which may hang the test in CI - StartInfo = new ProcessStartInfo("dotnet", $"publish {testAppProject} --self-contained -nodereuse:false /p:UseSharedCompilation=false") - { - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true, - WorkingDirectory = testAppPath, - }, - }; - - var expectedOutput = new System.Text.StringBuilder(); - process.OutputDataReceived += (sender, e) => - { - if (!string.IsNullOrEmpty(e.Data)) - { - this.testOutputHelper.WriteLine(e.Data); - expectedOutput.AppendLine(e.Data); - } - }; - - process.Start(); - process.BeginOutputReadLine(); - - Assert.True(process.WaitForExit(milliseconds: 240_000), "dotnet publish command timed out after 240 seconds."); - Assert.True(process.ExitCode == 0, "Publishing the AotCompatibility app failed. See test output for more details."); - - var warnings = expectedOutput.ToString().Split('\n', '\r').Where(line => line.Contains("warning IL")); - Assert.Equal(36, warnings.Count()); - } - } -} diff --git a/test/OpenTelemetry.AotCompatibility.Tests/OpenTelemetry.AotCompatibility.Tests.csproj b/test/OpenTelemetry.AotCompatibility.Tests/OpenTelemetry.AotCompatibility.Tests.csproj deleted file mode 100644 index be6c3a7e41b..00000000000 --- a/test/OpenTelemetry.AotCompatibility.Tests/OpenTelemetry.AotCompatibility.Tests.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - Test to ensure AOT compatilibity. - net7.0 - enable - enable - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - diff --git a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Logs/LoggerProviderBuilderExtensionsTests.cs b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Logs/LoggerProviderBuilderExtensionsTests.cs index cbe42fba156..627513786b7 100644 --- a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Logs/LoggerProviderBuilderExtensionsTests.cs +++ b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Logs/LoggerProviderBuilderExtensionsTests.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; diff --git a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Logs/TestLoggerProviderBuilder.cs b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Logs/TestLoggerProviderBuilder.cs index 61b0c411e11..f79d26bf1ea 100644 --- a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Logs/TestLoggerProviderBuilder.cs +++ b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Logs/TestLoggerProviderBuilder.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Logs; @@ -42,7 +29,11 @@ public override LoggerProviderBuilder AddInstrumentation(Func< } else { - this.Instrumentation.Add(instrumentationFactory()); + var instrumentation = instrumentationFactory(); + if (instrumentation is not null) + { + this.Instrumentation.Add(instrumentation); + } } return this; diff --git a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Metrics/MeterProviderBuilderExtensionsTests.cs b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Metrics/MeterProviderBuilderExtensionsTests.cs index cb0b2063b53..1b1cd7ee695 100644 --- a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Metrics/MeterProviderBuilderExtensionsTests.cs +++ b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Metrics/MeterProviderBuilderExtensionsTests.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; diff --git a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Metrics/TestMeterProviderBuilder.cs b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Metrics/TestMeterProviderBuilder.cs index 362dc70e40e..424b271417a 100644 --- a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Metrics/TestMeterProviderBuilder.cs +++ b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Metrics/TestMeterProviderBuilder.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Metrics; @@ -44,7 +31,11 @@ public override MeterProviderBuilder AddInstrumentation(Func Unit test project for OpenTelemetry .NET dependency injection extensions - - net7.0;net6.0 - $(TargetFrameworks);net462 + $(TargetFrameworksForTests) @@ -13,8 +11,7 @@ - - all + runtime; build; native; contentfiles; analyzers diff --git a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/ServiceCollectionExtensionsTests.cs b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/ServiceCollectionExtensionsTests.cs index 860b31ff411..fa3fb4e13d9 100644 --- a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/ServiceCollectionExtensionsTests.cs +++ b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/ServiceCollectionExtensionsTests.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Logs; diff --git a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Trace/TestTracerProviderBuilder.cs b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Trace/TestTracerProviderBuilder.cs index 73c48bcd940..db64a8549c2 100644 --- a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Trace/TestTracerProviderBuilder.cs +++ b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Trace/TestTracerProviderBuilder.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Trace; @@ -46,7 +33,11 @@ public override TracerProviderBuilder AddInstrumentation(Func< } else { - this.Instrumentation.Add(instrumentationFactory()); + var instrumentation = instrumentationFactory(); + if (instrumentation is not null) + { + this.Instrumentation.Add(instrumentation); + } } return this; diff --git a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Trace/TracerProviderBuilderExtensionsTests.cs b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Trace/TracerProviderBuilderExtensionsTests.cs index faae0245e79..d9b35bee02d 100644 --- a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Trace/TracerProviderBuilderExtensionsTests.cs +++ b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Trace/TracerProviderBuilderExtensionsTests.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; diff --git a/test/OpenTelemetry.Api.Tests/BaggageTests.cs b/test/OpenTelemetry.Api.Tests/BaggageTests.cs index 0ae5698e14f..e13aa68a502 100644 --- a/test/OpenTelemetry.Api.Tests/BaggageTests.cs +++ b/test/OpenTelemetry.Api.Tests/BaggageTests.cs @@ -1,313 +1,299 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Xunit; -namespace OpenTelemetry.Tests +namespace OpenTelemetry.Tests; + +public class BaggageTests { - public class BaggageTests - { - private const string K1 = "Key1"; - private const string K2 = "Key2"; - private const string K3 = "Key3"; + private const string K1 = "Key1"; + private const string K2 = "Key2"; + private const string K3 = "Key3"; - private const string V1 = "Value1"; - private const string V2 = "Value2"; - private const string V3 = "Value3"; + private const string V1 = "Value1"; + private const string V2 = "Value2"; + private const string V3 = "Value3"; - [Fact] - public void EmptyTest() - { - Assert.Empty(Baggage.GetBaggage()); - Assert.Empty(Baggage.Current.GetBaggage()); - } + [Fact] + public void EmptyTest() + { + Assert.Empty(Baggage.GetBaggage()); + Assert.Empty(Baggage.Current.GetBaggage()); + } - [Fact] - public void SetAndGetTest() + [Fact] + public void SetAndGetTest() + { + var list = new List>(2) { - var list = new List>(2) - { - new KeyValuePair(K1, V1), - new KeyValuePair(K2, V2), - }; - - Baggage.SetBaggage(K1, V1); - var baggage = Baggage.Current.SetBaggage(K2, V2); - Baggage.Current = baggage; - - Assert.NotEmpty(Baggage.GetBaggage()); - Assert.Equal(list, Baggage.GetBaggage(Baggage.Current)); - - Assert.Equal(V1, Baggage.GetBaggage(K1)); - Assert.Equal(V1, Baggage.GetBaggage(K1.ToLower())); - Assert.Equal(V1, Baggage.GetBaggage(K1.ToUpper())); - Assert.Null(Baggage.GetBaggage("NO_KEY")); - Assert.Equal(V2, Baggage.Current.GetBaggage(K2)); - - Assert.Throws(() => Baggage.GetBaggage(null)); - } + new KeyValuePair(K1, V1), + new KeyValuePair(K2, V2), + }; - [Fact] - public void SetExistingKeyTest() - { - var list = new List>(2) - { - new KeyValuePair(K1, V1), - }; + Baggage.SetBaggage(K1, V1); + var baggage = Baggage.Current.SetBaggage(K2, V2); + Baggage.Current = baggage; - Baggage.Current.SetBaggage(new KeyValuePair(K1, V1)); - var baggage = Baggage.SetBaggage(K1, V1); - Baggage.SetBaggage(new Dictionary { [K1] = V1 }, baggage); + Assert.NotEmpty(Baggage.GetBaggage()); + Assert.Equal(list, Baggage.GetBaggage(Baggage.Current)); - Assert.Equal(list, Baggage.GetBaggage()); - } + Assert.Equal(V1, Baggage.GetBaggage(K1)); + Assert.Equal(V1, Baggage.GetBaggage(K1.ToLower())); + Assert.Equal(V1, Baggage.GetBaggage(K1.ToUpper())); + Assert.Null(Baggage.GetBaggage("NO_KEY")); + Assert.Equal(V2, Baggage.Current.GetBaggage(K2)); + + Assert.Throws(() => Baggage.GetBaggage(null)); + } - [Fact] - public void SetNullValueTest() + [Fact] + public void SetExistingKeyTest() + { + var list = new List>(2) { - var baggage = Baggage.Current; - baggage = Baggage.SetBaggage(K1, V1, baggage); + new KeyValuePair(K1, V1), + }; + + Baggage.Current.SetBaggage(new KeyValuePair(K1, V1)); + var baggage = Baggage.SetBaggage(K1, V1); + Baggage.SetBaggage(new Dictionary { [K1] = V1 }, baggage); + + Assert.Equal(list, Baggage.GetBaggage()); + } - Assert.Equal(1, Baggage.Current.Count); - Assert.Equal(1, baggage.Count); + [Fact] + public void SetNullValueTest() + { + var baggage = Baggage.Current; + baggage = Baggage.SetBaggage(K1, V1, baggage); - Baggage.Current.SetBaggage(K2, null); + Assert.Equal(1, Baggage.Current.Count); + Assert.Equal(1, baggage.Count); - Assert.Equal(1, Baggage.Current.Count); + Baggage.Current.SetBaggage(K2, null); - Assert.Empty(Baggage.SetBaggage(K1, null).GetBaggage()); + Assert.Equal(1, Baggage.Current.Count); - Baggage.SetBaggage(K1, V1); - Baggage.SetBaggage(new Dictionary - { - [K1] = null, - [K2] = V2, - }); - Assert.Equal(1, Baggage.Current.Count); - Assert.Contains(Baggage.GetBaggage(), kvp => kvp.Key == K2); - } + Assert.Empty(Baggage.SetBaggage(K1, null).GetBaggage()); - [Fact] - public void RemoveTest() + Baggage.SetBaggage(K1, V1); + Baggage.SetBaggage(new Dictionary { - var empty = Baggage.Current; - var empty2 = Baggage.RemoveBaggage(K1); - Assert.True(empty == empty2); + [K1] = null, + [K2] = V2, + }); + Assert.Equal(1, Baggage.Current.Count); + Assert.Contains(Baggage.GetBaggage(), kvp => kvp.Key == K2); + } - var baggage = Baggage.SetBaggage(new Dictionary - { - [K1] = V1, - [K2] = V2, - [K3] = V3, - }); + [Fact] + public void RemoveTest() + { + var empty = Baggage.Current; + var empty2 = Baggage.RemoveBaggage(K1); + Assert.True(empty == empty2); - var baggage2 = Baggage.RemoveBaggage(K1, baggage); + var baggage = Baggage.SetBaggage(new Dictionary + { + [K1] = V1, + [K2] = V2, + [K3] = V3, + }); - Assert.Equal(3, baggage.Count); - Assert.Equal(2, baggage2.Count); + var baggage2 = Baggage.RemoveBaggage(K1, baggage); - Assert.DoesNotContain(new KeyValuePair(K1, V1), baggage2.GetBaggage()); - } + Assert.Equal(3, baggage.Count); + Assert.Equal(2, baggage2.Count); - [Fact] - public void ClearTest() + Assert.DoesNotContain(new KeyValuePair(K1, V1), baggage2.GetBaggage()); + } + + [Fact] + public void ClearTest() + { + var baggage = Baggage.SetBaggage(new Dictionary { - var baggage = Baggage.SetBaggage(new Dictionary - { - [K1] = V1, - [K2] = V2, - [K3] = V3, - }); + [K1] = V1, + [K2] = V2, + [K3] = V3, + }); - Assert.Equal(3, baggage.Count); + Assert.Equal(3, baggage.Count); - Baggage.ClearBaggage(); + Baggage.ClearBaggage(); - Assert.Equal(0, Baggage.Current.Count); - } + Assert.Equal(0, Baggage.Current.Count); + } - [Fact] - public void ContextFlowTest() - { - var baggage = Baggage.SetBaggage(K1, V1); - var baggage2 = Baggage.Current.SetBaggage(K2, V2); - Baggage.Current = baggage2; - var baggage3 = Baggage.SetBaggage(K3, V3); + [Fact] + public void ContextFlowTest() + { + var baggage = Baggage.SetBaggage(K1, V1); + var baggage2 = Baggage.Current.SetBaggage(K2, V2); + Baggage.Current = baggage2; + var baggage3 = Baggage.SetBaggage(K3, V3); - Assert.Equal(1, baggage.Count); - Assert.Equal(2, baggage2.Count); - Assert.Equal(3, baggage3.Count); + Assert.Equal(1, baggage.Count); + Assert.Equal(2, baggage2.Count); + Assert.Equal(3, baggage3.Count); - Baggage.Current = baggage; + Baggage.Current = baggage; - var baggage4 = Baggage.SetBaggage(K3, V3); + var baggage4 = Baggage.SetBaggage(K3, V3); - Assert.Equal(2, baggage4.Count); - Assert.DoesNotContain(new KeyValuePair(K2, V2), baggage4.GetBaggage()); - } + Assert.Equal(2, baggage4.Count); + Assert.DoesNotContain(new KeyValuePair(K2, V2), baggage4.GetBaggage()); + } - [Fact] - public void EnumeratorTest() + [Fact] + public void EnumeratorTest() + { + var list = new List>(2) { - var list = new List>(2) - { - new KeyValuePair(K1, V1), - new KeyValuePair(K2, V2), - }; + new KeyValuePair(K1, V1), + new KeyValuePair(K2, V2), + }; - var baggage = Baggage.SetBaggage(K1, V1); - baggage = Baggage.SetBaggage(K2, V2, baggage); + var baggage = Baggage.SetBaggage(K1, V1); + baggage = Baggage.SetBaggage(K2, V2, baggage); - var enumerator = Baggage.GetEnumerator(baggage); + var enumerator = Baggage.GetEnumerator(baggage); - Assert.True(enumerator.MoveNext()); - var tag1 = enumerator.Current; - Assert.True(enumerator.MoveNext()); - var tag2 = enumerator.Current; - Assert.False(enumerator.MoveNext()); + Assert.True(enumerator.MoveNext()); + var tag1 = enumerator.Current; + Assert.True(enumerator.MoveNext()); + var tag2 = enumerator.Current; + Assert.False(enumerator.MoveNext()); - Assert.Equal(list, new List> { tag1, tag2 }); + Assert.Equal(list, new List> { tag1, tag2 }); - Baggage.ClearBaggage(); + Baggage.ClearBaggage(); - enumerator = Baggage.GetEnumerator(); + enumerator = Baggage.GetEnumerator(); - Assert.False(enumerator.MoveNext()); - } + Assert.False(enumerator.MoveNext()); + } - [Fact] - public void EqualsTest() - { - var bc1 = new Baggage(new Dictionary() { [K1] = V1, [K2] = V2 }); - var bc2 = new Baggage(new Dictionary() { [K1] = V1, [K2] = V2 }); - var bc3 = new Baggage(new Dictionary() { [K2] = V2, [K1] = V1 }); - var bc4 = new Baggage(new Dictionary() { [K1] = V1, [K2] = V1 }); - var bc5 = new Baggage(new Dictionary() { [K1] = V2, [K2] = V1 }); - - Assert.True(bc1.Equals(bc2)); - - Assert.False(bc1.Equals(bc3)); - Assert.False(bc1.Equals(bc4)); - Assert.False(bc2.Equals(bc4)); - Assert.False(bc3.Equals(bc4)); - Assert.False(bc5.Equals(bc4)); - Assert.False(bc4.Equals(bc5)); - } + [Fact] + public void EqualsTest() + { + var bc1 = new Baggage(new Dictionary() { [K1] = V1, [K2] = V2 }); + var bc2 = new Baggage(new Dictionary() { [K1] = V1, [K2] = V2 }); + var bc3 = new Baggage(new Dictionary() { [K2] = V2, [K1] = V1 }); + var bc4 = new Baggage(new Dictionary() { [K1] = V1, [K2] = V1 }); + var bc5 = new Baggage(new Dictionary() { [K1] = V2, [K2] = V1 }); + + Assert.True(bc1.Equals(bc2)); + + Assert.False(bc1.Equals(bc3)); + Assert.False(bc1.Equals(bc4)); + Assert.False(bc2.Equals(bc4)); + Assert.False(bc3.Equals(bc4)); + Assert.False(bc5.Equals(bc4)); + Assert.False(bc4.Equals(bc5)); + } - [Fact] - public void CreateBaggageTest() - { - var baggage = Baggage.Create(null); - - Assert.Equal(default, baggage); - - baggage = Baggage.Create(new Dictionary - { - [K1] = V1, - ["key2"] = "value2", - ["KEY2"] = "VALUE2", - ["KEY3"] = "VALUE3", - ["Key3"] = null, - }); - - Assert.Equal(2, baggage.Count); - Assert.Contains(baggage.GetBaggage(), kvp => kvp.Key == K1); - Assert.Equal("VALUE2", Baggage.GetBaggage("key2", baggage)); - } + [Fact] + public void CreateBaggageTest() + { + var baggage = Baggage.Create(null); + + Assert.Equal(default, baggage); - [Fact] - public void EqualityTests() + baggage = Baggage.Create(new Dictionary { - var emptyBaggage = Baggage.Create(null); + [K1] = V1, + ["key2"] = "value2", + ["KEY2"] = "VALUE2", + ["KEY3"] = "VALUE3", + ["Key3"] = null, + }); + + Assert.Equal(2, baggage.Count); + Assert.Contains(baggage.GetBaggage(), kvp => kvp.Key == K1); + Assert.Equal("VALUE2", Baggage.GetBaggage("key2", baggage)); + } - var baggage = Baggage.SetBaggage(K1, V1); + [Fact] + public void EqualityTests() + { + var emptyBaggage = Baggage.Create(null); - Assert.NotEqual(emptyBaggage, baggage); + var baggage = Baggage.SetBaggage(K1, V1); - Assert.True(emptyBaggage != baggage); + Assert.NotEqual(emptyBaggage, baggage); - baggage = Baggage.ClearBaggage(baggage); + Assert.True(emptyBaggage != baggage); - Assert.Equal(emptyBaggage, baggage); + baggage = Baggage.ClearBaggage(baggage); - baggage = Baggage.SetBaggage(K1, V1); + Assert.Equal(emptyBaggage, baggage); - var baggage2 = Baggage.SetBaggage(null); + baggage = Baggage.SetBaggage(K1, V1); - Assert.Equal(baggage, baggage2); + var baggage2 = Baggage.SetBaggage(null); - Assert.False(baggage.Equals(this)); - Assert.True(baggage.Equals((object)baggage2)); - } + Assert.Equal(baggage, baggage2); - [Fact] - public void GetHashCodeTests() - { - var baggage = Baggage.Current; - var emptyBaggage = Baggage.Create(null); + Assert.False(baggage.Equals(this)); + Assert.True(baggage.Equals((object)baggage2)); + } - Assert.Equal(emptyBaggage.GetHashCode(), baggage.GetHashCode()); + [Fact] + public void GetHashCodeTests() + { + var baggage = Baggage.Current; + var emptyBaggage = Baggage.Create(null); - baggage = Baggage.SetBaggage(K1, V1, baggage); + Assert.Equal(emptyBaggage.GetHashCode(), baggage.GetHashCode()); - Assert.NotEqual(emptyBaggage.GetHashCode(), baggage.GetHashCode()); + baggage = Baggage.SetBaggage(K1, V1, baggage); - var expectedBaggage = Baggage.Create(new Dictionary { [K1] = V1 }); + Assert.NotEqual(emptyBaggage.GetHashCode(), baggage.GetHashCode()); - Assert.Equal(expectedBaggage.GetHashCode(), baggage.GetHashCode()); - } + var expectedBaggage = Baggage.Create(new Dictionary { [K1] = V1 }); - [Fact] - public async Task AsyncLocalTests() - { - Baggage.SetBaggage("key1", "value1"); + Assert.Equal(expectedBaggage.GetHashCode(), baggage.GetHashCode()); + } + + [Fact] + public async Task AsyncLocalTests() + { + Baggage.SetBaggage("key1", "value1"); - await InnerTask().ConfigureAwait(false); + await InnerTask(); - Baggage.SetBaggage("key4", "value4"); + Baggage.SetBaggage("key4", "value4"); - Assert.Equal(4, Baggage.Current.Count); - Assert.Equal("value1", Baggage.GetBaggage("key1")); - Assert.Equal("value2", Baggage.GetBaggage("key2")); - Assert.Equal("value3", Baggage.GetBaggage("key3")); - Assert.Equal("value4", Baggage.GetBaggage("key4")); + Assert.Equal(4, Baggage.Current.Count); + Assert.Equal("value1", Baggage.GetBaggage("key1")); + Assert.Equal("value2", Baggage.GetBaggage("key2")); + Assert.Equal("value3", Baggage.GetBaggage("key3")); + Assert.Equal("value4", Baggage.GetBaggage("key4")); - static async Task InnerTask() - { - Baggage.SetBaggage("key2", "value2"); + static async Task InnerTask() + { + Baggage.SetBaggage("key2", "value2"); - await Task.Yield(); + await Task.Yield(); - Baggage.SetBaggage("key3", "value3"); + Baggage.SetBaggage("key3", "value3"); - // key2 & key3 changes don't flow backward automatically - } + // key2 & key3 changes don't flow backward automatically } + } - [Fact] - public void ThreadSafetyTest() - { - Baggage.SetBaggage("rootKey", "rootValue"); // Note: Required to establish a root ExecutionContext containing the BaggageHolder we use as a lock + [Fact] + public void ThreadSafetyTest() + { + Baggage.SetBaggage("rootKey", "rootValue"); // Note: Required to establish a root ExecutionContext containing the BaggageHolder we use as a lock - Parallel.For(0, 100, (i) => - { - Baggage.SetBaggage($"key{i}", $"value{i}"); - }); + Parallel.For(0, 100, (i) => + { + Baggage.SetBaggage($"key{i}", $"value{i}"); + }); - Assert.Equal(101, Baggage.Current.Count); - } + Assert.Equal(101, Baggage.Current.Count); } } diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/PropagatorsTest.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/PropagatorsTest.cs index 2279a544987..b7993e0d848 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/PropagatorsTest.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/PropagatorsTest.cs @@ -1,49 +1,35 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Xunit; -namespace OpenTelemetry.Context.Propagation.Tests +namespace OpenTelemetry.Context.Propagation.Tests; + +public class PropagatorsTest : IDisposable { - public class PropagatorsTest : IDisposable + public PropagatorsTest() { - public PropagatorsTest() - { - Propagators.Reset(); - } + Propagators.Reset(); + } - [Fact] - public void DefaultTextMapPropagatorIsNoop() - { - Assert.IsType(Propagators.DefaultTextMapPropagator); - Assert.Same(Propagators.DefaultTextMapPropagator, Propagators.DefaultTextMapPropagator); - } + [Fact] + public void DefaultTextMapPropagatorIsNoop() + { + Assert.IsType(Propagators.DefaultTextMapPropagator); + Assert.Same(Propagators.DefaultTextMapPropagator, Propagators.DefaultTextMapPropagator); + } - [Fact] - public void CanSetPropagator() - { - var testPropagator = new TestPropagator(string.Empty, string.Empty); - Propagators.DefaultTextMapPropagator = testPropagator; - Assert.Same(testPropagator, Propagators.DefaultTextMapPropagator); - } + [Fact] + public void CanSetPropagator() + { + var testPropagator = new TestPropagator(string.Empty, string.Empty); + Propagators.DefaultTextMapPropagator = testPropagator; + Assert.Same(testPropagator, Propagators.DefaultTextMapPropagator); + } - public void Dispose() - { - Propagators.Reset(); - GC.SuppressFinalize(this); - } + public void Dispose() + { + Propagators.Reset(); + GC.SuppressFinalize(this); } } diff --git a/test/OpenTelemetry.Api.Tests/Context/RuntimeContextTest.cs b/test/OpenTelemetry.Api.Tests/Context/RuntimeContextTest.cs index 3b6dbbac0b6..1d25b4bf546 100644 --- a/test/OpenTelemetry.Api.Tests/Context/RuntimeContextTest.cs +++ b/test/OpenTelemetry.Api.Tests/Context/RuntimeContextTest.cs @@ -1,138 +1,124 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Xunit; -namespace OpenTelemetry.Context.Tests +namespace OpenTelemetry.Context.Tests; + +public class RuntimeContextTest : IDisposable { - public class RuntimeContextTest : IDisposable + public RuntimeContextTest() { - public RuntimeContextTest() - { - RuntimeContext.Clear(); - } + RuntimeContext.Clear(); + } - [Fact] - public static void RegisterSlotWithInvalidNameThrows() - { - Assert.Throws(() => RuntimeContext.RegisterSlot(string.Empty)); - Assert.Throws(() => RuntimeContext.RegisterSlot(null)); - } + [Fact] + public static void RegisterSlotWithInvalidNameThrows() + { + Assert.Throws(() => RuntimeContext.RegisterSlot(string.Empty)); + Assert.Throws(() => RuntimeContext.RegisterSlot(null)); + } - [Fact] - public static void RegisterSlotWithSameName() - { - var slot = RuntimeContext.RegisterSlot("testslot"); - Assert.NotNull(slot); - Assert.Throws(() => RuntimeContext.RegisterSlot("testslot")); - } + [Fact] + public static void RegisterSlotWithSameName() + { + var slot = RuntimeContext.RegisterSlot("testslot"); + Assert.NotNull(slot); + Assert.Throws(() => RuntimeContext.RegisterSlot("testslot")); + } - [Fact] - public static void GetSlotWithInvalidNameThrows() - { - Assert.Throws(() => RuntimeContext.GetSlot(string.Empty)); - Assert.Throws(() => RuntimeContext.GetSlot(null)); - } + [Fact] + public static void GetSlotWithInvalidNameThrows() + { + Assert.Throws(() => RuntimeContext.GetSlot(string.Empty)); + Assert.Throws(() => RuntimeContext.GetSlot(null)); + } - [Fact] - public void GetSlotReturnsNullForNonExistingSlot() - { - Assert.Throws(() => RuntimeContext.GetSlot("testslot")); - } + [Fact] + public void GetSlotReturnsNullForNonExistingSlot() + { + Assert.Throws(() => RuntimeContext.GetSlot("testslot")); + } - [Fact] - public void GetSlotReturnsNullWhenTypeNotMatchingExistingSlot() - { - RuntimeContext.RegisterSlot("testslot"); - Assert.Throws(() => RuntimeContext.GetSlot("testslot")); - } + [Fact] + public void GetSlotReturnsNullWhenTypeNotMatchingExistingSlot() + { + RuntimeContext.RegisterSlot("testslot"); + Assert.Throws(() => RuntimeContext.GetSlot("testslot")); + } - [Fact] - public void RegisterAndGetSlot() - { - var expectedSlot = RuntimeContext.RegisterSlot("testslot"); - Assert.NotNull(expectedSlot); - expectedSlot.Set(100); - var actualSlot = RuntimeContext.GetSlot("testslot"); - Assert.Same(expectedSlot, actualSlot); - Assert.Equal(100, expectedSlot.Get()); - } + [Fact] + public void RegisterAndGetSlot() + { + var expectedSlot = RuntimeContext.RegisterSlot("testslot"); + Assert.NotNull(expectedSlot); + expectedSlot.Set(100); + var actualSlot = RuntimeContext.GetSlot("testslot"); + Assert.Same(expectedSlot, actualSlot); + Assert.Equal(100, expectedSlot.Get()); + } #if NETFRAMEWORK - [Fact] - public void NetFrameworkGetSlotInAnotherAppDomain() - { - const string slotName = "testSlot"; - var slot = RuntimeContext.RegisterSlot(slotName); - slot.Set(100); + [Fact] + public void NetFrameworkGetSlotInAnotherAppDomain() + { + const string slotName = "testSlot"; + var slot = RuntimeContext.RegisterSlot(slotName); + slot.Set(100); - // Create an object in another AppDomain and try to access the slot - // value from it. + // Create an object in another AppDomain and try to access the slot + // value from it. - var domainSetup = AppDomain.CurrentDomain.SetupInformation; - var ad = AppDomain.CreateDomain("other-domain", null, domainSetup); + var domainSetup = AppDomain.CurrentDomain.SetupInformation; + var ad = AppDomain.CreateDomain("other-domain", null, domainSetup); - var remoteObjectTypeName = typeof(RemoteObject).FullName; - Assert.NotNull(remoteObjectTypeName); + var remoteObjectTypeName = typeof(RemoteObject).FullName; + Assert.NotNull(remoteObjectTypeName); - var obj = (RemoteObject)ad.CreateInstanceAndUnwrap( - typeof(RemoteObject).Assembly.FullName, - remoteObjectTypeName); + var obj = (RemoteObject)ad.CreateInstanceAndUnwrap( + typeof(RemoteObject).Assembly.FullName, + remoteObjectTypeName); - // Value we previously put into the slot ("100") would not be propagated - // across AppDomains. We should get default(int) = 0 back. - Assert.Equal(0, obj.GetValueFromContextSlot(slotName)); - } + // Value we previously put into the slot ("100") would not be propagated + // across AppDomains. We should get default(int) = 0 back. + Assert.Equal(0, obj.GetValueFromContextSlot(slotName)); + } #endif - public void Dispose() - { - RuntimeContext.Clear(); - GC.SuppressFinalize(this); - } + public void Dispose() + { + RuntimeContext.Clear(); + GC.SuppressFinalize(this); + } #if NETFRAMEWORK - private class RemoteObject : ContextBoundObject + private class RemoteObject : ContextBoundObject + { + public int GetValueFromContextSlot(string slotName) { - public int GetValueFromContextSlot(string slotName) + // Slot is not propagated across AppDomains, attempting to get + // an existing slot here should throw an ArgumentException. + try + { + RuntimeContext.GetSlot(slotName); + + throw new Exception("Should not have found an existing slot: " + slotName); + } + catch (ArgumentException) { - // Slot is not propagated across AppDomains, attempting to get - // an existing slot here should throw an ArgumentException. - try - { - RuntimeContext.GetSlot(slotName); - - throw new Exception("Should not have found an existing slot: " + slotName); - } - catch (ArgumentException) - { - // This is ok. - } - - // Now re-register this slot. - RuntimeContext.RegisterSlot(slotName); - - var slot = RuntimeContext.GetSlot(slotName); - - // We didn't put any value into this slot, so default(int) should be returned. - // Previously an exception was thrown at this point. - return slot.Get(); + // This is ok. } + + // Now re-register this slot. + RuntimeContext.RegisterSlot(slotName); + + var slot = RuntimeContext.GetSlot(slotName); + + // We didn't put any value into this slot, so default(int) should be returned. + // Previously an exception was thrown at this point. + return slot.Get(); } + } #endif - } } diff --git a/test/OpenTelemetry.Api.Tests/EventSourceTest.cs b/test/OpenTelemetry.Api.Tests/EventSourceTest.cs index 1077dbc23b9..0912a5e3f5f 100644 --- a/test/OpenTelemetry.Api.Tests/EventSourceTest.cs +++ b/test/OpenTelemetry.Api.Tests/EventSourceTest.cs @@ -1,31 +1,17 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Internal; using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Api.Tests +namespace OpenTelemetry.Api.Tests; + +public class EventSourceTest { - public class EventSourceTest + [Fact] + public void EventSourceTest_OpenTelemetryApiEventSource() { - [Fact] - public void EventSourceTest_OpenTelemetryApiEventSource() - { - EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(OpenTelemetryApiEventSource.Log); - } + EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(OpenTelemetryApiEventSource.Log); } } diff --git a/test/OpenTelemetry.Api.Tests/Internal/GuardTest.cs b/test/OpenTelemetry.Api.Tests/Internal/GuardTest.cs index d90cd5b7c29..455ba44d4b2 100644 --- a/test/OpenTelemetry.Api.Tests/Internal/GuardTest.cs +++ b/test/OpenTelemetry.Api.Tests/Internal/GuardTest.cs @@ -1,198 +1,186 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +#if NETFRAMEWORK using System.Runtime.CompilerServices; +#endif using Xunit; -namespace OpenTelemetry.Internal.Tests +namespace OpenTelemetry.Internal.Tests; + +public class GuardTest { - public class GuardTest + [Fact] + public void NullTest() { - [Fact] - public void NullTest() - { - // Valid - Guard.ThrowIfNull(1); - Guard.ThrowIfNull(1.0); - Guard.ThrowIfNull(new object()); - Guard.ThrowIfNull("hello"); - - // Invalid - object potato = null; - var ex1 = Assert.Throws(() => Guard.ThrowIfNull(potato)); - Assert.Contains("Must not be null", ex1.Message); - Assert.Equal("potato", ex1.ParamName); - - object @event = null; - var ex2 = Assert.Throws(() => Guard.ThrowIfNull(@event)); - Assert.Contains("Must not be null", ex2.Message); - Assert.Equal("@event", ex2.ParamName); - - Thing thing = null; - var ex3 = Assert.Throws(() => Guard.ThrowIfNull(thing?.Bar)); - Assert.Contains("Must not be null", ex3.Message); - Assert.Equal("thing?.Bar", ex3.ParamName); - } + // Valid + Guard.ThrowIfNull(1); + Guard.ThrowIfNull(1.0); + Guard.ThrowIfNull(new object()); + Guard.ThrowIfNull("hello"); + + // Invalid + object potato = null; + var ex1 = Assert.Throws(() => Guard.ThrowIfNull(potato)); + Assert.Contains("Must not be null", ex1.Message); + Assert.Equal("potato", ex1.ParamName); + + object @event = null; + var ex2 = Assert.Throws(() => Guard.ThrowIfNull(@event)); + Assert.Contains("Must not be null", ex2.Message); + Assert.Equal("@event", ex2.ParamName); + + Thing thing = null; + var ex3 = Assert.Throws(() => Guard.ThrowIfNull(thing?.Bar)); + Assert.Contains("Must not be null", ex3.Message); + Assert.Equal("thing?.Bar", ex3.ParamName); + } - [Fact] - public void NullOrEmptyTest() - { - // Valid - Guard.ThrowIfNullOrEmpty("a"); - Guard.ThrowIfNullOrEmpty(" "); - - // Invalid - var ex1 = Assert.Throws(() => Guard.ThrowIfNullOrEmpty(null)); - Assert.Contains("Must not be null or empty", ex1.Message); - Assert.Equal("null", ex1.ParamName); - - var ex2 = Assert.Throws(() => Guard.ThrowIfNullOrEmpty(string.Empty)); - Assert.Contains("Must not be null or empty", ex2.Message); - Assert.Equal("string.Empty", ex2.ParamName); - - var x = string.Empty; - var ex3 = Assert.Throws(() => Guard.ThrowIfNullOrEmpty(x)); - Assert.Contains("Must not be null or empty", ex3.Message); - Assert.Equal("x", ex3.ParamName); - } + [Fact] + public void NullOrEmptyTest() + { + // Valid + Guard.ThrowIfNullOrEmpty("a"); + Guard.ThrowIfNullOrEmpty(" "); + + // Invalid + var ex1 = Assert.Throws(() => Guard.ThrowIfNullOrEmpty(null)); + Assert.Contains("Must not be null or empty", ex1.Message); + Assert.Equal("null", ex1.ParamName); + + var ex2 = Assert.Throws(() => Guard.ThrowIfNullOrEmpty(string.Empty)); + Assert.Contains("Must not be null or empty", ex2.Message); + Assert.Equal("string.Empty", ex2.ParamName); + + var x = string.Empty; + var ex3 = Assert.Throws(() => Guard.ThrowIfNullOrEmpty(x)); + Assert.Contains("Must not be null or empty", ex3.Message); + Assert.Equal("x", ex3.ParamName); + } - [Fact] - public void NullOrWhitespaceTest() - { - // Valid - Guard.ThrowIfNullOrWhitespace("a"); + [Fact] + public void NullOrWhitespaceTest() + { + // Valid + Guard.ThrowIfNullOrWhitespace("a"); - // Invalid - var ex1 = Assert.Throws(() => Guard.ThrowIfNullOrWhitespace(null)); - Assert.Contains("Must not be null or whitespace", ex1.Message); - Assert.Equal("null", ex1.ParamName); + // Invalid + var ex1 = Assert.Throws(() => Guard.ThrowIfNullOrWhitespace(null)); + Assert.Contains("Must not be null or whitespace", ex1.Message); + Assert.Equal("null", ex1.ParamName); - var ex2 = Assert.Throws(() => Guard.ThrowIfNullOrWhitespace(string.Empty)); - Assert.Contains("Must not be null or whitespace", ex2.Message); - Assert.Equal("string.Empty", ex2.ParamName); + var ex2 = Assert.Throws(() => Guard.ThrowIfNullOrWhitespace(string.Empty)); + Assert.Contains("Must not be null or whitespace", ex2.Message); + Assert.Equal("string.Empty", ex2.ParamName); - var ex3 = Assert.Throws(() => Guard.ThrowIfNullOrWhitespace(" \t\n\r")); - Assert.Contains("Must not be null or whitespace", ex3.Message); - Assert.Equal("\" \\t\\n\\r\"", ex3.ParamName); - } + var ex3 = Assert.Throws(() => Guard.ThrowIfNullOrWhitespace(" \t\n\r")); + Assert.Contains("Must not be null or whitespace", ex3.Message); + Assert.Equal("\" \\t\\n\\r\"", ex3.ParamName); + } - [Fact] - public void InvalidTimeoutTest() - { - // Valid - Guard.ThrowIfInvalidTimeout(Timeout.Infinite); - Guard.ThrowIfInvalidTimeout(0); - Guard.ThrowIfInvalidTimeout(100); - - // Invalid - var ex1 = Assert.Throws(() => Guard.ThrowIfInvalidTimeout(-100)); - Assert.Contains("Must be non-negative or 'Timeout.Infinite'", ex1.Message); - Assert.Equal("-100", ex1.ParamName); - } + [Fact] + public void InvalidTimeoutTest() + { + // Valid + Guard.ThrowIfInvalidTimeout(Timeout.Infinite); + Guard.ThrowIfInvalidTimeout(0); + Guard.ThrowIfInvalidTimeout(100); + + // Invalid + var ex1 = Assert.Throws(() => Guard.ThrowIfInvalidTimeout(-100)); + Assert.Contains("Must be non-negative or 'Timeout.Infinite'", ex1.Message); + Assert.Equal("-100", ex1.ParamName); + } - [Fact] - public void RangeIntTest() - { - // Valid - Guard.ThrowIfOutOfRange(0); - Guard.ThrowIfOutOfRange(0, min: 0, max: 0); - Guard.ThrowIfOutOfRange(5, min: -10, max: 10); - Guard.ThrowIfOutOfRange(int.MinValue, min: int.MinValue, max: 10); - Guard.ThrowIfOutOfRange(int.MaxValue, min: 10, max: int.MaxValue); - - // Invalid - var ex1 = Assert.Throws(() => Guard.ThrowIfOutOfRange(-1, min: 0, max: 100, minName: "empty", maxName: "full")); - Assert.Contains("Must be in the range: [0: empty, 100: full]", ex1.Message); - - var ex2 = Assert.Throws(() => Guard.ThrowIfOutOfRange(-1, min: 0, max: 100, message: "error")); - Assert.Contains("error", ex2.Message); - } + [Fact] + public void RangeIntTest() + { + // Valid + Guard.ThrowIfOutOfRange(0); + Guard.ThrowIfOutOfRange(0, min: 0, max: 0); + Guard.ThrowIfOutOfRange(5, min: -10, max: 10); + Guard.ThrowIfOutOfRange(int.MinValue, min: int.MinValue, max: 10); + Guard.ThrowIfOutOfRange(int.MaxValue, min: 10, max: int.MaxValue); + + // Invalid + var ex1 = Assert.Throws(() => Guard.ThrowIfOutOfRange(-1, min: 0, max: 100, minName: "empty", maxName: "full")); + Assert.Contains("Must be in the range: [0: empty, 100: full]", ex1.Message); + + var ex2 = Assert.Throws(() => Guard.ThrowIfOutOfRange(-1, min: 0, max: 100, message: "error")); + Assert.Contains("error", ex2.Message); + } - [Fact] - public void RangeDoubleTest() - { - // Valid - Guard.ThrowIfOutOfRange(1.0, min: 1.0, max: 1.0); - Guard.ThrowIfOutOfRange(double.MinValue, min: double.MinValue, max: 10.0); - Guard.ThrowIfOutOfRange(double.MaxValue, min: 10.0, max: double.MaxValue); + [Fact] + public void RangeDoubleTest() + { + // Valid + Guard.ThrowIfOutOfRange(1.0, min: 1.0, max: 1.0); + Guard.ThrowIfOutOfRange(double.MinValue, min: double.MinValue, max: 10.0); + Guard.ThrowIfOutOfRange(double.MaxValue, min: 10.0, max: double.MaxValue); - // Invalid - var ex3 = Assert.Throws(() => Guard.ThrowIfOutOfRange(-1.1, min: 0.1, max: 99.9, minName: "empty", maxName: "full")); - Assert.Contains("Must be in the range: [0.1: empty, 99.9: full]", ex3.Message); + // Invalid + var ex3 = Assert.Throws(() => Guard.ThrowIfOutOfRange(-1.1, min: 0.1, max: 99.9, minName: "empty", maxName: "full")); + Assert.Contains("Must be in the range: [0.1: empty, 99.9: full]", ex3.Message); - var ex4 = Assert.Throws(() => Guard.ThrowIfOutOfRange(-1.1, min: 0.0, max: 100.0)); - Assert.Contains("Must be in the range: [0, 100]", ex4.Message); - } + var ex4 = Assert.Throws(() => Guard.ThrowIfOutOfRange(-1.1, min: 0.0, max: 100.0)); + Assert.Contains("Must be in the range: [0, 100]", ex4.Message); + } - [Fact] - public void TypeTest() - { - // Valid - Guard.ThrowIfNotOfType(0); - Guard.ThrowIfNotOfType(new object()); - Guard.ThrowIfNotOfType("hello"); - - // Invalid - var ex1 = Assert.Throws(() => Guard.ThrowIfNotOfType(100)); - Assert.Equal("Cannot cast '100' from 'System.Int32' to 'System.Double'", ex1.Message); - } + [Fact] + public void TypeTest() + { + // Valid + Guard.ThrowIfNotOfType(0); + Guard.ThrowIfNotOfType(new object()); + Guard.ThrowIfNotOfType("hello"); + + // Invalid + var ex1 = Assert.Throws(() => Guard.ThrowIfNotOfType(100)); + Assert.Equal("Cannot cast '100' from 'System.Int32' to 'System.Double'", ex1.Message); + } - [Fact] - public void ZeroTest() - { - // Valid - Guard.ThrowIfZero(1); + [Fact] + public void ZeroTest() + { + // Valid + Guard.ThrowIfZero(1); - // Invalid - var ex1 = Assert.Throws(() => Guard.ThrowIfZero(0)); - Assert.Contains("Must not be zero", ex1.Message); - Assert.Equal("0", ex1.ParamName); - } + // Invalid + var ex1 = Assert.Throws(() => Guard.ThrowIfZero(0)); + Assert.Contains("Must not be zero", ex1.Message); + Assert.Equal("0", ex1.ParamName); + } + + public class Thing + { + public string Bar { get; set; } + } - public class Thing +#if !NET6_0_OR_GREATER + /// + /// Borrowed from: . + /// + public class CallerArgumentExpressionAttributeTests + { + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("paramName")] + public static void Ctor_ParameterName_Roundtrip(string value) { - public string Bar { get; set; } + var caea = new CallerArgumentExpressionAttribute(value); + Assert.Equal(value, caea.ParameterName); } -#if !NET6_0_OR_GREATER - /// - /// Borrowed from: . - /// - public class CallerArgumentExpressionAttributeTests + [Fact] + public static void BasicTest() { - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("paramName")] - public static void Ctor_ParameterName_Roundtrip(string value) - { - var caea = new CallerArgumentExpressionAttribute(value); - Assert.Equal(value, caea.ParameterName); - } - - [Fact] - public static void BasicTest() - { - Assert.Equal("\"hello\"", GetValue("hello")); - Assert.Equal("3 + 2", GetValue(3 + 2)); - Assert.Equal("new object()", GetValue(new object())); - } - - private static string GetValue(object argument, [CallerArgumentExpression("argument")] string expr = null) => expr; + Assert.Equal("\"hello\"", GetValue("hello")); + Assert.Equal("3 + 2", GetValue(3 + 2)); + Assert.Equal("new object()", GetValue(new object())); } -#endif + + private static string GetValue(object argument, [CallerArgumentExpression("argument")] string expr = null) => expr; } +#endif } diff --git a/test/OpenTelemetry.Api.Tests/Internal/HttpSemanticConventionHelperTest.cs b/test/OpenTelemetry.Api.Tests/Internal/HttpSemanticConventionHelperTest.cs deleted file mode 100644 index 48a92230ab7..00000000000 --- a/test/OpenTelemetry.Api.Tests/Internal/HttpSemanticConventionHelperTest.cs +++ /dev/null @@ -1,67 +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 Xunit; -using static OpenTelemetry.Internal.HttpSemanticConventionHelper; - -namespace OpenTelemetry.Api.Tests.Internal; - -public class HttpSemanticConventionHelperTest -{ - [Fact] - public void VerifyFlags() - { - var testValue = HttpSemanticConvention.Dupe; - Assert.True(testValue.HasFlag(HttpSemanticConvention.Old)); - Assert.True(testValue.HasFlag(HttpSemanticConvention.New)); - - testValue = HttpSemanticConvention.Old; - Assert.True(testValue.HasFlag(HttpSemanticConvention.Old)); - Assert.False(testValue.HasFlag(HttpSemanticConvention.New)); - - testValue = HttpSemanticConvention.New; - Assert.False(testValue.HasFlag(HttpSemanticConvention.Old)); - Assert.True(testValue.HasFlag(HttpSemanticConvention.New)); - } - - [Fact] - public void VerifyGetSemanticConventionOptIn() - { - this.RunTestWithEnvironmentVariable(null, HttpSemanticConvention.Old); - this.RunTestWithEnvironmentVariable(string.Empty, HttpSemanticConvention.Old); - this.RunTestWithEnvironmentVariable("junk", HttpSemanticConvention.Old); - this.RunTestWithEnvironmentVariable("none", HttpSemanticConvention.Old); - this.RunTestWithEnvironmentVariable("NONE", HttpSemanticConvention.Old); - this.RunTestWithEnvironmentVariable("http", HttpSemanticConvention.New); - this.RunTestWithEnvironmentVariable("HTTP", HttpSemanticConvention.New); - this.RunTestWithEnvironmentVariable("http/dup", HttpSemanticConvention.Dupe); - this.RunTestWithEnvironmentVariable("HTTP/DUP", HttpSemanticConvention.Dupe); - } - - private void RunTestWithEnvironmentVariable(string value, HttpSemanticConvention expected) - { - try - { - Environment.SetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN", value); - - Assert.Equal(expected, GetSemanticConventionOptIn()); - } - finally - { - Environment.SetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN", null); - } - } -} diff --git a/test/OpenTelemetry.Api.Tests/Logs/LogRecordAttributeListTests.cs b/test/OpenTelemetry.Api.Tests/Logs/LogRecordAttributeListTests.cs index 21a583810df..c09c728cd55 100644 --- a/test/OpenTelemetry.Api.Tests/Logs/LogRecordAttributeListTests.cs +++ b/test/OpenTelemetry.Api.Tests/Logs/LogRecordAttributeListTests.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Xunit; diff --git a/test/OpenTelemetry.Api.Tests/Logs/LogRecordDataTests.cs b/test/OpenTelemetry.Api.Tests/Logs/LogRecordDataTests.cs index 700e68b03ef..89d32795d6f 100644 --- a/test/OpenTelemetry.Api.Tests/Logs/LogRecordDataTests.cs +++ b/test/OpenTelemetry.Api.Tests/Logs/LogRecordDataTests.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #nullable enable diff --git a/test/OpenTelemetry.Api.Tests/Logs/LogRecordSeverityExtensionsTests.cs b/test/OpenTelemetry.Api.Tests/Logs/LogRecordSeverityExtensionsTests.cs index 969743b0ebd..9035898cc66 100644 --- a/test/OpenTelemetry.Api.Tests/Logs/LogRecordSeverityExtensionsTests.cs +++ b/test/OpenTelemetry.Api.Tests/Logs/LogRecordSeverityExtensionsTests.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #nullable enable diff --git a/test/OpenTelemetry.Api.Tests/Logs/LoggerProviderTests.cs b/test/OpenTelemetry.Api.Tests/Logs/LoggerProviderTests.cs index f1db68c0a83..e26443707d2 100644 --- a/test/OpenTelemetry.Api.Tests/Logs/LoggerProviderTests.cs +++ b/test/OpenTelemetry.Api.Tests/Logs/LoggerProviderTests.cs @@ -1,21 +1,11 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #nullable enable +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif using Xunit; namespace OpenTelemetry.Logs.Tests; @@ -25,7 +15,7 @@ public sealed class LoggerProviderTests [Fact] public void NoopLoggerReturnedTest() { - using var provider = new LoggerProvider(); + using var provider = new NoopLoggerProvider(); var logger = provider.GetLogger(name: "TestLogger", version: "Version"); @@ -66,9 +56,18 @@ public void LoggerReturnedWithInstrumentationScopeTest() Assert.Null(logger.Version); } + private sealed class NoopLoggerProvider : LoggerProvider + { + } + private sealed class TestLoggerProvider : LoggerProvider { - protected override bool TryCreateLogger(string? name, out Logger? logger) + protected override bool TryCreateLogger( + string? name, +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + [NotNullWhen(true)] +#endif + out Logger? logger) { logger = new TestLogger(name); return true; diff --git a/test/OpenTelemetry.Api.Tests/OpenTelemetry.Api.Tests.csproj b/test/OpenTelemetry.Api.Tests/OpenTelemetry.Api.Tests.csproj index bb3c971fd96..b2e0fa8b6db 100644 --- a/test/OpenTelemetry.Api.Tests/OpenTelemetry.Api.Tests.csproj +++ b/test/OpenTelemetry.Api.Tests/OpenTelemetry.Api.Tests.csproj @@ -1,9 +1,7 @@ Unit test project for OpenTelemetry.Api - - net7.0;net6.0 - $(TargetFrameworks);net462 + $(TargetFrameworksForTests) $(NoWarn),CS0618 @@ -16,17 +14,17 @@ + - - - - - all + + + runtime; build; native; contentfiles; analyzers + diff --git a/test/OpenTelemetry.Api.Tests/Trace/ActivityExtensionsTest.cs b/test/OpenTelemetry.Api.Tests/Trace/ActivityExtensionsTest.cs index bb261512020..31fbf806725 100644 --- a/test/OpenTelemetry.Api.Tests/Trace/ActivityExtensionsTest.cs +++ b/test/OpenTelemetry.Api.Tests/Trace/ActivityExtensionsTest.cs @@ -1,238 +1,224 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Trace.Tests +namespace OpenTelemetry.Trace.Tests; + +public class ActivityExtensionsTest { - public class ActivityExtensionsTest + private const string ActivityName = "Test Activity"; + + [Fact] + public void SetStatus() { - private const string ActivityName = "Test Activity"; + var activitySourceName = Utils.GetCurrentMethodName(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .Build(); - [Fact] - public void SetStatus() - { - var activitySourceName = Utils.GetCurrentMethodName(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .Build(); + using var source = new ActivitySource(activitySourceName); + using var activity = source.StartActivity(ActivityName); + activity.SetStatus(Status.Ok); + activity?.Stop(); - using var source = new ActivitySource(activitySourceName); - using var activity = source.StartActivity(ActivityName); - activity.SetStatus(Status.Ok); - activity?.Stop(); + Assert.Equal(Status.Ok, activity.GetStatus()); + } - Assert.Equal(Status.Ok, activity.GetStatus()); - } + [Fact] + public void SetStatusWithDescription() + { + var activitySourceName = Utils.GetCurrentMethodName(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .Build(); + + using var source = new ActivitySource(activitySourceName); + using var activity = source.StartActivity(ActivityName); + activity.SetStatus(Status.Error.WithDescription("Not Found")); + activity?.Stop(); + + var status = activity.GetStatus(); + Assert.Equal(StatusCode.Error, status.StatusCode); + Assert.Equal("Not Found", status.Description); + } - [Fact] - public void SetStatusWithDescription() - { - var activitySourceName = Utils.GetCurrentMethodName(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .Build(); - - using var source = new ActivitySource(activitySourceName); - using var activity = source.StartActivity(ActivityName); - activity.SetStatus(Status.Error.WithDescription("Not Found")); - activity?.Stop(); - - var status = activity.GetStatus(); - Assert.Equal(StatusCode.Error, status.StatusCode); - Assert.Equal("Not Found", status.Description); - } - - [Fact] - public void SetStatusWithDescriptionTwice() - { - var activitySourceName = Utils.GetCurrentMethodName(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .Build(); - - using var source = new ActivitySource(activitySourceName); - using var activity = source.StartActivity(ActivityName); - activity.SetStatus(Status.Error.WithDescription("Not Found")); - activity.SetStatus(Status.Ok); - activity?.Stop(); - - var status = activity.GetStatus(); - Assert.Equal(StatusCode.Ok, status.StatusCode); - Assert.Null(status.Description); - } - - [Fact] - public void SetStatusWithIgnoredDescription() - { - var activitySourceName = Utils.GetCurrentMethodName(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .Build(); - - using var source = new ActivitySource(activitySourceName); - using var activity = source.StartActivity(ActivityName); - activity.SetStatus(Status.Ok.WithDescription("This should be ignored.")); - activity?.Stop(); - - var status = activity.GetStatus(); - Assert.Equal(StatusCode.Ok, status.StatusCode); - Assert.Null(status.Description); - } - - [Fact] - public void SetCancelledStatus() - { - var activitySourceName = Utils.GetCurrentMethodName(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .Build(); + [Fact] + public void SetStatusWithDescriptionTwice() + { + var activitySourceName = Utils.GetCurrentMethodName(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .Build(); + + using var source = new ActivitySource(activitySourceName); + using var activity = source.StartActivity(ActivityName); + activity.SetStatus(Status.Error.WithDescription("Not Found")); + activity.SetStatus(Status.Ok); + activity?.Stop(); + + var status = activity.GetStatus(); + Assert.Equal(StatusCode.Ok, status.StatusCode); + Assert.Null(status.Description); + } - using var source = new ActivitySource(activitySourceName); - using var activity = source.StartActivity(ActivityName); - activity.SetStatus(Status.Error); - activity?.Stop(); + [Fact] + public void SetStatusWithIgnoredDescription() + { + var activitySourceName = Utils.GetCurrentMethodName(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .Build(); + + using var source = new ActivitySource(activitySourceName); + using var activity = source.StartActivity(ActivityName); + activity.SetStatus(Status.Ok.WithDescription("This should be ignored.")); + activity?.Stop(); + + var status = activity.GetStatus(); + Assert.Equal(StatusCode.Ok, status.StatusCode); + Assert.Null(status.Description); + } - Assert.True(activity.GetStatus().StatusCode.Equals(Status.Error.StatusCode)); - } + [Fact] + public void SetCancelledStatus() + { + var activitySourceName = Utils.GetCurrentMethodName(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .Build(); - [Fact] - public void GetStatusWithNoStatusInActivity() - { - var activitySourceName = Utils.GetCurrentMethodName(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .Build(); + using var source = new ActivitySource(activitySourceName); + using var activity = source.StartActivity(ActivityName); + activity.SetStatus(Status.Error); + activity?.Stop(); - using var source = new ActivitySource(activitySourceName); - using var activity = source.StartActivity(ActivityName); - activity?.Stop(); + Assert.True(activity.GetStatus().StatusCode.Equals(Status.Error.StatusCode)); + } - Assert.Equal(Status.Unset, activity.GetStatus()); - } + [Fact] + public void GetStatusWithNoStatusInActivity() + { + var activitySourceName = Utils.GetCurrentMethodName(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .Build(); - [Fact] - public void LastSetStatusWins() - { - var activitySourceName = Utils.GetCurrentMethodName(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .Build(); - - using var source = new ActivitySource(activitySourceName); - using var activity = source.StartActivity(ActivityName); - activity.SetStatus(Status.Error); - activity.SetStatus(Status.Ok); - activity?.Stop(); - - Assert.Equal(Status.Ok, activity.GetStatus()); - } - - [Fact] - public void CheckRecordException() - { - var message = "message"; - var exception = new ArgumentNullException(message, new Exception(message)); - using var activity = new Activity("test-activity"); - activity.RecordException(exception); - - var @event = activity.Events.FirstOrDefault(e => e.Name == SemanticConventions.AttributeExceptionEventName); - Assert.Equal(message, @event.Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionMessage).Value); - Assert.Equal("System.ArgumentNullException", @event.Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionType).Value); - } - - [Fact] - public void RecordExceptionWithAdditionalTags() - { - var message = "message"; - var exception = new ArgumentNullException(message, new Exception(message)); - using var activity = new Activity("test-activity"); - - var tags = new TagList - { - { "key1", "value1" }, - { "key2", "value2" }, - }; - - activity.RecordException(exception, tags); - - // Additional tags passed in override attributes added from the exception - tags.Add(SemanticConventions.AttributeExceptionMessage, "SomeOtherExceptionMessage"); - tags.Add(SemanticConventions.AttributeExceptionType, "SomeOtherExceptionType"); - - activity.RecordException(exception, tags); - - var events = activity.Events.ToArray(); - Assert.Equal(2, events.Length); - - Assert.Equal(SemanticConventions.AttributeExceptionEventName, events[0].Name); - Assert.Equal(message, events[0].Tags.First(t => t.Key == SemanticConventions.AttributeExceptionMessage).Value); - Assert.Equal("System.ArgumentNullException", events[0].Tags.First(t => t.Key == SemanticConventions.AttributeExceptionType).Value); - Assert.Equal("value1", events[0].Tags.First(t => t.Key == "key1").Value); - Assert.Equal("value2", events[0].Tags.First(t => t.Key == "key2").Value); - - Assert.Equal(SemanticConventions.AttributeExceptionEventName, events[1].Name); - Assert.Equal("SomeOtherExceptionMessage", events[1].Tags.First(t => t.Key == SemanticConventions.AttributeExceptionMessage).Value); - Assert.Equal("SomeOtherExceptionType", events[1].Tags.First(t => t.Key == SemanticConventions.AttributeExceptionType).Value); - Assert.Equal("value1", events[1].Tags.First(t => t.Key == "key1").Value); - Assert.Equal("value2", events[1].Tags.First(t => t.Key == "key2").Value); - } - - [Fact] - public void GetTagValueEmpty() - { - using var activity = new Activity("Test"); + using var source = new ActivitySource(activitySourceName); + using var activity = source.StartActivity(ActivityName); + activity?.Stop(); - Assert.Null(activity.GetTagValue("Tag1")); - } + Assert.Equal(Status.Unset, activity.GetStatus()); + } - [Fact] - public void GetTagValue() - { - using var activity = new Activity("Test"); - activity.SetTag("Tag1", "Value1"); - - Assert.Equal("Value1", activity.GetTagValue("Tag1")); - Assert.Null(activity.GetTagValue("tag1")); - Assert.Null(activity.GetTagValue("Tag2")); - } - - [Theory] - [InlineData("Key", "Value", true)] - [InlineData("CustomTag", null, false)] - public void TryCheckFirstTag(string tagName, object expectedTagValue, bool expectedResult) - { - using var activity = new Activity("Test"); - activity.SetTag("Key", "Value"); + [Fact] + public void LastSetStatusWins() + { + var activitySourceName = Utils.GetCurrentMethodName(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .Build(); + + using var source = new ActivitySource(activitySourceName); + using var activity = source.StartActivity(ActivityName); + activity.SetStatus(Status.Error); + activity.SetStatus(Status.Ok); + activity?.Stop(); + + Assert.Equal(Status.Ok, activity.GetStatus()); + } - var result = activity.TryCheckFirstTag(tagName, out var tagValue); - Assert.Equal(expectedResult, result); - Assert.Equal(expectedTagValue, tagValue); - } + [Fact] + public void CheckRecordException() + { + var message = "message"; + var exception = new ArgumentNullException(message, new Exception(message)); + using var activity = new Activity("test-activity"); + activity.RecordException(exception); + + var @event = activity.Events.FirstOrDefault(e => e.Name == SemanticConventions.AttributeExceptionEventName); + Assert.Equal(message, @event.Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionMessage).Value); + Assert.Equal("System.ArgumentNullException", @event.Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionType).Value); + } - [Fact] - public void TryCheckFirstTagReturnsFalseForActivityWithNoTags() + [Fact] + public void RecordExceptionWithAdditionalTags() + { + var message = "message"; + var exception = new ArgumentNullException(message, new Exception(message)); + using var activity = new Activity("test-activity"); + + var tags = new TagList { - using var activity = new Activity("Test"); + { "key1", "value1" }, + { "key2", "value2" }, + }; + + activity.RecordException(exception, tags); + + // Additional tags passed in override attributes added from the exception + tags.Add(SemanticConventions.AttributeExceptionMessage, "SomeOtherExceptionMessage"); + tags.Add(SemanticConventions.AttributeExceptionType, "SomeOtherExceptionType"); + + activity.RecordException(exception, tags); + + var events = activity.Events.ToArray(); + Assert.Equal(2, events.Length); + + Assert.Equal(SemanticConventions.AttributeExceptionEventName, events[0].Name); + Assert.Equal(message, events[0].Tags.First(t => t.Key == SemanticConventions.AttributeExceptionMessage).Value); + Assert.Equal("System.ArgumentNullException", events[0].Tags.First(t => t.Key == SemanticConventions.AttributeExceptionType).Value); + Assert.Equal("value1", events[0].Tags.First(t => t.Key == "key1").Value); + Assert.Equal("value2", events[0].Tags.First(t => t.Key == "key2").Value); + + Assert.Equal(SemanticConventions.AttributeExceptionEventName, events[1].Name); + Assert.Equal("SomeOtherExceptionMessage", events[1].Tags.First(t => t.Key == SemanticConventions.AttributeExceptionMessage).Value); + Assert.Equal("SomeOtherExceptionType", events[1].Tags.First(t => t.Key == SemanticConventions.AttributeExceptionType).Value); + Assert.Equal("value1", events[1].Tags.First(t => t.Key == "key1").Value); + Assert.Equal("value2", events[1].Tags.First(t => t.Key == "key2").Value); + } + + [Fact] + public void GetTagValueEmpty() + { + using var activity = new Activity("Test"); + + Assert.Null(activity.GetTagValue("Tag1")); + } + + [Fact] + public void GetTagValue() + { + using var activity = new Activity("Test"); + activity.SetTag("Tag1", "Value1"); + + Assert.Equal("Value1", activity.GetTagValue("Tag1")); + Assert.Null(activity.GetTagValue("tag1")); + Assert.Null(activity.GetTagValue("Tag2")); + } + + [Theory] + [InlineData("Key", "Value", true)] + [InlineData("CustomTag", null, false)] + public void TryCheckFirstTag(string tagName, object expectedTagValue, bool expectedResult) + { + using var activity = new Activity("Test"); + activity.SetTag("Key", "Value"); + + var result = activity.TryCheckFirstTag(tagName, out var tagValue); + Assert.Equal(expectedResult, result); + Assert.Equal(expectedTagValue, tagValue); + } + + [Fact] + public void TryCheckFirstTagReturnsFalseForActivityWithNoTags() + { + using var activity = new Activity("Test"); - var result = activity.TryCheckFirstTag("Key", out var tagValue); - Assert.False(result); - Assert.Null(tagValue); - } + var result = activity.TryCheckFirstTag("Key", out var tagValue); + Assert.False(result); + Assert.Null(tagValue); } } diff --git a/test/OpenTelemetry.Api.Tests/Trace/Propagation/B3PropagatorTest.cs b/test/OpenTelemetry.Api.Tests/Trace/Propagation/B3PropagatorTest.cs index d95c8eb5a1d..8cc20c6ff5e 100644 --- a/test/OpenTelemetry.Api.Tests/Trace/Propagation/B3PropagatorTest.cs +++ b/test/OpenTelemetry.Api.Tests/Trace/Propagation/B3PropagatorTest.cs @@ -1,396 +1,383 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + using System.Diagnostics; using Xunit; using Xunit.Abstractions; -namespace OpenTelemetry.Context.Propagation.Tests +namespace OpenTelemetry.Context.Propagation.Tests; + +public class B3PropagatorTest { - public class B3PropagatorTest - { - private const string TraceIdBase16 = "ff000000000000000000000000000041"; - private const string TraceIdBase16EightBytes = "0000000000000041"; - private const string SpanIdBase16 = "ff00000000000041"; - private const string InvalidId = "abcdefghijklmnop"; - private const string InvalidSizeId = "0123456789abcdef00"; - private const ActivityTraceFlags TraceOptions = ActivityTraceFlags.Recorded; + private const string TraceIdBase16 = "ff000000000000000000000000000041"; + private const string TraceIdBase16EightBytes = "0000000000000041"; + private const string SpanIdBase16 = "ff00000000000041"; + private const string InvalidId = "abcdefghijklmnop"; + private const string InvalidSizeId = "0123456789abcdef00"; + private const ActivityTraceFlags TraceOptions = ActivityTraceFlags.Recorded; - private static readonly ActivityTraceId TraceId = ActivityTraceId.CreateFromString(TraceIdBase16.AsSpan()); - private static readonly ActivityTraceId TraceIdEightBytes = ActivityTraceId.CreateFromString(("0000000000000000" + TraceIdBase16EightBytes).AsSpan()); - private static readonly ActivitySpanId SpanId = ActivitySpanId.CreateFromString(SpanIdBase16.AsSpan()); + private static readonly ActivityTraceId TraceId = ActivityTraceId.CreateFromString(TraceIdBase16.AsSpan()); + private static readonly ActivityTraceId TraceIdEightBytes = ActivityTraceId.CreateFromString(("0000000000000000" + TraceIdBase16EightBytes).AsSpan()); + private static readonly ActivitySpanId SpanId = ActivitySpanId.CreateFromString(SpanIdBase16.AsSpan()); - private static readonly Action, string, string> Setter = (d, k, v) => d[k] = v; - private static readonly Func, string, IEnumerable> Getter = - (d, k) => - { - d.TryGetValue(k, out var v); - return new string[] { v }; - }; + private static readonly Action, string, string> Setter = (d, k, v) => d[k] = v; + private static readonly Func, string, IEnumerable> Getter = + (d, k) => + { + d.TryGetValue(k, out var v); + return new string[] { v }; + }; - private readonly B3Propagator b3propagator = new(); - private readonly B3Propagator b3PropagatorSingleHeader = new(true); + private readonly B3Propagator b3propagator = new(); + private readonly B3Propagator b3PropagatorSingleHeader = new(true); - private readonly ITestOutputHelper output; + private readonly ITestOutputHelper output; - public B3PropagatorTest(ITestOutputHelper output) - { - this.output = output; - } + public B3PropagatorTest(ITestOutputHelper output) + { + this.output = output; + } - [Fact] - public void Serialize_SampledContext() - { - var carrier = new Dictionary(); - this.b3propagator.Inject(new PropagationContext(new ActivityContext(TraceId, SpanId, TraceOptions), default), carrier, Setter); - this.ContainsExactly(carrier, new Dictionary { { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Sampled, "1" } }); - } + [Fact] + public void Serialize_SampledContext() + { + var carrier = new Dictionary(); + this.b3propagator.Inject(new PropagationContext(new ActivityContext(TraceId, SpanId, TraceOptions), default), carrier, Setter); + this.ContainsExactly(carrier, new Dictionary { { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Sampled, "1" } }); + } - [Fact] - public void Serialize_NotSampledContext() - { - var carrier = new Dictionary(); - var context = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None); - this.output.WriteLine(context.ToString()); - this.b3propagator.Inject(new PropagationContext(context, default), carrier, Setter); - this.ContainsExactly(carrier, new Dictionary { { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 } }); - } + [Fact] + public void Serialize_NotSampledContext() + { + var carrier = new Dictionary(); + var context = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None); + this.output.WriteLine(context.ToString()); + this.b3propagator.Inject(new PropagationContext(context, default), carrier, Setter); + this.ContainsExactly(carrier, new Dictionary { { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 } }); + } - [Fact] - public void ParseMissingSampledAndMissingFlag() + [Fact] + public void ParseMissingSampledAndMissingFlag() + { + var headersNotSampled = new Dictionary { - var headersNotSampled = new Dictionary - { - { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, - }; - var spanContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true); - Assert.Equal(new PropagationContext(spanContext, default), this.b3propagator.Extract(default, headersNotSampled, Getter)); - } + { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, + }; + var spanContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true); + Assert.Equal(new PropagationContext(spanContext, default), this.b3propagator.Extract(default, headersNotSampled, Getter)); + } - [Theory] - [InlineData("1")] - [InlineData("true")] - public void ParseSampled(string sampledValue) + [Theory] + [InlineData("1")] + [InlineData("true")] + public void ParseSampled(string sampledValue) + { + var headersSampled = new Dictionary { - var headersSampled = new Dictionary - { - { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Sampled, sampledValue }, - }; - var activityContext = new ActivityContext(TraceId, SpanId, TraceOptions, isRemote: true); - Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersSampled, Getter)); - } + { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Sampled, sampledValue }, + }; + var activityContext = new ActivityContext(TraceId, SpanId, TraceOptions, isRemote: true); + Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersSampled, Getter)); + } - [Theory] - [InlineData("0")] - [InlineData("false")] - [InlineData("something_else")] - public void ParseNotSampled(string sampledValue) + [Theory] + [InlineData("0")] + [InlineData("false")] + [InlineData("something_else")] + public void ParseNotSampled(string sampledValue) + { + var headersNotSampled = new Dictionary { - var headersNotSampled = new Dictionary - { - { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Sampled, sampledValue }, - }; - var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true); - Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersNotSampled, Getter)); - } + { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Sampled, sampledValue }, + }; + var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true); + Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersNotSampled, Getter)); + } - [Fact] - public void ParseFlag() + [Fact] + public void ParseFlag() + { + var headersFlagSampled = new Dictionary { - var headersFlagSampled = new Dictionary - { - { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Flags, "1" }, - }; - var activityContext = new ActivityContext(TraceId, SpanId, TraceOptions, isRemote: true); - Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersFlagSampled, Getter)); - } + { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Flags, "1" }, + }; + var activityContext = new ActivityContext(TraceId, SpanId, TraceOptions, isRemote: true); + Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersFlagSampled, Getter)); + } - [Fact] - public void ParseZeroFlag() + [Fact] + public void ParseZeroFlag() + { + var headersFlagNotSampled = new Dictionary { - var headersFlagNotSampled = new Dictionary - { - { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Flags, "0" }, - }; - var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true); - Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersFlagNotSampled, Getter)); - } + { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Flags, "0" }, + }; + var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true); + Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersFlagNotSampled, Getter)); + } - [Fact] - public void ParseEightBytesTraceId() - { - var headersEightBytes = new Dictionary - { - { B3Propagator.XB3TraceId, TraceIdBase16EightBytes }, - { B3Propagator.XB3SpanId, SpanIdBase16 }, - { B3Propagator.XB3Sampled, "1" }, - }; - var activityContext = new ActivityContext(TraceIdEightBytes, SpanId, TraceOptions, isRemote: true); - Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersEightBytes, Getter)); - } + [Fact] + public void ParseEightBytesTraceId() + { + var headersEightBytes = new Dictionary + { + { B3Propagator.XB3TraceId, TraceIdBase16EightBytes }, + { B3Propagator.XB3SpanId, SpanIdBase16 }, + { B3Propagator.XB3Sampled, "1" }, + }; + var activityContext = new ActivityContext(TraceIdEightBytes, SpanId, TraceOptions, isRemote: true); + Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersEightBytes, Getter)); + } - [Fact] - public void ParseEightBytesTraceId_NotSampledSpanContext() + [Fact] + public void ParseEightBytesTraceId_NotSampledSpanContext() + { + var headersEightBytes = new Dictionary { - var headersEightBytes = new Dictionary - { - { B3Propagator.XB3TraceId, TraceIdBase16EightBytes }, { B3Propagator.XB3SpanId, SpanIdBase16 }, - }; - var activityContext = new ActivityContext(TraceIdEightBytes, SpanId, ActivityTraceFlags.None, isRemote: true); - Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersEightBytes, Getter)); - } + { B3Propagator.XB3TraceId, TraceIdBase16EightBytes }, { B3Propagator.XB3SpanId, SpanIdBase16 }, + }; + var activityContext = new ActivityContext(TraceIdEightBytes, SpanId, ActivityTraceFlags.None, isRemote: true); + Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersEightBytes, Getter)); + } - [Fact] - public void ParseInvalidTraceId() + [Fact] + public void ParseInvalidTraceId() + { + var invalidHeaders = new Dictionary { - var invalidHeaders = new Dictionary - { - { B3Propagator.XB3TraceId, InvalidId }, { B3Propagator.XB3SpanId, SpanIdBase16 }, - }; - Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter)); - } + { B3Propagator.XB3TraceId, InvalidId }, { B3Propagator.XB3SpanId, SpanIdBase16 }, + }; + Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter)); + } - [Fact] - public void ParseInvalidTraceId_Size() + [Fact] + public void ParseInvalidTraceId_Size() + { + var invalidHeaders = new Dictionary { - var invalidHeaders = new Dictionary - { - { B3Propagator.XB3TraceId, InvalidSizeId }, { B3Propagator.XB3SpanId, SpanIdBase16 }, - }; + { B3Propagator.XB3TraceId, InvalidSizeId }, { B3Propagator.XB3SpanId, SpanIdBase16 }, + }; - Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter)); - } + Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter)); + } - [Fact] - public void ParseMissingTraceId() - { - var invalidHeaders = new Dictionary { { B3Propagator.XB3SpanId, SpanIdBase16 }, }; - Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter)); - } + [Fact] + public void ParseMissingTraceId() + { + var invalidHeaders = new Dictionary { { B3Propagator.XB3SpanId, SpanIdBase16 }, }; + Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter)); + } - [Fact] - public void ParseInvalidSpanId() + [Fact] + public void ParseInvalidSpanId() + { + var invalidHeaders = new Dictionary { - var invalidHeaders = new Dictionary - { - { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, InvalidId }, - }; - Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter)); - } + { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, InvalidId }, + }; + Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter)); + } - [Fact] - public void ParseInvalidSpanId_Size() + [Fact] + public void ParseInvalidSpanId_Size() + { + var invalidHeaders = new Dictionary { - var invalidHeaders = new Dictionary - { - { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, InvalidSizeId }, - }; - Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter)); - } + { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, InvalidSizeId }, + }; + Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter)); + } - [Fact] - public void ParseMissingSpanId() - { - var invalidHeaders = new Dictionary { { B3Propagator.XB3TraceId, TraceIdBase16 } }; - Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter)); - } + [Fact] + public void ParseMissingSpanId() + { + var invalidHeaders = new Dictionary { { B3Propagator.XB3TraceId, TraceIdBase16 } }; + Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter)); + } - [Fact] - public void Serialize_SampledContext_SingleHeader() - { - var carrier = new Dictionary(); - var activityContext = new ActivityContext(TraceId, SpanId, TraceOptions); - this.b3PropagatorSingleHeader.Inject(new PropagationContext(activityContext, default), carrier, Setter); - this.ContainsExactly(carrier, new Dictionary { { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-1" } }); - } + [Fact] + public void Serialize_SampledContext_SingleHeader() + { + var carrier = new Dictionary(); + var activityContext = new ActivityContext(TraceId, SpanId, TraceOptions); + this.b3PropagatorSingleHeader.Inject(new PropagationContext(activityContext, default), carrier, Setter); + this.ContainsExactly(carrier, new Dictionary { { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-1" } }); + } - [Fact] - public void Serialize_NotSampledContext_SingleHeader() - { - var carrier = new Dictionary(); - var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None); - this.output.WriteLine(activityContext.ToString()); - this.b3PropagatorSingleHeader.Inject(new PropagationContext(activityContext, default), carrier, Setter); - this.ContainsExactly(carrier, new Dictionary { { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}" } }); - } + [Fact] + public void Serialize_NotSampledContext_SingleHeader() + { + var carrier = new Dictionary(); + var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None); + this.output.WriteLine(activityContext.ToString()); + this.b3PropagatorSingleHeader.Inject(new PropagationContext(activityContext, default), carrier, Setter); + this.ContainsExactly(carrier, new Dictionary { { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}" } }); + } - [Fact] - public void ParseMissingSampledAndMissingFlag_SingleHeader() + [Fact] + public void ParseMissingSampledAndMissingFlag_SingleHeader() + { + var headersNotSampled = new Dictionary { - var headersNotSampled = new Dictionary - { - { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}" }, - }; - var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true); - Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersNotSampled, Getter)); - } + { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}" }, + }; + var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true); + Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersNotSampled, Getter)); + } - [Fact] - public void ParseSampled_SingleHeader() + [Fact] + public void ParseSampled_SingleHeader() + { + var headersSampled = new Dictionary { - var headersSampled = new Dictionary - { - { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-1" }, - }; - - Assert.Equal( - new PropagationContext(new ActivityContext(TraceId, SpanId, TraceOptions, isRemote: true), default), - this.b3PropagatorSingleHeader.Extract(default, headersSampled, Getter)); - } + { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-1" }, + }; + + Assert.Equal( + new PropagationContext(new ActivityContext(TraceId, SpanId, TraceOptions, isRemote: true), default), + this.b3PropagatorSingleHeader.Extract(default, headersSampled, Getter)); + } - [Fact] - public void ParseZeroSampled_SingleHeader() + [Fact] + public void ParseZeroSampled_SingleHeader() + { + var headersNotSampled = new Dictionary { - var headersNotSampled = new Dictionary - { - { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-0" }, - }; - - Assert.Equal( - new PropagationContext(new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true), default), - this.b3PropagatorSingleHeader.Extract(default, headersNotSampled, Getter)); - } + { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-0" }, + }; + + Assert.Equal( + new PropagationContext(new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true), default), + this.b3PropagatorSingleHeader.Extract(default, headersNotSampled, Getter)); + } - [Fact] - public void ParseFlag_SingleHeader() + [Fact] + public void ParseFlag_SingleHeader() + { + var headersFlagSampled = new Dictionary { - var headersFlagSampled = new Dictionary - { - { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-1" }, - }; - var activityContext = new ActivityContext(TraceId, SpanId, TraceOptions, isRemote: true); - Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersFlagSampled, Getter)); - } + { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-1" }, + }; + var activityContext = new ActivityContext(TraceId, SpanId, TraceOptions, isRemote: true); + Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersFlagSampled, Getter)); + } - [Fact] - public void ParseZeroFlag_SingleHeader() + [Fact] + public void ParseZeroFlag_SingleHeader() + { + var headersFlagNotSampled = new Dictionary { - var headersFlagNotSampled = new Dictionary - { - { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-0" }, - }; - var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true); - Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersFlagNotSampled, Getter)); - } + { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-0" }, + }; + var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true); + Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersFlagNotSampled, Getter)); + } - [Fact] - public void ParseEightBytesTraceId_SingleHeader() + [Fact] + public void ParseEightBytesTraceId_SingleHeader() + { + var headersEightBytes = new Dictionary { - var headersEightBytes = new Dictionary - { - { B3Propagator.XB3Combined, $"{TraceIdBase16EightBytes}-{SpanIdBase16}-1" }, - }; - var activityContext = new ActivityContext(TraceIdEightBytes, SpanId, TraceOptions, isRemote: true); - Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersEightBytes, Getter)); - } + { B3Propagator.XB3Combined, $"{TraceIdBase16EightBytes}-{SpanIdBase16}-1" }, + }; + var activityContext = new ActivityContext(TraceIdEightBytes, SpanId, TraceOptions, isRemote: true); + Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersEightBytes, Getter)); + } - [Fact] - public void ParseEightBytesTraceId_NotSampledSpanContext_SingleHeader() + [Fact] + public void ParseEightBytesTraceId_NotSampledSpanContext_SingleHeader() + { + var headersEightBytes = new Dictionary { - var headersEightBytes = new Dictionary - { - { B3Propagator.XB3Combined, $"{TraceIdBase16EightBytes}-{SpanIdBase16}" }, - }; - var activityContext = new ActivityContext(TraceIdEightBytes, SpanId, ActivityTraceFlags.None, isRemote: true); - Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersEightBytes, Getter)); - } + { B3Propagator.XB3Combined, $"{TraceIdBase16EightBytes}-{SpanIdBase16}" }, + }; + var activityContext = new ActivityContext(TraceIdEightBytes, SpanId, ActivityTraceFlags.None, isRemote: true); + Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersEightBytes, Getter)); + } - [Fact] - public void ParseInvalidTraceId_SingleHeader() + [Fact] + public void ParseInvalidTraceId_SingleHeader() + { + var invalidHeaders = new Dictionary { - var invalidHeaders = new Dictionary - { - { B3Propagator.XB3Combined, $"{InvalidId}-{SpanIdBase16}" }, - }; - Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter)); - } + { B3Propagator.XB3Combined, $"{InvalidId}-{SpanIdBase16}" }, + }; + Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter)); + } - [Fact] - public void ParseInvalidTraceId_Size_SingleHeader() + [Fact] + public void ParseInvalidTraceId_Size_SingleHeader() + { + var invalidHeaders = new Dictionary { - var invalidHeaders = new Dictionary - { - { B3Propagator.XB3Combined, $"{InvalidSizeId}-{SpanIdBase16}" }, - }; + { B3Propagator.XB3Combined, $"{InvalidSizeId}-{SpanIdBase16}" }, + }; - Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter)); - } + Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter)); + } - [Fact] - public void ParseMissingTraceId_SingleHeader() - { - var invalidHeaders = new Dictionary { { B3Propagator.XB3Combined, $"-{SpanIdBase16}" } }; - Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter)); - } + [Fact] + public void ParseMissingTraceId_SingleHeader() + { + var invalidHeaders = new Dictionary { { B3Propagator.XB3Combined, $"-{SpanIdBase16}" } }; + Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter)); + } - [Fact] - public void ParseInvalidSpanId_SingleHeader() + [Fact] + public void ParseInvalidSpanId_SingleHeader() + { + var invalidHeaders = new Dictionary { - var invalidHeaders = new Dictionary - { - { B3Propagator.XB3Combined, $"{TraceIdBase16}-{InvalidId}" }, - }; - Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter)); - } + { B3Propagator.XB3Combined, $"{TraceIdBase16}-{InvalidId}" }, + }; + Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter)); + } - [Fact] - public void ParseInvalidSpanId_Size_SingleHeader() + [Fact] + public void ParseInvalidSpanId_Size_SingleHeader() + { + var invalidHeaders = new Dictionary { - var invalidHeaders = new Dictionary - { - { B3Propagator.XB3Combined, $"{TraceIdBase16}-{InvalidSizeId}" }, - }; - Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter)); - } + { B3Propagator.XB3Combined, $"{TraceIdBase16}-{InvalidSizeId}" }, + }; + Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter)); + } - [Fact] - public void ParseMissingSpanId_SingleHeader() - { - var invalidHeaders = new Dictionary { { B3Propagator.XB3Combined, $"{TraceIdBase16}-" } }; - Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter)); - } + [Fact] + public void ParseMissingSpanId_SingleHeader() + { + var invalidHeaders = new Dictionary { { B3Propagator.XB3Combined, $"{TraceIdBase16}-" } }; + Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter)); + } + + [Fact] + public void Fields_list() + { + ContainsExactly( + this.b3propagator.Fields, + new List { B3Propagator.XB3TraceId, B3Propagator.XB3SpanId, B3Propagator.XB3ParentSpanId, B3Propagator.XB3Sampled, B3Propagator.XB3Flags }); + } - [Fact] - public void Fields_list() + private static void ContainsExactly(ISet list, List items) + { + Assert.Equal(items.Count, list.Count); + foreach (var item in items) { - ContainsExactly( - this.b3propagator.Fields, - new List { B3Propagator.XB3TraceId, B3Propagator.XB3SpanId, B3Propagator.XB3ParentSpanId, B3Propagator.XB3Sampled, B3Propagator.XB3Flags }); + Assert.Contains(item, list); } + } - private static void ContainsExactly(ISet list, List items) + private void ContainsExactly(IDictionary dict, IDictionary items) + { + foreach (var d in dict) { - Assert.Equal(items.Count, list.Count); - foreach (var item in items) - { - Assert.Contains(item, list); - } + this.output.WriteLine(d.Key + "=" + d.Value); } - private void ContainsExactly(IDictionary dict, IDictionary items) + Assert.Equal(items.Count, dict.Count); + foreach (var item in items) { - foreach (var d in dict) - { - this.output.WriteLine(d.Key + "=" + d.Value); - } - - Assert.Equal(items.Count, dict.Count); - foreach (var item in items) - { - Assert.Contains(item, dict); - } + Assert.Contains(item, dict); } } } diff --git a/test/OpenTelemetry.Api.Tests/Trace/Propagation/BaggagePropagatorTest.cs b/test/OpenTelemetry.Api.Tests/Trace/Propagation/BaggagePropagatorTest.cs index 76efea7bde6..097f63e039d 100644 --- a/test/OpenTelemetry.Api.Tests/Trace/Propagation/BaggagePropagatorTest.cs +++ b/test/OpenTelemetry.Api.Tests/Trace/Propagation/BaggagePropagatorTest.cs @@ -1,219 +1,205 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Net; using Xunit; -namespace OpenTelemetry.Context.Propagation.Tests -{ - public class BaggagePropagatorTest - { - private static readonly Func, string, IEnumerable> Getter = - (d, k) => - { - d.TryGetValue(k, out var v); - return new string[] { v }; - }; +namespace OpenTelemetry.Context.Propagation.Tests; - private static readonly Func>, string, IEnumerable> GetterList = - (d, k) => - { - return d.Where(i => i.Key == k).Select(i => i.Value); - }; +public class BaggagePropagatorTest +{ + private static readonly Func, string, IEnumerable> Getter = + (d, k) => + { + d.TryGetValue(k, out var v); + return new string[] { v }; + }; - private static readonly Action, string, string> Setter = (carrier, name, value) => + private static readonly Func>, string, IEnumerable> GetterList = + (d, k) => { - carrier[name] = value; + return d.Where(i => i.Key == k).Select(i => i.Value); }; - private readonly BaggagePropagator baggage = new(); + private static readonly Action, string, string> Setter = (carrier, name, value) => + { + carrier[name] = value; + }; - [Fact] - public void ValidateFieldsProperty() - { - Assert.Equal(new HashSet { BaggagePropagator.BaggageHeaderName }, this.baggage.Fields); - Assert.Single(this.baggage.Fields); - } + private readonly BaggagePropagator baggage = new(); - [Fact] - public void ValidateDefaultCarrierExtraction() - { - var propagationContext = this.baggage.Extract(default, null, null); - Assert.Equal(default, propagationContext); - } + [Fact] + public void ValidateFieldsProperty() + { + Assert.Equal(new HashSet { BaggagePropagator.BaggageHeaderName }, this.baggage.Fields); + Assert.Single(this.baggage.Fields); + } - [Fact] - public void ValidateDefaultGetterExtraction() - { - var carrier = new Dictionary(); - var propagationContext = this.baggage.Extract(default, carrier, null); - Assert.Equal(default, propagationContext); - } + [Fact] + public void ValidateDefaultCarrierExtraction() + { + var propagationContext = this.baggage.Extract(default, null, null); + Assert.Equal(default, propagationContext); + } - [Fact] - public void ValidateNoBaggageExtraction() - { - var carrier = new Dictionary(); - var propagationContext = this.baggage.Extract(default, carrier, Getter); - Assert.Equal(default, propagationContext); - } + [Fact] + public void ValidateDefaultGetterExtraction() + { + var carrier = new Dictionary(); + var propagationContext = this.baggage.Extract(default, carrier, null); + Assert.Equal(default, propagationContext); + } - [Fact] - public void ValidateOneBaggageExtraction() + [Fact] + public void ValidateNoBaggageExtraction() + { + var carrier = new Dictionary(); + var propagationContext = this.baggage.Extract(default, carrier, Getter); + Assert.Equal(default, propagationContext); + } + + [Fact] + public void ValidateOneBaggageExtraction() + { + var carrier = new Dictionary { - var carrier = new Dictionary - { - { BaggagePropagator.BaggageHeaderName, "name=test" }, - }; - var propagationContext = this.baggage.Extract(default, carrier, Getter); - Assert.False(propagationContext == default); - Assert.Single(propagationContext.Baggage.GetBaggage()); + { BaggagePropagator.BaggageHeaderName, "name=test" }, + }; + var propagationContext = this.baggage.Extract(default, carrier, Getter); + Assert.False(propagationContext == default); + Assert.Single(propagationContext.Baggage.GetBaggage()); - var baggage = propagationContext.Baggage.GetBaggage().FirstOrDefault(); + var baggage = propagationContext.Baggage.GetBaggage().FirstOrDefault(); - Assert.Equal("name", baggage.Key); - Assert.Equal("test", baggage.Value); - } + Assert.Equal("name", baggage.Key); + Assert.Equal("test", baggage.Value); + } - [Fact] - public void ValidateMultipleBaggageExtraction() + [Fact] + public void ValidateMultipleBaggageExtraction() + { + var carrier = new List> { - var carrier = new List> - { - new KeyValuePair(BaggagePropagator.BaggageHeaderName, "name1=test1"), - new KeyValuePair(BaggagePropagator.BaggageHeaderName, "name2=test2"), - new KeyValuePair(BaggagePropagator.BaggageHeaderName, "name2=test2"), - }; + new KeyValuePair(BaggagePropagator.BaggageHeaderName, "name1=test1"), + new KeyValuePair(BaggagePropagator.BaggageHeaderName, "name2=test2"), + new KeyValuePair(BaggagePropagator.BaggageHeaderName, "name2=test2"), + }; - var propagationContext = this.baggage.Extract(default, carrier, GetterList); + var propagationContext = this.baggage.Extract(default, carrier, GetterList); - Assert.False(propagationContext == default); - Assert.True(propagationContext.ActivityContext == default); + Assert.False(propagationContext == default); + Assert.True(propagationContext.ActivityContext == default); - Assert.Equal(2, propagationContext.Baggage.Count); + Assert.Equal(2, propagationContext.Baggage.Count); - var array = propagationContext.Baggage.GetBaggage().ToArray(); + var array = propagationContext.Baggage.GetBaggage().ToArray(); - Assert.Equal("name1", array[0].Key); - Assert.Equal("test1", array[0].Value); + Assert.Equal("name1", array[0].Key); + Assert.Equal("test1", array[0].Value); - Assert.Equal("name2", array[1].Key); - Assert.Equal("test2", array[1].Value); - } + Assert.Equal("name2", array[1].Key); + Assert.Equal("test2", array[1].Value); + } - [Fact] - public void ValidateLongBaggageExtraction() + [Fact] + public void ValidateLongBaggageExtraction() + { + var carrier = new Dictionary { - var carrier = new Dictionary - { - { BaggagePropagator.BaggageHeaderName, $"name={new string('x', 8186)},clientId=1234" }, - }; - var propagationContext = this.baggage.Extract(default, carrier, Getter); - Assert.False(propagationContext == default); - Assert.Single(propagationContext.Baggage.GetBaggage()); + { BaggagePropagator.BaggageHeaderName, $"name={new string('x', 8186)},clientId=1234" }, + }; + var propagationContext = this.baggage.Extract(default, carrier, Getter); + Assert.False(propagationContext == default); + Assert.Single(propagationContext.Baggage.GetBaggage()); - var array = propagationContext.Baggage.GetBaggage().ToArray(); + var array = propagationContext.Baggage.GetBaggage().ToArray(); - Assert.Equal("name", array[0].Key); - Assert.Equal(new string('x', 8186), array[0].Value); - } + Assert.Equal("name", array[0].Key); + Assert.Equal(new string('x', 8186), array[0].Value); + } - [Fact] - public void ValidateSpecialCharsBaggageExtraction() + [Fact] + public void ValidateSpecialCharsBaggageExtraction() + { + var encodedKey = WebUtility.UrlEncode("key2"); + var encodedValue = WebUtility.UrlEncode("!x_x,x-x&x(x\");:"); + var escapedKey = Uri.EscapeDataString("key()3"); + var escapedValue = Uri.EscapeDataString("value()!&;:"); + + Assert.Equal("key2", encodedKey); + Assert.Equal("!x_x%2Cx-x%26x(x%22)%3B%3A", encodedValue); + Assert.Equal("key%28%293", escapedKey); + Assert.Equal("value%28%29%21%26%3B%3A", escapedValue); + + var initialBaggage = $"key+1=value+1,{encodedKey}={encodedValue},{escapedKey}={escapedValue}"; + var carrier = new List> { - var encodedKey = WebUtility.UrlEncode("key2"); - var encodedValue = WebUtility.UrlEncode("!x_x,x-x&x(x\");:"); - var escapedKey = Uri.EscapeDataString("key()3"); - var escapedValue = Uri.EscapeDataString("value()!&;:"); - - Assert.Equal("key2", encodedKey); - Assert.Equal("!x_x%2Cx-x%26x(x%22)%3B%3A", encodedValue); - Assert.Equal("key%28%293", escapedKey); - Assert.Equal("value%28%29%21%26%3B%3A", escapedValue); - - var initialBaggage = $"key+1=value+1,{encodedKey}={encodedValue},{escapedKey}={escapedValue}"; - var carrier = new List> - { - new KeyValuePair(BaggagePropagator.BaggageHeaderName, initialBaggage), - }; + new KeyValuePair(BaggagePropagator.BaggageHeaderName, initialBaggage), + }; - var propagationContext = this.baggage.Extract(default, carrier, GetterList); + var propagationContext = this.baggage.Extract(default, carrier, GetterList); - Assert.False(propagationContext == default); - Assert.True(propagationContext.ActivityContext == default); + Assert.False(propagationContext == default); + Assert.True(propagationContext.ActivityContext == default); - Assert.Equal(3, propagationContext.Baggage.Count); + Assert.Equal(3, propagationContext.Baggage.Count); - var actualBaggage = propagationContext.Baggage.GetBaggage(); + var actualBaggage = propagationContext.Baggage.GetBaggage(); - Assert.Equal(3, actualBaggage.Count); + Assert.Equal(3, actualBaggage.Count); - Assert.True(actualBaggage.ContainsKey("key 1")); - Assert.Equal("value 1", actualBaggage["key 1"]); + Assert.True(actualBaggage.ContainsKey("key 1")); + Assert.Equal("value 1", actualBaggage["key 1"]); - Assert.True(actualBaggage.ContainsKey("key2")); - Assert.Equal("!x_x,x-x&x(x\");:", actualBaggage["key2"]); + Assert.True(actualBaggage.ContainsKey("key2")); + Assert.Equal("!x_x,x-x&x(x\");:", actualBaggage["key2"]); - Assert.True(actualBaggage.ContainsKey("key()3")); - Assert.Equal("value()!&;:", actualBaggage["key()3"]); - } + Assert.True(actualBaggage.ContainsKey("key()3")); + Assert.Equal("value()!&;:", actualBaggage["key()3"]); + } - [Fact] - public void ValidateEmptyBaggageInjection() - { - var carrier = new Dictionary(); - this.baggage.Inject(default, carrier, Setter); + [Fact] + public void ValidateEmptyBaggageInjection() + { + var carrier = new Dictionary(); + this.baggage.Inject(default, carrier, Setter); - Assert.Empty(carrier); - } + Assert.Empty(carrier); + } - [Fact] - public void ValidateBaggageInjection() - { - var carrier = new Dictionary(); - var propagationContext = new PropagationContext( - default, - new Baggage(new Dictionary - { - { "key1", "value1" }, - { "key2", "value2" }, - })); - - this.baggage.Inject(propagationContext, carrier, Setter); - - Assert.Single(carrier); - Assert.Equal("key1=value1,key2=value2", carrier[BaggagePropagator.BaggageHeaderName]); - } - - [Fact] - public void ValidateSpecialCharsBaggageInjection() - { - var carrier = new Dictionary(); - var propagationContext = new PropagationContext( - default, - new Baggage(new Dictionary - { - { "key 1", "value 1" }, - { "key2", "!x_x,x-x&x(x\");:" }, - })); - - this.baggage.Inject(propagationContext, carrier, Setter); - - Assert.Single(carrier); - Assert.Equal("key+1=value+1,key2=!x_x%2Cx-x%26x(x%22)%3B%3A", carrier[BaggagePropagator.BaggageHeaderName]); - } + [Fact] + public void ValidateBaggageInjection() + { + var carrier = new Dictionary(); + var propagationContext = new PropagationContext( + default, + new Baggage(new Dictionary + { + { "key1", "value1" }, + { "key2", "value2" }, + })); + + this.baggage.Inject(propagationContext, carrier, Setter); + + Assert.Single(carrier); + Assert.Equal("key1=value1,key2=value2", carrier[BaggagePropagator.BaggageHeaderName]); + } + + [Fact] + public void ValidateSpecialCharsBaggageInjection() + { + var carrier = new Dictionary(); + var propagationContext = new PropagationContext( + default, + new Baggage(new Dictionary + { + { "key 1", "value 1" }, + { "key2", "!x_x,x-x&x(x\");:" }, + })); + + this.baggage.Inject(propagationContext, carrier, Setter); + + Assert.Single(carrier); + Assert.Equal("key+1=value+1,key2=!x_x%2Cx-x%26x(x%22)%3B%3A", carrier[BaggagePropagator.BaggageHeaderName]); } } diff --git a/test/OpenTelemetry.Api.Tests/Trace/Propagation/CompositePropagatorTest.cs b/test/OpenTelemetry.Api.Tests/Trace/Propagation/CompositePropagatorTest.cs index d0aeaaa8e6b..f4066edcc1e 100644 --- a/test/OpenTelemetry.Api.Tests/Trace/Propagation/CompositePropagatorTest.cs +++ b/test/OpenTelemetry.Api.Tests/Trace/Propagation/CompositePropagatorTest.cs @@ -1,135 +1,121 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using Xunit; -namespace OpenTelemetry.Context.Propagation.Tests +namespace OpenTelemetry.Context.Propagation.Tests; + +public class CompositePropagatorTest { - public class CompositePropagatorTest + private static readonly string[] Empty = Array.Empty(); + private static readonly Func, string, IEnumerable> Getter = (headers, name) => { - private static readonly string[] Empty = Array.Empty(); - private static readonly Func, string, IEnumerable> Getter = (headers, name) => + count++; + if (headers.TryGetValue(name, out var value)) { - count++; - if (headers.TryGetValue(name, out var value)) - { - return new[] { value }; - } + return new[] { value }; + } - return Empty; - }; + return Empty; + }; - private static readonly Action, string, string> Setter = (carrier, name, value) => - { - carrier[name] = value; - }; + private static readonly Action, string, string> Setter = (carrier, name, value) => + { + carrier[name] = value; + }; - private static int count = 0; + private static int count = 0; - private readonly ActivityTraceId traceId = ActivityTraceId.CreateRandom(); - private readonly ActivitySpanId spanId = ActivitySpanId.CreateRandom(); + private readonly ActivityTraceId traceId = ActivityTraceId.CreateRandom(); + private readonly ActivitySpanId spanId = ActivitySpanId.CreateRandom(); - [Fact] - public void CompositePropagator_NullTextFormatList() - { - Assert.Throws(() => new CompositeTextMapPropagator(null)); - } + [Fact] + public void CompositePropagator_NullTextFormatList() + { + Assert.Throws(() => new CompositeTextMapPropagator(null)); + } - [Fact] - public void CompositePropagator_TestPropagator() + [Fact] + public void CompositePropagator_TestPropagator() + { + var compositePropagator = new CompositeTextMapPropagator(new List { - var compositePropagator = new CompositeTextMapPropagator(new List - { - new TestPropagator("custom-traceparent-1", "custom-tracestate-1"), - new TestPropagator("custom-traceparent-2", "custom-tracestate-2"), - }); - - var activityContext = new ActivityContext(this.traceId, this.spanId, ActivityTraceFlags.Recorded, traceState: null); - PropagationContext propagationContext = new PropagationContext(activityContext, default); - var carrier = new Dictionary(); - using var activity = new Activity("test"); - - compositePropagator.Inject(propagationContext, carrier, Setter); - Assert.Contains(carrier, kv => kv.Key == "custom-traceparent-1"); - Assert.Contains(carrier, kv => kv.Key == "custom-traceparent-2"); - } + new TestPropagator("custom-traceparent-1", "custom-tracestate-1"), + new TestPropagator("custom-traceparent-2", "custom-tracestate-2"), + }); + + var activityContext = new ActivityContext(this.traceId, this.spanId, ActivityTraceFlags.Recorded, traceState: null); + PropagationContext propagationContext = new PropagationContext(activityContext, default); + var carrier = new Dictionary(); + using var activity = new Activity("test"); + + compositePropagator.Inject(propagationContext, carrier, Setter); + Assert.Contains(carrier, kv => kv.Key == "custom-traceparent-1"); + Assert.Contains(carrier, kv => kv.Key == "custom-traceparent-2"); + } - [Fact] - public void CompositePropagator_UsingSameTag() - { - const string header01 = "custom-tracestate-01"; - const string header02 = "custom-tracestate-02"; + [Fact] + public void CompositePropagator_UsingSameTag() + { + const string header01 = "custom-tracestate-01"; + const string header02 = "custom-tracestate-02"; - var compositePropagator = new CompositeTextMapPropagator(new List - { - new TestPropagator("custom-traceparent", header01, true), - new TestPropagator("custom-traceparent", header02), - }); + var compositePropagator = new CompositeTextMapPropagator(new List + { + new TestPropagator("custom-traceparent", header01, true), + new TestPropagator("custom-traceparent", header02), + }); - var activityContext = new ActivityContext(this.traceId, this.spanId, ActivityTraceFlags.Recorded, traceState: null); - PropagationContext propagationContext = new PropagationContext(activityContext, default); + var activityContext = new ActivityContext(this.traceId, this.spanId, ActivityTraceFlags.Recorded, traceState: null); + PropagationContext propagationContext = new PropagationContext(activityContext, default); - var carrier = new Dictionary(); + var carrier = new Dictionary(); - compositePropagator.Inject(propagationContext, carrier, Setter); - Assert.Contains(carrier, kv => kv.Key == "custom-traceparent"); + compositePropagator.Inject(propagationContext, carrier, Setter); + Assert.Contains(carrier, kv => kv.Key == "custom-traceparent"); - // checking if the latest propagator is the one with the data. So, it will replace the previous one. - Assert.Equal($"00-{this.traceId}-{this.spanId}-{header02.Split('-').Last()}", carrier["custom-traceparent"]); + // checking if the latest propagator is the one with the data. So, it will replace the previous one. + Assert.Equal($"00-{this.traceId}-{this.spanId}-{header02.Split('-').Last()}", carrier["custom-traceparent"]); - // resetting counter - count = 0; - compositePropagator.Extract(default, carrier, Getter); + // resetting counter + count = 0; + compositePropagator.Extract(default, carrier, Getter); - // checking if we accessed only two times: header/headerstate options - // if that's true, we skipped the first one since we have a logic to for the default result - Assert.Equal(2, count); - } + // checking if we accessed only two times: header/headerstate options + // if that's true, we skipped the first one since we have a logic to for the default result + Assert.Equal(2, count); + } - [Fact] - public void CompositePropagator_ActivityContext_Baggage() + [Fact] + public void CompositePropagator_ActivityContext_Baggage() + { + var compositePropagator = new CompositeTextMapPropagator(new List { - var compositePropagator = new CompositeTextMapPropagator(new List - { - new TraceContextPropagator(), - new BaggagePropagator(), - }); - - var activityContext = new ActivityContext(this.traceId, this.spanId, ActivityTraceFlags.Recorded, traceState: null, isRemote: true); - var baggage = new Dictionary { ["key1"] = "value1" }; - - PropagationContext propagationContextActivityOnly = new PropagationContext(activityContext, default); - PropagationContext propagationContextBaggageOnly = new PropagationContext(default, new Baggage(baggage)); - PropagationContext propagationContextBoth = new PropagationContext(activityContext, new Baggage(baggage)); - - var carrier = new Dictionary(); - compositePropagator.Inject(propagationContextActivityOnly, carrier, Setter); - PropagationContext extractedContext = compositePropagator.Extract(default, carrier, Getter); - Assert.Equal(propagationContextActivityOnly, extractedContext); - - carrier = new Dictionary(); - compositePropagator.Inject(propagationContextBaggageOnly, carrier, Setter); - extractedContext = compositePropagator.Extract(default, carrier, Getter); - Assert.Equal(propagationContextBaggageOnly, extractedContext); - - carrier = new Dictionary(); - compositePropagator.Inject(propagationContextBoth, carrier, Setter); - extractedContext = compositePropagator.Extract(default, carrier, Getter); - Assert.Equal(propagationContextBoth, extractedContext); - } + new TraceContextPropagator(), + new BaggagePropagator(), + }); + + var activityContext = new ActivityContext(this.traceId, this.spanId, ActivityTraceFlags.Recorded, traceState: null, isRemote: true); + var baggage = new Dictionary { ["key1"] = "value1" }; + + PropagationContext propagationContextActivityOnly = new PropagationContext(activityContext, default); + PropagationContext propagationContextBaggageOnly = new PropagationContext(default, new Baggage(baggage)); + PropagationContext propagationContextBoth = new PropagationContext(activityContext, new Baggage(baggage)); + + var carrier = new Dictionary(); + compositePropagator.Inject(propagationContextActivityOnly, carrier, Setter); + PropagationContext extractedContext = compositePropagator.Extract(default, carrier, Getter); + Assert.Equal(propagationContextActivityOnly, extractedContext); + + carrier = new Dictionary(); + compositePropagator.Inject(propagationContextBaggageOnly, carrier, Setter); + extractedContext = compositePropagator.Extract(default, carrier, Getter); + Assert.Equal(propagationContextBaggageOnly, extractedContext); + + carrier = new Dictionary(); + compositePropagator.Inject(propagationContextBoth, carrier, Setter); + extractedContext = compositePropagator.Extract(default, carrier, Getter); + Assert.Equal(propagationContextBoth, extractedContext); } } diff --git a/test/OpenTelemetry.Api.Tests/Trace/Propagation/TestPropagator.cs b/test/OpenTelemetry.Api.Tests/Trace/Propagation/TestPropagator.cs index 6b59f3e7f4b..abd4a17b1e9 100644 --- a/test/OpenTelemetry.Api.Tests/Trace/Propagation/TestPropagator.cs +++ b/test/OpenTelemetry.Api.Tests/Trace/Propagation/TestPropagator.cs @@ -1,83 +1,69 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -namespace OpenTelemetry.Context.Propagation.Tests +namespace OpenTelemetry.Context.Propagation.Tests; + +public class TestPropagator : TextMapPropagator { - public class TestPropagator : TextMapPropagator + private readonly string idHeaderName; + private readonly string stateHeaderName; + private readonly bool defaultContext; + + public TestPropagator(string idHeaderName, string stateHeaderName, bool defaultContext = false) { - private readonly string idHeaderName; - private readonly string stateHeaderName; - private readonly bool defaultContext; + this.idHeaderName = idHeaderName; + this.stateHeaderName = stateHeaderName; + this.defaultContext = defaultContext; + } - public TestPropagator(string idHeaderName, string stateHeaderName, bool defaultContext = false) + public override ISet Fields => new HashSet() { this.idHeaderName, this.stateHeaderName }; + + public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) + { + if (this.defaultContext) { - this.idHeaderName = idHeaderName; - this.stateHeaderName = stateHeaderName; - this.defaultContext = defaultContext; + return context; } - public override ISet Fields => new HashSet() { this.idHeaderName, this.stateHeaderName }; - - public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) + IEnumerable id = getter(carrier, this.idHeaderName); + if (!id.Any()) { - if (this.defaultContext) - { - return context; - } - - IEnumerable id = getter(carrier, this.idHeaderName); - if (!id.Any()) - { - return context; - } - - var traceparentParsed = TraceContextPropagator.TryExtractTraceparent(id.First(), out var traceId, out var spanId, out var traceoptions); - if (!traceparentParsed) - { - return context; - } - - string tracestate = string.Empty; - IEnumerable tracestateCollection = getter(carrier, this.stateHeaderName); - if (tracestateCollection?.Any() ?? false) - { - TraceContextPropagator.TryExtractTracestate(tracestateCollection.ToArray(), out tracestate); - } + return context; + } - return new PropagationContext( - new ActivityContext(traceId, spanId, traceoptions, tracestate), - context.Baggage); + var traceparentParsed = TraceContextPropagator.TryExtractTraceparent(id.First(), out var traceId, out var spanId, out var traceoptions); + if (!traceparentParsed) + { + return context; } - public override void Inject(PropagationContext context, T carrier, Action setter) + string tracestate = string.Empty; + IEnumerable tracestateCollection = getter(carrier, this.stateHeaderName); + if (tracestateCollection?.Any() ?? false) { - string headerNumber = this.stateHeaderName.Split('-').Last(); + TraceContextPropagator.TryExtractTracestate(tracestateCollection.ToArray(), out tracestate); + } + + return new PropagationContext( + new ActivityContext(traceId, spanId, traceoptions, tracestate), + context.Baggage); + } + + public override void Inject(PropagationContext context, T carrier, Action setter) + { + string headerNumber = this.stateHeaderName.Split('-').Last(); - var traceparent = string.Concat("00-", context.ActivityContext.TraceId.ToHexString(), "-", context.ActivityContext.SpanId.ToHexString()); - traceparent = string.Concat(traceparent, "-", headerNumber); + var traceparent = string.Concat("00-", context.ActivityContext.TraceId.ToHexString(), "-", context.ActivityContext.SpanId.ToHexString()); + traceparent = string.Concat(traceparent, "-", headerNumber); - setter(carrier, this.idHeaderName, traceparent); + setter(carrier, this.idHeaderName, traceparent); - string tracestateStr = context.ActivityContext.TraceState; - if (tracestateStr?.Length > 0) - { - setter(carrier, this.stateHeaderName, tracestateStr); - } + string tracestateStr = context.ActivityContext.TraceState; + if (tracestateStr?.Length > 0) + { + setter(carrier, this.stateHeaderName, tracestateStr); } } } diff --git a/test/OpenTelemetry.Api.Tests/Trace/Propagation/TracestateUtilsTests.cs b/test/OpenTelemetry.Api.Tests/Trace/Propagation/TracestateUtilsTests.cs index fd36bff072f..499fe66cbf4 100644 --- a/test/OpenTelemetry.Api.Tests/Trace/Propagation/TracestateUtilsTests.cs +++ b/test/OpenTelemetry.Api.Tests/Trace/Propagation/TracestateUtilsTests.cs @@ -1,110 +1,96 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Xunit; -namespace OpenTelemetry.Context.Propagation.Tests +namespace OpenTelemetry.Context.Propagation.Tests; + +public class TracestateUtilsTests { - public class TracestateUtilsTests + [Theory] + [InlineData("")] + [InlineData(null)] + [InlineData(" ")] + [InlineData("\t")] + public void EmptyTracestate(string tracestate) { - [Theory] - [InlineData("")] - [InlineData(null)] - [InlineData(" ")] - [InlineData("\t")] - public void EmptyTracestate(string tracestate) - { - var tracestateEntries = new List>(); - Assert.False(TraceStateUtilsNew.AppendTraceState(tracestate, tracestateEntries)); - Assert.Empty(tracestateEntries); - } + var tracestateEntries = new List>(); + Assert.False(TraceStateUtilsNew.AppendTraceState(tracestate, tracestateEntries)); + Assert.Empty(tracestateEntries); + } - [Theory] - [InlineData("k=")] - [InlineData("=v")] - [InlineData("kv")] - [InlineData("k =v")] - [InlineData("k\t=v")] - [InlineData("k=v,k=v")] - [InlineData("k1=v1,,,k2=v2")] - [InlineData("k=morethan256......................................................................................................................................................................................................................................................")] - [InlineData("v=morethan256......................................................................................................................................................................................................................................................")] - public void InvalidTracestate(string tracestate) - { - var tracestateEntries = new List>(); - Assert.False(TraceStateUtilsNew.AppendTraceState(tracestate, tracestateEntries)); - Assert.Empty(tracestateEntries); - } + [Theory] + [InlineData("k=")] + [InlineData("=v")] + [InlineData("kv")] + [InlineData("k =v")] + [InlineData("k\t=v")] + [InlineData("k=v,k=v")] + [InlineData("k1=v1,,,k2=v2")] + [InlineData("k=morethan256......................................................................................................................................................................................................................................................")] + [InlineData("v=morethan256......................................................................................................................................................................................................................................................")] + public void InvalidTracestate(string tracestate) + { + var tracestateEntries = new List>(); + Assert.False(TraceStateUtilsNew.AppendTraceState(tracestate, tracestateEntries)); + Assert.Empty(tracestateEntries); + } - [Fact] - public void MaxEntries() - { - var tracestateEntries = new List>(); - var tracestate = - "k0=v,k1=v,k2=v,k3=v,k4=v,k5=v,k6=v,k7=v1,k8=v,k9=v,k10=v,k11=v,k12=v,k13=v,k14=v,k15=v,k16=v,k17=v,k18=v,k19=v,k20=v,k21=v,k22=v,k23=v,k24=v,k25=v,k26=v,k27=v1,k28=v,k29=v,k30=v,k31=v"; - Assert.True(TraceStateUtilsNew.AppendTraceState(tracestate, tracestateEntries)); - Assert.Equal(32, tracestateEntries.Count); - Assert.Equal( - "k0=v,k1=v,k2=v,k3=v,k4=v,k5=v,k6=v,k7=v1,k8=v,k9=v,k10=v,k11=v,k12=v,k13=v,k14=v,k15=v,k16=v,k17=v,k18=v,k19=v,k20=v,k21=v,k22=v,k23=v,k24=v,k25=v,k26=v,k27=v1,k28=v,k29=v,k30=v,k31=v", - TraceStateUtilsNew.GetString(tracestateEntries)); - } + [Fact] + public void MaxEntries() + { + var tracestateEntries = new List>(); + var tracestate = + "k0=v,k1=v,k2=v,k3=v,k4=v,k5=v,k6=v,k7=v1,k8=v,k9=v,k10=v,k11=v,k12=v,k13=v,k14=v,k15=v,k16=v,k17=v,k18=v,k19=v,k20=v,k21=v,k22=v,k23=v,k24=v,k25=v,k26=v,k27=v1,k28=v,k29=v,k30=v,k31=v"; + Assert.True(TraceStateUtilsNew.AppendTraceState(tracestate, tracestateEntries)); + Assert.Equal(32, tracestateEntries.Count); + Assert.Equal( + "k0=v,k1=v,k2=v,k3=v,k4=v,k5=v,k6=v,k7=v1,k8=v,k9=v,k10=v,k11=v,k12=v,k13=v,k14=v,k15=v,k16=v,k17=v,k18=v,k19=v,k20=v,k21=v,k22=v,k23=v,k24=v,k25=v,k26=v,k27=v1,k28=v,k29=v,k30=v,k31=v", + TraceStateUtilsNew.GetString(tracestateEntries)); + } - [Fact] - public void TooManyEntries() - { - var tracestateEntries = new List>(); - var tracestate = - "k0=v,k1=v,k2=v,k3=v,k4=v,k5=v,k6=v,k7=v1,k8=v,k9=v,k10=v,k11=v,k12=v,k13=v,k14=v,k15=v,k16=v,k17=v,k18=v,k19=v,k20=v,k21=v,k22=v,k23=v,k24=v,k25=v,k26=v,k27=v1,k28=v,k29=v,k30=v,k31=v,k32=v"; - Assert.False(TraceStateUtilsNew.AppendTraceState(tracestate, tracestateEntries)); - Assert.Empty(tracestateEntries); - } + [Fact] + public void TooManyEntries() + { + var tracestateEntries = new List>(); + var tracestate = + "k0=v,k1=v,k2=v,k3=v,k4=v,k5=v,k6=v,k7=v1,k8=v,k9=v,k10=v,k11=v,k12=v,k13=v,k14=v,k15=v,k16=v,k17=v,k18=v,k19=v,k20=v,k21=v,k22=v,k23=v,k24=v,k25=v,k26=v,k27=v1,k28=v,k29=v,k30=v,k31=v,k32=v"; + Assert.False(TraceStateUtilsNew.AppendTraceState(tracestate, tracestateEntries)); + Assert.Empty(tracestateEntries); + } - [Theory] - [InlineData("k=v", "k", "v")] - [InlineData(" k=v ", "k", "v")] - [InlineData("\tk=v", "k", "v")] - [InlineData(" k= v ", "k", "v")] - [InlineData(",k=v,", "k", "v")] - [InlineData(", k= v, ", "k", "v")] - [InlineData("k=\tv", "k", "v")] - [InlineData("k=v\t", "k", "v")] - [InlineData("1k=v", "1k", "v")] - public void ValidPair(string pair, string expectedKey, string expectedValue) - { - var tracestateEntries = new List>(); - Assert.True(TraceStateUtilsNew.AppendTraceState(pair, tracestateEntries)); - Assert.Single(tracestateEntries); - Assert.Equal(new KeyValuePair(expectedKey, expectedValue), tracestateEntries.Single()); - Assert.Equal($"{expectedKey}={expectedValue}", TraceStateUtilsNew.GetString(tracestateEntries)); - } + [Theory] + [InlineData("k=v", "k", "v")] + [InlineData(" k=v ", "k", "v")] + [InlineData("\tk=v", "k", "v")] + [InlineData(" k= v ", "k", "v")] + [InlineData(",k=v,", "k", "v")] + [InlineData(", k= v, ", "k", "v")] + [InlineData("k=\tv", "k", "v")] + [InlineData("k=v\t", "k", "v")] + [InlineData("1k=v", "1k", "v")] + public void ValidPair(string pair, string expectedKey, string expectedValue) + { + var tracestateEntries = new List>(); + Assert.True(TraceStateUtilsNew.AppendTraceState(pair, tracestateEntries)); + Assert.Single(tracestateEntries); + Assert.Equal(new KeyValuePair(expectedKey, expectedValue), tracestateEntries.Single()); + Assert.Equal($"{expectedKey}={expectedValue}", TraceStateUtilsNew.GetString(tracestateEntries)); + } - [Theory] - [InlineData("k1=v1,k2=v2")] - [InlineData(" k1=v1 , k2=v2")] - [InlineData(" ,k1=v1,k2=v2")] - [InlineData("k1=v1,k2=v2, ")] - public void ValidPairs(string tracestate) - { - var tracestateEntries = new List>(); - Assert.True(TraceStateUtilsNew.AppendTraceState(tracestate, tracestateEntries)); - Assert.Equal(2, tracestateEntries.Count); - Assert.Contains(new KeyValuePair("k1", "v1"), tracestateEntries); - Assert.Contains(new KeyValuePair("k2", "v2"), tracestateEntries); + [Theory] + [InlineData("k1=v1,k2=v2")] + [InlineData(" k1=v1 , k2=v2")] + [InlineData(" ,k1=v1,k2=v2")] + [InlineData("k1=v1,k2=v2, ")] + public void ValidPairs(string tracestate) + { + var tracestateEntries = new List>(); + Assert.True(TraceStateUtilsNew.AppendTraceState(tracestate, tracestateEntries)); + Assert.Equal(2, tracestateEntries.Count); + Assert.Contains(new KeyValuePair("k1", "v1"), tracestateEntries); + Assert.Contains(new KeyValuePair("k2", "v2"), tracestateEntries); - Assert.Equal("k1=v1,k2=v2", TraceStateUtilsNew.GetString(tracestateEntries)); - } + Assert.Equal("k1=v1,k2=v2", TraceStateUtilsNew.GetString(tracestateEntries)); } } diff --git a/test/OpenTelemetry.Api.Tests/Trace/SpanAttributesTest.cs b/test/OpenTelemetry.Api.Tests/Trace/SpanAttributesTest.cs index 613403e8656..f0c5c7c110f 100644 --- a/test/OpenTelemetry.Api.Tests/Trace/SpanAttributesTest.cs +++ b/test/OpenTelemetry.Api.Tests/Trace/SpanAttributesTest.cs @@ -1,83 +1,69 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Xunit; -namespace OpenTelemetry.Trace.Tests +namespace OpenTelemetry.Trace.Tests; + +public class SpanAttributesTest { - public class SpanAttributesTest + [Fact] + public void ValidateConstructor() { - [Fact] - public void ValidateConstructor() - { - var spanAttribute = new SpanAttributes(); - Assert.Empty(spanAttribute.Attributes); - } + var spanAttribute = new SpanAttributes(); + Assert.Empty(spanAttribute.Attributes); + } - [Fact] - public void ValidateAddMethods() - { - var spanAttribute = new SpanAttributes(); - spanAttribute.Add("key_string", "string"); - spanAttribute.Add("key_a_string", new string[] { "string" }); + [Fact] + public void ValidateAddMethods() + { + var spanAttribute = new SpanAttributes(); + spanAttribute.Add("key_string", "string"); + spanAttribute.Add("key_a_string", new string[] { "string" }); - spanAttribute.Add("key_double", 1.01); - spanAttribute.Add("key_a_double", new double[] { 1.01 }); + spanAttribute.Add("key_double", 1.01); + spanAttribute.Add("key_a_double", new double[] { 1.01 }); - spanAttribute.Add("key_bool", true); - spanAttribute.Add("key_a_bool", new bool[] { true }); + spanAttribute.Add("key_bool", true); + spanAttribute.Add("key_a_bool", new bool[] { true }); - spanAttribute.Add("key_long", 1); - spanAttribute.Add("key_a_long", new long[] { 1 }); + spanAttribute.Add("key_long", 1); + spanAttribute.Add("key_a_long", new long[] { 1 }); - Assert.Equal(8, spanAttribute.Attributes.Count); - } + Assert.Equal(8, spanAttribute.Attributes.Count); + } - [Fact] - public void ValidateNullKey() - { - var spanAttribute = new SpanAttributes(); - Assert.Throws(() => spanAttribute.Add(null, "null key")); - } + [Fact] + public void ValidateNullKey() + { + var spanAttribute = new SpanAttributes(); + Assert.Throws(() => spanAttribute.Add(null, "null key")); + } - [Fact] - public void ValidateSameKey() - { - var spanAttribute = new SpanAttributes(); - spanAttribute.Add("key", "value1"); - spanAttribute.Add("key", "value2"); - Assert.Equal("value2", spanAttribute.Attributes["key"]); - } + [Fact] + public void ValidateSameKey() + { + var spanAttribute = new SpanAttributes(); + spanAttribute.Add("key", "value1"); + spanAttribute.Add("key", "value2"); + Assert.Equal("value2", spanAttribute.Attributes["key"]); + } - [Fact] - public void ValidateConstructorWithList() - { - var spanAttributes = new SpanAttributes( - new List>() - { - new KeyValuePair("Span attribute int", 1), - new KeyValuePair("Span attribute string", "str"), - }); - Assert.Equal(2, spanAttributes.Attributes.Count); - } + [Fact] + public void ValidateConstructorWithList() + { + var spanAttributes = new SpanAttributes( + new List>() + { + new KeyValuePair("Span attribute int", 1), + new KeyValuePair("Span attribute string", "str"), + }); + Assert.Equal(2, spanAttributes.Attributes.Count); + } - [Fact] - public void ValidateConstructorWithNullList() - { - Assert.Throws(() => new SpanAttributes(null)); - } + [Fact] + public void ValidateConstructorWithNullList() + { + Assert.Throws(() => new SpanAttributes(null)); } } diff --git a/test/OpenTelemetry.Api.Tests/Trace/StatusTest.cs b/test/OpenTelemetry.Api.Tests/Trace/StatusTest.cs index 11b1b9b6f34..f3898d06891 100644 --- a/test/OpenTelemetry.Api.Tests/Trace/StatusTest.cs +++ b/test/OpenTelemetry.Api.Tests/Trace/StatusTest.cs @@ -1,120 +1,106 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Xunit; -namespace OpenTelemetry.Trace.Tests +namespace OpenTelemetry.Trace.Tests; + +public class StatusTest { - public class StatusTest + [Fact] + public void Status_Ok() + { + Assert.Equal(StatusCode.Ok, Status.Ok.StatusCode); + Assert.Null(Status.Ok.Description); + } + + [Fact] + public void CheckingDefaultStatus() + { + Assert.Equal(default, Status.Unset); + } + + [Fact] + public void CreateStatus_Error_WithDescription() + { + var status = Status.Error.WithDescription("This is an error."); + Assert.Equal(StatusCode.Error, status.StatusCode); + Assert.Equal("This is an error.", status.Description); + } + + [Fact] + public void CreateStatus_Ok_WithDescription() + { + var status = Status.Ok.WithDescription("This is will not be set."); + Assert.Equal(StatusCode.Ok, status.StatusCode); + Assert.Null(status.Description); + } + + [Fact] + public void Equality() + { + var status1 = new Status(StatusCode.Ok); + var status2 = new Status(StatusCode.Ok); + object status3 = new Status(StatusCode.Ok); + + Assert.Equal(status1, status2); + Assert.True(status1 == status2); + Assert.True(status1.Equals(status3)); + } + + [Fact] + public void Equality_WithDescription() + { + var status1 = new Status(StatusCode.Error, "error"); + var status2 = new Status(StatusCode.Error, "error"); + + Assert.Equal(status1, status2); + Assert.True(status1 == status2); + } + + [Fact] + public void Not_Equality() + { + var status1 = new Status(StatusCode.Ok); + var status2 = new Status(StatusCode.Error); + object notStatus = 1; + + Assert.NotEqual(status1, status2); + Assert.True(status1 != status2); + Assert.False(status1.Equals(notStatus)); + } + + [Fact] + public void Not_Equality_WithDescription1() + { + var status1 = new Status(StatusCode.Ok, "ok"); + var status2 = new Status(StatusCode.Error, "error"); + + Assert.NotEqual(status1, status2); + Assert.True(status1 != status2); + } + + [Fact] + public void Not_Equality_WithDescription2() + { + var status1 = new Status(StatusCode.Ok); + var status2 = new Status(StatusCode.Error, "error"); + + Assert.NotEqual(status1, status2); + Assert.True(status1 != status2); + } + + [Fact] + public void TestToString() + { + var status = new Status(StatusCode.Ok); + Assert.Equal($"Status{{StatusCode={status.StatusCode}, Description={status.Description}}}", status.ToString()); + } + + [Fact] + public void TestGetHashCode() { - [Fact] - public void Status_Ok() - { - Assert.Equal(StatusCode.Ok, Status.Ok.StatusCode); - Assert.Null(Status.Ok.Description); - } - - [Fact] - public void CheckingDefaultStatus() - { - Assert.Equal(default, Status.Unset); - } - - [Fact] - public void CreateStatus_Error_WithDescription() - { - var status = Status.Error.WithDescription("This is an error."); - Assert.Equal(StatusCode.Error, status.StatusCode); - Assert.Equal("This is an error.", status.Description); - } - - [Fact] - public void CreateStatus_Ok_WithDescription() - { - var status = Status.Ok.WithDescription("This is will not be set."); - Assert.Equal(StatusCode.Ok, status.StatusCode); - Assert.Null(status.Description); - } - - [Fact] - public void Equality() - { - var status1 = new Status(StatusCode.Ok); - var status2 = new Status(StatusCode.Ok); - object status3 = new Status(StatusCode.Ok); - - Assert.Equal(status1, status2); - Assert.True(status1 == status2); - Assert.True(status1.Equals(status3)); - } - - [Fact] - public void Equality_WithDescription() - { - var status1 = new Status(StatusCode.Error, "error"); - var status2 = new Status(StatusCode.Error, "error"); - - Assert.Equal(status1, status2); - Assert.True(status1 == status2); - } - - [Fact] - public void Not_Equality() - { - var status1 = new Status(StatusCode.Ok); - var status2 = new Status(StatusCode.Error); - object notStatus = 1; - - Assert.NotEqual(status1, status2); - Assert.True(status1 != status2); - Assert.False(status1.Equals(notStatus)); - } - - [Fact] - public void Not_Equality_WithDescription1() - { - var status1 = new Status(StatusCode.Ok, "ok"); - var status2 = new Status(StatusCode.Error, "error"); - - Assert.NotEqual(status1, status2); - Assert.True(status1 != status2); - } - - [Fact] - public void Not_Equality_WithDescription2() - { - var status1 = new Status(StatusCode.Ok); - var status2 = new Status(StatusCode.Error, "error"); - - Assert.NotEqual(status1, status2); - Assert.True(status1 != status2); - } - - [Fact] - public void TestToString() - { - var status = new Status(StatusCode.Ok); - Assert.Equal($"Status{{StatusCode={status.StatusCode}, Description={status.Description}}}", status.ToString()); - } - - [Fact] - public void TestGetHashCode() - { - var status = new Status(StatusCode.Ok); - Assert.NotEqual(0, status.GetHashCode()); - } + var status = new Status(StatusCode.Ok); + Assert.NotEqual(0, status.GetHashCode()); } } diff --git a/test/OpenTelemetry.Api.Tests/Trace/TelemetrySpanTest.cs b/test/OpenTelemetry.Api.Tests/Trace/TelemetrySpanTest.cs index 3ca9a4ef4cb..6434569e1d9 100644 --- a/test/OpenTelemetry.Api.Tests/Trace/TelemetrySpanTest.cs +++ b/test/OpenTelemetry.Api.Tests/Trace/TelemetrySpanTest.cs @@ -1,85 +1,71 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using Xunit; -namespace OpenTelemetry.Trace.Tests +namespace OpenTelemetry.Trace.Tests; + +public class TelemetrySpanTest { - public class TelemetrySpanTest + [Fact] + public void CheckRecordExceptionData() { - [Fact] - public void CheckRecordExceptionData() - { - string message = "message"; + string message = "message"; - using Activity activity = new Activity("exception-test"); - using TelemetrySpan telemetrySpan = new TelemetrySpan(activity); - telemetrySpan.RecordException(new ArgumentNullException(message, new Exception("new-exception"))); - Assert.Single(activity.Events); + using Activity activity = new Activity("exception-test"); + using TelemetrySpan telemetrySpan = new TelemetrySpan(activity); + telemetrySpan.RecordException(new ArgumentNullException(message, new Exception("new-exception"))); + Assert.Single(activity.Events); - var @event = telemetrySpan.Activity.Events.FirstOrDefault(q => q.Name == SemanticConventions.AttributeExceptionEventName); - Assert.Equal(message, @event.Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionMessage).Value); - Assert.Equal(typeof(ArgumentNullException).Name, @event.Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionType).Value); - } + var @event = telemetrySpan.Activity.Events.FirstOrDefault(q => q.Name == SemanticConventions.AttributeExceptionEventName); + Assert.Equal(message, @event.Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionMessage).Value); + Assert.Equal(typeof(ArgumentNullException).Name, @event.Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionType).Value); + } - [Fact] - public void CheckRecordExceptionData2() - { - string type = "ArgumentNullException"; - string message = "message"; - string stack = "stack"; + [Fact] + public void CheckRecordExceptionData2() + { + string type = "ArgumentNullException"; + string message = "message"; + string stack = "stack"; - using Activity activity = new Activity("exception-test"); - using TelemetrySpan telemetrySpan = new TelemetrySpan(activity); - telemetrySpan.RecordException(type, message, stack); - Assert.Single(activity.Events); + using Activity activity = new Activity("exception-test"); + using TelemetrySpan telemetrySpan = new TelemetrySpan(activity); + telemetrySpan.RecordException(type, message, stack); + Assert.Single(activity.Events); - var @event = telemetrySpan.Activity.Events.FirstOrDefault(q => q.Name == SemanticConventions.AttributeExceptionEventName); - Assert.Equal(message, @event.Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionMessage).Value); - Assert.Equal(type, @event.Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionType).Value); - Assert.Equal(stack, @event.Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionStacktrace).Value); - } + var @event = telemetrySpan.Activity.Events.FirstOrDefault(q => q.Name == SemanticConventions.AttributeExceptionEventName); + Assert.Equal(message, @event.Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionMessage).Value); + Assert.Equal(type, @event.Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionType).Value); + Assert.Equal(stack, @event.Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionStacktrace).Value); + } - [Fact] - public void CheckRecordExceptionEmpty() - { - using Activity activity = new Activity("exception-test"); - using TelemetrySpan telemetrySpan = new TelemetrySpan(activity); - telemetrySpan.RecordException(string.Empty, string.Empty, string.Empty); - Assert.Empty(activity.Events); + [Fact] + public void CheckRecordExceptionEmpty() + { + using Activity activity = new Activity("exception-test"); + using TelemetrySpan telemetrySpan = new TelemetrySpan(activity); + telemetrySpan.RecordException(string.Empty, string.Empty, string.Empty); + Assert.Empty(activity.Events); - telemetrySpan.RecordException(null); - Assert.Empty(activity.Events); - } + telemetrySpan.RecordException(null); + Assert.Empty(activity.Events); + } - [Fact] - public void ParentIds() - { - using var parentActivity = new Activity("parentOperation"); - parentActivity.Start(); // can't generate the Id until the operation is started - using var parentSpan = new TelemetrySpan(parentActivity); + [Fact] + public void ParentIds() + { + using var parentActivity = new Activity("parentOperation"); + parentActivity.Start(); // can't generate the Id until the operation is started + using var parentSpan = new TelemetrySpan(parentActivity); - // ParentId should be unset - Assert.Equal(default, parentSpan.ParentSpanId); + // ParentId should be unset + Assert.Equal(default, parentSpan.ParentSpanId); - using var childActivity = new Activity("childOperation").SetParentId(parentActivity.Id); - using var childSpan = new TelemetrySpan(childActivity); + using var childActivity = new Activity("childOperation").SetParentId(parentActivity.Id); + using var childSpan = new TelemetrySpan(childActivity); - Assert.Equal(parentSpan.Context.SpanId, childSpan.ParentSpanId); - } + Assert.Equal(parentSpan.Context.SpanId, childSpan.ParentSpanId); } } diff --git a/test/OpenTelemetry.Api.Tests/Trace/TracerTest.cs b/test/OpenTelemetry.Api.Tests/Trace/TracerTest.cs index 148991b5f75..7c71f41cda0 100644 --- a/test/OpenTelemetry.Api.Tests/Trace/TracerTest.cs +++ b/test/OpenTelemetry.Api.Tests/Trace/TracerTest.cs @@ -1,297 +1,406 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +using Microsoft.Coyote; +using Microsoft.Coyote.SystematicTesting; +using OpenTelemetry.Tests; using Xunit; +using Xunit.Abstractions; -namespace OpenTelemetry.Trace.Tests +namespace OpenTelemetry.Trace.Tests; + +public class TracerTest : IDisposable { - public class TracerTest : IDisposable + private readonly ITestOutputHelper output; + private readonly Tracer tracer; + + public TracerTest(ITestOutputHelper output) { - // TODO: This is only a basic test. This must cover the entire shim API scenarios. - private readonly Tracer tracer; + this.output = output; + this.tracer = TracerProvider.Default.GetTracer("tracername", "tracerversion"); + } - public TracerTest() - { - this.tracer = TracerProvider.Default.GetTracer("tracername", "tracerversion"); - } + [Fact] + public void CurrentSpanNullByDefault() + { + var current = Tracer.CurrentSpan; + Assert.True(IsNoopSpan(current)); + Assert.False(current.Context.IsValid); + } - [Fact] - public void CurrentSpanNullByDefault() - { - var current = Tracer.CurrentSpan; - Assert.True(IsNoopSpan(current)); - Assert.False(current.Context.IsValid); - } + [Fact] + public void TracerStartWithSpan() + { + Tracer.WithSpan(TelemetrySpan.NoopInstance); + var current = Tracer.CurrentSpan; + Assert.Same(current, TelemetrySpan.NoopInstance); + } + + [Fact] + public void TracerStartReturnsNoopSpanWhenNoSdk() + { + var span = this.tracer.StartSpan("name"); + Assert.True(IsNoopSpan(span)); + Assert.False(span.Context.IsValid); + Assert.False(span.IsRecording); + } + + [Fact] + public void Tracer_StartRootSpan_BadArgs_NullSpanName() + { + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("tracername") + .Build(); + + var span1 = this.tracer.StartRootSpan(null); + Assert.True(string.IsNullOrEmpty(span1.Activity.DisplayName)); + + var span2 = this.tracer.StartRootSpan(null, SpanKind.Client); + Assert.True(string.IsNullOrEmpty(span2.Activity.DisplayName)); + + var span3 = this.tracer.StartRootSpan(null, SpanKind.Client, default); + Assert.True(string.IsNullOrEmpty(span3.Activity.DisplayName)); + } + + [Fact] + public async Task Tracer_StartRootSpan_StartsNewTrace() + { + var exportedItems = new List(); + + using var tracer = Sdk.CreateTracerProviderBuilder() + .AddSource("tracername") + .AddInMemoryExporter(exportedItems) + .Build(); - [Fact] - public void TracerStartWithSpan() + async Task DoSomeAsyncWork() { - Tracer.WithSpan(TelemetrySpan.NoopInstance); - var current = Tracer.CurrentSpan; - Assert.Same(current, TelemetrySpan.NoopInstance); + await Task.Delay(10); + using (tracer.GetTracer("tracername").StartRootSpan("RootSpan2")) + { + await Task.Delay(10); + } } - [Fact] - public void TracerStartReturnsNoopSpanWhenNoSdk() + using (tracer.GetTracer("tracername").StartActiveSpan("RootSpan1")) { - var span = this.tracer.StartSpan("name"); - Assert.True(IsNoopSpan(span)); - Assert.False(span.Context.IsValid); - Assert.False(span.IsRecording); + await DoSomeAsyncWork(); } - [Fact] - public void Tracer_StartRootSpan_BadArgs_NullSpanName() - { - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("tracername") - .Build(); + Assert.Equal(2, exportedItems.Count); - var span1 = this.tracer.StartRootSpan(null); - Assert.Null(span1.Activity.DisplayName); + var rootSpan2 = exportedItems[0]; + var rootSpan1 = exportedItems[1]; + Assert.Equal("RootSpan2", rootSpan2.DisplayName); + Assert.Equal("RootSpan1", rootSpan1.DisplayName); + Assert.Equal(default, rootSpan1.ParentSpanId); - var span2 = this.tracer.StartRootSpan(null, SpanKind.Client); - Assert.Null(span2.Activity.DisplayName); + // This is where this test currently fails + // rootSpan2 should be a root span of a new trace and not a child of rootSpan1 + Assert.Equal(default, rootSpan2.ParentSpanId); + Assert.NotEqual(rootSpan2.TraceId, rootSpan1.TraceId); + Assert.NotEqual(rootSpan2.ParentSpanId, rootSpan1.SpanId); + } - var span3 = this.tracer.StartRootSpan(null, SpanKind.Client, default); - Assert.Null(span3.Activity.DisplayName); - } + [Fact] + public void Tracer_StartSpan_BadArgs_NullSpanName() + { + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("tracername") + .Build(); - [Fact(Skip = "See https://github.com/open-telemetry/opentelemetry-dotnet/issues/2803")] - public async Task Tracer_StartRootSpan_StartsNewTrace() - { - var exportedItems = new List(); + var span1 = this.tracer.StartSpan(null); + Assert.True(string.IsNullOrEmpty(span1.Activity.DisplayName)); - using var tracer = Sdk.CreateTracerProviderBuilder() - .AddSource("tracername") - .AddInMemoryExporter(exportedItems) - .Build(); + var span2 = this.tracer.StartSpan(null, SpanKind.Client); + Assert.True(string.IsNullOrEmpty(span2.Activity.DisplayName)); - async Task DoSomeAsyncWork() - { - await Task.Delay(10).ConfigureAwait(false); - using (tracer.GetTracer("tracername").StartRootSpan("RootSpan2")) - { - await Task.Delay(10).ConfigureAwait(false); - } - } + var span3 = this.tracer.StartSpan(null, SpanKind.Client, null); + Assert.True(string.IsNullOrEmpty(span3.Activity.DisplayName)); + } - using (tracer.GetTracer("tracername").StartActiveSpan("RootSpan1")) - { - await DoSomeAsyncWork().ConfigureAwait(false); - } + [Fact] + public void Tracer_StartActiveSpan_BadArgs_NullSpanName() + { + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("tracername") + .Build(); - Assert.Equal(2, exportedItems.Count); + var span1 = this.tracer.StartActiveSpan(null); + Assert.True(string.IsNullOrEmpty(span1.Activity.DisplayName)); - var rootSpan2 = exportedItems[0]; - var rootSpan1 = exportedItems[1]; - Assert.Equal("RootSpan2", rootSpan2.DisplayName); - Assert.Equal("RootSpan1", rootSpan1.DisplayName); - Assert.Equal(default, rootSpan1.ParentSpanId); + var span2 = this.tracer.StartActiveSpan(null, SpanKind.Client); + Assert.True(string.IsNullOrEmpty(span2.Activity.DisplayName)); - // This is where this test currently fails - // rootSpan2 should be a root span of a new trace and not a child of rootSpan1 - Assert.Equal(default, rootSpan2.ParentSpanId); - Assert.NotEqual(rootSpan2.TraceId, rootSpan1.TraceId); - Assert.NotEqual(rootSpan2.ParentSpanId, rootSpan1.SpanId); - } + var span3 = this.tracer.StartActiveSpan(null, SpanKind.Client, null); + Assert.True(string.IsNullOrEmpty(span3.Activity.DisplayName)); + } - [Fact] - public void Tracer_StartSpan_BadArgs_NullSpanName() - { - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("tracername") - .Build(); + [Fact] + public void Tracer_StartSpan_FromParent_BadArgs_NullSpanName() + { + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("tracername") + .Build(); - var span1 = this.tracer.StartSpan(null); - Assert.Null(span1.Activity.DisplayName); + var span1 = this.tracer.StartSpan(null, SpanKind.Client, TelemetrySpan.NoopInstance); + Assert.True(string.IsNullOrEmpty(span1.Activity.DisplayName)); - var span2 = this.tracer.StartSpan(null, SpanKind.Client); - Assert.Null(span2.Activity.DisplayName); + var span2 = this.tracer.StartSpan(null, SpanKind.Client, TelemetrySpan.NoopInstance, default); + Assert.True(string.IsNullOrEmpty(span2.Activity.DisplayName)); + } - var span3 = this.tracer.StartSpan(null, SpanKind.Client, null); - Assert.Null(span3.Activity.DisplayName); - } + [Fact] + public void Tracer_StartSpan_FromParentContext_BadArgs_NullSpanName() + { + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("tracername") + .Build(); - [Fact] - public void Tracer_StartActiveSpan_BadArgs_NullSpanName() - { - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("tracername") - .Build(); + var blankContext = default(SpanContext); - var span1 = this.tracer.StartActiveSpan(null); - Assert.Null(span1.Activity.DisplayName); + var span1 = this.tracer.StartSpan(null, SpanKind.Client, blankContext); + Assert.True(string.IsNullOrEmpty(span1.Activity.DisplayName)); - var span2 = this.tracer.StartActiveSpan(null, SpanKind.Client); - Assert.Null(span2.Activity.DisplayName); + var span2 = this.tracer.StartSpan(null, SpanKind.Client, blankContext, default); + Assert.True(string.IsNullOrEmpty(span2.Activity.DisplayName)); + } - var span3 = this.tracer.StartActiveSpan(null, SpanKind.Client, null); - Assert.Null(span3.Activity.DisplayName); - } + [Fact] + public void Tracer_StartActiveSpan_FromParent_BadArgs_NullSpanName() + { + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("tracername") + .Build(); - [Fact] - public void Tracer_StartSpan_FromParent_BadArgs_NullSpanName() - { - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("tracername") - .Build(); + var span1 = this.tracer.StartActiveSpan(null, SpanKind.Client, TelemetrySpan.NoopInstance); + Assert.True(string.IsNullOrEmpty(span1.Activity.DisplayName)); - var span1 = this.tracer.StartSpan(null, SpanKind.Client, TelemetrySpan.NoopInstance); - Assert.Null(span1.Activity.DisplayName); + var span2 = this.tracer.StartActiveSpan(null, SpanKind.Client, TelemetrySpan.NoopInstance, default); + Assert.True(string.IsNullOrEmpty(span2.Activity.DisplayName)); + } - var span2 = this.tracer.StartSpan(null, SpanKind.Client, TelemetrySpan.NoopInstance, default); - Assert.Null(span2.Activity.DisplayName); - } + [Fact] + public void Tracer_StartActiveSpan_FromParentContext_BadArgs_NullSpanName() + { + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("tracername") + .Build(); - [Fact] - public void Tracer_StartSpan_FromParentContext_BadArgs_NullSpanName() - { - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("tracername") - .Build(); + var blankContext = default(SpanContext); - var blankContext = default(SpanContext); + var span1 = this.tracer.StartActiveSpan(null, SpanKind.Client, blankContext); + Assert.True(string.IsNullOrEmpty(span1.Activity.DisplayName)); - var span1 = this.tracer.StartSpan(null, SpanKind.Client, blankContext); - Assert.Null(span1.Activity.DisplayName); + var span2 = this.tracer.StartActiveSpan(null, SpanKind.Client, blankContext, default); + Assert.True(string.IsNullOrEmpty(span2.Activity.DisplayName)); + } - var span2 = this.tracer.StartSpan(null, SpanKind.Client, blankContext, default); - Assert.Null(span2.Activity.DisplayName); - } + [Fact] + public void Tracer_StartActiveSpan_CreatesActiveSpan() + { + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("tracername") + .Build(); - [Fact] - public void Tracer_StartActiveSpan_FromParent_BadArgs_NullSpanName() - { - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("tracername") - .Build(); + var span1 = this.tracer.StartActiveSpan("Test"); + Assert.Equal(span1.Activity.SpanId, Tracer.CurrentSpan.Context.SpanId); - var span1 = this.tracer.StartActiveSpan(null, SpanKind.Client, TelemetrySpan.NoopInstance); - Assert.Null(span1.Activity.DisplayName); + var span2 = this.tracer.StartActiveSpan("Test", SpanKind.Client); + Assert.Equal(span2.Activity.SpanId, Tracer.CurrentSpan.Context.SpanId); - var span2 = this.tracer.StartActiveSpan(null, SpanKind.Client, TelemetrySpan.NoopInstance, default); - Assert.Null(span2.Activity.DisplayName); - } + var span = this.tracer.StartSpan("foo"); + Tracer.WithSpan(span); - [Fact] - public void Tracer_StartActiveSpan_FromParentContext_BadArgs_NullSpanName() - { - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("tracername") - .Build(); + var span3 = this.tracer.StartActiveSpan("Test", SpanKind.Client, span); + Assert.Equal(span3.Activity.SpanId, Tracer.CurrentSpan.Context.SpanId); - var blankContext = default(SpanContext); + var spanContext = new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded); + var span4 = this.tracer.StartActiveSpan("Test", SpanKind.Client, spanContext); + Assert.Equal(span4.Activity.SpanId, Tracer.CurrentSpan.Context.SpanId); + } - var span1 = this.tracer.StartActiveSpan(null, SpanKind.Client, blankContext); - Assert.Null(span1.Activity.DisplayName); + [Fact] + public void GetCurrentSpanBlank() + { + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("tracername") + .Build(); + Assert.False(Tracer.CurrentSpan.Context.IsValid); + } - var span2 = this.tracer.StartActiveSpan(null, SpanKind.Client, blankContext, default); - Assert.Null(span2.Activity.DisplayName); - } + [Fact] + public void GetCurrentSpanBlankWontThrowOnEnd() + { + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("tracername") + .Build(); + var current = Tracer.CurrentSpan; + current.End(); + } - [Fact] - public void Tracer_StartActiveSpan_CreatesActiveSpan() - { - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("tracername") - .Build(); + [Fact] + public void GetCurrentSpan() + { + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("tracername") + .Build(); - var span1 = this.tracer.StartActiveSpan("Test"); - Assert.Equal(span1.Activity.SpanId, Tracer.CurrentSpan.Context.SpanId); + var span = this.tracer.StartSpan("foo"); + Tracer.WithSpan(span); - var span2 = this.tracer.StartActiveSpan("Test", SpanKind.Client); - Assert.Equal(span2.Activity.SpanId, Tracer.CurrentSpan.Context.SpanId); + Assert.Equal(span.Context.SpanId, Tracer.CurrentSpan.Context.SpanId); + Assert.True(Tracer.CurrentSpan.Context.IsValid); + } - var span = this.tracer.StartSpan("foo"); - Tracer.WithSpan(span); + [Fact] + public void CreateSpan_Sampled() + { + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("tracername") + .Build(); + var span = this.tracer.StartSpan("foo"); + Assert.True(span.IsRecording); + } - var span3 = this.tracer.StartActiveSpan("Test", SpanKind.Client, span); - Assert.Equal(span3.Activity.SpanId, Tracer.CurrentSpan.Context.SpanId); + [Fact] + public void CreateSpan_NotSampled() + { + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("tracername") + .SetSampler(new AlwaysOffSampler()) + .Build(); - var spanContext = new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded); - var span4 = this.tracer.StartActiveSpan("Test", SpanKind.Client, spanContext); - Assert.Equal(span4.Activity.SpanId, Tracer.CurrentSpan.Context.SpanId); - } + var span = this.tracer.StartSpan("foo"); + Assert.False(span.IsRecording); + } - [Fact] - public void GetCurrentSpanBlank() - { - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("tracername") - .Build(); - Assert.False(Tracer.CurrentSpan.Context.IsValid); - } + [Fact] + public void TracerBecomesNoopWhenParentProviderIsDisposedTest() + { + TracerProvider provider = null; + Tracer tracer = null; - [Fact] - public void GetCurrentSpanBlankWontThrowOnEnd() + using (var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("mytracer") + .Build()) { - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("tracername") - .Build(); - var current = Tracer.CurrentSpan; - current.End(); + provider = tracerProvider; + tracer = tracerProvider.GetTracer("mytracer"); + + var span1 = tracer.StartSpan("foo"); + Assert.True(span1.IsRecording); } - [Fact] - public void GetCurrentSpan() - { - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("tracername") - .Build(); + var span2 = tracer.StartSpan("foo"); + Assert.False(span2.IsRecording); - var span = this.tracer.StartSpan("foo"); - Tracer.WithSpan(span); + var tracer2 = provider.GetTracer("mytracer"); - Assert.Equal(span.Context.SpanId, Tracer.CurrentSpan.Context.SpanId); - Assert.True(Tracer.CurrentSpan.Context.IsValid); - } + var span3 = tracer2.StartSpan("foo"); + Assert.False(span3.IsRecording); + } - [Fact] - public void CreateSpan_Sampled() - { - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("tracername") - .Build(); - var span = this.tracer.StartSpan("foo"); - Assert.True(span.IsRecording); - } + [SkipUnlessEnvVarFoundFact("OTEL_RUN_COYOTE_TESTS")] + [Trait("CategoryName", "CoyoteConcurrencyTests")] + public void TracerConcurrencyTest() + { + var config = Configuration.Create() + .WithTestingIterations(100) + .WithMemoryAccessRaceCheckingEnabled(true); - [Fact] - public void CreateSpan_NotSampled() - { - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("tracername") - .SetSampler(new AlwaysOffSampler()) - .Build(); + var test = TestingEngine.Create(config, InnerTest); + + test.Run(); - var span = this.tracer.StartSpan("foo"); - Assert.False(span.IsRecording); + 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}"); + } } - public void Dispose() + if (test.TryEmitCoverageReports(dir, $"{nameof(this.TracerConcurrencyTest)}_CoyoteOutput", out reportPaths)) { - Activity.Current = null; - GC.SuppressFinalize(this); + foreach (var reportPath in reportPaths) + { + this.output.WriteLine($"Coverage report: {reportPath}"); + } } - private static bool IsNoopSpan(TelemetrySpan span) + Assert.Equal(0, test.TestReport.NumOfFoundBugs); + + static void InnerTest() { - return span.Activity == null; + 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; + GC.SuppressFinalize(this); + } + + 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.Console.Tests/ConsoleActivityExporterTest.cs b/test/OpenTelemetry.Exporter.Console.Tests/ConsoleActivityExporterTest.cs index d7400d3fece..e3305513d1d 100644 --- a/test/OpenTelemetry.Exporter.Console.Tests/ConsoleActivityExporterTest.cs +++ b/test/OpenTelemetry.Exporter.Console.Tests/ConsoleActivityExporterTest.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Tests; diff --git a/test/OpenTelemetry.Exporter.Console.Tests/OpenTelemetry.Exporter.Console.Tests.csproj b/test/OpenTelemetry.Exporter.Console.Tests/OpenTelemetry.Exporter.Console.Tests.csproj index 5b20af14dda..8745db77efc 100644 --- a/test/OpenTelemetry.Exporter.Console.Tests/OpenTelemetry.Exporter.Console.Tests.csproj +++ b/test/OpenTelemetry.Exporter.Console.Tests/OpenTelemetry.Exporter.Console.Tests.csproj @@ -2,16 +2,13 @@ Unit test project for Console Exporter for OpenTelemetry - - net7.0;net6.0 - $(TargetFrameworks);net462 + $(TargetFrameworksForTests) - - all + runtime; build; native; contentfiles; analyzers diff --git a/test/OpenTelemetry.Exporter.Jaeger.Tests/EventSourceTest.cs b/test/OpenTelemetry.Exporter.Jaeger.Tests/EventSourceTest.cs deleted file mode 100644 index 6e4807ccbeb..00000000000 --- a/test/OpenTelemetry.Exporter.Jaeger.Tests/EventSourceTest.cs +++ /dev/null @@ -1,31 +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 OpenTelemetry.Exporter.Jaeger.Implementation; -using OpenTelemetry.Tests; -using Xunit; - -namespace OpenTelemetry.Exporter.Jaeger.Tests -{ - public class EventSourceTest - { - [Fact] - public void EventSourceTest_JaegerExporterEventSource() - { - EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(JaegerExporterEventSource.Log); - } - } -} diff --git a/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/Int128Test.cs b/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/Int128Test.cs deleted file mode 100644 index 68ddc6323c9..00000000000 --- a/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/Int128Test.cs +++ /dev/null @@ -1,34 +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 Xunit; - -namespace OpenTelemetry.Exporter.Jaeger.Implementation.Tests -{ - public class Int128Test - { - [Fact] - public void Int128ConversionWorksAsExpected() - { - var id = ActivityTraceId.CreateFromBytes(new byte[] { 0x1a, 0x0f, 0x54, 0x63, 0x25, 0xa8, 0x56, 0x43, 0x1a, 0x4c, 0x24, 0xea, 0xa8, 0x60, 0xb0, 0xe8 }); - var int128 = new Int128(id); - - Assert.Equal(unchecked(0x1a0f546325a85643), int128.High); - Assert.Equal(unchecked(0x1a4c24eaa860b0e8), int128.Low); - } - } -} diff --git a/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/JaegerActivityConversionTest.cs b/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/JaegerActivityConversionTest.cs deleted file mode 100644 index cb9dc9c4c7d..00000000000 --- a/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/JaegerActivityConversionTest.cs +++ /dev/null @@ -1,862 +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 OpenTelemetry.Internal; -using OpenTelemetry.Resources; -using OpenTelemetry.Trace; -using Xunit; - -namespace OpenTelemetry.Exporter.Jaeger.Implementation.Tests -{ - public class JaegerActivityConversionTest - { - static JaegerActivityConversionTest() - { - Activity.DefaultIdFormat = ActivityIdFormat.W3C; - Activity.ForceDefaultIdFormat = true; - - var listener = new ActivityListener - { - ShouldListenTo = _ => true, - Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, - }; - - ActivitySource.AddActivityListener(listener); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void JaegerActivityConverterTest_ConvertActivityToJaegerSpan_AllPropertiesSet(bool isRootSpan) - { - using var activity = CreateTestActivity(isRootSpan: isRootSpan); - var traceIdAsInt = new Int128(activity.Context.TraceId); - var spanIdAsInt = new Int128(activity.Context.SpanId); - var linkTraceIdAsInt = new Int128(activity.Links.Single().Context.TraceId); - var linkSpanIdAsInt = new Int128(activity.Links.Single().Context.SpanId); - - var jaegerSpan = activity.ToJaegerSpan(); - - Assert.Equal("Name", jaegerSpan.OperationName); - Assert.Equal(2, jaegerSpan.Logs.Count); - - Assert.Equal(traceIdAsInt.High, jaegerSpan.TraceIdHigh); - Assert.Equal(traceIdAsInt.Low, jaegerSpan.TraceIdLow); - Assert.Equal(spanIdAsInt.Low, jaegerSpan.SpanId); - Assert.Equal(new Int128(activity.ParentSpanId).Low, jaegerSpan.ParentSpanId); - - Assert.Equal(activity.Links.Count(), jaegerSpan.References.Count); - var references = jaegerSpan.References.ToArray(); - var jaegerRef = references[0]; - Assert.Equal(linkTraceIdAsInt.High, jaegerRef.TraceIdHigh); - Assert.Equal(linkTraceIdAsInt.Low, jaegerRef.TraceIdLow); - Assert.Equal(linkSpanIdAsInt.Low, jaegerRef.SpanId); - - Assert.Equal(0x1, jaegerSpan.Flags); - - Assert.Equal(activity.StartTimeUtc.ToEpochMicroseconds(), jaegerSpan.StartTime); - Assert.Equal(TimeSpanToMicroseconds(activity.Duration), jaegerSpan.Duration); - - var tags = jaegerSpan.Tags.ToArray(); - var tag = tags[0]; - Assert.Equal(JaegerTagType.STRING, tag.VType); - Assert.Equal("stringKey", tag.Key); - Assert.Equal("value", tag.VStr); - tag = tags[1]; - Assert.Equal(JaegerTagType.LONG, tag.VType); - Assert.Equal("longKey", tag.Key); - Assert.Equal(1, tag.VLong); - tag = tags[2]; - Assert.Equal(JaegerTagType.LONG, tag.VType); - Assert.Equal("longKey2", tag.Key); - Assert.Equal(1, tag.VLong); - tag = tags[3]; - Assert.Equal(JaegerTagType.DOUBLE, tag.VType); - Assert.Equal("doubleKey", tag.Key); - Assert.Equal(1, tag.VDouble); - tag = tags[4]; - Assert.Equal(JaegerTagType.DOUBLE, tag.VType); - Assert.Equal("doubleKey2", tag.Key); - Assert.Equal(1, tag.VDouble); - tag = tags[5]; - Assert.Equal(JaegerTagType.BOOL, tag.VType); - Assert.Equal("boolKey", tag.Key); - Assert.Equal(true, tag.VBool); - - var logs = jaegerSpan.Logs.ToArray(); - var jaegerLog = logs[0]; - Assert.Equal(activity.Events.First().Timestamp.ToEpochMicroseconds(), jaegerLog.Timestamp); - Assert.Equal(3, jaegerLog.Fields.Count); - var eventFields = jaegerLog.Fields.ToArray(); - var eventField = eventFields[0]; - Assert.Equal("key", eventField.Key); - Assert.Equal("value", eventField.VStr); - eventField = eventFields[1]; - Assert.Equal("string_array", eventField.Key); - Assert.Equal(@"[""a"",""b""]", eventField.VStr); - eventField = eventFields[2]; - Assert.Equal("event", eventField.Key); - Assert.Equal("Event1", eventField.VStr); - - Assert.Equal(activity.Events.First().Timestamp.ToEpochMicroseconds(), jaegerLog.Timestamp); - - jaegerLog = logs[1]; - Assert.Equal(2, jaegerLog.Fields.Count); - eventFields = jaegerLog.Fields.ToArray(); - eventField = eventFields[0]; - Assert.Equal("key", eventField.Key); - Assert.Equal("value", eventField.VStr); - eventField = eventFields[1]; - Assert.Equal("event", eventField.Key); - Assert.Equal("Event2", eventField.VStr); - } - - [Fact] - public void JaegerActivityConverterTest_ConvertActivityToJaegerSpan_NoAttributes() - { - using var activity = CreateTestActivity(setAttributes: false); - var traceIdAsInt = new Int128(activity.Context.TraceId); - var spanIdAsInt = new Int128(activity.Context.SpanId); - var linkTraceIdAsInt = new Int128(activity.Links.Single().Context.TraceId); - var linkSpanIdAsInt = new Int128(activity.Links.Single().Context.SpanId); - - var jaegerSpan = activity.ToJaegerSpan(); - - Assert.Equal("Name", jaegerSpan.OperationName); - Assert.Equal(2, jaegerSpan.Logs.Count); - - Assert.Equal(traceIdAsInt.High, jaegerSpan.TraceIdHigh); - Assert.Equal(traceIdAsInt.Low, jaegerSpan.TraceIdLow); - Assert.Equal(spanIdAsInt.Low, jaegerSpan.SpanId); - Assert.Equal(new Int128(activity.ParentSpanId).Low, jaegerSpan.ParentSpanId); - - Assert.Equal(activity.Links.Count(), jaegerSpan.References.Count); - var references = jaegerSpan.References.ToArray(); - var jaegerRef = references[0]; - Assert.Equal(linkTraceIdAsInt.High, jaegerRef.TraceIdHigh); - Assert.Equal(linkTraceIdAsInt.Low, jaegerRef.TraceIdLow); - Assert.Equal(linkSpanIdAsInt.Low, jaegerRef.SpanId); - - Assert.Equal(0x1, jaegerSpan.Flags); - - Assert.Equal(activity.StartTimeUtc.ToEpochMicroseconds(), jaegerSpan.StartTime); - Assert.Equal(TimeSpanToMicroseconds(activity.Duration), jaegerSpan.Duration); - - // 2 tags: span.kind & library.name. - Assert.Equal(2, jaegerSpan.Tags.Count); - - var logs = jaegerSpan.Logs.ToArray(); - var jaegerLog = logs[0]; - Assert.Equal(activity.Events.First().Timestamp.ToEpochMicroseconds(), jaegerLog.Timestamp); - Assert.Equal(3, jaegerLog.Fields.Count); - var eventFields = jaegerLog.Fields.ToArray(); - var eventField = eventFields[0]; - Assert.Equal("key", eventField.Key); - Assert.Equal("value", eventField.VStr); - eventField = eventFields[2]; - Assert.Equal("event", eventField.Key); - Assert.Equal("Event1", eventField.VStr); - - Assert.Equal(activity.Events.First().Timestamp.ToEpochMicroseconds(), jaegerLog.Timestamp); - - jaegerLog = logs[1]; - Assert.Equal(2, jaegerLog.Fields.Count); - eventFields = jaegerLog.Fields.ToArray(); - eventField = eventFields[0]; - Assert.Equal("key", eventField.Key); - Assert.Equal("value", eventField.VStr); - eventField = eventFields[1]; - Assert.Equal("event", eventField.Key); - Assert.Equal("Event2", eventField.VStr); - } - - [Fact] - public void JaegerActivityConverterTest_ConvertActivityToJaegerSpan_NoEvents() - { - using var activity = CreateTestActivity(addEvents: false); - var traceIdAsInt = new Int128(activity.Context.TraceId); - var spanIdAsInt = new Int128(activity.Context.SpanId); - var linkTraceIdAsInt = new Int128(activity.Links.Single().Context.TraceId); - var linkSpanIdAsInt = new Int128(activity.Links.Single().Context.SpanId); - - var jaegerSpan = activity.ToJaegerSpan(); - - Assert.Equal("Name", jaegerSpan.OperationName); - Assert.Empty(jaegerSpan.Logs); - - Assert.Equal(traceIdAsInt.High, jaegerSpan.TraceIdHigh); - Assert.Equal(traceIdAsInt.Low, jaegerSpan.TraceIdLow); - Assert.Equal(spanIdAsInt.Low, jaegerSpan.SpanId); - Assert.Equal(new Int128(activity.ParentSpanId).Low, jaegerSpan.ParentSpanId); - - Assert.Equal(activity.Links.Count(), jaegerSpan.References.Count); - var references = jaegerSpan.References.ToArray(); - var jaegerRef = references[0]; - Assert.Equal(linkTraceIdAsInt.High, jaegerRef.TraceIdHigh); - Assert.Equal(linkTraceIdAsInt.Low, jaegerRef.TraceIdLow); - Assert.Equal(linkSpanIdAsInt.Low, jaegerRef.SpanId); - - Assert.Equal(0x1, jaegerSpan.Flags); - - Assert.Equal(activity.StartTimeUtc.ToEpochMicroseconds(), jaegerSpan.StartTime); - Assert.Equal(TimeSpanToMicroseconds(activity.Duration), jaegerSpan.Duration); - - var tags = jaegerSpan.Tags.ToArray(); - var tag = tags[0]; - Assert.Equal(JaegerTagType.STRING, tag.VType); - Assert.Equal("stringKey", tag.Key); - Assert.Equal("value", tag.VStr); - tag = tags[1]; - Assert.Equal(JaegerTagType.LONG, tag.VType); - Assert.Equal("longKey", tag.Key); - Assert.Equal(1, tag.VLong); - tag = tags[2]; - Assert.Equal(JaegerTagType.LONG, tag.VType); - Assert.Equal("longKey2", tag.Key); - Assert.Equal(1, tag.VLong); - tag = tags[3]; - Assert.Equal(JaegerTagType.DOUBLE, tag.VType); - Assert.Equal("doubleKey", tag.Key); - Assert.Equal(1, tag.VDouble); - tag = tags[4]; - Assert.Equal(JaegerTagType.DOUBLE, tag.VType); - Assert.Equal("doubleKey2", tag.Key); - Assert.Equal(1, tag.VDouble); - tag = tags[5]; - Assert.Equal(JaegerTagType.BOOL, tag.VType); - Assert.Equal("boolKey", tag.Key); - Assert.Equal(true, tag.VBool); - } - - [Fact] - public void JaegerActivityConverterTest_ConvertActivityToJaegerSpan_NoLinks() - { - using var activity = CreateTestActivity(addLinks: false, ticksToAdd: 8000); - var traceIdAsInt = new Int128(activity.Context.TraceId); - var spanIdAsInt = new Int128(activity.Context.SpanId); - - var jaegerSpan = activity.ToJaegerSpan(); - - Assert.Equal("Name", jaegerSpan.OperationName); - Assert.Equal(2, jaegerSpan.Logs.Count); - - Assert.Equal(traceIdAsInt.High, jaegerSpan.TraceIdHigh); - Assert.Equal(traceIdAsInt.Low, jaegerSpan.TraceIdLow); - Assert.Equal(spanIdAsInt.Low, jaegerSpan.SpanId); - Assert.Equal(new Int128(activity.ParentSpanId).Low, jaegerSpan.ParentSpanId); - - Assert.Empty(jaegerSpan.References); - - Assert.Equal(0x1, jaegerSpan.Flags); - - Assert.Equal(activity.StartTimeUtc.ToEpochMicroseconds(), jaegerSpan.StartTime); - Assert.Equal(TimeSpanToMicroseconds(activity.Duration), jaegerSpan.Duration); - - var tags = jaegerSpan.Tags.ToArray(); - var tag = tags[0]; - Assert.Equal(JaegerTagType.STRING, tag.VType); - Assert.Equal("stringKey", tag.Key); - Assert.Equal("value", tag.VStr); - tag = tags[1]; - Assert.Equal(JaegerTagType.LONG, tag.VType); - Assert.Equal("longKey", tag.Key); - Assert.Equal(1, tag.VLong); - tag = tags[2]; - Assert.Equal(JaegerTagType.LONG, tag.VType); - Assert.Equal("longKey2", tag.Key); - Assert.Equal(1, tag.VLong); - tag = tags[3]; - Assert.Equal(JaegerTagType.DOUBLE, tag.VType); - Assert.Equal("doubleKey", tag.Key); - Assert.Equal(1, tag.VDouble); - tag = tags[4]; - Assert.Equal(JaegerTagType.DOUBLE, tag.VType); - Assert.Equal("doubleKey2", tag.Key); - Assert.Equal(1, tag.VDouble); - tag = tags[5]; - Assert.Equal(JaegerTagType.BOOL, tag.VType); - Assert.Equal("boolKey", tag.Key); - Assert.Equal(true, tag.VBool); - - tag = tags[6]; - Assert.Equal(JaegerTagType.STRING, tag.VType); - Assert.Equal("int_array", tag.Key); - Assert.Equal(System.Text.Json.JsonSerializer.Serialize(new[] { 1, 2 }), tag.VStr); - - tag = tags[7]; - Assert.Equal(JaegerTagType.STRING, tag.VType); - Assert.Equal("bool_array", tag.Key); - Assert.Equal(System.Text.Json.JsonSerializer.Serialize(new[] { true, false }), tag.VStr); - - tag = tags[8]; - Assert.Equal(JaegerTagType.STRING, tag.VType); - Assert.Equal("double_array", tag.Key); - Assert.Equal(System.Text.Json.JsonSerializer.Serialize(new[] { 1, 1.1 }), tag.VStr); - - tag = tags[9]; - Assert.Equal(JaegerTagType.STRING, tag.VType); - Assert.Equal("string_array", tag.Key); - Assert.Equal(System.Text.Json.JsonSerializer.Serialize(new[] { "a", "b" }), tag.VStr); - - tag = tags[10]; - Assert.Equal(JaegerTagType.STRING, tag.VType); - Assert.Equal("obj_array", tag.Key); - Assert.Equal(System.Text.Json.JsonSerializer.Serialize(new[] { 1.ToString(), false.ToString(), new object().ToString(), "string", string.Empty, null }), tag.VStr); - - // The second to last tag should be span.kind in this case - tag = tags[tags.Length - 2]; - Assert.Equal(JaegerTagType.STRING, tag.VType); - Assert.Equal("span.kind", tag.Key); - Assert.Equal("client", tag.VStr); - - // The last tag should be library.name in this case - tag = tags[tags.Length - 1]; - Assert.Equal(JaegerTagType.STRING, tag.VType); - Assert.Equal("otel.library.name", tag.Key); - Assert.Equal(nameof(CreateTestActivity), tag.VStr); - - var logs = jaegerSpan.Logs.ToArray(); - var jaegerLog = logs[0]; - Assert.Equal(activity.Events.First().Timestamp.ToEpochMicroseconds(), jaegerLog.Timestamp); - Assert.Equal(3, jaegerLog.Fields.Count); - var eventFields = jaegerLog.Fields.ToArray(); - var eventField = eventFields[0]; - Assert.Equal("key", eventField.Key); - Assert.Equal("value", eventField.VStr); - eventField = eventFields[2]; - Assert.Equal("event", eventField.Key); - Assert.Equal("Event1", eventField.VStr); - Assert.Equal(activity.Events.First().Timestamp.ToEpochMicroseconds(), jaegerLog.Timestamp); - - jaegerLog = logs[1]; - Assert.Equal(2, jaegerLog.Fields.Count); - eventFields = jaegerLog.Fields.ToArray(); - eventField = eventFields[0]; - Assert.Equal("key", eventField.Key); - Assert.Equal("value", eventField.VStr); - eventField = eventFields[1]; - Assert.Equal("event", eventField.Key); - Assert.Equal("Event2", eventField.VStr); - } - - [Fact] - public void JaegerActivityConverterTest_GenerateJaegerSpan_RemoteEndpointOmittedByDefault() - { - // Arrange - using var span = CreateTestActivity(); - - // Act - var jaegerSpan = span.ToJaegerSpan(); - - // Assert - Assert.DoesNotContain(jaegerSpan.Tags, t => t.Key == "peer.service"); - } - - [Fact] - public void JaegerActivityConverterTest_GenerateJaegerSpan_RemoteEndpointResolution() - { - // Arrange - using var span = CreateTestActivity( - additionalAttributes: new Dictionary - { - ["net.peer.name"] = "RemoteServiceName", - }); - - // Act - var jaegerSpan = span.ToJaegerSpan(); - - // Assert - Assert.Contains(jaegerSpan.Tags, t => t.Key == "peer.service"); - Assert.Equal("RemoteServiceName", jaegerSpan.Tags.First(t => t.Key == "peer.service").VStr); - } - - [Fact] - public void JaegerActivityConverterTest_GenerateJaegerSpan_PeerServiceNameIgnoredForServerSpan() - { - // Arrange - using var span = CreateTestActivity( - additionalAttributes: new Dictionary - { - ["http.host"] = "DiscardedRemoteServiceName", - }, - kind: ActivityKind.Server); - - // Act - var jaegerSpan = span.ToJaegerSpan(); - - // Assert - Assert.Null(jaegerSpan.PeerServiceName); - Assert.Empty(jaegerSpan.Tags.Where(t => t.Key == "peer.service")); - } - - [Theory] - [MemberData(nameof(RemoteEndpointPriorityTestCase.GetTestCases), MemberType = typeof(RemoteEndpointPriorityTestCase))] - public void JaegerActivityConverterTest_GenerateJaegerSpan_RemoteEndpointResolutionPriority(RemoteEndpointPriorityTestCase testCase) - { - // Arrange - using var activity = CreateTestActivity(additionalAttributes: testCase.RemoteEndpointAttributes); - - // Act - var jaegerSpan = activity.ToJaegerSpan(); - - // Assert - var tags = jaegerSpan.Tags.Where(t => t.Key == "peer.service"); - Assert.Single(tags); - var tag = tags.First(); - Assert.Equal(testCase.ExpectedResult, tag.VStr); - } - - [Fact] - public void JaegerActivityConverterTest_NullTagValueTest() - { - // Arrange - using var activity = CreateTestActivity(additionalAttributes: new Dictionary { ["nullTag"] = null }); - - // Act - var jaegerSpan = activity.ToJaegerSpan(); - - // Assert - Assert.DoesNotContain(jaegerSpan.Tags, t => t.Key == "nullTag"); - } - - [Theory] - [InlineData(StatusCode.Unset, "unset", "")] - [InlineData(StatusCode.Ok, "Ok", "")] - [InlineData(StatusCode.Error, "ERROR", "error description")] - [InlineData(StatusCode.Unset, "iNvAlId", "")] - public void JaegerActivityConverterTest_Status_ErrorFlagTest(StatusCode expectedStatusCode, string statusCodeTagValue, string statusDescription) - { - // Arrange - using var activity = CreateTestActivity(); - activity.SetTag(SpanAttributeConstants.StatusCodeKey, statusCodeTagValue); - activity.SetTag(SpanAttributeConstants.StatusDescriptionKey, statusDescription); - - // Act - var jaegerSpan = activity.ToJaegerSpan(); - - // Assert - - Assert.Equal(expectedStatusCode, activity.GetStatus().StatusCode); - - if (expectedStatusCode == StatusCode.Unset) - { - Assert.DoesNotContain(jaegerSpan.Tags, t => t.Key == SpanAttributeConstants.StatusCodeKey); - } - else - { - Assert.Equal( - StatusHelper.GetTagValueForStatusCode(expectedStatusCode), - jaegerSpan.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.StatusCodeKey).VStr); - } - - if (expectedStatusCode == StatusCode.Error) - { - Assert.Contains( - jaegerSpan.Tags, t => - t.Key == JaegerActivityExtensions.JaegerErrorFlagTagName && - t.VType == JaegerTagType.BOOL && (t.VBool ?? false)); - Assert.Contains( - jaegerSpan.Tags, t => - t.Key == SpanAttributeConstants.StatusDescriptionKey && - t.VType == JaegerTagType.STRING && t.VStr.Equals(statusDescription)); - } - else - { - Assert.DoesNotContain(jaegerSpan.Tags, t => t.Key == JaegerActivityExtensions.JaegerErrorFlagTagName); - } - } - - [Theory] - [InlineData(ActivityStatusCode.Unset)] - [InlineData(ActivityStatusCode.Ok)] - [InlineData(ActivityStatusCode.Error)] - public void ToJaegerSpan_Activity_Status_And_StatusDescription_is_Set(ActivityStatusCode expectedStatusCode) - { - // Arrange - using var activity = CreateTestActivity(); - activity.SetStatus(expectedStatusCode); - - // Act - var jaegerSpan = activity.ToJaegerSpan(); - - // Assert - if (expectedStatusCode == ActivityStatusCode.Unset) - { - Assert.DoesNotContain(jaegerSpan.Tags, t => t.Key == SpanAttributeConstants.StatusCodeKey); - } - else if (expectedStatusCode == ActivityStatusCode.Ok) - { - Assert.Equal("OK", jaegerSpan.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.StatusCodeKey).VStr); - } - - // expectedStatusCode is Error - else - { - Assert.Equal("ERROR", jaegerSpan.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.StatusCodeKey).VStr); - } - - if (expectedStatusCode == ActivityStatusCode.Error) - { - Assert.Contains( - jaegerSpan.Tags, t => - t.Key == JaegerActivityExtensions.JaegerErrorFlagTagName && - t.VType == JaegerTagType.BOOL && (t.VBool ?? false)); - } - else - { - Assert.DoesNotContain( - jaegerSpan.Tags, t => - t.Key == JaegerActivityExtensions.JaegerErrorFlagTagName); - } - } - - [Fact] - public void ActivityStatus_Takes_precedence_Over_Status_Tags_ActivityStatusCodeIsOk() - { - // Arrange. - using var activity = CreateTestActivity(); - const string TagDescriptionOnError = "Description when TagStatusCode is Error."; - activity.SetStatus(ActivityStatusCode.Ok); - activity.SetTag(SpanAttributeConstants.StatusCodeKey, "ERROR"); - activity.SetTag(SpanAttributeConstants.StatusDescriptionKey, TagDescriptionOnError); - - // Enrich activity with additional tags. - activity.SetTag("myCustomTag", "myCustomTagValue"); - - // Act. - var jaegerSpan = activity.ToJaegerSpan(); - - // Assert. - Assert.Equal("OK", jaegerSpan.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.StatusCodeKey).VStr); - - Assert.Contains(jaegerSpan.Tags, t => t.Key == "otel.status_code" && t.VStr == "OK"); - Assert.DoesNotContain(jaegerSpan.Tags, t => t.Key == "otel.status_code" && t.VStr == "ERROR"); - Assert.DoesNotContain(jaegerSpan.Tags, t => t.Key == JaegerActivityExtensions.JaegerErrorFlagTagName); - Assert.DoesNotContain(jaegerSpan.Tags, t => t.Key == SpanAttributeConstants.StatusDescriptionKey && - t.VType == JaegerTagType.STRING && t.VStr.Equals(TagDescriptionOnError)); - - // Ensure additional Activity tags were being converted. - Assert.Contains(jaegerSpan.Tags, t => t.Key == "myCustomTag" && t.VStr == "myCustomTagValue"); - } - - [Fact] - public void ActivityStatus_Takes_precedence_Over_Status_Tags_ActivityStatusCodeIsError() - { - // Arrange. - using var activity = CreateTestActivity(); - const string StatusDescriptionOnError = "Description when ActivityStatusCode is Error."; - activity.SetStatus(ActivityStatusCode.Error, StatusDescriptionOnError); - activity.SetTag(SpanAttributeConstants.StatusCodeKey, "OK"); - - // Enrich activity with additional tags. - activity.SetTag("myCustomTag", "myCustomTagValue"); - - // Act. - var jaegerSpan = activity.ToJaegerSpan(); - - // Assert. - Assert.Contains(jaegerSpan.Tags, t => t.Key == "otel.status_code" && t.VStr == "ERROR"); - Assert.Contains(jaegerSpan.Tags, t => t.Key == JaegerActivityExtensions.JaegerErrorFlagTagName); - Assert.Contains(jaegerSpan.Tags, t => t.Key == SpanAttributeConstants.StatusDescriptionKey && - t.VType == JaegerTagType.STRING && t.VStr.Equals(StatusDescriptionOnError)); - - Assert.DoesNotContain(jaegerSpan.Tags, t => t.Key == "otel.status_code" && t.VStr == "OK"); - - // Ensure additional Activity tags were being converted. - Assert.Contains(jaegerSpan.Tags, t => t.Key == "myCustomTag" && t.VStr == "myCustomTagValue"); - } - - [Fact] - public void ActivityDescription_Takes_precedence_Over_Status_Tags_When_ActivityStatusCodeIsError() - { - // Arrange. - using var activity = CreateTestActivity(); - - const string StatusDescriptionOnError = "Description when ActivityStatusCode is Error."; - const string TagDescriptionOnError = "Description when TagStatusCode is Error."; - activity.SetStatus(ActivityStatusCode.Error, StatusDescriptionOnError); - activity.SetTag(SpanAttributeConstants.StatusCodeKey, "ERROR"); - activity.SetTag(SpanAttributeConstants.StatusDescriptionKey, TagDescriptionOnError); - - // Enrich activity with additional tags. - activity.SetTag("myCustomTag", "myCustomTagValue"); - - // Act. - var jaegerSpan = activity.ToJaegerSpan(); - - // Assert. - Assert.Equal("ERROR", jaegerSpan.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.StatusCodeKey).VStr); - - Assert.Contains( - jaegerSpan.Tags, t => - t.Key == JaegerActivityExtensions.JaegerErrorFlagTagName && - t.VType == JaegerTagType.BOOL && (t.VBool ?? false)); - - Assert.Contains( - jaegerSpan.Tags, t => - t.Key == SpanAttributeConstants.StatusDescriptionKey && - t.VType == JaegerTagType.STRING && t.VStr.Equals(StatusDescriptionOnError)); - - // Ensure additional Activity tags were being converted. - Assert.Contains(jaegerSpan.Tags, t => t.Key == "myCustomTag" && t.VStr == "myCustomTagValue"); - } - - internal static Activity CreateTestActivity( - bool setAttributes = true, - Dictionary additionalAttributes = null, - bool addEvents = true, - bool addLinks = true, - Resource resource = null, - ActivityKind kind = ActivityKind.Client, - bool isRootSpan = false, - Status? status = null, - long ticksToAdd = 60 * TimeSpan.TicksPerSecond) - { - var startTimestamp = DateTime.UtcNow; - var endTimestamp = startTimestamp.AddTicks(ticksToAdd); - var eventTimestamp = DateTime.UtcNow; - var traceId = ActivityTraceId.CreateFromString("e8ea7e9ac72de94e91fabc613f9686b2".AsSpan()); - - var parentSpanId = isRootSpan ? default : ActivitySpanId.CreateFromBytes(new byte[] { 12, 23, 34, 45, 56, 67, 78, 89 }); - - var attributes = new Dictionary - { - { "exceptionFromToString", new MyToStringMethodThrowsAnException() }, - { "exceptionFromToStringInArray", new MyToStringMethodThrowsAnException[] { new MyToStringMethodThrowsAnException() } }, - { "stringKey", "value" }, - { "longKey", 1L }, - { "longKey2", 1 }, - { "doubleKey", 1D }, - { "doubleKey2", 1F }, - { "boolKey", true }, - { "int_array", new int[] { 1, 2 } }, - { "bool_array", new bool[] { true, false } }, - { "double_array", new double[] { 1.0, 1.1 } }, - { "string_array", new string[] { "a", "b" } }, - { "obj_array", new object[] { 1, false, new object(), "string", string.Empty, null } }, - }; - if (additionalAttributes != null) - { - foreach (var attribute in additionalAttributes) - { - attributes.Add(attribute.Key, attribute.Value); - } - } - - var events = new List - { - new ActivityEvent( - "Event1", - eventTimestamp, - new ActivityTagsCollection(new Dictionary - { - { "key", "value" }, - { "string_array", new string[] { "a", "b" } }, - })), - new ActivityEvent( - "Event2", - eventTimestamp, - new ActivityTagsCollection(new Dictionary - { - { "key", "value" }, - })), - }; - - var linkedSpanId = ActivitySpanId.CreateFromString("888915b6286b9c41".AsSpan()); - - using var activitySource = new ActivitySource(nameof(CreateTestActivity)); - - var tags = setAttributes ? - attributes - : null; - var links = addLinks ? - new[] - { - new ActivityLink(new ActivityContext( - traceId, - linkedSpanId, - ActivityTraceFlags.Recorded)), - } - : null; - - using var activity = activitySource.StartActivity( - "Name", - kind, - parentContext: new ActivityContext(traceId, parentSpanId, ActivityTraceFlags.Recorded), - tags, - links, - startTime: startTimestamp); - - if (addEvents) - { - foreach (var evnt in events) - { - activity.AddEvent(evnt); - } - } - - if (status.HasValue) - { - activity.SetStatus(status.Value); - } - - activity.SetEndTime(endTimestamp); - activity.Stop(); - - return activity; - } - - private static long TimeSpanToMicroseconds(TimeSpan timeSpan) - { - return timeSpan.Ticks / (TimeSpan.TicksPerMillisecond / 1000); - } - - public class RemoteEndpointPriorityTestCase - { - public string Name { get; set; } - - public string ExpectedResult { get; set; } - - public Dictionary RemoteEndpointAttributes { get; set; } - - public static IEnumerable GetTestCases() - { - yield return new object[] - { - new RemoteEndpointPriorityTestCase - { - Name = "Highest priority name = net.peer.name", - ExpectedResult = "RemoteServiceName", - RemoteEndpointAttributes = new Dictionary - { - ["http.host"] = "DiscardedRemoteServiceName", - ["net.peer.name"] = "RemoteServiceName", - ["peer.hostname"] = "DiscardedRemoteServiceName", - }, - }, - }; - - yield return new object[] - { - new RemoteEndpointPriorityTestCase - { - Name = "Highest priority name = SemanticConventions.AttributePeerService", - ExpectedResult = "RemoteServiceName", - RemoteEndpointAttributes = new Dictionary - { - [SemanticConventions.AttributePeerService] = "RemoteServiceName", - ["http.host"] = "DiscardedRemoteServiceName", - ["net.peer.name"] = "DiscardedRemoteServiceName", - ["net.peer.port"] = "1234", - ["peer.hostname"] = "DiscardedRemoteServiceName", - }, - }, - }; - - yield return new object[] - { - new RemoteEndpointPriorityTestCase - { - Name = "Only has net.peer.name and net.peer.port", - ExpectedResult = "RemoteServiceName:1234", - RemoteEndpointAttributes = new Dictionary - { - ["net.peer.name"] = "RemoteServiceName", - ["net.peer.port"] = "1234", - }, - }, - }; - - yield return new object[] - { - new RemoteEndpointPriorityTestCase - { - Name = "net.peer.port is an int", - ExpectedResult = "RemoteServiceName:1234", - RemoteEndpointAttributes = new Dictionary - { - ["net.peer.name"] = "RemoteServiceName", - ["net.peer.port"] = 1234, - }, - }, - }; - - yield return new object[] - { - new RemoteEndpointPriorityTestCase - { - Name = "Has net.peer.name and net.peer.port", - ExpectedResult = "RemoteServiceName:1234", - RemoteEndpointAttributes = new Dictionary - { - ["http.host"] = "DiscardedRemoteServiceName", - ["net.peer.name"] = "RemoteServiceName", - ["net.peer.port"] = "1234", - ["peer.hostname"] = "DiscardedRemoteServiceName", - }, - }, - }; - - yield return new object[] - { - new RemoteEndpointPriorityTestCase - { - Name = "Has net.peer.ip and net.peer.port", - ExpectedResult = "1.2.3.4:1234", - RemoteEndpointAttributes = new Dictionary - { - ["http.host"] = "DiscardedRemoteServiceName", - ["net.peer.ip"] = "1.2.3.4", - ["net.peer.port"] = "1234", - ["peer.hostname"] = "DiscardedRemoteServiceName", - }, - }, - }; - - yield return new object[] - { - new RemoteEndpointPriorityTestCase - { - Name = "Has net.peer.name, net.peer.ip, and net.peer.port", - ExpectedResult = "RemoteServiceName:1234", - RemoteEndpointAttributes = new Dictionary - { - ["http.host"] = "DiscardedRemoteServiceName", - ["net.peer.name"] = "RemoteServiceName", - ["net.peer.ip"] = "1.2.3.4", - ["net.peer.port"] = "1234", - ["peer.hostname"] = "DiscardedRemoteServiceName", - }, - }, - }; - } - - public override string ToString() - { - return this.Name; - } - } - - private class MyToStringMethodThrowsAnException - { - public override string ToString() - { - throw new Exception("Nope."); - } - } - } -} diff --git a/test/OpenTelemetry.Exporter.Jaeger.Tests/JaegerExporterOptionsTests.cs b/test/OpenTelemetry.Exporter.Jaeger.Tests/JaegerExporterOptionsTests.cs deleted file mode 100644 index e8c2f7c48ce..00000000000 --- a/test/OpenTelemetry.Exporter.Jaeger.Tests/JaegerExporterOptionsTests.cs +++ /dev/null @@ -1,157 +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 Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using OpenTelemetry.Trace; -using Xunit; - -namespace OpenTelemetry.Exporter.Jaeger.Tests -{ - public class JaegerExporterOptionsTests : IDisposable - { - public JaegerExporterOptionsTests() - { - ClearEnvVars(); - } - - public void Dispose() - { - ClearEnvVars(); - GC.SuppressFinalize(this); - } - - [Fact] - public void JaegerExporterOptions_Defaults() - { - var options = new JaegerExporterOptions(); - - Assert.Equal("localhost", options.AgentHost); - Assert.Equal(6831, options.AgentPort); - Assert.Equal(4096, options.MaxPayloadSizeInBytes); - Assert.Equal(ExportProcessorType.Batch, options.ExportProcessorType); - Assert.Equal(JaegerExportProtocol.UdpCompactThrift, options.Protocol); - Assert.Equal(JaegerExporterOptions.DefaultJaegerEndpoint, options.Endpoint.ToString()); - } - - [Fact] - public void JaegerExporterOptions_EnvironmentVariableOverride() - { - Environment.SetEnvironmentVariable(JaegerExporterOptions.OTelAgentHostEnvVarKey, "jaeger-host"); - Environment.SetEnvironmentVariable(JaegerExporterOptions.OTelAgentPortEnvVarKey, "123"); - Environment.SetEnvironmentVariable(JaegerExporterOptions.OTelProtocolEnvVarKey, "http/thrift.binary"); - Environment.SetEnvironmentVariable(JaegerExporterOptions.OTelEndpointEnvVarKey, "http://custom-endpoint:12345"); - - var options = new JaegerExporterOptions(); - - Assert.Equal("jaeger-host", options.AgentHost); - Assert.Equal(123, options.AgentPort); - Assert.Equal(JaegerExportProtocol.HttpBinaryThrift, options.Protocol); - Assert.Equal(new Uri("http://custom-endpoint:12345"), options.Endpoint); - } - - [Fact] - public void JaegerExporterOptions_InvalidEnvironmentVariableOverride() - { - Environment.SetEnvironmentVariable(JaegerExporterOptions.OTelAgentPortEnvVarKey, "invalid"); - Environment.SetEnvironmentVariable(JaegerExporterOptions.OTelProtocolEnvVarKey, "invalid"); - - var options = new JaegerExporterOptions(); - - Assert.Equal("localhost", options.AgentHost); - Assert.Equal(default(JaegerExportProtocol), options.Protocol); - } - - [Fact] - public void JaegerExporterOptions_SetterOverridesEnvironmentVariable() - { - Environment.SetEnvironmentVariable(JaegerExporterOptions.OTelAgentHostEnvVarKey, "envvar-host"); - - var options = new JaegerExporterOptions - { - AgentHost = "incode-host", - }; - - Assert.Equal("incode-host", options.AgentHost); - } - - [Fact] - public void JaegerExporterOptions_EnvironmentVariableNames() - { - Assert.Equal("OTEL_EXPORTER_JAEGER_PROTOCOL", JaegerExporterOptions.OTelProtocolEnvVarKey); - Assert.Equal("OTEL_EXPORTER_JAEGER_AGENT_HOST", JaegerExporterOptions.OTelAgentHostEnvVarKey); - Assert.Equal("OTEL_EXPORTER_JAEGER_AGENT_PORT", JaegerExporterOptions.OTelAgentPortEnvVarKey); - Assert.Equal("OTEL_EXPORTER_JAEGER_ENDPOINT", JaegerExporterOptions.OTelEndpointEnvVarKey); - } - - [Fact] - public void JaegerExporterOptions_FromConfigurationTest() - { - var values = new Dictionary() - { - [JaegerExporterOptions.OTelProtocolEnvVarKey] = "http/thrift.binary", - [JaegerExporterOptions.OTelAgentHostEnvVarKey] = "jaeger-host", - [JaegerExporterOptions.OTelAgentPortEnvVarKey] = "123", - [JaegerExporterOptions.OTelEndpointEnvVarKey] = "http://custom-endpoint:12345", - ["OTEL_BSP_MAX_QUEUE_SIZE"] = "18", - ["OTEL_BSP_MAX_EXPORT_BATCH_SIZE"] = "2", - ["Jaeger:BatchExportProcessorOptions:MaxExportBatchSize"] = "5", - }; - - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(values) - .Build(); - - IServiceCollection services = null; - - using var provider = Sdk.CreateTracerProviderBuilder() - .ConfigureServices(s => - { - services = s; - services.AddSingleton(configuration); - services.Configure(configuration.GetSection("Jaeger")); - }) - .AddJaegerExporter() - .Build(); - - Assert.NotNull(services); - - using var serviceProvider = services.BuildServiceProvider(); - - var options = serviceProvider.GetRequiredService>().CurrentValue; - - Assert.Equal("jaeger-host", options.AgentHost); - Assert.Equal(123, options.AgentPort); - Assert.Equal(JaegerExportProtocol.HttpBinaryThrift, options.Protocol); - Assert.Equal(new Uri("http://custom-endpoint:12345"), options.Endpoint); - Assert.Equal(18, options.BatchExportProcessorOptions.MaxQueueSize); - - // Note: - // 1. OTEL_BSP_MAX_EXPORT_BATCH_SIZE is processed in BatchExportActivityProcessorOptions ctor and sets MaxExportBatchSize to 2. - // 2. Jaeger:BatchExportProcessorOptions:MaxExportBatchSize is processed by options binder after ctor and sets MaxExportBatchSize to 5. - Assert.Equal(5, options.BatchExportProcessorOptions.MaxExportBatchSize); - } - - private static void ClearEnvVars() - { - Environment.SetEnvironmentVariable(JaegerExporterOptions.OTelProtocolEnvVarKey, null); - Environment.SetEnvironmentVariable(JaegerExporterOptions.OTelAgentHostEnvVarKey, null); - Environment.SetEnvironmentVariable(JaegerExporterOptions.OTelAgentPortEnvVarKey, null); - Environment.SetEnvironmentVariable(JaegerExporterOptions.OTelEndpointEnvVarKey, null); - } - } -} diff --git a/test/OpenTelemetry.Exporter.Jaeger.Tests/JaegerExporterProtocolParserTests.cs b/test/OpenTelemetry.Exporter.Jaeger.Tests/JaegerExporterProtocolParserTests.cs deleted file mode 100644 index c75a52d674a..00000000000 --- a/test/OpenTelemetry.Exporter.Jaeger.Tests/JaegerExporterProtocolParserTests.cs +++ /dev/null @@ -1,34 +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 Xunit; - -namespace OpenTelemetry.Exporter.Jaeger.Tests; - -public class JaegerExporterProtocolParserTests -{ - [Theory] - [InlineData("udp/thrift.compact", true, JaegerExportProtocol.UdpCompactThrift)] - [InlineData("http/thrift.binary", true, JaegerExportProtocol.HttpBinaryThrift)] - [InlineData("unsupported", false, default(JaegerExportProtocol))] - public void TryParse_Protocol_MapsToCorrectValue(string protocol, bool expectedResult, JaegerExportProtocol expectedExportProtocol) - { - var result = JaegerExporterProtocolParser.TryParse(protocol, out var exportProtocol); - - Assert.Equal(expectedExportProtocol, exportProtocol); - Assert.Equal(expectedResult, result); - } -} diff --git a/test/OpenTelemetry.Exporter.Jaeger.Tests/JaegerExporterTests.cs b/test/OpenTelemetry.Exporter.Jaeger.Tests/JaegerExporterTests.cs deleted file mode 100644 index 411fbeba37b..00000000000 --- a/test/OpenTelemetry.Exporter.Jaeger.Tests/JaegerExporterTests.cs +++ /dev/null @@ -1,426 +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.Collections.Concurrent; -using System.Diagnostics; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using OpenTelemetry.Exporter.Jaeger.Implementation; -using OpenTelemetry.Exporter.Jaeger.Implementation.Tests; -using OpenTelemetry.Resources; -using OpenTelemetry.Tests; -using OpenTelemetry.Trace; -using Thrift.Protocol; -using Xunit; - -namespace OpenTelemetry.Exporter.Jaeger.Tests -{ - public class JaegerExporterTests - { - [Fact] - public void AddJaegerExporterNamedOptionsSupported() - { - int defaultExporterOptionsConfigureOptionsInvocations = 0; - int namedExporterOptionsConfigureOptionsInvocations = 0; - - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .ConfigureServices(services => - { - services.Configure(o => defaultExporterOptionsConfigureOptionsInvocations++); - - services.Configure("Exporter2", o => namedExporterOptionsConfigureOptionsInvocations++); - }) - .AddJaegerExporter() - .AddJaegerExporter("Exporter2", o => { }) - .Build(); - - Assert.Equal(1, defaultExporterOptionsConfigureOptionsInvocations); - Assert.Equal(1, namedExporterOptionsConfigureOptionsInvocations); - } - - [Fact] - public void JaegerExporter_BadArgs() - { - TracerProviderBuilder builder = null; - Assert.Throws(() => builder.AddJaegerExporter()); - } - - [Fact] - public void JaegerTraceExporter_ctor_NullServiceNameAllowed() - { - using var jaegerTraceExporter = new JaegerExporter(new JaegerExporterOptions()); - Assert.NotNull(jaegerTraceExporter); - } - - [Fact] - public void UserHttpFactoryCalled() - { - JaegerExporterOptions options = new JaegerExporterOptions(); - - var defaultFactory = options.HttpClientFactory; - - int invocations = 0; - options.Protocol = JaegerExportProtocol.HttpBinaryThrift; - options.HttpClientFactory = () => - { - invocations++; - return defaultFactory(); - }; - - using (var exporter = new JaegerExporter(options)) - { - Assert.Equal(1, invocations); - } - - using (var provider = Sdk.CreateTracerProviderBuilder() - .AddJaegerExporter(o => - { - o.Protocol = JaegerExportProtocol.HttpBinaryThrift; - o.HttpClientFactory = options.HttpClientFactory; - }) - .Build()) - { - Assert.Equal(2, invocations); - } - - options.HttpClientFactory = null; - Assert.Throws(() => - { - using var exporter = new JaegerExporter(options); - }); - - options.HttpClientFactory = () => null; - Assert.Throws(() => - { - using var exporter = new JaegerExporter(options); - }); - } - - [Fact] - public void ServiceProviderHttpClientFactoryInvoked() - { - IServiceCollection services = new ServiceCollection(); - - services.AddHttpClient(); - - int invocations = 0; - - services.AddHttpClient("JaegerExporter", configureClient: (client) => invocations++); - - services.AddOpenTelemetry().WithTracing(builder => builder - .AddJaegerExporter(o => o.Protocol = JaegerExportProtocol.HttpBinaryThrift)); - - using var serviceProvider = services.BuildServiceProvider(); - - var tracerProvider = serviceProvider.GetRequiredService(); - - Assert.Equal(1, invocations); - } - - [Theory] - [InlineData("/api/traces")] - [InlineData("/foo/bar")] - [InlineData("/")] - public void HttpClient_Posts_To_Configured_Endpoint(string uriPath) - { - // Arrange - ConcurrentDictionary responses = new ConcurrentDictionary(); - using var testServer = TestHttpServer.RunServer( - context => - { - context.Response.StatusCode = 200; - - using StreamReader readStream = new StreamReader(context.Request.InputStream); - - string requestContent = readStream.ReadToEnd(); - - responses.TryAdd( - Guid.Parse(context.Request.QueryString["requestId"]), - context.Request.Url.LocalPath); - - context.Response.OutputStream.Close(); - }, - out var testServerHost, - out var testServerPort); - - var requestId = Guid.NewGuid(); - var options = new JaegerExporterOptions - { - Endpoint = new Uri($"http://{testServerHost}:{testServerPort}{uriPath}?requestId={requestId}"), - Protocol = JaegerExportProtocol.HttpBinaryThrift, - ExportProcessorType = ExportProcessorType.Simple, - }; - - using var jaegerExporter = new JaegerExporter(options); - - // Act - jaegerExporter.SetResourceAndInitializeBatch(Resource.Empty); - jaegerExporter.AppendSpan(CreateTestJaegerSpan()); - jaegerExporter.SendCurrentBatch(); - - // Assert - Assert.True(responses.ContainsKey(requestId)); - Assert.Equal(uriPath, responses[requestId]); - } - - [Fact] - public void JaegerTraceExporter_SetResource_UpdatesServiceName() - { - using var jaegerTraceExporter = new JaegerExporter(new JaegerExporterOptions()); - var process = jaegerTraceExporter.Process; - - jaegerTraceExporter.SetResourceAndInitializeBatch(Resource.Empty); - - Assert.StartsWith("unknown_service:", process.ServiceName); - - jaegerTraceExporter.SetResourceAndInitializeBatch(ResourceBuilder.CreateEmpty().AddService("MyService").Build()); - - Assert.Equal("MyService", process.ServiceName); - - jaegerTraceExporter.SetResourceAndInitializeBatch(ResourceBuilder.CreateEmpty().AddService("MyService", "MyNamespace").Build()); - - Assert.Equal("MyNamespace.MyService", process.ServiceName); - } - - [Fact] - public void JaegerTraceExporter_SetResource_CreatesTags() - { - using var jaegerTraceExporter = new JaegerExporter(new JaegerExporterOptions()); - var process = jaegerTraceExporter.Process; - - jaegerTraceExporter.SetResourceAndInitializeBatch(ResourceBuilder.CreateEmpty().AddAttributes(new Dictionary - { - ["Tag"] = "value", - }).Build()); - - Assert.NotNull(process.Tags); - Assert.Single(process.Tags); - Assert.Equal("value", process.Tags["Tag"].VStr); - } - - [Fact] - public void JaegerTraceExporter_SetResource_CombinesTags() - { - using var jaegerTraceExporter = new JaegerExporter(new JaegerExporterOptions()); - var process = jaegerTraceExporter.Process; - - JaegerTagTransformer.Instance.TryTransformTag(new KeyValuePair("Tag1", "value1"), out var result); - process.Tags = new Dictionary { ["Tag1"] = result }; - - jaegerTraceExporter.SetResourceAndInitializeBatch(ResourceBuilder.CreateEmpty().AddAttributes(new Dictionary - { - ["Tag2"] = "value2", - }).Build()); - - Assert.NotNull(process.Tags); - Assert.Equal(2, process.Tags.Count); - Assert.Equal("value1", process.Tags["Tag1"].VStr); - Assert.Equal("value2", process.Tags["Tag2"].VStr); - } - - [Fact] - public void JaegerTraceExporter_SetResource_IgnoreServiceResources() - { - using var jaegerTraceExporter = new JaegerExporter(new JaegerExporterOptions()); - var process = jaegerTraceExporter.Process; - - jaegerTraceExporter.SetResourceAndInitializeBatch(ResourceBuilder.CreateEmpty().AddAttributes(new Dictionary - { - [ResourceSemanticConventions.AttributeServiceName] = "servicename", - [ResourceSemanticConventions.AttributeServiceNamespace] = "servicenamespace", - }).Build()); - - Assert.Null(process.Tags); - } - - [Fact] - public void JaegerTraceExporter_SetResource_UpdatesServiceNameFromIConfiguration() - { - var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder() - .ConfigureServices(services => - { - Dictionary configuration = new() - { - ["OTEL_SERVICE_NAME"] = "myservicename", - }; - - services.AddSingleton( - new ConfigurationBuilder().AddInMemoryCollection(configuration).Build()); - }); - - var jaegerTraceExporter = new JaegerExporter(new JaegerExporterOptions()); - - tracerProviderBuilder.AddProcessor(new BatchActivityExportProcessor(jaegerTraceExporter)); - - using var provider = tracerProviderBuilder.Build(); - - var process = jaegerTraceExporter.Process; - - jaegerTraceExporter.SetResourceAndInitializeBatch(Resource.Empty); - - Assert.Equal("myservicename", process.ServiceName); - } - - [Fact] - public void JaegerTraceExporter_BuildBatchesToTransmit_FlushedBatch() - { - // Arrange - using var jaegerExporter = new JaegerExporter(new JaegerExporterOptions { MaxPayloadSizeInBytes = 1500 }); - jaegerExporter.SetResourceAndInitializeBatch(Resource.Empty); - - // Act - jaegerExporter.AppendSpan(CreateTestJaegerSpan()); - jaegerExporter.AppendSpan(CreateTestJaegerSpan()); - jaegerExporter.AppendSpan(CreateTestJaegerSpan()); - - // Assert - Assert.Equal(1U, jaegerExporter.NumberOfSpansInCurrentBatch); - } - - [Theory] - [InlineData("Compact", 1500)] - [InlineData("Binary", 2200)] - public void JaegerTraceExporter_SpansSplitToBatches_SpansIncludedInBatches(string protocolType, int maxPayloadSizeInBytes) - { - TProtocolFactory protocolFactory = protocolType == "Compact" - ? new TCompactProtocol.Factory() - : new TBinaryProtocol.Factory(); - var client = new TestJaegerClient(); - - // Arrange - using var jaegerExporter = new JaegerExporter( - new JaegerExporterOptions { MaxPayloadSizeInBytes = maxPayloadSizeInBytes }, - protocolFactory, - client); - jaegerExporter.SetResourceAndInitializeBatch(Resource.Empty); - - // Create six spans, each taking more space than the previous one - var spans = new JaegerSpan[6]; - for (int i = 0; i < 6; i++) - { - spans[i] = CreateTestJaegerSpan( - additionalAttributes: new Dictionary - { - ["foo"] = new string('_', 10 * i), - }); - } - - var protocol = protocolFactory.GetProtocol(); - var serializedSpans = spans.Select(s => - { - s.Write(protocol); - var data = protocol.WrittenData.ToArray(); - protocol.Clear(); - return data; - }).ToArray(); - - // Act - var sentBatches = new List(); - foreach (var span in spans) - { - jaegerExporter.AppendSpan(span); - var sentBatch = client.LastWrittenData; - if (sentBatch != null) - { - sentBatches.Add(sentBatch); - client.LastWrittenData = null; - } - } - - // Assert - - // Appending the six spans will send two batches with the first four spans - Assert.Equal(2, sentBatches.Count); - Assert.True( - ContainsSequence(sentBatches[0], serializedSpans[0]), - "Expected span data not found in sent batch"); - Assert.True( - ContainsSequence(sentBatches[0], serializedSpans[1]), - "Expected span data not found in sent batch"); - - Assert.True( - ContainsSequence(sentBatches[1], serializedSpans[2]), - "Expected span data not found in sent batch"); - Assert.True( - ContainsSequence(sentBatches[1], serializedSpans[3]), - "Expected span data not found in sent batch"); - - // jaegerExporter.Batch should contain the two remaining spans - Assert.Equal(2U, jaegerExporter.NumberOfSpansInCurrentBatch); - jaegerExporter.SendCurrentBatch(); - Assert.True(client.LastWrittenData != null); - var serializedBatch = client.LastWrittenData; - Assert.True( - ContainsSequence(serializedBatch, serializedSpans[4]), - "Expected span data not found in unsent batch"); - Assert.True( - ContainsSequence(serializedBatch, serializedSpans[5]), - "Expected span data not found in unsent batch"); - } - - internal static JaegerSpan CreateTestJaegerSpan( - bool setAttributes = true, - Dictionary additionalAttributes = null, - bool addEvents = true, - bool addLinks = true, - Resource resource = null, - ActivityKind kind = ActivityKind.Client) - { - return JaegerActivityConversionTest - .CreateTestActivity( - setAttributes, additionalAttributes, addEvents, addLinks, resource, kind) - .ToJaegerSpan(); - } - - private static bool ContainsSequence(byte[] source, byte[] pattern) - { - for (var start = 0; start < (source.Length - pattern.Length + 1); start++) - { - if (source.Skip(start).Take(pattern.Length).SequenceEqual(pattern)) - { - return true; - } - } - - return false; - } - - private sealed class TestJaegerClient : IJaegerClient - { - public bool Connected => true; - - public byte[] LastWrittenData { get; set; } - - public void Close() - { - } - - public void Connect() - { - } - - public void Dispose() - { - } - - public int Send(byte[] buffer, int offset, int count) - { - this.LastWrittenData = new ArraySegment(buffer, offset, count).ToArray(); - return count; - } - } - } -} diff --git a/test/OpenTelemetry.Exporter.Jaeger.Tests/OpenTelemetry.Exporter.Jaeger.Tests.csproj b/test/OpenTelemetry.Exporter.Jaeger.Tests/OpenTelemetry.Exporter.Jaeger.Tests.csproj deleted file mode 100644 index deca1575f02..00000000000 --- a/test/OpenTelemetry.Exporter.Jaeger.Tests/OpenTelemetry.Exporter.Jaeger.Tests.csproj +++ /dev/null @@ -1,34 +0,0 @@ - - - Unit test project for Jaeger Exporter for OpenTelemetry - - net7.0;net6.0 - $(TargetFrameworks);net462 - - - disable - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - - - - - - - - - diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/BaseOtlpHttpExportClientTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/BaseOtlpHttpExportClientTests.cs index 1feeb03a4ad..68011107b7f 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/BaseOtlpHttpExportClientTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/BaseOtlpHttpExportClientTests.cs @@ -1,66 +1,54 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +#if NETFRAMEWORK using System.Net.Http; +#endif using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; using Xunit; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +public class BaseOtlpHttpExportClientTests { - public class BaseOtlpHttpExportClientTests + [Theory] + [InlineData(null, null, "http://localhost:4318/signal/path")] + [InlineData(null, "http://from.otel.exporter.env.var", "http://from.otel.exporter.env.var/signal/path")] + [InlineData("https://custom.host", null, "https://custom.host")] + [InlineData("http://custom.host:44318/custom/path", null, "http://custom.host:44318/custom/path")] + [InlineData("https://custom.host", "http://from.otel.exporter.env.var", "https://custom.host")] + public void ValidateOtlpHttpExportClientEndpoint(string optionEndpoint, string endpointEnvVar, string expectedExporterEndpoint) { - [Theory] - [InlineData(null, null, "http://localhost:4318/signal/path")] - [InlineData(null, "http://from.otel.exporter.env.var", "http://from.otel.exporter.env.var/signal/path")] - [InlineData("https://custom.host", null, "https://custom.host")] - [InlineData("http://custom.host:44318/custom/path", null, "http://custom.host:44318/custom/path")] - [InlineData("https://custom.host", "http://from.otel.exporter.env.var", "https://custom.host")] - public void ValidateOtlpHttpExportClientEndpoint(string optionEndpoint, string endpointEnvVar, string expectedExporterEndpoint) + try { - try - { - Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, endpointEnvVar); + Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, endpointEnvVar); - OtlpExporterOptions options = new() { Protocol = OtlpExportProtocol.HttpProtobuf }; + OtlpExporterOptions options = new() { Protocol = OtlpExportProtocol.HttpProtobuf }; - if (optionEndpoint != null) - { - options.Endpoint = new Uri(optionEndpoint); - } - - var exporterClient = new TestOtlpHttpExportClient(options, new HttpClient()); - Assert.Equal(new Uri(expectedExporterEndpoint), exporterClient.Endpoint); - } - finally + if (optionEndpoint != null) { - Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, null); + options.Endpoint = new Uri(optionEndpoint); } + + var exporterClient = new TestOtlpHttpExportClient(options, new HttpClient()); + Assert.Equal(new Uri(expectedExporterEndpoint), exporterClient.Endpoint); + } + finally + { + Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, null); } + } - internal class TestOtlpHttpExportClient : BaseOtlpHttpExportClient + internal class TestOtlpHttpExportClient : BaseOtlpHttpExportClient + { + public TestOtlpHttpExportClient(OtlpExporterOptions options, HttpClient httpClient) + : base(options, httpClient, "signal/path") { - public TestOtlpHttpExportClient(OtlpExporterOptions options, HttpClient httpClient) - : base(options, httpClient, "signal/path") - { - } + } - protected override HttpContent CreateHttpContent(string exportRequest) - { - throw new NotImplementedException(); - } + protected override HttpContent CreateHttpContent(string exportRequest) + { + throw new NotImplementedException(); } } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/EventSourceTest.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/EventSourceTest.cs index a90b5d1758c..2f4903d5596 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/EventSourceTest.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/EventSourceTest.cs @@ -1,31 +1,17 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +public class EventSourceTest { - public class EventSourceTest + [Fact] + public void EventSourceTest_OpenTelemetryProtocolExporterEventSource() { - [Fact] - public void EventSourceTest_OpenTelemetryProtocolExporterEventSource() - { - EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(OpenTelemetryProtocolExporterEventSource.Log); - } + EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(OpenTelemetryProtocolExporterEventSource.Log); } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/ExporterClientValidationTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/ExporterClientValidationTests.cs index a06e3c59321..e7c1a0b2d7a 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/ExporterClientValidationTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/ExporterClientValidationTests.cs @@ -1,76 +1,62 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; using Xunit; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +public class ExporterClientValidationTests : Http2UnencryptedSupportTests { - public class ExporterClientValidationTests : Http2UnencryptedSupportTests - { - private const string HttpEndpoint = "http://localhost:4173"; - private const string HttpsEndpoint = "https://localhost:4173"; + private const string HttpEndpoint = "http://localhost:4173"; + private const string HttpsEndpoint = "https://localhost:4173"; - [Fact] - public void ExporterClientValidation_FlagIsEnabledForHttpEndpoint() + [Fact] + public void ExporterClientValidation_FlagIsEnabledForHttpEndpoint() + { + var options = new OtlpExporterOptions { - var options = new OtlpExporterOptions - { - Endpoint = new Uri(HttpEndpoint), - }; + Endpoint = new Uri(HttpEndpoint), + }; - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - var exception = Record.Exception(() => ExporterClientValidation.EnsureUnencryptedSupportIsEnabled(options)); - Assert.Null(exception); - } + var exception = Record.Exception(() => ExporterClientValidation.EnsureUnencryptedSupportIsEnabled(options)); + Assert.Null(exception); + } - [Fact] - public void ExporterClientValidation_FlagIsNotEnabledForHttpEndpoint() + [Fact] + public void ExporterClientValidation_FlagIsNotEnabledForHttpEndpoint() + { + var options = new OtlpExporterOptions { - var options = new OtlpExporterOptions - { - Endpoint = new Uri(HttpEndpoint), - }; + Endpoint = new Uri(HttpEndpoint), + }; - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", false); + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", false); - var exception = Record.Exception(() => ExporterClientValidation.EnsureUnencryptedSupportIsEnabled(options)); + var exception = Record.Exception(() => ExporterClientValidation.EnsureUnencryptedSupportIsEnabled(options)); - if (Environment.Version.Major == 3) - { - Assert.NotNull(exception); - Assert.IsType(exception); - } - else - { - Assert.Null(exception); - } + if (Environment.Version.Major == 3) + { + Assert.NotNull(exception); + Assert.IsType(exception); } - - [Fact] - public void ExporterClientValidation_FlagIsNotEnabledForHttpsEndpoint() + else { - var options = new OtlpExporterOptions - { - Endpoint = new Uri(HttpsEndpoint), - }; - - var exception = Record.Exception(() => ExporterClientValidation.EnsureUnencryptedSupportIsEnabled(options)); Assert.Null(exception); } } + + [Fact] + public void ExporterClientValidation_FlagIsNotEnabledForHttpsEndpoint() + { + var options = new OtlpExporterOptions + { + Endpoint = new Uri(HttpsEndpoint), + }; + + var exception = Record.Exception(() => ExporterClientValidation.EnsureUnencryptedSupportIsEnabled(options)); + Assert.Null(exception); + } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Http2UnencryptedSupportTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Http2UnencryptedSupportTests.cs index 990f4ab0fe0..673260f281d 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Http2UnencryptedSupportTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Http2UnencryptedSupportTests.cs @@ -1,44 +1,30 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +public class Http2UnencryptedSupportTests : IDisposable { - public class Http2UnencryptedSupportTests : IDisposable + private readonly bool initialFlagStatus; + + public Http2UnencryptedSupportTests() { - private readonly bool initialFlagStatus; + this.initialFlagStatus = DetermineInitialFlagStatus(); + } - public Http2UnencryptedSupportTests() - { - this.initialFlagStatus = DetermineInitialFlagStatus(); - } + public void Dispose() + { + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", this.initialFlagStatus); + GC.SuppressFinalize(this); + } - public void Dispose() + private static bool DetermineInitialFlagStatus() + { + if (AppContext.TryGetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", out var flag)) { - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", this.initialFlagStatus); - GC.SuppressFinalize(this); + return flag; } - private static bool DetermineInitialFlagStatus() - { - if (AppContext.TryGetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", out var flag)) - { - return flag; - } - - return false; - } + return false; } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs index 76998e52340..ad216916c1f 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs @@ -1,26 +1,10 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -using System.Net.Http; #if !NET6_0_OR_GREATER -using System.Threading.Tasks; +using System.Net.Http; #endif -using Moq; -using Moq.Protected; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; using OpenTelemetry.Resources; @@ -28,189 +12,159 @@ using Xunit; using OtlpCollector = OpenTelemetry.Proto.Collector.Trace.V1; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +public class OtlpHttpTraceExportClientTests { - public class OtlpHttpTraceExportClientTests + private static readonly SdkLimitOptions DefaultSdkLimitOptions = new(); + + static OtlpHttpTraceExportClientTests() { - private static readonly SdkLimitOptions DefaultSdkLimitOptions = new(); + Activity.DefaultIdFormat = ActivityIdFormat.W3C; + Activity.ForceDefaultIdFormat = true; - static OtlpHttpTraceExportClientTests() + var listener = new ActivityListener { - Activity.DefaultIdFormat = ActivityIdFormat.W3C; - Activity.ForceDefaultIdFormat = true; + ShouldListenTo = _ => true, + Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, + }; - var listener = new ActivityListener - { - ShouldListenTo = _ => true, - Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, - }; + ActivitySource.AddActivityListener(listener); + } - ActivitySource.AddActivityListener(listener); - } + [Fact] + public void NewOtlpHttpTraceExportClient_OtlpExporterOptions_ExporterHasCorrectProperties() + { + var header1 = new { Name = "hdr1", Value = "val1" }; + var header2 = new { Name = "hdr2", Value = "val2" }; - [Fact] - public void NewOtlpHttpTraceExportClient_OtlpExporterOptions_ExporterHasCorrectProperties() + var options = new OtlpExporterOptions { - var header1 = new { Name = "hdr1", Value = "val1" }; - var header2 = new { Name = "hdr2", Value = "val2" }; - - var options = new OtlpExporterOptions - { - Headers = $"{header1.Name}={header1.Value}, {header2.Name} = {header2.Value}", - }; + Headers = $"{header1.Name}={header1.Value}, {header2.Name} = {header2.Value}", + }; - var client = new OtlpHttpTraceExportClient(options, options.HttpClientFactory()); + var client = new OtlpHttpTraceExportClient(options, options.HttpClientFactory()); - Assert.NotNull(client.HttpClient); + Assert.NotNull(client.HttpClient); - Assert.Equal(2 + OtlpExporterOptions.StandardHeaders.Length, client.Headers.Count); - Assert.Contains(client.Headers, kvp => kvp.Key == header1.Name && kvp.Value == header1.Value); - Assert.Contains(client.Headers, kvp => kvp.Key == header2.Name && kvp.Value == header2.Value); + Assert.Equal(2 + OtlpExporterOptions.StandardHeaders.Length, client.Headers.Count); + Assert.Contains(client.Headers, kvp => kvp.Key == header1.Name && kvp.Value == header1.Value); + Assert.Contains(client.Headers, kvp => kvp.Key == header2.Name && kvp.Value == header2.Value); - for (int i = 0; i < OtlpExporterOptions.StandardHeaders.Length; i++) - { - Assert.Contains(client.Headers, entry => entry.Key == OtlpExporterOptions.StandardHeaders[i].Key && entry.Value == OtlpExporterOptions.StandardHeaders[i].Value); - } + for (int i = 0; i < OtlpExporterOptions.StandardHeaders.Length; i++) + { + Assert.Contains(client.Headers, entry => entry.Key == OtlpExporterOptions.StandardHeaders[i].Key && entry.Value == OtlpExporterOptions.StandardHeaders[i].Value); } + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void SendExportRequest_ExportTraceServiceRequest_SendsCorrectHttpRequest(bool includeServiceNameInResource) + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SendExportRequest_ExportTraceServiceRequest_SendsCorrectHttpRequest(bool includeServiceNameInResource) + { + // Arrange + var evenTags = new[] { new KeyValuePair("k0", "v0") }; + var oddTags = new[] { new KeyValuePair("k1", "v1") }; + var sources = new[] { - // Arrange - var evenTags = new[] { new KeyValuePair("k0", "v0") }; - var oddTags = new[] { new KeyValuePair("k1", "v1") }; - var sources = new[] - { - new ActivitySource("even", "2.4.6"), - new ActivitySource("odd", "1.3.5"), - }; - var header1 = new { Name = "hdr1", Value = "val1" }; - var header2 = new { Name = "hdr2", Value = "val2" }; + new ActivitySource("even", "2.4.6"), + new ActivitySource("odd", "1.3.5"), + }; + var header1 = new { Name = "hdr1", Value = "val1" }; + var header2 = new { Name = "hdr2", Value = "val2" }; - var options = new OtlpExporterOptions - { - Endpoint = new Uri("http://localhost:4317"), - Headers = $"{header1.Name}={header1.Value}, {header2.Name} = {header2.Value}", - }; + var options = new OtlpExporterOptions + { + Endpoint = new Uri("http://localhost:4317"), + Headers = $"{header1.Name}={header1.Value}, {header2.Name} = {header2.Value}", + }; - var httpHandlerMock = new Mock(); + var testHttpHandler = new TestHttpMessageHandler(); - HttpRequestMessage httpRequest = null; - var httpRequestContent = Array.Empty(); + var httpRequestContent = Array.Empty(); - httpHandlerMock.Protected() -#if NET6_0_OR_GREATER - .Setup("Send", ItExpr.IsAny(), ItExpr.IsAny()) - .Returns((HttpRequestMessage request, CancellationToken token) => - { - return new HttpResponseMessage(); - }) - .Callback((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; - }) -#else - .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .ReturnsAsync((HttpRequestMessage request, CancellationToken token) => - { - return new HttpResponseMessage(); - }) - .Callback(async (r, ct) => - { - httpRequest = r; + var exportClient = new OtlpHttpTraceExportClient(options, new HttpClient(testHttpHandler)); - // 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); - }) -#endif - .Verifiable(); + var resourceBuilder = ResourceBuilder.CreateEmpty(); + if (includeServiceNameInResource) + { + resourceBuilder.AddAttributes( + new List> + { + new(ResourceSemanticConventions.AttributeServiceName, "service_name"), + new(ResourceSemanticConventions.AttributeServiceNamespace, "ns_1"), + }); + } - var exportClient = new OtlpHttpTraceExportClient(options, new HttpClient(httpHandlerMock.Object)); + var builder = Sdk.CreateTracerProviderBuilder() + .SetResourceBuilder(resourceBuilder) + .AddSource(sources[0].Name) + .AddSource(sources[1].Name); - var resourceBuilder = ResourceBuilder.CreateEmpty(); - if (includeServiceNameInResource) - { - resourceBuilder.AddAttributes( - new List> - { - new KeyValuePair(ResourceSemanticConventions.AttributeServiceName, "service_name"), - new KeyValuePair(ResourceSemanticConventions.AttributeServiceNamespace, "ns_1"), - }); - } + using var openTelemetrySdk = builder.Build(); - var builder = Sdk.CreateTracerProviderBuilder() - .SetResourceBuilder(resourceBuilder) - .AddSource(sources[0].Name) - .AddSource(sources[1].Name); + var exportedItems = new List(); + var processor = new BatchActivityExportProcessor(new InMemoryExporter(exportedItems)); + const int numOfSpans = 10; + bool isEven; + for (var i = 0; i < numOfSpans; i++) + { + isEven = i % 2 == 0; + var source = sources[i % 2]; + var activityKind = isEven ? ActivityKind.Client : ActivityKind.Server; + var activityTags = isEven ? evenTags : oddTags; - using var openTelemetrySdk = builder.Build(); + using Activity activity = source.StartActivity($"span-{i}", activityKind, parentContext: default, activityTags); + processor.OnEnd(activity); + } - var exportedItems = new List(); - var processor = new BatchActivityExportProcessor(new InMemoryExporter(exportedItems)); - const int numOfSpans = 10; - bool isEven; - for (var i = 0; i < numOfSpans; i++) - { - isEven = i % 2 == 0; - var source = sources[i % 2]; - var activityKind = isEven ? ActivityKind.Client : ActivityKind.Server; - var activityTags = isEven ? evenTags : oddTags; + processor.Shutdown(); - using Activity activity = source.StartActivity($"span-{i}", activityKind, parentContext: default, activityTags); - processor.OnEnd(activity); - } + var batch = new Batch([.. exportedItems], exportedItems.Count); + RunTest(batch); - processor.Shutdown(); + void RunTest(Batch batch) + { + var request = new OtlpCollector.ExportTraceServiceRequest(); - var batch = new Batch(exportedItems.ToArray(), exportedItems.Count); - RunTest(batch); + request.AddBatch(DefaultSdkLimitOptions, resourceBuilder.Build().ToOtlpResource(), batch); - void RunTest(Batch batch) - { - var request = new OtlpCollector.ExportTraceServiceRequest(); + // Act + var result = exportClient.SendExportRequest(request); - request.AddBatch(DefaultSdkLimitOptions, resourceBuilder.Build().ToOtlpResource(), batch); + var httpRequest = testHttpHandler.HttpRequestMessage; - // Act - var result = exportClient.SendExportRequest(request); + // Assert + Assert.True(result.Success); + Assert.NotNull(httpRequest); + Assert.Equal(HttpMethod.Post, httpRequest.Method); + Assert.Equal("http://localhost:4317/", httpRequest.RequestUri.AbsoluteUri); + Assert.Equal(OtlpExporterOptions.StandardHeaders.Length + 2, httpRequest.Headers.Count()); + Assert.Contains(httpRequest.Headers, h => h.Key == header1.Name && h.Value.First() == header1.Value); + Assert.Contains(httpRequest.Headers, h => h.Key == header2.Name && h.Value.First() == header2.Value); - // Assert - Assert.True(result); - Assert.NotNull(httpRequest); - Assert.Equal(HttpMethod.Post, httpRequest.Method); - Assert.Equal("http://localhost:4317/", httpRequest.RequestUri.AbsoluteUri); - Assert.Equal(OtlpExporterOptions.StandardHeaders.Length + 2, httpRequest.Headers.Count()); - Assert.Contains(httpRequest.Headers, h => h.Key == header1.Name && h.Value.First() == header1.Value); - Assert.Contains(httpRequest.Headers, h => h.Key == header2.Name && h.Value.First() == header2.Value); - - for (int i = 0; i < OtlpExporterOptions.StandardHeaders.Length; i++) - { - Assert.Contains(httpRequest.Headers, entry => entry.Key == OtlpExporterOptions.StandardHeaders[i].Key && entry.Value.First() == OtlpExporterOptions.StandardHeaders[i].Value); - } + for (int i = 0; i < OtlpExporterOptions.StandardHeaders.Length; i++) + { + Assert.Contains(httpRequest.Headers, entry => entry.Key == OtlpExporterOptions.StandardHeaders[i].Key && entry.Value.First() == OtlpExporterOptions.StandardHeaders[i].Value); + } - Assert.NotNull(httpRequest.Content); - Assert.IsType(httpRequest.Content); - Assert.Contains(httpRequest.Content.Headers, h => h.Key == "Content-Type" && h.Value.First() == OtlpHttpTraceExportClient.MediaContentType); + Assert.NotNull(testHttpHandler.HttpRequestContent); + Assert.IsType(httpRequest.Content); + Assert.Contains(httpRequest.Content.Headers, h => h.Key == "Content-Type" && h.Value.First() == OtlpHttpTraceExportClient.MediaContentType); - var exportTraceRequest = OtlpCollector.ExportTraceServiceRequest.Parser.ParseFrom(httpRequestContent); - Assert.NotNull(exportTraceRequest); - Assert.Single(exportTraceRequest.ResourceSpans); + var exportTraceRequest = OtlpCollector.ExportTraceServiceRequest.Parser.ParseFrom(testHttpHandler.HttpRequestContent); + Assert.NotNull(exportTraceRequest); + Assert.Single(exportTraceRequest.ResourceSpans); - var resourceSpan = exportTraceRequest.ResourceSpans.First(); - if (includeServiceNameInResource) - { - Assert.Contains(resourceSpan.Resource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.StringValue == "service_name"); - Assert.Contains(resourceSpan.Resource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceNamespace && kvp.Value.StringValue == "ns_1"); - } - else - { - Assert.Contains(resourceSpan.Resource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.ToString().Contains("unknown_service:")); - } + var resourceSpan = exportTraceRequest.ResourceSpans.First(); + if (includeServiceNameInResource) + { + Assert.Contains(resourceSpan.Resource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.StringValue == "service_name"); + Assert.Contains(resourceSpan.Resource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceNamespace && kvp.Value.StringValue == "ns_1"); + } + else + { + Assert.Contains(resourceSpan.Resource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.ToString().Contains("unknown_service:")); } } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/.gitignore b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/.gitignore new file mode 100644 index 00000000000..feada150421 --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/.gitignore @@ -0,0 +1,3 @@ +# Self-signed cert generated by integration test +otel-collector.crt +otel-collector.key diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Dockerfile b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/Dockerfile similarity index 66% rename from test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Dockerfile rename to test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/Dockerfile index 372a501f7da..691524a9d28 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Dockerfile +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/Dockerfile @@ -1,19 +1,20 @@ # Create a container for running the OpenTelemetry Collector integration tests. # This should be run from the root of the repo: -# docker build --file test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Dockerfile +# docker build --file test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/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 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.Exporter.OpenTelemetryProtocol.Tests" -RUN dotnet publish "OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.csproj" -c "${PUBLISH_CONFIGURATION}" -f "${PUBLISH_FRAMEWORK}" -o /drop -p:IntegrationBuild=true -p:TARGET_FRAMEWORK=${PUBLISH_FRAMEWORK} +RUN dotnet publish "OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.csproj" -c "${PUBLISH_CONFIGURATION}" -f "${PUBLISH_FRAMEWORK}" -o /drop -p:IntegrationBuild=true FROM mcr.microsoft.com/dotnet/sdk:${TEST_SDK_VERSION} AS final WORKDIR /test COPY --from=build /drop . -ENTRYPOINT ["dotnet", "vstest", "OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.dll", "--logger:console;verbosity=detailed"] + +RUN apt-get update && apt-get install ca-certificates diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs new file mode 100644 index 00000000000..11b5b797127 --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs @@ -0,0 +1,339 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Diagnostics.Tracing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Tests; +using OpenTelemetry.Trace; +using Xunit; +using Xunit.Abstractions; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +public sealed class IntegrationTests : IDisposable +{ + private const string CollectorHostnameEnvVarName = "OTEL_COLLECTOR_HOSTNAME"; + private const int ExportIntervalMilliseconds = 10000; + private static readonly SdkLimitOptions DefaultSdkLimitOptions = new(); + private static readonly string CollectorHostname = SkipUnlessEnvVarFoundTheoryAttribute.GetEnvironmentVariable(CollectorHostnameEnvVarName); + private readonly OpenTelemetryEventListener openTelemetryEventListener; + + public IntegrationTests(ITestOutputHelper outputHelper) + { + this.openTelemetryEventListener = new(outputHelper); + } + + public void Dispose() + { + this.openTelemetryEventListener.Dispose(); + } + + [InlineData(OtlpExportProtocol.Grpc, ":4317", ExportProcessorType.Batch, false)] + [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/traces", ExportProcessorType.Batch, false)] + [InlineData(OtlpExportProtocol.Grpc, ":4317", ExportProcessorType.Batch, true)] + [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/traces", ExportProcessorType.Batch, true)] + [InlineData(OtlpExportProtocol.Grpc, ":4317", ExportProcessorType.Simple, false)] + [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/traces", ExportProcessorType.Simple, false)] + [InlineData(OtlpExportProtocol.Grpc, ":4317", ExportProcessorType.Simple, true)] + [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/traces", ExportProcessorType.Simple, true)] + [InlineData(OtlpExportProtocol.Grpc, ":5317", ExportProcessorType.Simple, true, "https")] + [InlineData(OtlpExportProtocol.HttpProtobuf, ":5318/v1/traces", ExportProcessorType.Simple, true, "https")] + [Trait("CategoryName", "CollectorIntegrationTests")] + [SkipUnlessEnvVarFoundTheory(CollectorHostnameEnvVarName)] + public void TraceExportResultIsSuccess(OtlpExportProtocol protocol, string endpoint, ExportProcessorType exportProcessorType, bool forceFlush, string scheme = "http") + { + using EventWaitHandle handle = new ManualResetEvent(false); + + var exporterOptions = new OtlpExporterOptions + { + Endpoint = new Uri($"{scheme}://{CollectorHostname}{endpoint}"), + Protocol = protocol, + ExportProcessorType = exportProcessorType, + BatchExportProcessorOptions = new() + { + ScheduledDelayMilliseconds = ExportIntervalMilliseconds, + }, + }; + + DelegatingExporter delegatingExporter = null; + var exportResults = new List(); + + var activitySourceName = "otlp.collector.test"; + + var builder = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName); + + builder.AddProcessor(OtlpTraceExporterHelperExtensions.BuildOtlpExporterProcessor( + exporterOptions, + DefaultSdkLimitOptions, + serviceProvider: null, + configureExporterInstance: otlpExporter => + { + delegatingExporter = new DelegatingExporter + { + OnExportFunc = (batch) => + { + var result = otlpExporter.Export(batch); + exportResults.Add(result); + handle.Set(); + return result; + }, + }; + return delegatingExporter; + })); + + using (var tracerProvider = builder.Build()) + { + using var source = new ActivitySource(activitySourceName); + var activity = source.StartActivity($"{protocol} Test Activity"); + activity?.Stop(); + + Assert.NotNull(delegatingExporter); + + if (forceFlush) + { + Assert.True(tracerProvider.ForceFlush()); + Assert.Single(exportResults); + Assert.Equal(ExportResult.Success, exportResults[0]); + } + else if (exporterOptions.ExportProcessorType == ExportProcessorType.Batch) + { + Assert.True(handle.WaitOne(ExportIntervalMilliseconds * 2)); + Assert.Single(exportResults); + Assert.Equal(ExportResult.Success, exportResults[0]); + } + } + + if (!forceFlush && exportProcessorType == ExportProcessorType.Simple) + { + Assert.Single(exportResults); + Assert.Equal(ExportResult.Success, exportResults[0]); + } + } + + [InlineData(OtlpExportProtocol.Grpc, ":4317", false, false)] + [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/metrics", false, false)] + [InlineData(OtlpExportProtocol.Grpc, ":4317", false, true)] + [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/metrics", false, true)] + [InlineData(OtlpExportProtocol.Grpc, ":4317", true, false)] + [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/metrics", true, false)] + [InlineData(OtlpExportProtocol.Grpc, ":4317", true, true)] + [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/metrics", true, true)] + [InlineData(OtlpExportProtocol.Grpc, ":5317", true, true, "https")] + [InlineData(OtlpExportProtocol.HttpProtobuf, ":5318/v1/metrics", true, true, "https")] + [Trait("CategoryName", "CollectorIntegrationTests")] + [SkipUnlessEnvVarFoundTheory(CollectorHostnameEnvVarName)] + public void MetricExportResultIsSuccess(OtlpExportProtocol protocol, string endpoint, bool useManualExport, bool forceFlush, string scheme = "http") + { + using EventWaitHandle handle = new ManualResetEvent(false); + + var exporterOptions = new OtlpExporterOptions + { + Endpoint = new Uri($"{scheme}://{CollectorHostname}{endpoint}"), + Protocol = protocol, + }; + + DelegatingExporter delegatingExporter = null; + var exportResults = new List(); + + var meterName = "otlp.collector.test"; + + var builder = Sdk.CreateMeterProviderBuilder() + .AddMeter(meterName); + + var readerOptions = new MetricReaderOptions(); + readerOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = useManualExport ? Timeout.Infinite : ExportIntervalMilliseconds; + + builder.AddReader(OtlpMetricExporterExtensions.BuildOtlpExporterMetricReader( + exporterOptions, + readerOptions, + serviceProvider: null, + configureExporterInstance: otlpExporter => + { + delegatingExporter = new DelegatingExporter + { + OnExportFunc = (batch) => + { + var result = otlpExporter.Export(batch); + exportResults.Add(result); + handle.Set(); + return result; + }, + }; + return delegatingExporter; + })); + + using (var meterProvider = builder.Build()) + { + using var meter = new Meter(meterName); + + var counter = meter.CreateCounter("test_counter"); + + counter.Add(18); + + Assert.NotNull(delegatingExporter); + + if (forceFlush) + { + Assert.True(meterProvider.ForceFlush()); + Assert.Single(exportResults); + Assert.Equal(ExportResult.Success, exportResults[0]); + } + else if (!useManualExport) + { + Assert.True(handle.WaitOne(ExportIntervalMilliseconds * 2)); + Assert.Single(exportResults); + Assert.Equal(ExportResult.Success, exportResults[0]); + } + } + + if (!forceFlush && useManualExport) + { + Assert.Single(exportResults); + Assert.Equal(ExportResult.Success, exportResults[0]); + } + } + + [InlineData(OtlpExportProtocol.Grpc, ":4317", ExportProcessorType.Batch)] + [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/logs", ExportProcessorType.Batch)] + [InlineData(OtlpExportProtocol.Grpc, ":4317", ExportProcessorType.Simple)] + [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/logs", ExportProcessorType.Simple)] + [InlineData(OtlpExportProtocol.Grpc, ":5317", ExportProcessorType.Simple, "https")] + [InlineData(OtlpExportProtocol.HttpProtobuf, ":5318/v1/logs", ExportProcessorType.Simple, "https")] + [Trait("CategoryName", "CollectorIntegrationTests")] + [SkipUnlessEnvVarFoundTheory(CollectorHostnameEnvVarName)] + public void LogExportResultIsSuccess(OtlpExportProtocol protocol, string endpoint, ExportProcessorType exportProcessorType, string scheme = "http") + { + using EventWaitHandle handle = new ManualResetEvent(false); + + var exporterOptions = new OtlpExporterOptions + { + Endpoint = new Uri($"{scheme}://{CollectorHostname}{endpoint}"), + Protocol = protocol, + }; + + DelegatingExporter delegatingExporter = null; + var exportResults = new List(); + var processorOptions = new LogRecordExportProcessorOptions + { + ExportProcessorType = exportProcessorType, + BatchExportProcessorOptions = new() + { + ScheduledDelayMilliseconds = ExportIntervalMilliseconds, + }, + }; + + using var loggerFactory = LoggerFactory.Create(builder => + { + builder + .AddOpenTelemetry(options => options + .AddProcessor(sp => + OtlpLogExporterHelperExtensions.BuildOtlpLogExporter( + sp, + exporterOptions, + processorOptions, + new SdkLimitOptions(), + new ExperimentalOptions(), + configureExporterInstance: otlpExporter => + { + delegatingExporter = new DelegatingExporter + { + OnExportFunc = (batch) => + { + var result = otlpExporter.Export(batch); + exportResults.Add(result); + handle.Set(); + return result; + }, + }; + return delegatingExporter; + }))); + }); + + var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); + logger.LogInformation("Hello from {name} {price}.", "tomato", 2.99); + + switch (processorOptions.ExportProcessorType) + { + case ExportProcessorType.Batch: + Assert.True(handle.WaitOne(ExportIntervalMilliseconds * 2)); + Assert.Single(exportResults); + Assert.Equal(ExportResult.Success, exportResults[0]); + break; + case ExportProcessorType.Simple: + Assert.Single(exportResults); + Assert.Equal(ExportResult.Success, exportResults[0]); + break; + default: + throw new NotSupportedException("Unexpected processor type encountered."); + } + } + + [Trait("CategoryName", "CollectorIntegrationTests")] + [SkipUnlessEnvVarFoundFact(CollectorHostnameEnvVarName)] + public void ConstructingGrpcExporterFailsWhenHttp2UnencryptedSupportIsDisabledForNetcoreapp31() + { + // Adding the OtlpExporter creates a GrpcChannel. + // This switch must be set before creating a GrpcChannel/HttpClient when calling an insecure gRPC service. + // We want to fail fast so we are disabling it + // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", false); + + var exporterOptions = new OtlpExporterOptions + { + Endpoint = new Uri($"http://{CollectorHostname}:4317"), + }; + + var exception = Record.Exception(() => new OtlpTraceExporter(exporterOptions)); + + if (Environment.Version.Major == 3) + { + Assert.NotNull(exception); + } + else + { + Assert.Null(exception); + } + } + + private sealed class OpenTelemetryEventListener : EventListener + { + private readonly ITestOutputHelper outputHelper; + + public OpenTelemetryEventListener(ITestOutputHelper outputHelper) + { + this.outputHelper = outputHelper; + } + + protected override void OnEventSourceCreated(EventSource eventSource) + { + base.OnEventSourceCreated(eventSource); + + if (eventSource.Name.StartsWith("OpenTelemetry", StringComparison.OrdinalIgnoreCase)) + { + this.EnableEvents(eventSource, EventLevel.Verbose, EventKeywords.All); + } + } + + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + string message; + if (eventData.Message != null && (eventData.Payload?.Count ?? 0) > 0) + { + message = string.Format(eventData.Message, eventData.Payload.ToArray()); + } + else + { + message = eventData.Message; + } + + this.outputHelper.WriteLine(message); + } + } +} diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/README.md b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/README.md new file mode 100644 index 00000000000..1d73259601f --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/README.md @@ -0,0 +1,16 @@ +# OTLP exporter integration tests + +This directory contains a suite of integration tests that can be run using +docker compose. + +Run the following command from the root of the repository to run the +integration tests locally: + +```shell +docker compose \ + --file=test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/docker-compose.yml \ + --project-directory=. \ + up \ + --exit-code-from=tests \ + --build +``` diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/create-cert.sh b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/create-cert.sh new file mode 100755 index 00000000000..24c6fe319cd --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/create-cert.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Generate self-signed certificate for the collector +openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 \ + -subj "/CN=otel-collector" \ + -keyout /otel-collector.key -out /otel-collector.crt + +# Copy the certificate and private key file to shared volume that the collector +# container and test container can access +cp /otel-collector.crt /otel-collector.key /cfg + +chmod 644 /cfg/otel-collector.key + +# The integration test is run via docker-compose with the --exit-code-from +# option. The --exit-code-from option implies --abort-on-container-exit +# which means when any container exits then all containers are stopped. +# Since the container running this script would be otherwise short-lived +# we sleep here. If the test does not finish within this time then the test +# container will be stopped and have a non-zero exit code. +sleep 300 diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/docker-compose.yml b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/docker-compose.yml similarity index 51% rename from test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/docker-compose.yml rename to test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/docker-compose.yml index 2c2e7f4a6a0..cad1ff0695d 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/docker-compose.yml +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/docker-compose.yml @@ -1,24 +1,31 @@ # Starts an OpenTelemetry Collector and then runs the OTLP exporter integration tests. # This should be run from the root of the repo: -# docker-compose --file=test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/docker-compose.yml --project-directory=. up --exit-code-from=tests --build +# docker-compose --file=test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/docker-compose.yml --project-directory=. up --exit-code-from=tests --build version: '3.7' services: + create-cert: + image: mcr.microsoft.com/dotnet/sdk:7.0 + volumes: + - ./test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest:/cfg + command: /cfg/create-cert.sh + otel-collector: image: otel/opentelemetry-collector volumes: - - ./test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests:/cfg - command: --config=/cfg/config.yaml - ports: - - "4317:4317" - - "4318:4318" + - ./test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest:/cfg + command: --config=/cfg/otel-collector-config.yaml + depends_on: + - create-cert tests: build: context: . - dockerfile: ./test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Dockerfile - command: --TestCaseFilter:CategoryName=CollectorIntegrationTests + dockerfile: ./test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/Dockerfile + volumes: + - ./test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest:/cfg + command: /cfg/run-test.sh environment: - OTEL_COLLECTOR_HOSTNAME=otel-collector - OTEL_MOCK_COLLECTOR_HOSTNAME=mock-otel-collector diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/config.yaml b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/otel-collector-config.yaml similarity index 51% rename from test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/config.yaml rename to test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/otel-collector-config.yaml index ad4cea542b8..477de40fe74 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/config.yaml +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/otel-collector-config.yaml @@ -11,16 +11,31 @@ receivers: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 + otlp/tls: + protocols: + grpc: + endpoint: 0.0.0.0:5317 + tls: + cert_file: /cfg/otel-collector.crt + key_file: /cfg/otel-collector.key + http: + endpoint: 0.0.0.0:5318 + tls: + cert_file: /cfg/otel-collector.crt + key_file: /cfg/otel-collector.key exporters: logging: - loglevel: debug + verbosity: detailed service: pipelines: traces: - receivers: [otlp] + receivers: [otlp, otlp/tls] exporters: [logging] metrics: - receivers: [otlp] + receivers: [otlp, otlp/tls] + exporters: [logging] + logs: + receivers: [otlp, otlp/tls] exporters: [logging] diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/run-test.sh b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/run-test.sh new file mode 100755 index 00000000000..5444a40a454 --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/run-test.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Trust the self-signed certificated used by the collector +cp /cfg/otel-collector.crt /usr/local/share/ca-certificates/ +update-ca-certificates --verbose + +dotnet test OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.dll --TestCaseFilter:CategoryName=CollectorIntegrationTests --logger "console;verbosity=detailed" diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTests.cs deleted file mode 100644 index d17d10b1b76..00000000000 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTests.cs +++ /dev/null @@ -1,271 +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 System.Diagnostics.Metrics; -using System.Diagnostics.Tracing; -using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; -using OpenTelemetry.Metrics; -using OpenTelemetry.Tests; -using OpenTelemetry.Trace; -using Xunit; -using Xunit.Abstractions; - -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests -{ - public sealed class IntegrationTests : IDisposable - { - private const string CollectorHostnameEnvVarName = "OTEL_COLLECTOR_HOSTNAME"; - private const int ExportIntervalMilliseconds = 10000; - private static readonly SdkLimitOptions DefaultSdkLimitOptions = new(); - private static readonly string CollectorHostname = SkipUnlessEnvVarFoundTheoryAttribute.GetEnvironmentVariable(CollectorHostnameEnvVarName); - private readonly OpenTelemetryEventListener openTelemetryEventListener; - - public IntegrationTests(ITestOutputHelper outputHelper) - { - this.openTelemetryEventListener = new(outputHelper); - } - - public void Dispose() - { - this.openTelemetryEventListener.Dispose(); - } - - [InlineData(OtlpExportProtocol.Grpc, ":4317", ExportProcessorType.Batch, false)] - [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/traces", ExportProcessorType.Batch, false)] - [InlineData(OtlpExportProtocol.Grpc, ":4317", ExportProcessorType.Batch, true)] - [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/traces", ExportProcessorType.Batch, true)] - [InlineData(OtlpExportProtocol.Grpc, ":4317", ExportProcessorType.Simple, false)] - [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/traces", ExportProcessorType.Simple, false)] - [InlineData(OtlpExportProtocol.Grpc, ":4317", ExportProcessorType.Simple, true)] - [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/traces", ExportProcessorType.Simple, true)] - [Trait("CategoryName", "CollectorIntegrationTests")] - [SkipUnlessEnvVarFoundTheory(CollectorHostnameEnvVarName)] - public void TraceExportResultIsSuccess(OtlpExportProtocol protocol, string endpoint, ExportProcessorType exportProcessorType, bool forceFlush) - { - using EventWaitHandle handle = new ManualResetEvent(false); - - var exporterOptions = new OtlpExporterOptions - { - Endpoint = new Uri($"http://{CollectorHostname}{endpoint}"), - Protocol = protocol, - ExportProcessorType = exportProcessorType, - BatchExportProcessorOptions = new() - { - ScheduledDelayMilliseconds = ExportIntervalMilliseconds, - }, - }; - - DelegatingExporter delegatingExporter = null; - var exportResults = new List(); - - var activitySourceName = "otlp.collector.test"; - - var builder = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName); - - builder.AddProcessor(OtlpTraceExporterHelperExtensions.BuildOtlpExporterProcessor( - exporterOptions, - DefaultSdkLimitOptions, - serviceProvider: null, - configureExporterInstance: otlpExporter => - { - delegatingExporter = new DelegatingExporter - { - OnExportFunc = (batch) => - { - var result = otlpExporter.Export(batch); - exportResults.Add(result); - handle.Set(); - return result; - }, - }; - return delegatingExporter; - })); - - using (var tracerProvider = builder.Build()) - { - using var source = new ActivitySource(activitySourceName); - var activity = source.StartActivity($"{protocol} Test Activity"); - activity?.Stop(); - - Assert.NotNull(delegatingExporter); - - if (forceFlush) - { - Assert.True(tracerProvider.ForceFlush()); - Assert.Single(exportResults); - Assert.Equal(ExportResult.Success, exportResults[0]); - } - else if (exporterOptions.ExportProcessorType == ExportProcessorType.Batch) - { - Assert.True(handle.WaitOne(ExportIntervalMilliseconds * 2)); - Assert.Single(exportResults); - Assert.Equal(ExportResult.Success, exportResults[0]); - } - } - - if (!forceFlush && exportProcessorType == ExportProcessorType.Simple) - { - Assert.Single(exportResults); - Assert.Equal(ExportResult.Success, exportResults[0]); - } - } - - [InlineData(OtlpExportProtocol.Grpc, ":4317", false, false)] - [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/metrics", false, false)] - [InlineData(OtlpExportProtocol.Grpc, ":4317", false, true)] - [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/metrics", false, true)] - [InlineData(OtlpExportProtocol.Grpc, ":4317", true, false)] - [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/metrics", true, false)] - [InlineData(OtlpExportProtocol.Grpc, ":4317", true, true)] - [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/metrics", true, true)] - [Trait("CategoryName", "CollectorIntegrationTests")] - [SkipUnlessEnvVarFoundTheory(CollectorHostnameEnvVarName)] - public void MetricExportResultIsSuccess(OtlpExportProtocol protocol, string endpoint, bool useManualExport, bool forceFlush) - { - using EventWaitHandle handle = new ManualResetEvent(false); - - var exporterOptions = new OtlpExporterOptions - { - Endpoint = new Uri($"http://{CollectorHostname}{endpoint}"), - Protocol = protocol, - }; - - DelegatingExporter delegatingExporter = null; - var exportResults = new List(); - - var meterName = "otlp.collector.test"; - - var builder = Sdk.CreateMeterProviderBuilder() - .AddMeter(meterName); - - var readerOptions = new MetricReaderOptions(); - readerOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = useManualExport ? Timeout.Infinite : ExportIntervalMilliseconds; - - builder.AddReader(OtlpMetricExporterExtensions.BuildOtlpExporterMetricReader( - exporterOptions, - readerOptions, - serviceProvider: null, - configureExporterInstance: otlpExporter => - { - delegatingExporter = new DelegatingExporter - { - OnExportFunc = (batch) => - { - var result = otlpExporter.Export(batch); - exportResults.Add(result); - handle.Set(); - return result; - }, - }; - return delegatingExporter; - })); - - using (var meterProvider = builder.Build()) - { - using var meter = new Meter(meterName); - - var counter = meter.CreateCounter("test_counter"); - - counter.Add(18); - - Assert.NotNull(delegatingExporter); - - if (forceFlush) - { - Assert.True(meterProvider.ForceFlush()); - Assert.Single(exportResults); - Assert.Equal(ExportResult.Success, exportResults[0]); - } - else if (!useManualExport) - { - Assert.True(handle.WaitOne(ExportIntervalMilliseconds * 2)); - Assert.Single(exportResults); - Assert.Equal(ExportResult.Success, exportResults[0]); - } - } - - if (!forceFlush && useManualExport) - { - Assert.Single(exportResults); - Assert.Equal(ExportResult.Success, exportResults[0]); - } - } - - [Trait("CategoryName", "CollectorIntegrationTests")] - [SkipUnlessEnvVarFoundFact(CollectorHostnameEnvVarName)] - public void ConstructingGrpcExporterFailsWhenHttp2UnencryptedSupportIsDisabledForNetcoreapp31() - { - // Adding the OtlpExporter creates a GrpcChannel. - // This switch must be set before creating a GrpcChannel/HttpClient when calling an insecure gRPC service. - // We want to fail fast so we are disabling it - // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", false); - - var exporterOptions = new OtlpExporterOptions - { - Endpoint = new Uri($"http://{CollectorHostname}:4317"), - }; - - var exception = Record.Exception(() => new OtlpTraceExporter(exporterOptions)); - - if (Environment.Version.Major == 3) - { - Assert.NotNull(exception); - } - else - { - Assert.Null(exception); - } - } - - private sealed class OpenTelemetryEventListener : EventListener - { - private readonly ITestOutputHelper outputHelper; - - public OpenTelemetryEventListener(ITestOutputHelper outputHelper) - { - this.outputHelper = outputHelper; - } - - protected override void OnEventSourceCreated(EventSource eventSource) - { - base.OnEventSourceCreated(eventSource); - - if (eventSource.Name.StartsWith("OpenTelemetry", StringComparison.OrdinalIgnoreCase)) - { - this.EnableEvents(eventSource, EventLevel.Verbose, EventKeywords.All); - } - } - - protected override void OnEventWritten(EventWrittenEventArgs eventData) - { - string message; - if (eventData.Message != null && (eventData.Payload?.Count ?? 0) > 0) - { - message = string.Format(eventData.Message, eventData.Payload.ToArray()); - } - else - { - message = eventData.Message; - } - - this.outputHelper.WriteLine(message); - } - } - } -} diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/MockCollectorIntegrationTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/MockCollectorIntegrationTests.cs index 0cad1f777cd..51536cfd477 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/MockCollectorIntegrationTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/MockCollectorIntegrationTests.cs @@ -1,25 +1,8 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #if !NETFRAMEWORK -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; using Grpc.Core; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -67,15 +50,15 @@ public async Task TestRecoveryAfterFailedExport() endpoints.MapGrpcService(); }); })) - .StartAsync().ConfigureAwait(false); + .StartAsync(); - var httpClient = new HttpClient() { BaseAddress = new System.Uri("http://localhost:5050") }; + 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 System.Uri("http://localhost:4317") }); + var otlpExporter = new OtlpTraceExporter(new OtlpExporterOptions() { Endpoint = new Uri("http://localhost:4317") }); var delegatingExporter = new DelegatingExporter { OnExportFunc = (batch) => @@ -105,7 +88,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.OpenTelemetryProtocol.Tests/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.csproj b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.csproj index 6949715aac0..ad60a2385a2 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.csproj +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.csproj @@ -1,35 +1,21 @@ - - net7.0;net6.0 - $(TargetFrameworks);net462 - $(TARGET_FRAMEWORK) - + $(TargetFrameworksForTests) disable + + + + - - - all + runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpAttributeTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpAttributeTests.cs index d5b3af9b6d8..7d5fce165e5 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpAttributeTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpAttributeTests.cs @@ -1,172 +1,208 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; using Xunit; using OtlpCommon = OpenTelemetry.Proto.Common.V1; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +public class OtlpAttributeTests { - public class OtlpAttributeTests + [Fact] + public void NullValueAttribute() + { + var kvp = new KeyValuePair("key", null); + Assert.False(OtlpKeyValueTransformer.Instance.TryTransformTag(kvp, out var _)); + } + + [Fact] + public void EmptyArrays() + { + var kvp = new KeyValuePair("key", Array.Empty()); + Assert.True(OtlpKeyValueTransformer.Instance.TryTransformTag(kvp, out var attribute)); + Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.ArrayValue, attribute.Value.ValueCase); + Assert.Empty(attribute.Value.ArrayValue.Values); + + kvp = new KeyValuePair("key", Array.Empty()); + Assert.True(OtlpKeyValueTransformer.Instance.TryTransformTag(kvp, out attribute)); + Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.ArrayValue, attribute.Value.ValueCase); + Assert.Empty(attribute.Value.ArrayValue.Values); + } + + [Theory] + [InlineData(sbyte.MaxValue)] + [InlineData(byte.MaxValue)] + [InlineData(short.MaxValue)] + [InlineData(ushort.MaxValue)] + [InlineData(int.MaxValue)] + [InlineData(uint.MaxValue)] + [InlineData(long.MaxValue)] + [InlineData(new sbyte[] { 1, 2, 3 })] + [InlineData(new byte[] { 1, 2, 3 })] + [InlineData(new short[] { 1, 2, 3 })] + [InlineData(new ushort[] { 1, 2, 3 })] + [InlineData(new int[] { 1, 2, 3 })] + [InlineData(new uint[] { 1, 2, 3 })] + [InlineData(new long[] { 1, 2, 3 })] + public void IntegralTypesSupported(object value) { - [Fact] - public void NullValueAttribute() + var kvp = new KeyValuePair("key", value); + Assert.True(OtlpKeyValueTransformer.Instance.TryTransformTag(kvp, out var attribute)); + + switch (value) { - var kvp = new KeyValuePair("key", null); - Assert.False(OtlpKeyValueTransformer.Instance.TryTransformTag(kvp, out var _)); + case Array array: + Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.ArrayValue, attribute.Value.ValueCase); + var expectedArray = new long[array.Length]; + for (var i = 0; i < array.Length; i++) + { + expectedArray[i] = Convert.ToInt64(array.GetValue(i)); + } + + Assert.Equal(expectedArray, attribute.Value.ArrayValue.Values.Select(x => x.IntValue)); + break; + default: + Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.IntValue, attribute.Value.ValueCase); + Assert.Equal(Convert.ToInt64(value), attribute.Value.IntValue); + break; } + } + + [Theory] + [InlineData(float.MaxValue)] + [InlineData(double.MaxValue)] + [InlineData(new float[] { 1, 2, 3 })] + [InlineData(new double[] { 1, 2, 3 })] + public void FloatingPointTypesSupported(object value) + { + var kvp = new KeyValuePair("key", value); + Assert.True(OtlpKeyValueTransformer.Instance.TryTransformTag(kvp, out var attribute)); - [Fact] - public void EmptyArrays() + switch (value) { - var kvp = new KeyValuePair("key", Array.Empty()); - Assert.True(OtlpKeyValueTransformer.Instance.TryTransformTag(kvp, out var attribute)); - Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.ArrayValue, attribute.Value.ValueCase); - Assert.Empty(attribute.Value.ArrayValue.Values); + case Array array: + Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.ArrayValue, attribute.Value.ValueCase); + var expectedArray = new double[array.Length]; + for (var i = 0; i < array.Length; i++) + { + expectedArray[i] = Convert.ToDouble(array.GetValue(i)); + } - kvp = new KeyValuePair("key", Array.Empty()); - Assert.True(OtlpKeyValueTransformer.Instance.TryTransformTag(kvp, out attribute)); - Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.ArrayValue, attribute.Value.ValueCase); - Assert.Empty(attribute.Value.ArrayValue.Values); + Assert.Equal(expectedArray, attribute.Value.ArrayValue.Values.Select(x => x.DoubleValue)); + break; + default: + Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.DoubleValue, attribute.Value.ValueCase); + Assert.Equal(Convert.ToDouble(value), attribute.Value.DoubleValue); + break; } + } - [Theory] - [InlineData(sbyte.MaxValue)] - [InlineData(byte.MaxValue)] - [InlineData(short.MaxValue)] - [InlineData(ushort.MaxValue)] - [InlineData(int.MaxValue)] - [InlineData(uint.MaxValue)] - [InlineData(long.MaxValue)] - [InlineData(new sbyte[] { 1, 2, 3 })] - [InlineData(new byte[] { 1, 2, 3 })] - [InlineData(new short[] { 1, 2, 3 })] - [InlineData(new ushort[] { 1, 2, 3 })] - [InlineData(new int[] { 1, 2, 3 })] - [InlineData(new uint[] { 1, 2, 3 })] - [InlineData(new long[] { 1, 2, 3 })] - public void IntegralTypesSupported(object value) + [Theory] + [InlineData(true)] + [InlineData(new bool[] { true, false, true })] + public void BooleanTypeSupported(object value) + { + var kvp = new KeyValuePair("key", value); + Assert.True(OtlpKeyValueTransformer.Instance.TryTransformTag(kvp, out var attribute)); + + switch (value) { - var kvp = new KeyValuePair("key", value); - Assert.True(OtlpKeyValueTransformer.Instance.TryTransformTag(kvp, out var attribute)); + case Array array: + Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.ArrayValue, attribute.Value.ValueCase); + var expectedArray = new bool[array.Length]; + for (var i = 0; i < array.Length; i++) + { + expectedArray[i] = Convert.ToBoolean(array.GetValue(i)); + } - switch (value) - { - case Array array: - Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.ArrayValue, attribute.Value.ValueCase); - var expectedArray = new long[array.Length]; - for (var i = 0; i < array.Length; i++) - { - expectedArray[i] = Convert.ToInt64(array.GetValue(i)); - } - - Assert.Equal(expectedArray, attribute.Value.ArrayValue.Values.Select(x => x.IntValue)); - break; - default: - Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.IntValue, attribute.Value.ValueCase); - Assert.Equal(Convert.ToInt64(value), attribute.Value.IntValue); - break; - } + Assert.Equal(expectedArray, attribute.Value.ArrayValue.Values.Select(x => x.BoolValue)); + break; + default: + Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.BoolValue, attribute.Value.ValueCase); + Assert.Equal(Convert.ToBoolean(value), attribute.Value.BoolValue); + break; } + } + + [Theory] + [InlineData(char.MaxValue)] + [InlineData("string")] + public void StringTypesSupported(object value) + { + var kvp = new KeyValuePair("key", value); + Assert.True(OtlpKeyValueTransformer.Instance.TryTransformTag(kvp, out var attribute)); + Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.StringValue, attribute.Value.ValueCase); + Assert.Equal(Convert.ToString(value), attribute.Value.StringValue); + } + + [Fact] + public void StringArrayTypesSupported() + { + var charArray = new char[] { 'a', 'b', 'c' }; + var stringArray = new string[] { "a", "b", "c", string.Empty, null }; + + var kvp = new KeyValuePair("key", charArray); + Assert.True(OtlpKeyValueTransformer.Instance.TryTransformTag(kvp, out var attribute)); + Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.ArrayValue, attribute.Value.ValueCase); + Assert.Equal(charArray.Select(x => x.ToString()), attribute.Value.ArrayValue.Values.Select(x => x.StringValue)); + + kvp = new KeyValuePair("key", stringArray); + Assert.True(OtlpKeyValueTransformer.Instance.TryTransformTag(kvp, out attribute)); + Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.ArrayValue, attribute.Value.ValueCase); - [Theory] - [InlineData(float.MaxValue)] - [InlineData(double.MaxValue)] - [InlineData(new float[] { 1, 2, 3 })] - [InlineData(new double[] { 1, 2, 3 })] - public void FloatingPointTypesSupported(object value) + for (var i = 0; i < stringArray.Length; ++i) { - var kvp = new KeyValuePair("key", value); - Assert.True(OtlpKeyValueTransformer.Instance.TryTransformTag(kvp, out var attribute)); + var expectedValue = stringArray[i]; + var expectedValueCase = expectedValue != null + ? OtlpCommon.AnyValue.ValueOneofCase.StringValue + : OtlpCommon.AnyValue.ValueOneofCase.None; - switch (value) + Assert.Equal(expectedValueCase, attribute.Value.ArrayValue.Values[i].ValueCase); + if (expectedValueCase != OtlpCommon.AnyValue.ValueOneofCase.None) { - case Array array: - Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.ArrayValue, attribute.Value.ValueCase); - var expectedArray = new double[array.Length]; - for (var i = 0; i < array.Length; i++) - { - expectedArray[i] = Convert.ToDouble(array.GetValue(i)); - } - - Assert.Equal(expectedArray, attribute.Value.ArrayValue.Values.Select(x => x.DoubleValue)); - break; - default: - Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.DoubleValue, attribute.Value.ValueCase); - Assert.Equal(Convert.ToDouble(value), attribute.Value.DoubleValue); - break; + Assert.Equal(expectedValue, attribute.Value.ArrayValue.Values[i].StringValue); } } + } - [Theory] - [InlineData(true)] - [InlineData(new bool[] { true, false, true })] - public void BooleanTypeSupported(object value) + [Fact] + public void ToStringIsCalledForAllOtherTypes() + { + var testValues = new object[] { - var kvp = new KeyValuePair("key", value); - Assert.True(OtlpKeyValueTransformer.Instance.TryTransformTag(kvp, out var attribute)); + (nint)int.MaxValue, + (nuint)uint.MaxValue, + decimal.MaxValue, + new object(), + }; - switch (value) - { - case Array array: - Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.ArrayValue, attribute.Value.ValueCase); - var expectedArray = new bool[array.Length]; - for (var i = 0; i < array.Length; i++) - { - expectedArray[i] = Convert.ToBoolean(array.GetValue(i)); - } - - Assert.Equal(expectedArray, attribute.Value.ArrayValue.Values.Select(x => x.BoolValue)); - break; - default: - Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.BoolValue, attribute.Value.ValueCase); - Assert.Equal(Convert.ToBoolean(value), attribute.Value.BoolValue); - break; - } - } + var testArrayValues = new object[] + { + new nint[] { 1, 2, 3 }, + new nuint[] { 1, 2, 3 }, + new decimal[] { 1, 2, 3 }, + new object[] { 1, new object(), false, null }, + }; - [Theory] - [InlineData(char.MaxValue)] - [InlineData("string")] - public void StringTypesSupported(object value) + foreach (var value in testValues) { var kvp = new KeyValuePair("key", value); Assert.True(OtlpKeyValueTransformer.Instance.TryTransformTag(kvp, out var attribute)); Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.StringValue, attribute.Value.ValueCase); - Assert.Equal(Convert.ToString(value), attribute.Value.StringValue); + Assert.Equal(value.ToString(), attribute.Value.StringValue); } - [Fact] - public void StringArrayTypesSupported() + foreach (var value in testArrayValues) { - var charArray = new char[] { 'a', 'b', 'c' }; - var stringArray = new string[] { "a", "b", "c", string.Empty, null }; - - var kvp = new KeyValuePair("key", charArray); + var kvp = new KeyValuePair("key", value); Assert.True(OtlpKeyValueTransformer.Instance.TryTransformTag(kvp, out var attribute)); Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.ArrayValue, attribute.Value.ValueCase); - Assert.Equal(charArray.Select(x => x.ToString()), attribute.Value.ArrayValue.Values.Select(x => x.StringValue)); - kvp = new KeyValuePair("key", stringArray); - Assert.True(OtlpKeyValueTransformer.Instance.TryTransformTag(kvp, out attribute)); - Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.ArrayValue, attribute.Value.ValueCase); - - for (var i = 0; i < stringArray.Length; ++i) + var array = value as Array; + for (var i = 0; i < attribute.Value.ArrayValue.Values.Count; ++i) { - var expectedValue = stringArray[i]; + var expectedValue = array.GetValue(i)?.ToString(); var expectedValueCase = expectedValue != null ? OtlpCommon.AnyValue.ValueOneofCase.StringValue : OtlpCommon.AnyValue.ValueOneofCase.None; @@ -174,77 +210,27 @@ public void StringArrayTypesSupported() Assert.Equal(expectedValueCase, attribute.Value.ArrayValue.Values[i].ValueCase); if (expectedValueCase != OtlpCommon.AnyValue.ValueOneofCase.None) { - Assert.Equal(expectedValue, attribute.Value.ArrayValue.Values[i].StringValue); - } - } - } - - [Fact] - public void ToStringIsCalledForAllOtherTypes() - { - var testValues = new object[] - { - (nint)int.MaxValue, - (nuint)uint.MaxValue, - decimal.MaxValue, - new object(), - }; - - var testArrayValues = new object[] - { - new nint[] { 1, 2, 3 }, - new nuint[] { 1, 2, 3 }, - new decimal[] { 1, 2, 3 }, - new object[] { 1, new object(), false, null }, - }; - - foreach (var value in testValues) - { - var kvp = new KeyValuePair("key", value); - Assert.True(OtlpKeyValueTransformer.Instance.TryTransformTag(kvp, out var attribute)); - Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.StringValue, attribute.Value.ValueCase); - Assert.Equal(value.ToString(), attribute.Value.StringValue); - } - - foreach (var value in testArrayValues) - { - var kvp = new KeyValuePair("key", value); - Assert.True(OtlpKeyValueTransformer.Instance.TryTransformTag(kvp, out var attribute)); - Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.ArrayValue, attribute.Value.ValueCase); - - var array = value as Array; - for (var i = 0; i < attribute.Value.ArrayValue.Values.Count; ++i) - { - var expectedValue = array.GetValue(i)?.ToString(); - var expectedValueCase = expectedValue != null - ? OtlpCommon.AnyValue.ValueOneofCase.StringValue - : OtlpCommon.AnyValue.ValueOneofCase.None; - - Assert.Equal(expectedValueCase, attribute.Value.ArrayValue.Values[i].ValueCase); - if (expectedValueCase != OtlpCommon.AnyValue.ValueOneofCase.None) - { - Assert.Equal(array.GetValue(i).ToString(), attribute.Value.ArrayValue.Values[i].StringValue); - } + Assert.Equal(array.GetValue(i).ToString(), attribute.Value.ArrayValue.Values[i].StringValue); } } } + } - [Fact] - public void ExceptionInToStringIsCaught() - { - var kvp = new KeyValuePair("key", new MyToStringMethodThrowsAnException()); - Assert.False(OtlpKeyValueTransformer.Instance.TryTransformTag(kvp, out var _)); + [Fact] + public void ExceptionInToStringIsCaught() + { + var kvp = new KeyValuePair("key", new MyToStringMethodThrowsAnException()); + Assert.False(OtlpKeyValueTransformer.Instance.TryTransformTag(kvp, out var _)); - kvp = new KeyValuePair("key", new object[] { 1, false, new MyToStringMethodThrowsAnException() }); - Assert.False(OtlpKeyValueTransformer.Instance.TryTransformTag(kvp, out var _)); - } + kvp = new KeyValuePair("key", new object[] { 1, false, new MyToStringMethodThrowsAnException() }); + Assert.False(OtlpKeyValueTransformer.Instance.TryTransformTag(kvp, out var _)); + } - private class MyToStringMethodThrowsAnException + private class MyToStringMethodThrowsAnException + { + public override string ToString() { - public override string ToString() - { - throw new Exception("Nope."); - } + throw new Exception("Nope."); } } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExportProtocolParserTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExportProtocolParserTests.cs index baec4253e73..07d7a92612c 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExportProtocolParserTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExportProtocolParserTests.cs @@ -1,35 +1,21 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Xunit; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +public class OtlpExportProtocolParserTests : Http2UnencryptedSupportTests { - public class OtlpExportProtocolParserTests : Http2UnencryptedSupportTests + [Theory] + [InlineData("grpc", true, OtlpExportProtocol.Grpc)] + [InlineData("http/protobuf", true, OtlpExportProtocol.HttpProtobuf)] + [InlineData("unsupported", false, default(OtlpExportProtocol))] + public void TryParse_Protocol_MapsToCorrectValue(string protocol, bool expectedResult, OtlpExportProtocol expectedExportProtocol) { - [Theory] - [InlineData("grpc", true, OtlpExportProtocol.Grpc)] - [InlineData("http/protobuf", true, OtlpExportProtocol.HttpProtobuf)] - [InlineData("unsupported", false, default(OtlpExportProtocol))] - public void TryParse_Protocol_MapsToCorrectValue(string protocol, bool expectedResult, OtlpExportProtocol expectedExportProtocol) - { - var result = OtlpExportProtocolParser.TryParse(protocol, out var exportProtocol); + var result = OtlpExportProtocolParser.TryParse(protocol, out var exportProtocol); - Assert.Equal(expectedExportProtocol, exportProtocol); - Assert.Equal(expectedResult, result); - } + Assert.Equal(expectedExportProtocol, exportProtocol); + Assert.Equal(expectedResult, result); } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs index 74bae7c6ddb..37a41697f26 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs @@ -1,173 +1,160 @@ -// // 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 Grpc.Core; +// SPDX-License-Identifier: Apache-2.0 + using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; using Xunit; using Xunit.Sdk; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +public class OtlpExporterOptionsExtensionsTests : Http2UnencryptedSupportTests { - public class OtlpExporterOptionsExtensionsTests : Http2UnencryptedSupportTests + [Theory] + [InlineData("key=value", new string[] { "key" }, new string[] { "value" })] + [InlineData("key1=value1,key2=value2", new string[] { "key1", "key2" }, new string[] { "value1", "value2" })] + [InlineData("key1 = value1, key2=value2 ", new string[] { "key1", "key2" }, new string[] { "value1", "value2" })] + [InlineData("key==value", new string[] { "key" }, new string[] { "=value" })] + [InlineData("access-token=abc=/123,timeout=1234", new string[] { "access-token", "timeout" }, new string[] { "abc=/123", "1234" })] + [InlineData("key1=value1;key2=value2", new string[] { "key1" }, new string[] { "value1;key2=value2" })] // semicolon is not treated as a delimiter (https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specifying-headers-via-environment-variables) + [InlineData("Authorization=Basic%20AAA", new string[] { "authorization" }, new string[] { "Basic AAA" })] + [InlineData("Authorization=Basic AAA", new string[] { "authorization" }, new string[] { "Basic AAA" })] + public void GetMetadataFromHeadersWorksCorrectFormat(string headers, string[] keys, string[] values) { - [Theory] - [InlineData("key=value", new string[] { "key" }, new string[] { "value" })] - [InlineData("key1=value1,key2=value2", new string[] { "key1", "key2" }, new string[] { "value1", "value2" })] - [InlineData("key1 = value1, key2=value2 ", new string[] { "key1", "key2" }, new string[] { "value1", "value2" })] - [InlineData("key==value", new string[] { "key" }, new string[] { "=value" })] - [InlineData("access-token=abc=/123,timeout=1234", new string[] { "access-token", "timeout" }, new string[] { "abc=/123", "1234" })] - [InlineData("key1=value1;key2=value2", new string[] { "key1" }, new string[] { "value1;key2=value2" })] // semicolon is not treated as a delimiter (https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specifying-headers-via-environment-variables) - public void GetMetadataFromHeadersWorksCorrectFormat(string headers, string[] keys, string[] values) + var options = new OtlpExporterOptions { - var options = new OtlpExporterOptions - { - Headers = headers, - }; - var metadata = options.GetMetadataFromHeaders(); + Headers = headers, + }; + var metadata = options.GetMetadataFromHeaders(); - Assert.Equal(OtlpExporterOptions.StandardHeaders.Length + keys.Length, metadata.Count); + Assert.Equal(OtlpExporterOptions.StandardHeaders.Length + keys.Length, metadata.Count); - for (int i = 0; i < keys.Length; i++) - { - Assert.Contains(metadata, entry => entry.Key == keys[i] && entry.Value == values[i]); - } - - for (int i = 0; i < OtlpExporterOptions.StandardHeaders.Length; i++) - { - // Metadata key is always converted to lowercase. - // See: https://cloud.google.com/dotnet/docs/reference/Grpc.Core/latest/Grpc.Core.Metadata.Entry#Grpc_Core_Metadata_Entry__ctor_System_String_System_String_ - Assert.Contains(metadata, entry => entry.Key == OtlpExporterOptions.StandardHeaders[i].Key.ToLower() && entry.Value == OtlpExporterOptions.StandardHeaders[i].Value); - } + for (int i = 0; i < keys.Length; i++) + { + Assert.Contains(metadata, entry => entry.Key == keys[i] && entry.Value == values[i]); } - [Theory] - [InlineData("headers")] - [InlineData("key,value")] - public void GetMetadataFromHeadersThrowsExceptionOnInvalidFormat(string headers) + for (int i = 0; i < OtlpExporterOptions.StandardHeaders.Length; i++) { - try - { - var options = new OtlpExporterOptions - { - Headers = headers, - }; - var metadata = options.GetMetadataFromHeaders(); - } - catch (Exception ex) - { - Assert.IsType(ex); - Assert.Equal("Headers provided in an invalid format.", ex.Message); - return; - } - - throw new XunitException("GetMetadataFromHeaders did not throw an exception for invalid input headers"); + // Metadata key is always converted to lowercase. + // See: https://cloud.google.com/dotnet/docs/reference/Grpc.Core/latest/Grpc.Core.Metadata.Entry#Grpc_Core_Metadata_Entry__ctor_System_String_System_String_ + Assert.Contains(metadata, entry => entry.Key == OtlpExporterOptions.StandardHeaders[i].Key.ToLower() && entry.Value == OtlpExporterOptions.StandardHeaders[i].Value); } + } - [Theory] - [InlineData("")] - [InlineData(null)] - public void GetHeaders_NoOptionHeaders_ReturnsStandardHeaders(string optionHeaders) + [Theory] + [InlineData("headers")] + [InlineData("key,value")] + public void GetMetadataFromHeadersThrowsExceptionOnInvalidFormat(string headers) + { + try { var options = new OtlpExporterOptions { - Headers = optionHeaders, + Headers = headers, }; - - var headers = options.GetHeaders>((d, k, v) => d.Add(k, v)); - - Assert.Equal(OtlpExporterOptions.StandardHeaders.Length, headers.Count); - - for (int i = 0; i < OtlpExporterOptions.StandardHeaders.Length; i++) - { - Assert.Contains(headers, entry => entry.Key == OtlpExporterOptions.StandardHeaders[i].Key && entry.Value == OtlpExporterOptions.StandardHeaders[i].Value); - } + var metadata = options.GetMetadataFromHeaders(); } + catch (Exception ex) + { + Assert.IsType(ex); + Assert.Equal("Headers provided in an invalid format.", ex.Message); + return; + } + + throw new XunitException("GetMetadataFromHeaders did not throw an exception for invalid input headers"); + } - [Theory] - [InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcTraceExportClient))] - [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpTraceExportClient))] - public void GetTraceExportClient_SupportedProtocol_ReturnsCorrectExportClient(OtlpExportProtocol protocol, Type expectedExportClientType) + [Theory] + [InlineData("")] + [InlineData(null)] + public void GetHeaders_NoOptionHeaders_ReturnsStandardHeaders(string optionHeaders) + { + var options = new OtlpExporterOptions { - if (protocol == OtlpExportProtocol.Grpc && Environment.Version.Major == 3) - { - // Adding the OtlpExporter creates a GrpcChannel. - // This switch must be set before creating a GrpcChannel when calling an insecure HTTP/2 endpoint. - // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - } + Headers = optionHeaders, + }; - var options = new OtlpExporterOptions - { - Protocol = protocol, - }; + var headers = options.GetHeaders>((d, k, v) => d.Add(k, v)); - var exportClient = options.GetTraceExportClient(); + Assert.Equal(OtlpExporterOptions.StandardHeaders.Length, headers.Count); - Assert.Equal(expectedExportClientType, exportClient.GetType()); + for (int i = 0; i < OtlpExporterOptions.StandardHeaders.Length; i++) + { + Assert.Contains(headers, entry => entry.Key == OtlpExporterOptions.StandardHeaders[i].Key && entry.Value == OtlpExporterOptions.StandardHeaders[i].Value); } + } - [Fact] - public void GetTraceExportClient_GetClientForGrpcWithoutUnencryptedFlag_ThrowsException() + [Theory] + [InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcTraceExportClient))] + [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpTraceExportClient))] + public void GetTraceExportClient_SupportedProtocol_ReturnsCorrectExportClient(OtlpExportProtocol protocol, Type expectedExportClientType) + { + if (protocol == OtlpExportProtocol.Grpc && Environment.Version.Major == 3) { // Adding the OtlpExporter creates a GrpcChannel. // This switch must be set before creating a GrpcChannel when calling an insecure HTTP/2 endpoint. // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", false); + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + } - var options = new OtlpExporterOptions - { - Protocol = OtlpExportProtocol.Grpc, - }; + var options = new OtlpExporterOptions + { + Protocol = protocol, + }; - var exception = Record.Exception(() => options.GetTraceExportClient()); + var exportClient = options.GetTraceExportClient(); - if (Environment.Version.Major == 3) - { - Assert.NotNull(exception); - Assert.IsType(exception); - } - else - { - Assert.Null(exception); - } - } + Assert.Equal(expectedExportClientType, exportClient.GetType()); + } + + [Fact] + public void GetTraceExportClient_GetClientForGrpcWithoutUnencryptedFlag_ThrowsException() + { + // Adding the OtlpExporter creates a GrpcChannel. + // This switch must be set before creating a GrpcChannel when calling an insecure HTTP/2 endpoint. + // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", false); - [Fact] - public void GetTraceExportClient_UnsupportedProtocol_Throws() + var options = new OtlpExporterOptions { - var options = new OtlpExporterOptions - { - Protocol = (OtlpExportProtocol)123, - }; + Protocol = OtlpExportProtocol.Grpc, + }; + + var exception = Record.Exception(() => options.GetTraceExportClient()); - Assert.Throws(() => options.GetTraceExportClient()); + if (Environment.Version.Major == 3) + { + Assert.NotNull(exception); + Assert.IsType(exception); + } + else + { + Assert.Null(exception); } + } - [Theory] - [InlineData("http://test:8888", "http://test:8888/v1/traces")] - [InlineData("http://test:8888/", "http://test:8888/v1/traces")] - [InlineData("http://test:8888/v1/traces", "http://test:8888/v1/traces")] - [InlineData("http://test:8888/v1/traces/", "http://test:8888/v1/traces/")] - public void AppendPathIfNotPresent_TracesPath_AppendsCorrectly(string inputUri, string expectedUri) + [Fact] + public void GetTraceExportClient_UnsupportedProtocol_Throws() + { + var options = new OtlpExporterOptions { - var uri = new Uri(inputUri, UriKind.Absolute); + Protocol = (OtlpExportProtocol)123, + }; - var resultUri = uri.AppendPathIfNotPresent("v1/traces"); + Assert.Throws(() => options.GetTraceExportClient()); + } - Assert.Equal(expectedUri, resultUri.AbsoluteUri); - } + [Theory] + [InlineData("http://test:8888", "http://test:8888/v1/traces")] + [InlineData("http://test:8888/", "http://test:8888/v1/traces")] + [InlineData("http://test:8888/v1/traces", "http://test:8888/v1/traces")] + [InlineData("http://test:8888/v1/traces/", "http://test:8888/v1/traces/")] + public void AppendPathIfNotPresent_TracesPath_AppendsCorrectly(string inputUri, string expectedUri) + { + var uri = new Uri(inputUri, UriKind.Absolute); + + var resultUri = uri.AppendPathIfNotPresent("v1/traces"); + + Assert.Equal(expectedUri, resultUri.AbsoluteUri); } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs index a985d48ece5..619dd7999e9 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs @@ -1,171 +1,157 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.Configuration; using Xunit; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +public class OtlpExporterOptionsTests : IDisposable { - public class OtlpExporterOptionsTests : IDisposable + public OtlpExporterOptionsTests() { - public OtlpExporterOptionsTests() - { - ClearEnvVars(); - } + ClearEnvVars(); + } - public void Dispose() - { - ClearEnvVars(); - GC.SuppressFinalize(this); - } + public void Dispose() + { + ClearEnvVars(); + GC.SuppressFinalize(this); + } - [Fact] - public void OtlpExporterOptions_Defaults() - { - var options = new OtlpExporterOptions(); + [Fact] + public void OtlpExporterOptions_Defaults() + { + var options = new OtlpExporterOptions(); - Assert.Equal(new Uri("http://localhost:4317"), options.Endpoint); - Assert.Null(options.Headers); - Assert.Equal(10000, options.TimeoutMilliseconds); - Assert.Equal(OtlpExportProtocol.Grpc, options.Protocol); - } + Assert.Equal(new Uri("http://localhost:4317"), options.Endpoint); + Assert.Null(options.Headers); + Assert.Equal(10000, options.TimeoutMilliseconds); + Assert.Equal(OtlpExportProtocol.Grpc, options.Protocol); + } - [Fact] - public void OtlpExporterOptions_DefaultsForHttpProtobuf() - { - var options = new OtlpExporterOptions - { - Protocol = OtlpExportProtocol.HttpProtobuf, - }; - Assert.Equal(new Uri("http://localhost:4318"), options.Endpoint); - Assert.Null(options.Headers); - Assert.Equal(10000, options.TimeoutMilliseconds); - Assert.Equal(OtlpExportProtocol.HttpProtobuf, options.Protocol); - } - - [Fact] - public void OtlpExporterOptions_EnvironmentVariableOverride() + [Fact] + public void OtlpExporterOptions_DefaultsForHttpProtobuf() + { + var options = new OtlpExporterOptions { - Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, "http://test:8888"); - Environment.SetEnvironmentVariable(OtlpExporterOptions.HeadersEnvVarName, "A=2,B=3"); - Environment.SetEnvironmentVariable(OtlpExporterOptions.TimeoutEnvVarName, "2000"); - Environment.SetEnvironmentVariable(OtlpExporterOptions.ProtocolEnvVarName, "http/protobuf"); + Protocol = OtlpExportProtocol.HttpProtobuf, + }; + Assert.Equal(new Uri("http://localhost:4318"), options.Endpoint); + Assert.Null(options.Headers); + Assert.Equal(10000, options.TimeoutMilliseconds); + Assert.Equal(OtlpExportProtocol.HttpProtobuf, options.Protocol); + } - var options = new OtlpExporterOptions(); + [Fact] + public void OtlpExporterOptions_EnvironmentVariableOverride() + { + Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, "http://test:8888"); + Environment.SetEnvironmentVariable(OtlpExporterOptions.HeadersEnvVarName, "A=2,B=3"); + Environment.SetEnvironmentVariable(OtlpExporterOptions.TimeoutEnvVarName, "2000"); + Environment.SetEnvironmentVariable(OtlpExporterOptions.ProtocolEnvVarName, "http/protobuf"); - Assert.Equal(new Uri("http://test:8888"), options.Endpoint); - Assert.Equal("A=2,B=3", options.Headers); - Assert.Equal(2000, options.TimeoutMilliseconds); - Assert.Equal(OtlpExportProtocol.HttpProtobuf, options.Protocol); - } + var options = new OtlpExporterOptions(); - [Fact] - public void OtlpExporterOptions_UsingIConfiguration() - { - var values = new Dictionary() - { - [OtlpExporterOptions.EndpointEnvVarName] = "http://test:8888", - [OtlpExporterOptions.HeadersEnvVarName] = "A=2,B=3", - [OtlpExporterOptions.TimeoutEnvVarName] = "2000", - [OtlpExporterOptions.ProtocolEnvVarName] = "http/protobuf", - }; - - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(values) - .Build(); - - var options = new OtlpExporterOptions(configuration, new()); - - Assert.Equal(new Uri("http://test:8888"), options.Endpoint); - Assert.Equal("A=2,B=3", options.Headers); - Assert.Equal(2000, options.TimeoutMilliseconds); - Assert.Equal(OtlpExportProtocol.HttpProtobuf, options.Protocol); - } - - [Fact] - public void OtlpExporterOptions_InvalidEnvironmentVariableOverride() + Assert.Equal(new Uri("http://test:8888"), options.Endpoint); + Assert.Equal("A=2,B=3", options.Headers); + Assert.Equal(2000, options.TimeoutMilliseconds); + Assert.Equal(OtlpExportProtocol.HttpProtobuf, options.Protocol); + } + + [Fact] + public void OtlpExporterOptions_UsingIConfiguration() + { + var values = new Dictionary() { - Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, "invalid"); - Environment.SetEnvironmentVariable(OtlpExporterOptions.TimeoutEnvVarName, "invalid"); - Environment.SetEnvironmentVariable(OtlpExporterOptions.ProtocolEnvVarName, "invalid"); + [OtlpExporterOptions.EndpointEnvVarName] = "http://test:8888", + [OtlpExporterOptions.HeadersEnvVarName] = "A=2,B=3", + [OtlpExporterOptions.TimeoutEnvVarName] = "2000", + [OtlpExporterOptions.ProtocolEnvVarName] = "http/protobuf", + }; + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(values) + .Build(); + + var options = new OtlpExporterOptions(configuration, new()); + + Assert.Equal(new Uri("http://test:8888"), options.Endpoint); + Assert.Equal("A=2,B=3", options.Headers); + Assert.Equal(2000, options.TimeoutMilliseconds); + Assert.Equal(OtlpExportProtocol.HttpProtobuf, options.Protocol); + } - var options = new OtlpExporterOptions(); + [Fact] + public void OtlpExporterOptions_InvalidEnvironmentVariableOverride() + { + Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, "invalid"); + Environment.SetEnvironmentVariable(OtlpExporterOptions.TimeoutEnvVarName, "invalid"); + Environment.SetEnvironmentVariable(OtlpExporterOptions.ProtocolEnvVarName, "invalid"); - Assert.Equal(new Uri("http://localhost:4317"), options.Endpoint); - Assert.Equal(10000, options.TimeoutMilliseconds); - Assert.Equal(default, options.Protocol); - } + var options = new OtlpExporterOptions(); - [Fact] - public void OtlpExporterOptions_SetterOverridesEnvironmentVariable() - { - Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, "http://test:8888"); - Environment.SetEnvironmentVariable(OtlpExporterOptions.HeadersEnvVarName, "A=2,B=3"); - Environment.SetEnvironmentVariable(OtlpExporterOptions.TimeoutEnvVarName, "2000"); - Environment.SetEnvironmentVariable(OtlpExporterOptions.ProtocolEnvVarName, "grpc"); - - var options = new OtlpExporterOptions - { - Endpoint = new Uri("http://localhost:200"), - Headers = "C=3", - TimeoutMilliseconds = 40000, - Protocol = OtlpExportProtocol.HttpProtobuf, - }; - - Assert.Equal(new Uri("http://localhost:200"), options.Endpoint); - Assert.Equal("C=3", options.Headers); - Assert.Equal(40000, options.TimeoutMilliseconds); - Assert.Equal(OtlpExportProtocol.HttpProtobuf, options.Protocol); - } - - [Fact] - public void OtlpExporterOptions_ProtocolSetterDoesNotOverrideCustomEndpointFromEnvVariables() + Assert.Equal(new Uri("http://localhost:4317"), options.Endpoint); + Assert.Equal(10000, options.TimeoutMilliseconds); + Assert.Equal(default, options.Protocol); + } + + [Fact] + public void OtlpExporterOptions_SetterOverridesEnvironmentVariable() + { + Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, "http://test:8888"); + Environment.SetEnvironmentVariable(OtlpExporterOptions.HeadersEnvVarName, "A=2,B=3"); + Environment.SetEnvironmentVariable(OtlpExporterOptions.TimeoutEnvVarName, "2000"); + Environment.SetEnvironmentVariable(OtlpExporterOptions.ProtocolEnvVarName, "grpc"); + + var options = new OtlpExporterOptions { - Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, "http://test:8888"); + Endpoint = new Uri("http://localhost:200"), + Headers = "C=3", + TimeoutMilliseconds = 40000, + Protocol = OtlpExportProtocol.HttpProtobuf, + }; + + Assert.Equal(new Uri("http://localhost:200"), options.Endpoint); + Assert.Equal("C=3", options.Headers); + Assert.Equal(40000, options.TimeoutMilliseconds); + Assert.Equal(OtlpExportProtocol.HttpProtobuf, options.Protocol); + } - var options = new OtlpExporterOptions { Protocol = OtlpExportProtocol.Grpc }; + [Fact] + public void OtlpExporterOptions_ProtocolSetterDoesNotOverrideCustomEndpointFromEnvVariables() + { + Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, "http://test:8888"); - Assert.Equal(new Uri("http://test:8888"), options.Endpoint); - Assert.Equal(OtlpExportProtocol.Grpc, options.Protocol); - } + var options = new OtlpExporterOptions { Protocol = OtlpExportProtocol.Grpc }; - [Fact] - public void OtlpExporterOptions_ProtocolSetterDoesNotOverrideCustomEndpointFromSetter() - { - var options = new OtlpExporterOptions { Endpoint = new Uri("http://test:8888"), Protocol = OtlpExportProtocol.Grpc }; + Assert.Equal(new Uri("http://test:8888"), options.Endpoint); + Assert.Equal(OtlpExportProtocol.Grpc, options.Protocol); + } - Assert.Equal(new Uri("http://test:8888"), options.Endpoint); - Assert.Equal(OtlpExportProtocol.Grpc, options.Protocol); - } + [Fact] + public void OtlpExporterOptions_ProtocolSetterDoesNotOverrideCustomEndpointFromSetter() + { + var options = new OtlpExporterOptions { Endpoint = new Uri("http://test:8888"), Protocol = OtlpExportProtocol.Grpc }; - [Fact] - public void OtlpExporterOptions_EnvironmentVariableNames() - { - Assert.Equal("OTEL_EXPORTER_OTLP_ENDPOINT", OtlpExporterOptions.EndpointEnvVarName); - Assert.Equal("OTEL_EXPORTER_OTLP_HEADERS", OtlpExporterOptions.HeadersEnvVarName); - Assert.Equal("OTEL_EXPORTER_OTLP_TIMEOUT", OtlpExporterOptions.TimeoutEnvVarName); - Assert.Equal("OTEL_EXPORTER_OTLP_PROTOCOL", OtlpExporterOptions.ProtocolEnvVarName); - } + Assert.Equal(new Uri("http://test:8888"), options.Endpoint); + Assert.Equal(OtlpExportProtocol.Grpc, options.Protocol); + } - private static void ClearEnvVars() - { - Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, null); - Environment.SetEnvironmentVariable(OtlpExporterOptions.HeadersEnvVarName, null); - Environment.SetEnvironmentVariable(OtlpExporterOptions.TimeoutEnvVarName, null); - Environment.SetEnvironmentVariable(OtlpExporterOptions.ProtocolEnvVarName, null); - } + [Fact] + public void OtlpExporterOptions_EnvironmentVariableNames() + { + Assert.Equal("OTEL_EXPORTER_OTLP_ENDPOINT", OtlpExporterOptions.EndpointEnvVarName); + Assert.Equal("OTEL_EXPORTER_OTLP_HEADERS", OtlpExporterOptions.HeadersEnvVarName); + Assert.Equal("OTEL_EXPORTER_OTLP_TIMEOUT", OtlpExporterOptions.TimeoutEnvVarName); + Assert.Equal("OTEL_EXPORTER_OTLP_PROTOCOL", OtlpExporterOptions.ProtocolEnvVarName); + } + + private static void ClearEnvVars() + { + Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, null); + Environment.SetEnvironmentVariable(OtlpExporterOptions.HeadersEnvVarName, null); + Environment.SetEnvironmentVariable(OtlpExporterOptions.TimeoutEnvVarName, null); + Environment.SetEnvironmentVariable(OtlpExporterOptions.ProtocolEnvVarName, null); } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs index 2830d95b048..a01d992040b 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs @@ -1,29 +1,19 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Collections.ObjectModel; using System.Diagnostics; +using System.Reflection; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Moq; +using Microsoft.Extensions.Options; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; -using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; using OpenTelemetry.Internal; using OpenTelemetry.Logs; +using OpenTelemetry.Resources; using OpenTelemetry.Tests; using OpenTelemetry.Trace; using Xunit; @@ -32,1105 +22,1597 @@ using OtlpCommon = OpenTelemetry.Proto.Common.V1; using OtlpLogs = OpenTelemetry.Proto.Logs.V1; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +public class OtlpLogExporterTests : Http2UnencryptedSupportTests { - public class OtlpLogExporterTests : Http2UnencryptedSupportTests + private static readonly SdkLimitOptions DefaultSdkLimitOptions = new(); + + [Fact] + public void AddOtlpExporterWithNamedOptions() { - private static readonly SdkLimitOptions DefaultSdkLimitOptions = new(); + int defaultConfigureExporterOptionsInvocations = 0; + int namedConfigureExporterOptionsInvocations = 0; - [Fact] - public void AddOtlpLogExporterReceivesAttributesWithParseStateValueSetToFalse() - { - bool optionsValidated = false; + int defaultConfigureSdkLimitsOptionsInvocations = 0; + int namedConfigureSdkLimitsOptionsInvocations = 0; - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - var logRecords = new List(); - using var loggerFactory = LoggerFactory.Create(builder => + using var loggerProvider = Sdk.CreateLoggerProviderBuilder() + .ConfigureServices(services => { - builder - .AddOpenTelemetry(options => options - .AddInMemoryExporter(logRecords) - .AddOtlpExporter()); + services.Configure(o => defaultConfigureExporterOptionsInvocations++); + services.Configure(o => defaultConfigureExporterOptionsInvocations++); + services.Configure(o => defaultConfigureExporterOptionsInvocations++); + + services.Configure("Exporter2", o => namedConfigureExporterOptionsInvocations++); + services.Configure("Exporter2", o => namedConfigureExporterOptionsInvocations++); + services.Configure("Exporter2", o => namedConfigureExporterOptionsInvocations++); + + services.Configure("Exporter3", o => namedConfigureExporterOptionsInvocations++); + services.Configure("Exporter3", o => namedConfigureExporterOptionsInvocations++); + services.Configure("Exporter3", o => namedConfigureExporterOptionsInvocations++); + + services.Configure(o => defaultConfigureSdkLimitsOptionsInvocations++); + services.Configure("Exporter2", o => namedConfigureSdkLimitsOptionsInvocations++); + services.Configure("Exporter3", o => namedConfigureSdkLimitsOptionsInvocations++); + }) + .AddOtlpExporter() + .AddOtlpExporter("Exporter2", o => { }) + .AddOtlpExporter("Exporter3", o => { }) + .Build(); + + Assert.Equal(3, defaultConfigureExporterOptionsInvocations); + Assert.Equal(6, namedConfigureExporterOptionsInvocations); + + // Note: SdkLimitOptions does NOT support named options. We only allow a + // single instance for a given IServiceCollection. + Assert.Equal(1, defaultConfigureSdkLimitsOptionsInvocations); + Assert.Equal(0, namedConfigureSdkLimitsOptionsInvocations); + } - builder.Services.Configure(o => - { - optionsValidated = true; - Assert.False(o.ParseStateValues); - }); - }); + [Fact] + public void UserHttpFactoryCalledWhenUsingHttpProtobuf() + { + OtlpExporterOptions options = new OtlpExporterOptions(); - Assert.True(optionsValidated); + var defaultFactory = options.HttpClientFactory; - var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); - logger.LogInformation("Hello from {name} {price}.", "tomato", 2.99); - Assert.Single(logRecords); - var logRecord = logRecords[0]; -#pragma warning disable CS0618 // Type or member is obsolete - Assert.NotNull(logRecord.State); -#pragma warning restore CS0618 // Type or member is obsolete - Assert.NotNull(logRecord.Attributes); + int invocations = 0; + options.Protocol = OtlpExportProtocol.HttpProtobuf; + options.HttpClientFactory = () => + { + invocations++; + return defaultFactory(); + }; + + using (var exporter = new OtlpLogExporter(options)) + { + Assert.Equal(1, invocations); + } + + using (var provider = Sdk.CreateLoggerProviderBuilder() + .AddOtlpExporter(o => + { + o.Protocol = OtlpExportProtocol.HttpProtobuf; + o.HttpClientFactory = options.HttpClientFactory; + }) + .Build()) + { + Assert.Equal(2, invocations); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void AddOtlpLogExporterParseStateValueCanBeTurnedOff(bool parseState) + options.HttpClientFactory = null; + Assert.Throws(() => { + using var exporter = new OtlpLogExporter(options); + }); + } + + [Fact] + public void AddOtlpExporterSetsDefaultBatchExportProcessor() + { + if (Environment.Version.Major == 3) + { + // Adding the OtlpExporter creates a GrpcChannel. + // This switch must be set before creating a GrpcChannel when calling an insecure HTTP/2 endpoint. + // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - var logRecords = new List(); - using var loggerFactory = LoggerFactory.Create(builder => - { - builder - .AddOpenTelemetry(options => - { - options.ParseStateValues = parseState; - options - .AddInMemoryExporter(logRecords) - .AddOtlpExporter(); - }); - }); + } - var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); - logger.Log(LogLevel.Information, default, new { propertyA = "valueA" }, null, (s, e) => "Custom state log message"); - Assert.Single(logRecords); + var loggerProvider = Sdk.CreateLoggerProviderBuilder() + .AddOtlpExporter() + .Build(); - var logRecord = logRecords[0]; + CheckProcessorDefaults(); -#pragma warning disable CS0618 // Type or member is obsolete - if (parseState) - { - Assert.Null(logRecord.State); - Assert.NotNull(logRecord.Attributes); + loggerProvider.Dispose(); - // Note: We currently do not support parsing custom states which do - // not implement the standard interfaces. We return empty attributes - // for these. - Assert.Empty(logRecord.Attributes); - } - else - { - Assert.NotNull(logRecord.State); - Assert.Null(logRecord.Attributes); - } -#pragma warning restore CS0618 // Type or member is obsolete + void CheckProcessorDefaults() + { + var bindingFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic; + + var processor = typeof(BaseProcessor) + .Assembly + .GetType("OpenTelemetry.Logs.LoggerProviderSdk") + .GetProperty("Processor", bindingFlags) + .GetValue(loggerProvider) as BatchExportProcessor; + + Assert.NotNull(processor); + + var scheduledDelayMilliseconds = typeof(BatchExportProcessor) + .GetField("scheduledDelayMilliseconds", bindingFlags) + .GetValue(processor); + + Assert.Equal(5000, scheduledDelayMilliseconds); } + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void AddOtlpLogExporterParseStateValueCanBeTurnedOffHosting(bool parseState) - { - var logRecords = new List(); + [Fact] + public void AddOtlpLogExporterReceivesAttributesWithParseStateValueSetToFalse() + { + bool optionsValidated = false; - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - var hostBuilder = new HostBuilder(); - hostBuilder.ConfigureLogging(logging => logging + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + var logRecords = new List(); + using var loggerFactory = LoggerFactory.Create(builder => + { + builder .AddOpenTelemetry(options => options .AddInMemoryExporter(logRecords) - .AddOtlpExporter())); + .AddOtlpExporter()); - hostBuilder.ConfigureServices(services => - services.Configure(options => options.ParseStateValues = parseState)); - - var host = hostBuilder.Build(); - var loggerFactory = host.Services.GetService(); - var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); - logger.Log(LogLevel.Information, default, new { propertyA = "valueA" }, null, (s, e) => "Custom state log message"); - Assert.Single(logRecords); + builder.Services.Configure(o => + { + optionsValidated = true; + Assert.False(o.ParseStateValues); + }); + }); - var logRecord = logRecords[0]; + Assert.True(optionsValidated); + var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); + logger.LogInformation("Hello from {name} {price}.", "tomato", 2.99); + Assert.Single(logRecords); + var logRecord = logRecords[0]; #pragma warning disable CS0618 // Type or member is obsolete - if (parseState) - { - Assert.Null(logRecord.State); - Assert.NotNull(logRecord.Attributes); - - // Note: We currently do not support parsing custom states which do - // not implement the standard interfaces. We return empty attributes - // for these. - Assert.Empty(logRecord.Attributes); - } - else - { - Assert.NotNull(logRecord.State); - Assert.Null(logRecord.Attributes); - } + Assert.NotNull(logRecord.State); #pragma warning restore CS0618 // Type or member is obsolete - } + Assert.NotNull(logRecord.Attributes); + } - [Fact] - public void OtlpLogRecordTestWhenStateValuesArePopulated() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddOtlpLogExporterParseStateValueCanBeTurnedOff(bool parseState) + { + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + var logRecords = new List(); + using var loggerFactory = LoggerFactory.Create(builder => { - var logRecords = new List(); - using var loggerFactory = LoggerFactory.Create(builder => - { - builder.AddOpenTelemetry(options => + builder + .AddOpenTelemetry(options => { - options.IncludeFormattedMessage = true; - options.ParseStateValues = true; - options.AddInMemoryExporter(logRecords); + options.ParseStateValues = parseState; + options + .AddInMemoryExporter(logRecords) + .AddOtlpExporter(); }); - }); + }); + + var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); + logger.Log(LogLevel.Information, default, new { propertyA = "valueA" }, null, (s, e) => "Custom state log message"); + Assert.Single(logRecords); + + var logRecord = logRecords[0]; + +#pragma warning disable CS0618 // Type or member is obsolete + if (parseState) + { + Assert.Null(logRecord.State); + Assert.NotNull(logRecord.Attributes); - var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); - logger.LogInformation("Hello from {name} {price}.", "tomato", 2.99); + // Note: We currently do not support parsing custom states which do + // not implement the standard interfaces. We return empty attributes + // for these. + Assert.Empty(logRecord.Attributes); + } + else + { + Assert.NotNull(logRecord.State); + Assert.Null(logRecord.Attributes); + } +#pragma warning restore CS0618 // Type or member is obsolete + } - Assert.Single(logRecords); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddOtlpLogExporterParseStateValueCanBeTurnedOffHosting(bool parseState) + { + var logRecords = new List(); - var logRecord = logRecords[0]; - var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + var hostBuilder = new HostBuilder(); + hostBuilder.ConfigureLogging(logging => logging + .AddOpenTelemetry(options => options + .AddInMemoryExporter(logRecords) + .AddOtlpExporter())); - Assert.NotNull(otlpLogRecord); - Assert.Equal("Hello from tomato 2.99.", otlpLogRecord.Body.StringValue); - Assert.Equal(4, otlpLogRecord.Attributes.Count); + hostBuilder.ConfigureServices(services => + services.Configure(options => options.ParseStateValues = parseState)); - var attribute = otlpLogRecord.Attributes[0]; - Assert.Equal("dotnet.ilogger.category", attribute.Key); - Assert.Equal("OtlpLogExporterTests", attribute.Value.StringValue); + var host = hostBuilder.Build(); + var loggerFactory = host.Services.GetService(); + var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); + logger.Log(LogLevel.Information, default, new { propertyA = "valueA" }, null, (s, e) => "Custom state log message"); + Assert.Single(logRecords); - attribute = otlpLogRecord.Attributes[1]; - Assert.Equal("name", attribute.Key); - Assert.Equal("tomato", attribute.Value.StringValue); + var logRecord = logRecords[0]; - attribute = otlpLogRecord.Attributes[2]; - Assert.Equal("price", attribute.Key); - Assert.Equal(2.99, attribute.Value.DoubleValue); +#pragma warning disable CS0618 // Type or member is obsolete + if (parseState) + { + Assert.Null(logRecord.State); + Assert.NotNull(logRecord.Attributes); - attribute = otlpLogRecord.Attributes[3]; - Assert.Equal("{OriginalFormat}", attribute.Key); - Assert.Equal("Hello from {name} {price}.", attribute.Value.StringValue); + // Note: We currently do not support parsing custom states which do + // not implement the standard interfaces. We return empty attributes + // for these. + Assert.Empty(logRecord.Attributes); + } + else + { + Assert.NotNull(logRecord.State); + Assert.Null(logRecord.Attributes); } +#pragma warning restore CS0618 // Type or member is obsolete + } - [Fact] - public void CheckToOtlpLogRecordLoggerCategory() + [Fact] + public void OtlpLogRecordTestWhenStateValuesArePopulated() + { + var logRecords = new List(); + using var loggerFactory = LoggerFactory.Create(builder => { - var logRecords = new List(); - using var loggerFactory = LoggerFactory.Create(builder => + builder.AddOpenTelemetry(options => { - builder.AddOpenTelemetry(options => - { - options.AddInMemoryExporter(logRecords); - }); + options.IncludeFormattedMessage = true; + options.ParseStateValues = true; + options.AddInMemoryExporter(logRecords); }); + }); - var logger1 = loggerFactory.CreateLogger("CategoryA"); - logger1.LogInformation("Hello"); - Assert.Single(logRecords); + var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); + logger.LogInformation("Hello from {name} {price}.", "tomato", 2.99); - var logRecord = logRecords[0]; - var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); - Assert.NotNull(otlpLogRecord); - Assert.Single(otlpLogRecord.Attributes); + Assert.Single(logRecords); - var attribute = otlpLogRecord.Attributes[0]; - Assert.Equal("dotnet.ilogger.category", attribute.Key); - Assert.Equal("CategoryA", attribute.Value.StringValue); + var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - logRecords.Clear(); - var logger2 = loggerFactory.CreateLogger(string.Empty); - logger2.LogInformation("Hello"); - Assert.Single(logRecords); + var logRecord = logRecords[0]; + var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); - logRecord = logRecords[0]; - otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); - Assert.NotNull(otlpLogRecord); - Assert.Empty(otlpLogRecord.Attributes); - } + Assert.NotNull(otlpLogRecord); + Assert.Equal("Hello from tomato 2.99.", otlpLogRecord.Body.StringValue); + Assert.Equal(3, otlpLogRecord.Attributes.Count); + var index = 0; + var attribute = otlpLogRecord.Attributes[index]; + + Assert.Equal("name", attribute.Key); + Assert.Equal("tomato", attribute.Value.StringValue); - [Fact] - public void CheckToOtlpLogRecordEventId() + attribute = otlpLogRecord.Attributes[++index]; + Assert.Equal("price", attribute.Key); + Assert.Equal(2.99, attribute.Value.DoubleValue); + + attribute = otlpLogRecord.Attributes[++index]; + Assert.Equal("{OriginalFormat}", attribute.Key); + Assert.Equal("Hello from {name} {price}.", attribute.Value.StringValue); + } + + [Theory] + [InlineData("true")] + [InlineData("false")] + [InlineData(null)] + public void CheckToOtlpLogRecordEventId(string emitLogEventAttributes) + { + var logRecords = new List(); + using var loggerFactory = LoggerFactory.Create(builder => { - var logRecords = new List(); - using var loggerFactory = LoggerFactory.Create(builder => + builder.AddOpenTelemetry(options => { - builder.AddOpenTelemetry(options => - { - options.IncludeFormattedMessage = true; - options.ParseStateValues = true; - options.AddInMemoryExporter(logRecords); - }); + options.IncludeFormattedMessage = true; + options.ParseStateValues = true; + options.AddInMemoryExporter(logRecords); }); + }); - var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); - logger.LogInformation(new EventId(10, null), "Hello from {name} {price}.", "tomato", 2.99); - Assert.Single(logRecords); + var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); + logger.LogInformation(new EventId(10, null), "Hello from {name} {price}.", "tomato", 2.99); + Assert.Single(logRecords); - var logRecord = logRecords[0]; - var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { [ExperimentalOptions.EmitLogEventEnvVar] = emitLogEventAttributes }) + .Build(); - Assert.NotNull(otlpLogRecord); - Assert.Equal("Hello from tomato 2.99.", otlpLogRecord.Body.StringValue); + var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new(configuration)); - var otlpLogRecordAttributes = otlpLogRecord.Attributes.ToString(); + var logRecord = logRecords[0]; - // Event - Assert.Contains("Id", otlpLogRecordAttributes); - Assert.Contains("10", otlpLogRecordAttributes); + var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); - logRecords.Clear(); + Assert.NotNull(otlpLogRecord); + Assert.Equal("Hello from tomato 2.99.", otlpLogRecord.Body.StringValue); - logger.LogInformation(new EventId(10, "MyEvent10"), "Hello from {name} {price}.", "tomato", 2.99); - Assert.Single(logRecords); + // Event + var otlpLogRecordAttributes = otlpLogRecord.Attributes.ToString(); + if (emitLogEventAttributes == "true") + { + Assert.Contains(ExperimentalOptions.LogRecordEventIdAttribute, otlpLogRecordAttributes); + Assert.Contains("10", otlpLogRecordAttributes); + } + else + { + Assert.DoesNotContain(ExperimentalOptions.LogRecordEventIdAttribute, otlpLogRecordAttributes); + } + + logRecords.Clear(); - logRecord = logRecords[0]; - otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); - Assert.NotNull(otlpLogRecord); - Assert.Equal("Hello from tomato 2.99.", otlpLogRecord.Body.StringValue); + logger.LogInformation(new EventId(10, "MyEvent10"), "Hello from {name} {price}.", "tomato", 2.99); + Assert.Single(logRecords); - otlpLogRecordAttributes = otlpLogRecord.Attributes.ToString(); + logRecord = logRecords[0]; + otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + Assert.NotNull(otlpLogRecord); + Assert.Equal("Hello from tomato 2.99.", otlpLogRecord.Body.StringValue); - // Event - Assert.Contains("Id", otlpLogRecordAttributes); + // Event + otlpLogRecordAttributes = otlpLogRecord.Attributes.ToString(); + if (emitLogEventAttributes == "true") + { + Assert.Contains(ExperimentalOptions.LogRecordEventIdAttribute, otlpLogRecordAttributes); Assert.Contains("10", otlpLogRecordAttributes); - Assert.Contains("Name", otlpLogRecordAttributes); + Assert.Contains(ExperimentalOptions.LogRecordEventNameAttribute, otlpLogRecordAttributes); Assert.Contains("MyEvent10", otlpLogRecordAttributes); } + else + { + Assert.DoesNotContain(ExperimentalOptions.LogRecordEventIdAttribute, otlpLogRecordAttributes); + Assert.DoesNotContain(ExperimentalOptions.LogRecordEventNameAttribute, otlpLogRecordAttributes); + } + } - [Fact] - public void CheckToOtlpLogRecordTimestamps() + [Fact] + public void CheckToOtlpLogRecordTimestamps() + { + var logRecords = new List(); + using var loggerFactory = LoggerFactory.Create(builder => { - var logRecords = new List(); - using var loggerFactory = LoggerFactory.Create(builder => + builder.AddOpenTelemetry(options => { - builder.AddOpenTelemetry(options => - { - options.AddInMemoryExporter(logRecords); - }); + options.AddInMemoryExporter(logRecords); }); + }); - var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); - logger.LogInformation("Log message"); - var logRecord = logRecords[0]; - var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); + var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); + logger.LogInformation("Log message"); + var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - Assert.True(otlpLogRecord.TimeUnixNano > 0); - Assert.True(otlpLogRecord.ObservedTimeUnixNano > 0); - } + var logRecord = logRecords[0]; + var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); - [Fact] - public void CheckToOtlpLogRecordTraceIdSpanIdFlagWithNoActivity() + Assert.True(otlpLogRecord.TimeUnixNano > 0); + Assert.True(otlpLogRecord.ObservedTimeUnixNano > 0); + } + + [Fact] + public void CheckToOtlpLogRecordTraceIdSpanIdFlagWithNoActivity() + { + var logRecords = new List(); + using var loggerFactory = LoggerFactory.Create(builder => { - var logRecords = new List(); - using var loggerFactory = LoggerFactory.Create(builder => + builder.AddOpenTelemetry(options => { - builder.AddOpenTelemetry(options => - { - options.AddInMemoryExporter(logRecords); - }); + options.AddInMemoryExporter(logRecords); }); + }); - var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); - logger.LogInformation("Log when there is no activity."); - var logRecord = logRecords[0]; - var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); + var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); + logger.LogInformation("Log when there is no activity."); - Assert.Null(Activity.Current); - Assert.True(otlpLogRecord.TraceId.IsEmpty); - Assert.True(otlpLogRecord.SpanId.IsEmpty); - Assert.True(otlpLogRecord.Flags == 0); - } + var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); + + var logRecord = logRecords[0]; + var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); - [Fact] - public void CheckToOtlpLogRecordSpanIdTraceIdAndFlag() + Assert.Null(Activity.Current); + Assert.True(otlpLogRecord.TraceId.IsEmpty); + Assert.True(otlpLogRecord.SpanId.IsEmpty); + Assert.True(otlpLogRecord.Flags == 0); + } + + [Fact] + public void CheckToOtlpLogRecordSpanIdTraceIdAndFlag() + { + var logRecords = new List(); + using var loggerFactory = LoggerFactory.Create(builder => { - var logRecords = new List(); - using var loggerFactory = LoggerFactory.Create(builder => + builder.AddOpenTelemetry(options => { - builder.AddOpenTelemetry(options => - { - options.AddInMemoryExporter(logRecords); - }); + options.AddInMemoryExporter(logRecords); }); + }); - var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); + var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); - ActivityTraceId expectedTraceId = default; - ActivitySpanId expectedSpanId = default; - using (var activity = new Activity(Utils.GetCurrentMethodName()).Start()) - { - logger.LogInformation("Log within an activity."); + ActivityTraceId expectedTraceId = default; + ActivitySpanId expectedSpanId = default; + using (var activity = new Activity(Utils.GetCurrentMethodName()).Start()) + { + logger.LogInformation("Log within an activity."); - expectedTraceId = activity.TraceId; - expectedSpanId = activity.SpanId; - } + expectedTraceId = activity.TraceId; + expectedSpanId = activity.SpanId; + } - var logRecord = logRecords[0]; - var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); + var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - Assert.Equal(expectedTraceId.ToString(), ActivityTraceId.CreateFromBytes(otlpLogRecord.TraceId.ToByteArray()).ToString()); - Assert.Equal(expectedSpanId.ToString(), ActivitySpanId.CreateFromBytes(otlpLogRecord.SpanId.ToByteArray()).ToString()); - Assert.Equal((uint)logRecord.TraceFlags, otlpLogRecord.Flags); - } + var logRecord = logRecords[0]; + var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); - [Theory] - [InlineData(LogLevel.Trace)] - [InlineData(LogLevel.Debug)] - [InlineData(LogLevel.Information)] - [InlineData(LogLevel.Warning)] - [InlineData(LogLevel.Error)] - [InlineData(LogLevel.Critical)] - public void CheckToOtlpLogRecordSeverityLevelAndText(LogLevel logLevel) - { - var logRecords = new List(); - using var loggerFactory = LoggerFactory.Create(builder => + Assert.Equal(expectedTraceId.ToString(), ActivityTraceId.CreateFromBytes(otlpLogRecord.TraceId.ToByteArray()).ToString()); + Assert.Equal(expectedSpanId.ToString(), ActivitySpanId.CreateFromBytes(otlpLogRecord.SpanId.ToByteArray()).ToString()); + Assert.Equal((uint)logRecord.TraceFlags, otlpLogRecord.Flags); + } + + [Theory] + [InlineData(LogLevel.Trace)] + [InlineData(LogLevel.Debug)] + [InlineData(LogLevel.Information)] + [InlineData(LogLevel.Warning)] + [InlineData(LogLevel.Error)] + [InlineData(LogLevel.Critical)] + public void CheckToOtlpLogRecordSeverityLevelAndText(LogLevel logLevel) + { + var logRecords = new List(); + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddOpenTelemetry(options => { - builder.AddOpenTelemetry(options => - { - options.AddInMemoryExporter(logRecords); - options.IncludeFormattedMessage = true; - }) - .AddFilter("CheckToOtlpLogRecordSeverityLevelAndText", LogLevel.Trace); - }); + options.AddInMemoryExporter(logRecords); + options.IncludeFormattedMessage = true; + }) + .AddFilter("CheckToOtlpLogRecordSeverityLevelAndText", LogLevel.Trace); + }); - var logger = loggerFactory.CreateLogger("CheckToOtlpLogRecordSeverityLevelAndText"); - logger.Log(logLevel, "Hello from {name} {price}.", "tomato", 2.99); - Assert.Single(logRecords); + var logger = loggerFactory.CreateLogger("CheckToOtlpLogRecordSeverityLevelAndText"); + logger.Log(logLevel, "Hello from {name} {price}.", "tomato", 2.99); + Assert.Single(logRecords); - var logRecord = logRecords[0]; - var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); + var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - Assert.NotNull(otlpLogRecord); + var logRecord = logRecords[0]; + var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + + Assert.NotNull(otlpLogRecord); #pragma warning disable CS0618 // Type or member is obsolete - Assert.Equal(logRecord.LogLevel.ToString(), otlpLogRecord.SeverityText); + Assert.Equal(logRecord.LogLevel.ToString(), otlpLogRecord.SeverityText); #pragma warning restore CS0618 // Type or member is obsolete - Assert.Equal((int)logRecord.Severity, (int)otlpLogRecord.SeverityNumber); - switch (logLevel) - { - case LogLevel.Trace: - Assert.Equal(OtlpLogs.SeverityNumber.Trace, otlpLogRecord.SeverityNumber); - break; - case LogLevel.Debug: - Assert.Equal(OtlpLogs.SeverityNumber.Debug, otlpLogRecord.SeverityNumber); - break; - case LogLevel.Information: - Assert.Equal(OtlpLogs.SeverityNumber.Info, otlpLogRecord.SeverityNumber); - break; - case LogLevel.Warning: - Assert.Equal(OtlpLogs.SeverityNumber.Warn, otlpLogRecord.SeverityNumber); - break; - case LogLevel.Error: - Assert.Equal(OtlpLogs.SeverityNumber.Error, otlpLogRecord.SeverityNumber); - break; - case LogLevel.Critical: - Assert.Equal(OtlpLogs.SeverityNumber.Fatal, otlpLogRecord.SeverityNumber); - break; - } + Assert.Equal((int)logRecord.Severity, (int)otlpLogRecord.SeverityNumber); + switch (logLevel) + { + case LogLevel.Trace: + Assert.Equal(OtlpLogs.SeverityNumber.Trace, otlpLogRecord.SeverityNumber); + break; + case LogLevel.Debug: + Assert.Equal(OtlpLogs.SeverityNumber.Debug, otlpLogRecord.SeverityNumber); + break; + case LogLevel.Information: + Assert.Equal(OtlpLogs.SeverityNumber.Info, otlpLogRecord.SeverityNumber); + break; + case LogLevel.Warning: + Assert.Equal(OtlpLogs.SeverityNumber.Warn, otlpLogRecord.SeverityNumber); + break; + case LogLevel.Error: + Assert.Equal(OtlpLogs.SeverityNumber.Error, otlpLogRecord.SeverityNumber); + break; + case LogLevel.Critical: + Assert.Equal(OtlpLogs.SeverityNumber.Fatal, otlpLogRecord.SeverityNumber); + break; } + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void CheckToOtlpLogRecordBodyIsPopulated(bool includeFormattedMessage) + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CheckToOtlpLogRecordBodyIsPopulated(bool includeFormattedMessage) + { + var logRecords = new List(); + using var loggerFactory = LoggerFactory.Create(builder => { - var logRecords = new List(); - using var loggerFactory = LoggerFactory.Create(builder => + builder.AddOpenTelemetry(options => { - builder.AddOpenTelemetry(options => - { - options.AddInMemoryExporter(logRecords); - options.IncludeFormattedMessage = includeFormattedMessage; - options.ParseStateValues = true; - }); + options.AddInMemoryExporter(logRecords); + options.IncludeFormattedMessage = includeFormattedMessage; + options.ParseStateValues = true; }); + }); - var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); + var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); - // Scenario 1 - Using ExtensionMethods on ILogger.Log - logger.LogInformation("OpenTelemetry {Greeting} {Subject}!", "Hello", "World"); - Assert.Single(logRecords); + // Scenario 1 - Using ExtensionMethods on ILogger.Log + logger.LogInformation("OpenTelemetry {Greeting} {Subject}!", "Hello", "World"); + Assert.Single(logRecords); - var logRecord = logRecords[0]; - var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); + var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - Assert.NotNull(otlpLogRecord); - if (includeFormattedMessage) - { - Assert.Equal(logRecord.FormattedMessage, otlpLogRecord.Body.StringValue); - } - else - { - Assert.Equal("OpenTelemetry {Greeting} {Subject}!", otlpLogRecord.Body.StringValue); - } + var logRecord = logRecords[0]; + var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); - logRecords.Clear(); + Assert.NotNull(otlpLogRecord); + if (includeFormattedMessage) + { + Assert.Equal(logRecord.FormattedMessage, otlpLogRecord.Body.StringValue); + } + else + { + Assert.Equal("OpenTelemetry {Greeting} {Subject}!", otlpLogRecord.Body.StringValue); + } - // Scenario 2 - Using the raw ILogger.Log Method - logger.Log(LogLevel.Information, default, "state", exception: null, (st, ex) => "Formatted Message"); - Assert.Single(logRecords); + logRecords.Clear(); - logRecord = logRecords[0]; - otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); + // Scenario 2 - Using the raw ILogger.Log Method + logger.Log(LogLevel.Information, default, "state", exception: null, (st, ex) => "Formatted Message"); + Assert.Single(logRecords); - Assert.NotNull(otlpLogRecord); + logRecord = logRecords[0]; + otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); - // Formatter is always called if no template can be found. - Assert.Equal(logRecord.FormattedMessage, otlpLogRecord.Body.StringValue); - Assert.Equal(logRecord.Body, otlpLogRecord.Body.StringValue); + Assert.NotNull(otlpLogRecord); - logRecords.Clear(); + // Formatter is always called if no template can be found. + Assert.Equal(logRecord.FormattedMessage, otlpLogRecord.Body.StringValue); + Assert.Equal(logRecord.Body, otlpLogRecord.Body.StringValue); - // Scenario 3 - Using the raw ILogger.Log Method, but with null - // formatter. - logger.Log(LogLevel.Information, default, "state", exception: null, formatter: null); - Assert.Single(logRecords); + logRecords.Clear(); - logRecord = logRecords[0]; - otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); + // Scenario 3 - Using the raw ILogger.Log Method, but with null + // formatter. + logger.Log(LogLevel.Information, default, "state", exception: null, formatter: null); + Assert.Single(logRecords); - Assert.NotNull(otlpLogRecord); + logRecord = logRecords[0]; + otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); - // There is no formatter, we call ToString on state - Assert.Equal("state", otlpLogRecord.Body.StringValue); - } + Assert.NotNull(otlpLogRecord); - [Fact] - public void CheckToOtlpLogRecordExceptionAttributes() + // There is no formatter, we call ToString on state + Assert.Equal("state", otlpLogRecord.Body.StringValue); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void LogRecordBodyIsExportedWhenUsingBridgeApi(bool isBodySet) + { + LogRecordAttributeList attributes = default; + attributes.Add("name", "tomato"); + attributes.Add("price", 2.99); + attributes.Add("{OriginalFormat}", "Hello from {name} {price}."); + + var logRecords = new List(); + + using (var loggerProvider = Sdk.CreateLoggerProviderBuilder() + .AddInMemoryExporter(logRecords) + .Build()) { - var logRecords = new List(); - using var loggerFactory = LoggerFactory.Create(builder => + var logger = loggerProvider.GetLogger(); + + logger.EmitLog(new LogRecordData() { - builder.AddOpenTelemetry(options => - { - options.AddInMemoryExporter(logRecords); - }); + Body = isBodySet ? "Hello world" : null, }); - var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); - logger.LogInformation(new Exception("Exception Message"), "Exception Occurred"); + logger.EmitLog(new LogRecordData(), attributes); + } - var logRecord = logRecords[0]; - var loggedException = logRecord.Exception; - var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); + Assert.Equal(2, logRecords.Count); - Assert.NotNull(otlpLogRecord); - var otlpLogRecordAttributes = otlpLogRecord.Attributes.ToString(); - Assert.Contains(SemanticConventions.AttributeExceptionType, otlpLogRecordAttributes); - Assert.Contains(logRecord.Exception.GetType().Name, otlpLogRecordAttributes); + var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - Assert.Contains(SemanticConventions.AttributeExceptionMessage, otlpLogRecordAttributes); - Assert.Contains(logRecord.Exception.Message, otlpLogRecordAttributes); + var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecords[0]); - Assert.Contains(SemanticConventions.AttributeExceptionStacktrace, otlpLogRecordAttributes); - Assert.Contains(logRecord.Exception.ToInvariantString(), otlpLogRecordAttributes); + if (isBodySet) + { + Assert.Equal("Hello world", otlpLogRecord.Body?.StringValue); } - - [Fact] - public void CheckToOtlpLogRecordRespectsAttributeLimits() + else { - var sdkLimitOptions = new SdkLimitOptions - { - AttributeCountLimit = 3, // 3 => LogCategory, exception.type and exception.message - AttributeValueLengthLimit = 8, - }; + Assert.Null(otlpLogRecord.Body); + } + + otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecords[1]); - var logRecords = new List(); - using var loggerFactory = LoggerFactory.Create(builder => + Assert.Equal(2, otlpLogRecord.Attributes.Count); + + var index = 0; + var attribute = otlpLogRecord.Attributes[index]; + Assert.Equal("name", attribute.Key); + Assert.Equal("tomato", attribute.Value.StringValue); + + attribute = otlpLogRecord.Attributes[++index]; + Assert.Equal("price", attribute.Key); + Assert.Equal(2.99, attribute.Value.DoubleValue); + + Assert.Equal("Hello from {name} {price}.", otlpLogRecord.Body.StringValue); + } + + [Fact] + public void CheckToOtlpLogRecordExceptionAttributes() + { + var logRecords = new List(); + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddOpenTelemetry(options => { - builder.AddOpenTelemetry(options => - { - options.AddInMemoryExporter(logRecords); - }); + options.AddInMemoryExporter(logRecords); }); + }); - var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); - logger.LogInformation(new NotSupportedException("I'm the exception message."), "Exception Occurred"); + var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); + logger.LogInformation(new Exception("Exception Message"), "Exception Occurred"); - var logRecord = logRecords[0]; - var otlpLogRecord = logRecord.ToOtlpLog(sdkLimitOptions); + var logRecord = logRecords[0]; + var loggedException = logRecord.Exception; - Assert.NotNull(otlpLogRecord); - Assert.Equal(1u, otlpLogRecord.DroppedAttributesCount); + var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - var exceptionTypeAtt = TryGetAttribute(otlpLogRecord, SemanticConventions.AttributeExceptionType); - Assert.NotNull(exceptionTypeAtt); + var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); - // "NotSuppo" == first 8 chars from the exception typename "NotSupportedException" - Assert.Equal("NotSuppo", exceptionTypeAtt.Value.StringValue); - var exceptionMessageAtt = TryGetAttribute(otlpLogRecord, SemanticConventions.AttributeExceptionMessage); - Assert.NotNull(exceptionMessageAtt); + Assert.NotNull(otlpLogRecord); + var otlpLogRecordAttributes = otlpLogRecord.Attributes.ToString(); - // "I'm the " == first 8 chars from the exception message - Assert.Equal("I'm the ", exceptionMessageAtt.Value.StringValue); + Assert.Contains(SemanticConventions.AttributeExceptionType, otlpLogRecordAttributes); + Assert.Contains(logRecord.Exception.GetType().Name, otlpLogRecordAttributes); - var exceptionStackTraceAtt = TryGetAttribute(otlpLogRecord, SemanticConventions.AttributeExceptionStacktrace); - Assert.Null(exceptionStackTraceAtt); - } + Assert.Contains(SemanticConventions.AttributeExceptionMessage, otlpLogRecordAttributes); + Assert.Contains(logRecord.Exception.Message, otlpLogRecordAttributes); + + Assert.Contains(SemanticConventions.AttributeExceptionStacktrace, otlpLogRecordAttributes); + Assert.Contains(logRecord.Exception.ToInvariantString(), otlpLogRecordAttributes); + } + + [Fact] + public void CheckToOtlpLogRecordRespectsAttributeLimits() + { + var sdkLimitOptions = new SdkLimitOptions + { + AttributeCountLimit = 2, + AttributeValueLengthLimit = 8, + }; - [Fact] - public void Export_WhenExportClientIsProvidedInCtor_UsesProvidedExportClient() + var logRecords = new List(); + using var loggerFactory = LoggerFactory.Create(builder => { - // Arrange. - var fakeExportClient = new Mock>(); - var emptyLogRecords = Array.Empty(); - var emptyBatch = new Batch(emptyLogRecords, emptyLogRecords.Length); - var sut = new OtlpLogExporter( - new OtlpExporterOptions(), - new SdkLimitOptions(), - fakeExportClient.Object); + builder.AddOpenTelemetry(options => + { + options.ParseStateValues = true; + options.AddInMemoryExporter(logRecords); + }); + }); - // Act. - var result = sut.Export(emptyBatch); + var logger = loggerFactory.CreateLogger(string.Empty); + logger.LogInformation("OpenTelemetry {AttributeOne} {AttributeTwo} {AttributeThree}!", "I'm an attribute", "I too am an attribute", "I get dropped :("); - // Assert. - fakeExportClient.Verify(x => x.SendExportRequest(It.IsAny(), default), Times.Once()); - } + var otlpLogRecordTransformer = new OtlpLogRecordTransformer(sdkLimitOptions, new()); - [Fact] - public void Export_WhenExportClientThrowsException_ReturnsExportResultFailure() - { - // Arrange. - var fakeExportClient = new Mock>(); - var emptyLogRecords = Array.Empty(); - var emptyBatch = new Batch(emptyLogRecords, emptyLogRecords.Length); - fakeExportClient - .Setup(_ => _.SendExportRequest(It.IsAny(), default)) - .Throws(new Exception("Test Exception")); - var sut = new OtlpLogExporter( - new OtlpExporterOptions(), - new SdkLimitOptions(), - fakeExportClient.Object); - - // Act. - var result = sut.Export(emptyBatch); - - // Assert. - Assert.Equal(ExportResult.Failure, result); - } + var logRecord = logRecords[0]; + var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); - [Fact] - public void Export_WhenExportIsSuccessful_ReturnsExportResultSuccess() - { - // Arrange. - var fakeExportClient = new Mock>(); - var emptyLogRecords = Array.Empty(); - var emptyBatch = new Batch(emptyLogRecords, emptyLogRecords.Length); - fakeExportClient - .Setup(_ => _.SendExportRequest(It.IsAny(), default)) - .Returns(true); - var sut = new OtlpLogExporter( - new OtlpExporterOptions(), - new SdkLimitOptions(), - fakeExportClient.Object); - - // Act. - var result = sut.Export(emptyBatch); - - // Assert. - Assert.Equal(ExportResult.Success, result); - } + Assert.NotNull(otlpLogRecord); + Assert.Equal(1u, otlpLogRecord.DroppedAttributesCount); + + var attribute = TryGetAttribute(otlpLogRecord, "AttributeOne"); + Assert.NotNull(attribute); - [Fact] - public void ToOtlpLog_WhenOptionsIncludeScopesIsFalse_DoesNotContainScopeAttribute() + // "I'm an a" == first 8 chars from the first attribute "I'm an attribute" + Assert.Equal("I'm an a", attribute.Value.StringValue); + attribute = TryGetAttribute(otlpLogRecord, "AttributeTwo"); + Assert.NotNull(attribute); + + // "I too am" == first 8 chars from the second attribute "I too am an attribute" + Assert.Equal("I too am", attribute.Value.StringValue); + + attribute = TryGetAttribute(otlpLogRecord, "AttributeThree"); + Assert.Null(attribute); + } + + [Fact] + public void Export_WhenExportClientIsProvidedInCtor_UsesProvidedExportClient() + { + // Arrange. + var testExportClient = new TestExportClient(); + var transmissionHandler = new OtlpExporterTransmissionHandler(testExportClient); + var emptyLogRecords = Array.Empty(); + var emptyBatch = new Batch(emptyLogRecords, emptyLogRecords.Length); + var sut = new OtlpLogExporter( + new OtlpExporterOptions(), + new SdkLimitOptions(), + new ExperimentalOptions(), + transmissionHandler); + + // Act. + sut.Export(emptyBatch); + + // Assert. + Assert.True(testExportClient.SendExportRequestCalled); + } + + [Fact] + public void Export_WhenExportClientThrowsException_ReturnsExportResultFailure() + { + // Arrange. + var testExportClient = new TestExportClient(throwException: true); + var transmissionHandler = new OtlpExporterTransmissionHandler(testExportClient); + var emptyLogRecords = Array.Empty(); + var emptyBatch = new Batch(emptyLogRecords, emptyLogRecords.Length); + var sut = new OtlpLogExporter( + new OtlpExporterOptions(), + new SdkLimitOptions(), + new ExperimentalOptions(), + transmissionHandler); + + // Act. + var result = sut.Export(emptyBatch); + + // Assert. + Assert.Equal(ExportResult.Failure, result); + } + + [Fact] + public void Export_WhenExportIsSuccessful_ReturnsExportResultSuccess() + { + // Arrange. + var testExportClient = new TestExportClient(); + var transmissionHandler = new OtlpExporterTransmissionHandler(testExportClient); + var emptyLogRecords = Array.Empty(); + var emptyBatch = new Batch(emptyLogRecords, emptyLogRecords.Length); + var sut = new OtlpLogExporter( + new OtlpExporterOptions(), + new SdkLimitOptions(), + new ExperimentalOptions(), + transmissionHandler); + + // Act. + var result = sut.Export(emptyBatch); + + // Assert. + Assert.Equal(ExportResult.Success, result); + } + + [Fact] + public void ToOtlpLog_WhenOptionsIncludeScopesIsFalse_DoesNotContainScopeAttribute() + { + // Arrange. + var logRecords = new List(1); + using var loggerFactory = LoggerFactory.Create(builder => { - // Arrange. - var logRecords = new List(1); - using var loggerFactory = LoggerFactory.Create(builder => + builder.AddOpenTelemetry(options => { - builder.AddOpenTelemetry(options => - { - options.IncludeScopes = false; - options.AddInMemoryExporter(logRecords); - }); + options.IncludeScopes = false; + options.AddInMemoryExporter(logRecords); }); - var logger = loggerFactory.CreateLogger("Some category"); + }); + var logger = loggerFactory.CreateLogger("Some category"); - const string expectedScopeKey = "Some scope key"; - const string expectedScopeValue = "Some scope value"; - - // Act. - using (logger.BeginScope(new List> - { - new KeyValuePair(expectedScopeKey, expectedScopeValue), - })) - { - logger.LogInformation("Some log information message."); - } + const string expectedScopeKey = "Some scope key"; + const string expectedScopeValue = "Some scope value"; - // Assert. - var logRecord = logRecords.Single(); - var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); - var actualScope = TryGetAttribute(otlpLogRecord, expectedScopeKey); - Assert.Null(actualScope); + // Act. + using (logger.BeginScope(new List> + { + new KeyValuePair(expectedScopeKey, expectedScopeValue), + })) + { + logger.LogInformation("Some log information message."); } - [Theory] - [InlineData("Some scope value")] - [InlineData('a')] - public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_ContainsScopeAttributeStringValue(object scopeValue) + // Assert. + var logRecord = logRecords.Single(); + var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); + var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + var actualScope = TryGetAttribute(otlpLogRecord, expectedScopeKey); + Assert.Null(actualScope); + } + + [Theory] + [InlineData("Some scope value")] + [InlineData('a')] + public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_ContainsScopeAttributeStringValue(object scopeValue) + { + // Arrange. + var logRecords = new List(1); + using var loggerFactory = LoggerFactory.Create(builder => { - // Arrange. - var logRecords = new List(1); - using var loggerFactory = LoggerFactory.Create(builder => + builder.AddOpenTelemetry(options => { - builder.AddOpenTelemetry(options => - { - options.IncludeScopes = true; - options.AddInMemoryExporter(logRecords); - }); + options.IncludeScopes = true; + options.AddInMemoryExporter(logRecords); }); - var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); + }); + var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); - const string scopeKey = "Some scope key"; + const string scopeKey = "Some scope key"; - // Act. - using (logger.BeginScope(new List> - { - new KeyValuePair(scopeKey, scopeValue), - })) - { - logger.LogInformation("Some log information message."); - } - - // Assert. - var logRecord = logRecords.Single(); - var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); - Assert.Equal(2, otlpLogRecord.Attributes.Count); - var actualScope = TryGetAttribute(otlpLogRecord, scopeKey); - Assert.NotNull(actualScope); - Assert.Equal(scopeKey, actualScope.Key); - Assert.Equal(ValueOneofCase.StringValue, actualScope.Value.ValueCase); - Assert.Equal(scopeValue.ToString(), actualScope.Value.StringValue); + // Act. + using (logger.BeginScope(new List> + { + new KeyValuePair(scopeKey, scopeValue), + })) + { + logger.LogInformation("Some log information message."); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_ContainsScopeAttributeBoolValue(bool scopeValue) + // Assert. + var logRecord = logRecords.Single(); + var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); + var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + Assert.Single(otlpLogRecord.Attributes); + var actualScope = TryGetAttribute(otlpLogRecord, scopeKey); + Assert.NotNull(actualScope); + Assert.Equal(scopeKey, actualScope.Key); + Assert.Equal(ValueOneofCase.StringValue, actualScope.Value.ValueCase); + Assert.Equal(scopeValue.ToString(), actualScope.Value.StringValue); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_ContainsScopeAttributeBoolValue(bool scopeValue) + { + // Arrange. + var logRecords = new List(1); + using var loggerFactory = LoggerFactory.Create(builder => { - // Arrange. - var logRecords = new List(1); - using var loggerFactory = LoggerFactory.Create(builder => + builder.AddOpenTelemetry(options => { - builder.AddOpenTelemetry(options => - { - options.IncludeScopes = true; - options.AddInMemoryExporter(logRecords); - }); + options.IncludeScopes = true; + options.AddInMemoryExporter(logRecords); }); - var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); - - const string scopeKey = "Some scope key"; + }); + var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); - // Act. - using (logger.BeginScope(new List> - { - new KeyValuePair(scopeKey, scopeValue), - })) - { - logger.LogInformation("Some log information message."); - } + const string scopeKey = "Some scope key"; - // Assert. - var logRecord = logRecords.Single(); - var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); - Assert.Equal(2, otlpLogRecord.Attributes.Count); - var actualScope = TryGetAttribute(otlpLogRecord, scopeKey); - Assert.NotNull(actualScope); - Assert.Equal(scopeKey, actualScope.Key); - Assert.Equal(ValueOneofCase.BoolValue, actualScope.Value.ValueCase); - Assert.Equal(scopeValue.ToString(), actualScope.Value.BoolValue.ToString()); + // Act. + using (logger.BeginScope(new List> + { + new KeyValuePair(scopeKey, scopeValue), + })) + { + logger.LogInformation("Some log information message."); } - [Theory] - [InlineData(byte.MinValue)] - [InlineData(byte.MaxValue)] - [InlineData(sbyte.MinValue)] - [InlineData(sbyte.MaxValue)] - [InlineData(short.MinValue)] - [InlineData(short.MaxValue)] - [InlineData(ushort.MinValue)] - [InlineData(ushort.MaxValue)] - [InlineData(int.MinValue)] - [InlineData(int.MaxValue)] - [InlineData(uint.MinValue)] - [InlineData(uint.MaxValue)] - [InlineData(long.MinValue)] - [InlineData(long.MaxValue)] - public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_ContainsScopeAttributeIntValue(object scopeValue) - { - // Arrange. - var logRecords = new List(1); - using var loggerFactory = LoggerFactory.Create(builder => + // Assert. + var logRecord = logRecords.Single(); + var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); + var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + Assert.Single(otlpLogRecord.Attributes); + var actualScope = TryGetAttribute(otlpLogRecord, scopeKey); + Assert.NotNull(actualScope); + Assert.Equal(scopeKey, actualScope.Key); + Assert.Equal(ValueOneofCase.BoolValue, actualScope.Value.ValueCase); + Assert.Equal(scopeValue.ToString(), actualScope.Value.BoolValue.ToString()); + } + + [Theory] + [InlineData(byte.MinValue)] + [InlineData(byte.MaxValue)] + [InlineData(sbyte.MinValue)] + [InlineData(sbyte.MaxValue)] + [InlineData(short.MinValue)] + [InlineData(short.MaxValue)] + [InlineData(ushort.MinValue)] + [InlineData(ushort.MaxValue)] + [InlineData(int.MinValue)] + [InlineData(int.MaxValue)] + [InlineData(uint.MinValue)] + [InlineData(uint.MaxValue)] + [InlineData(long.MinValue)] + [InlineData(long.MaxValue)] + public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_ContainsScopeAttributeIntValue(object scopeValue) + { + // Arrange. + var logRecords = new List(1); + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddOpenTelemetry(options => { - builder.AddOpenTelemetry(options => - { - options.IncludeScopes = true; - options.AddInMemoryExporter(logRecords); - }); + options.IncludeScopes = true; + options.AddInMemoryExporter(logRecords); }); - var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); + }); + var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); - const string scopeKey = "Some scope key"; - - // Act. - using (logger.BeginScope(new List> - { - new KeyValuePair(scopeKey, scopeValue), - })) - { - logger.LogInformation("Some log information message."); - } + const string scopeKey = "Some scope key"; - // Assert. - var logRecord = logRecords.Single(); - var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); - Assert.Equal(2, otlpLogRecord.Attributes.Count); - var actualScope = TryGetAttribute(otlpLogRecord, scopeKey); - Assert.NotNull(actualScope); - Assert.Equal(scopeKey, actualScope.Key); - Assert.Equal(ValueOneofCase.IntValue, actualScope.Value.ValueCase); - Assert.Equal(scopeValue.ToString(), actualScope.Value.IntValue.ToString()); + // Act. + using (logger.BeginScope(new List> + { + new KeyValuePair(scopeKey, scopeValue), + })) + { + logger.LogInformation("Some log information message."); } - [Theory] - [InlineData(float.MinValue)] - [InlineData(float.MaxValue)] - public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_ContainsScopeAttributeDoubleValueForFloat(float scopeValue) + // Assert. + var logRecord = logRecords.Single(); + var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); + var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + Assert.Single(otlpLogRecord.Attributes); + var actualScope = TryGetAttribute(otlpLogRecord, scopeKey); + Assert.NotNull(actualScope); + Assert.Equal(scopeKey, actualScope.Key); + Assert.Equal(ValueOneofCase.IntValue, actualScope.Value.ValueCase); + Assert.Equal(scopeValue.ToString(), actualScope.Value.IntValue.ToString()); + } + + [Theory] + [InlineData(float.MinValue)] + [InlineData(float.MaxValue)] + public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_ContainsScopeAttributeDoubleValueForFloat(float scopeValue) + { + // Arrange. + var logRecords = new List(1); + using var loggerFactory = LoggerFactory.Create(builder => { - // Arrange. - var logRecords = new List(1); - using var loggerFactory = LoggerFactory.Create(builder => + builder.AddOpenTelemetry(options => { - builder.AddOpenTelemetry(options => - { - options.IncludeScopes = true; - options.AddInMemoryExporter(logRecords); - }); + options.IncludeScopes = true; + options.AddInMemoryExporter(logRecords); }); - var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); + }); + var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); - const string scopeKey = "Some scope key"; - - // Act. - using (logger.BeginScope(new List> - { - new KeyValuePair(scopeKey, scopeValue), - })) - { - logger.LogInformation("Some log information message."); - } + const string scopeKey = "Some scope key"; - // Assert. - var logRecord = logRecords.Single(); - var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); - Assert.Equal(2, otlpLogRecord.Attributes.Count); - var actualScope = TryGetAttribute(otlpLogRecord, scopeKey); - Assert.NotNull(actualScope); - Assert.Equal(scopeKey, actualScope.Key); - Assert.Equal(ValueOneofCase.DoubleValue, actualScope.Value.ValueCase); - Assert.Equal(((double)scopeValue).ToString(), actualScope.Value.DoubleValue.ToString()); + // Act. + using (logger.BeginScope(new List> + { + new KeyValuePair(scopeKey, scopeValue), + })) + { + logger.LogInformation("Some log information message."); } - [Theory] - [InlineData(double.MinValue)] - [InlineData(double.MaxValue)] - public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_ContainsScopeAttributeDoubleValueForDouble(double scopeValue) + // Assert. + var logRecord = logRecords.Single(); + var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); + var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + Assert.Single(otlpLogRecord.Attributes); + var actualScope = TryGetAttribute(otlpLogRecord, scopeKey); + Assert.NotNull(actualScope); + Assert.Equal(scopeKey, actualScope.Key); + Assert.Equal(ValueOneofCase.DoubleValue, actualScope.Value.ValueCase); + Assert.Equal(((double)scopeValue).ToString(), actualScope.Value.DoubleValue.ToString()); + } + + [Theory] + [InlineData(double.MinValue)] + [InlineData(double.MaxValue)] + public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_ContainsScopeAttributeDoubleValueForDouble(double scopeValue) + { + // Arrange. + var logRecords = new List(1); + using var loggerFactory = LoggerFactory.Create(builder => { - // Arrange. - var logRecords = new List(1); - using var loggerFactory = LoggerFactory.Create(builder => + builder.AddOpenTelemetry(options => { - builder.AddOpenTelemetry(options => - { - options.IncludeScopes = true; - options.AddInMemoryExporter(logRecords); - }); + options.IncludeScopes = true; + options.AddInMemoryExporter(logRecords); }); - var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); + }); + var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); - const string scopeKey = "Some scope key"; + const string scopeKey = "Some scope key"; - // Act. - using (logger.BeginScope(new List> - { - new KeyValuePair(scopeKey, scopeValue), - })) - { - logger.LogInformation("Some log information message."); - } - - // Assert. - var logRecord = logRecords.Single(); - var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); - Assert.Equal(2, otlpLogRecord.Attributes.Count); - var actualScope = TryGetAttribute(otlpLogRecord, scopeKey); - Assert.NotNull(actualScope); - Assert.Equal(scopeKey, actualScope.Key); - Assert.Equal(scopeValue.ToString(), actualScope.Value.DoubleValue.ToString()); + // Act. + using (logger.BeginScope(new List> + { + new KeyValuePair(scopeKey, scopeValue), + })) + { + logger.LogInformation("Some log information message."); } - [Fact] - public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_AndScopeStateIsOfTypeString_ScopeIsIgnored() + // Assert. + var logRecord = logRecords.Single(); + var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); + var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + Assert.Single(otlpLogRecord.Attributes); + var actualScope = TryGetAttribute(otlpLogRecord, scopeKey); + Assert.NotNull(actualScope); + Assert.Equal(scopeKey, actualScope.Key); + Assert.Equal(scopeValue.ToString(), actualScope.Value.DoubleValue.ToString()); + } + + [Fact] + public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_AndScopeStateIsOfTypeString_ScopeIsIgnored() + { + // Arrange. + var logRecords = new List(1); + using var loggerFactory = LoggerFactory.Create(builder => { - // Arrange. - var logRecords = new List(1); - using var loggerFactory = LoggerFactory.Create(builder => + builder.AddOpenTelemetry(options => { - builder.AddOpenTelemetry(options => - { - options.IncludeScopes = true; - options.AddInMemoryExporter(logRecords); - }); + options.IncludeScopes = true; + options.AddInMemoryExporter(logRecords); }); - var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); - - const string scopeState = "Some scope state"; + }); + var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); - // Act. - using (logger.BeginScope(scopeState)) - { - logger.LogInformation("Some log information message."); - } + const string scopeState = "Some scope state"; - // Assert. - var logRecord = logRecords.Single(); - var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); - Assert.NotNull(otlpLogRecord); - Assert.Single(otlpLogRecord.Attributes); + // Act. + using (logger.BeginScope(scopeState)) + { + logger.LogInformation("Some log information message."); } - [Theory] - [InlineData(typeof(int))] - [InlineData(typeof(float))] - [InlineData(typeof(decimal))] - [InlineData(typeof(char))] - [InlineData(typeof(bool))] - public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_AndScopeStateIsOfPrimitiveTypes_ScopeIsIgnored(Type typeOfScopeState) - { - // Arrange. - var logRecords = new List(1); - using var loggerFactory = LoggerFactory.Create(builder => + // Assert. + var logRecord = logRecords.Single(); + var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); + var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + Assert.NotNull(otlpLogRecord); + Assert.Empty(otlpLogRecord.Attributes); + } + + [Theory] + [InlineData(typeof(int))] + [InlineData(typeof(float))] + [InlineData(typeof(decimal))] + [InlineData(typeof(char))] + [InlineData(typeof(bool))] + public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_AndScopeStateIsOfPrimitiveTypes_ScopeIsIgnored(Type typeOfScopeState) + { + // Arrange. + var logRecords = new List(1); + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddOpenTelemetry(options => { - builder.AddOpenTelemetry(options => - { - options.IncludeScopes = true; - options.AddInMemoryExporter(logRecords); - }); + options.IncludeScopes = true; + options.AddInMemoryExporter(logRecords); }); - var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); + }); + var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); - var scopeState = Activator.CreateInstance(typeOfScopeState); + var scopeState = Activator.CreateInstance(typeOfScopeState); + + // Act. + using (logger.BeginScope(scopeState)) + { + logger.LogInformation("Some log information message."); + } + + // Assert. + var logRecord = logRecords.Single(); + var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); + var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + Assert.NotNull(otlpLogRecord); + Assert.Empty(otlpLogRecord.Attributes); + } - // Act. - using (logger.BeginScope(scopeState)) + [Fact] + public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_AndScopeStateIsOfDictionaryType_ScopeIsProcessed() + { + // Arrange. + var logRecords = new List(1); + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddOpenTelemetry(options => { - logger.LogInformation("Some log information message."); - } + options.IncludeScopes = true; + options.AddInMemoryExporter(logRecords); + }); + }); + var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); + + const string scopeKey = "Some scope key"; + const string scopeValue = "Some scope value"; + var scopeState = new Dictionary() { { scopeKey, scopeValue } }; - // Assert. - var logRecord = logRecords.Single(); - var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); - Assert.NotNull(otlpLogRecord); - Assert.Single(otlpLogRecord.Attributes); + // Act. + using (logger.BeginScope(scopeState)) + { + logger.LogInformation("Some log information message."); } - [Fact] - public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_AndScopeStateIsOfDictionaryType_ScopeIsProcessed() + // Assert. + var logRecord = logRecords.Single(); + var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); + var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + Assert.Single(otlpLogRecord.Attributes); + var actualScope = TryGetAttribute(otlpLogRecord, scopeKey); + Assert.NotNull(actualScope); + Assert.Equal(scopeKey, actualScope.Key); + Assert.Equal(scopeValue, actualScope.Value.StringValue); + } + + [Theory] + [InlineData(typeof(List>))] + [InlineData(typeof(ReadOnlyCollection>))] + [InlineData(typeof(HashSet>))] + public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_AndScopeStateIsOfEnumerableType_ScopeIsProcessed(Type typeOfScopeState) + { + // Arrange. + var logRecords = new List(1); + using var loggerFactory = LoggerFactory.Create(builder => { - // Arrange. - var logRecords = new List(1); - using var loggerFactory = LoggerFactory.Create(builder => + builder.AddOpenTelemetry(options => { - builder.AddOpenTelemetry(options => - { - options.IncludeScopes = true; - options.AddInMemoryExporter(logRecords); - }); + options.IncludeScopes = true; + options.AddInMemoryExporter(logRecords); }); - var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); + }); + var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); + + const string scopeKey = "Some scope key"; + const string scopeValue = "Some scope value"; + var scopeValues = new List> { new KeyValuePair(scopeKey, scopeValue) }; + var scopeState = Activator.CreateInstance(typeOfScopeState, scopeValues) as ICollection>; + + // Act. + using (logger.BeginScope(scopeState)) + { + logger.LogInformation("Some log information message."); + } - const string scopeKey = "Some scope key"; - const string scopeValue = "Some scope value"; - var scopeState = new Dictionary() { { scopeKey, scopeValue } }; + // Assert. + var logRecord = logRecords.Single(); + var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); + var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + Assert.Single(otlpLogRecord.Attributes); + var actualScope = TryGetAttribute(otlpLogRecord, scopeKey); + Assert.NotNull(actualScope); + Assert.Equal(scopeKey, actualScope.Key); + Assert.Equal(scopeValue, actualScope.Value.StringValue); + } - // Act. - using (logger.BeginScope(scopeState)) + [Theory] + [InlineData("Same scope key", "Same scope key")] + [InlineData("Scope key 1", "Scope key 2")] + public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_AndMultipleScopesAreAdded_ContainsAllAddedScopeValues(string scopeKey1, string scopeKey2) + { + // Arrange. + var logRecords = new List(1); + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddOpenTelemetry(options => { - logger.LogInformation("Some log information message."); - } + options.IncludeScopes = true; + options.AddInMemoryExporter(logRecords); + }); + }); + var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); + + const string scopeValue1 = "Some scope value"; + const string scopeValue2 = "Some other scope value"; - // Assert. - var logRecord = logRecords.Single(); - var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); - Assert.Equal(2, otlpLogRecord.Attributes.Count); - var actualScope = TryGetAttribute(otlpLogRecord, scopeKey); - Assert.NotNull(actualScope); - Assert.Equal(scopeKey, actualScope.Key); - Assert.Equal(scopeValue, actualScope.Value.StringValue); + // Act. + using (logger.BeginScope(new List> + { + new KeyValuePair(scopeKey1, scopeValue1), + new KeyValuePair(scopeKey2, scopeValue2), + })) + { + logger.LogInformation("Some log information message."); } - [Theory] - [InlineData(typeof(List>))] - [InlineData(typeof(ReadOnlyCollection>))] - [InlineData(typeof(HashSet>))] - public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_AndScopeStateIsOfEnumerableType_ScopeIsProcessed(Type typeOfScopeState) + // Assert. + var logRecord = logRecords.Single(); + var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); + var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + var allScopeValues = otlpLogRecord.Attributes + .Where(_ => _.Key == scopeKey1 || _.Key == scopeKey2) + .Select(_ => _.Value.StringValue); + Assert.Equal(2, otlpLogRecord.Attributes.Count); + Assert.Equal(2, allScopeValues.Count()); + Assert.Contains(scopeValue1, allScopeValues); + Assert.Contains(scopeValue2, allScopeValues); + } + + [Theory] + [InlineData("Same scope key", "Same scope key")] + [InlineData("Scope key 1", "Scope key 2")] + public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_AndMultipleScopeLevelsAreAdded_ContainsAllAddedScopeValues(string scopeKey1, string scopeKey2) + { + // Arrange. + var logRecords = new List(1); + using var loggerFactory = LoggerFactory.Create(builder => { - // Arrange. - var logRecords = new List(1); - using var loggerFactory = LoggerFactory.Create(builder => + builder.AddOpenTelemetry(options => { - builder.AddOpenTelemetry(options => - { - options.IncludeScopes = true; - options.AddInMemoryExporter(logRecords); - }); + options.IncludeScopes = true; + options.AddInMemoryExporter(logRecords); }); - var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); + }); + var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); - const string scopeKey = "Some scope key"; - const string scopeValue = "Some scope value"; - var scopeValues = new List> { new KeyValuePair(scopeKey, scopeValue) }; - var scopeState = Activator.CreateInstance(typeOfScopeState, scopeValues) as ICollection>; + const string scopeValue1 = "Some scope value"; + const string scopeValue2 = "Some other scope value"; - // Act. - using (logger.BeginScope(scopeState)) + // Act. + using (logger.BeginScope(new List> { new KeyValuePair(scopeKey1, scopeValue1) })) + { + using (logger.BeginScope(new List> { new KeyValuePair(scopeKey2, scopeValue2) })) { logger.LogInformation("Some log information message."); } - - // Assert. - var logRecord = logRecords.Single(); - var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); - Assert.Equal(2, otlpLogRecord.Attributes.Count); - var actualScope = TryGetAttribute(otlpLogRecord, scopeKey); - Assert.NotNull(actualScope); - Assert.Equal(scopeKey, actualScope.Key); - Assert.Equal(scopeValue, actualScope.Value.StringValue); } - [Theory] - [InlineData("Same scope key", "Same scope key")] - [InlineData("Scope key 1", "Scope key 2")] - public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_AndMultipleScopesAreAdded_ContainsAllAddedScopeValues(string scopeKey1, string scopeKey2) + // Assert. + var logRecord = logRecords.Single(); + var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); + var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + var allScopeValues = otlpLogRecord.Attributes + .Where(_ => _.Key == scopeKey1 || _.Key == scopeKey2) + .Select(_ => _.Value.StringValue); + Assert.Equal(2, otlpLogRecord.Attributes.Count); + Assert.Equal(2, allScopeValues.Count()); + Assert.Contains(scopeValue1, allScopeValues); + Assert.Contains(scopeValue2, allScopeValues); + } + + [Theory] + [InlineData("Same scope key", "Same scope key")] + [InlineData("Scope key 1", "Scope key 2")] + public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_AndScopeIsUsedInLogMethod_ContainsAllAddedScopeValues(string scopeKey1, string scopeKey2) + { + // Arrange. + var logRecords = new List(1); + using var loggerFactory = LoggerFactory.Create(builder => { - // Arrange. - var logRecords = new List(1); - using var loggerFactory = LoggerFactory.Create(builder => + builder.AddOpenTelemetry(options => { - builder.AddOpenTelemetry(options => - { - options.IncludeScopes = true; - options.AddInMemoryExporter(logRecords); - }); + options.IncludeScopes = true; + options.AddInMemoryExporter(logRecords); }); - var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); + }); + var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); - const string scopeValue1 = "Some scope value"; - const string scopeValue2 = "Some other scope value"; + const string scopeValue1 = "Some scope value"; + const string scopeValue2 = "Some other scope value"; - // Act. - using (logger.BeginScope(new List> - { - new KeyValuePair(scopeKey1, scopeValue1), - new KeyValuePair(scopeKey2, scopeValue2), - })) - { - logger.LogInformation("Some log information message."); - } + // Act. + using (logger.BeginScope(new List> + { + new KeyValuePair(scopeKey1, scopeValue1), + })) + { + logger.Log( + LogLevel.Error, + new EventId(1), + new List> { new KeyValuePair(scopeKey2, scopeValue2) }, + exception: new Exception("Some exception message"), + formatter: (s, e) => string.Empty); + } + + // Assert. + var logRecord = logRecords.Single(); + var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); + var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + var allScopeValues = otlpLogRecord.Attributes + .Where(_ => _.Key == scopeKey1 || _.Key == scopeKey2) + .Select(_ => _.Value.StringValue); + Assert.Equal(5, otlpLogRecord.Attributes.Count); + Assert.Equal(2, allScopeValues.Count()); + Assert.Contains(scopeValue1, allScopeValues); + Assert.Contains(scopeValue2, allScopeValues); + } + + [Fact] + public void AddOtlpLogExporterDefaultOptionsTest() + { + var options = new OpenTelemetryLoggerOptions(); + + options.AddOtlpExporter(); - // Assert. - var logRecord = logRecords.Single(); - var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); - var allScopeValues = otlpLogRecord.Attributes - .Where(_ => _.Key == scopeKey1 || _.Key == scopeKey2) - .Select(_ => _.Value.StringValue); - Assert.Equal(3, otlpLogRecord.Attributes.Count); - Assert.Equal(2, allScopeValues.Count()); - Assert.Contains(scopeValue1, allScopeValues); - Assert.Contains(scopeValue2, allScopeValues); + var provider = new OpenTelemetryLoggerProvider(new TestOptionsMonitor(options)); + + var processor = GetProcessor(provider); + + Assert.NotNull(processor); + + var batchProcesor = processor as BatchLogRecordExportProcessor; + + Assert.NotNull(batchProcesor); + + var batchProcessorType = typeof(BatchExportProcessor); + + Assert.Equal(5000, batchProcessorType.GetField("scheduledDelayMilliseconds", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(batchProcesor)); + } + + [Theory] + [InlineData(ExportProcessorType.Simple)] + [InlineData(ExportProcessorType.Batch)] + public void AddOtlpLogExporterLogRecordProcessorOptionsTest(ExportProcessorType processorType) + { + var options = new OpenTelemetryLoggerOptions(); + + options.AddOtlpExporter((o, l) => + { + l.ExportProcessorType = processorType; + l.BatchExportProcessorOptions = new BatchExportLogRecordProcessorOptions() { ScheduledDelayMilliseconds = 1000 }; + }); + + var provider = new OpenTelemetryLoggerProvider(new TestOptionsMonitor(options)); + + var processor = GetProcessor(provider); + + Assert.NotNull(processor); + + if (processorType == ExportProcessorType.Batch) + { + var batchProcesor = processor as BatchLogRecordExportProcessor; + + Assert.NotNull(batchProcesor); + + var batchProcessorType = typeof(BatchExportProcessor); + + Assert.Equal(1000, batchProcessorType.GetField("scheduledDelayMilliseconds", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(batchProcesor)); + } + else + { + var simpleProcesor = processor as SimpleLogRecordExportProcessor; + + Assert.NotNull(simpleProcesor); } + } - [Theory] - [InlineData("Same scope key", "Same scope key")] - [InlineData("Scope key 1", "Scope key 2")] - public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_AndMultipleScopeLevelsAreAdded_ContainsAllAddedScopeValues(string scopeKey1, string scopeKey2) + [Fact] + public void ValidateInstrumentationScope() + { + var logRecords = new List(); + using var loggerFactory = LoggerFactory.Create(builder => { - // Arrange. - var logRecords = new List(1); - using var loggerFactory = LoggerFactory.Create(builder => + builder + .AddOpenTelemetry(options => options + .AddInMemoryExporter(logRecords)); + }); + + var logger1 = loggerFactory.CreateLogger("OtlpLogExporterTests-A"); + logger1.LogInformation("Hello from red-tomato"); + + var logger2 = loggerFactory.CreateLogger("OtlpLogExporterTests-B"); + logger2.LogInformation("Hello from green-tomato"); + + Assert.Equal(2, logRecords.Count); + + var batch = new Batch(logRecords.ToArray(), logRecords.Count); + var logRecordTransformer = new OtlpLogRecordTransformer(new(), new()); + + var resourceBuilder = ResourceBuilder.CreateEmpty(); + var processResource = resourceBuilder.Build().ToOtlpResource(); + + var request = logRecordTransformer.BuildExportRequest(processResource, batch); + + Assert.Single(request.ResourceLogs); + + var scope1 = request.ResourceLogs[0].ScopeLogs.First(); + var scope2 = request.ResourceLogs[0].ScopeLogs.Last(); + + Assert.Equal("OtlpLogExporterTests-A", scope1.Scope.Name); + Assert.Equal("OtlpLogExporterTests-B", scope2.Scope.Name); + + Assert.Single(scope1.LogRecords); + Assert.Single(scope2.LogRecords); + + var logrecord1 = scope1.LogRecords[0]; + var logrecord2 = scope2.LogRecords[0]; + + Assert.Equal("Hello from red-tomato", logrecord1.Body.StringValue); + + Assert.Equal("Hello from green-tomato", logrecord2.Body.StringValue); + + // Validate LogListPool + Assert.Empty(OtlpLogRecordTransformer.LogListPool); + logRecordTransformer.Return(request); + Assert.Equal(2, OtlpLogRecordTransformer.LogListPool.Count); + + request = logRecordTransformer.BuildExportRequest(processResource, batch); + + Assert.Single(request.ResourceLogs); + + // ScopeLogs will be reused. + Assert.Empty(OtlpLogRecordTransformer.LogListPool); + } + + [Theory] + [InlineData(null)] + [InlineData("logging")] + public void VerifyEnvironmentVariablesTakenFromIConfigurationWhenUsingLoggerFactoryCreate(string optionsName) + { + RunVerifyEnvironmentVariablesTakenFromIConfigurationTest( + optionsName, + configure => { - builder.AddOpenTelemetry(options => + var factory = LoggerFactory.Create(logging => { - options.IncludeScopes = true; - options.AddInMemoryExporter(logRecords); + configure(logging.Services); + + logging.AddOpenTelemetry(o => o.AddOtlpExporter(optionsName, configure: null)); }); - }); - var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); - const string scopeValue1 = "Some scope value"; - const string scopeValue2 = "Some other scope value"; + return (factory, factory); + }); + } - // Act. - using (logger.BeginScope(new List> { new KeyValuePair(scopeKey1, scopeValue1) })) + [Theory] + [InlineData(null)] + [InlineData("logging")] + public void VerifyEnvironmentVariablesTakenFromIConfigurationWhenUsingLoggingBuilder(string optionsName) + { + RunVerifyEnvironmentVariablesTakenFromIConfigurationTest( + optionsName, + configure => { - using (logger.BeginScope(new List> { new KeyValuePair(scopeKey2, scopeValue2) })) - { - logger.LogInformation("Some log information message."); - } - } + var services = new ServiceCollection(); + + configure(services); + + services.AddLogging( + logging => logging.AddOpenTelemetry(o => + o.AddOtlpExporter(optionsName, configure: null))); + + var sp = services.BuildServiceProvider(); + + var factory = sp.GetRequiredService(); - // Assert. - var logRecord = logRecords.Single(); - var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); - var allScopeValues = otlpLogRecord.Attributes - .Where(_ => _.Key == scopeKey1 || _.Key == scopeKey2) - .Select(_ => _.Value.StringValue); - Assert.Equal(3, otlpLogRecord.Attributes.Count); - Assert.Equal(2, allScopeValues.Count()); - Assert.Contains(scopeValue1, allScopeValues); - Assert.Contains(scopeValue2, allScopeValues); + return (sp, factory); + }); + } + + [Theory] + [InlineData("my_instrumentation_scope_name", "my_instrumentation_scope_name")] + [InlineData(null, "")] + public void LogRecordLoggerNameIsExportedWhenUsingBridgeApi(string loggerName, string expectedScopeName) + { + LogRecordAttributeList attributes = default; + attributes.Add("name", "tomato"); + attributes.Add("price", 2.99); + attributes.Add("{OriginalFormat}", "Hello from {name} {price}."); + + var logRecords = new List(); + + using (var loggerProvider = Sdk.CreateLoggerProviderBuilder() + .AddInMemoryExporter(logRecords) + .Build()) + { + var logger = loggerProvider.GetLogger(loggerName); + + logger.EmitLog(new LogRecordData()); } - [Theory] - [InlineData("Same scope key", "Same scope key")] - [InlineData("Scope key 1", "Scope key 2")] - public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_AndScopeIsUsedInLogMethod_ContainsAllAddedScopeValues(string scopeKey1, string scopeKey2) + Assert.Single(logRecords); + + var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); + + var batch = new Batch(new[] { logRecords[0] }, 1); + + var request = otlpLogRecordTransformer.BuildExportRequest( + new Proto.Resource.V1.Resource(), + batch); + + Assert.NotNull(request); + Assert.Single(request.ResourceLogs); + Assert.Single(request.ResourceLogs[0].ScopeLogs); + + Assert.Equal(expectedScopeName, request.ResourceLogs[0].ScopeLogs[0].Scope?.Name); + } + + private static void RunVerifyEnvironmentVariablesTakenFromIConfigurationTest( + string optionsName, + Func, (IDisposable Container, ILoggerFactory LoggerFactory)> createLoggerFactoryFunc) + { + var values = new Dictionary() + { + [OtlpExporterOptions.EndpointEnvVarName] = "http://test:8888", + }; + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(values) + .Build(); + + var configureDelegateCalled = false; + var configureExportProcessorOptionsCalled = false; + var configureBatchOptionsCalled = false; + + var tracingConfigureDelegateCalled = false; + var unnamedConfigureDelegateCalled = false; + var allConfigureDelegateCalled = false; + + var testState = createLoggerFactoryFunc(services => { - // Arrange. - var logRecords = new List(1); - using var loggerFactory = LoggerFactory.Create(builder => + services.AddSingleton(configuration); + + services.Configure(optionsName, o => { - builder.AddOpenTelemetry(options => - { - options.IncludeScopes = true; - options.AddInMemoryExporter(logRecords); - }); + configureDelegateCalled = true; + Assert.Equal(new Uri("http://test:8888"), o.Endpoint); + }); + + services.Configure(optionsName, o => + { + configureExportProcessorOptionsCalled = true; + }); + + services.Configure(optionsName, o => + { + configureBatchOptionsCalled = true; }); - var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); - const string scopeValue1 = "Some scope value"; - const string scopeValue2 = "Some other scope value"; + services.Configure("tracing", o => + { + tracingConfigureDelegateCalled = true; + }); - // Act. - using (logger.BeginScope(new List> + services.Configure(o => { - new KeyValuePair(scopeKey1, scopeValue1), - })) + unnamedConfigureDelegateCalled = true; + }); + + services.ConfigureAll(o => { - logger.Log( - LogLevel.Error, - new EventId(1), - new List> { new KeyValuePair(scopeKey2, scopeValue2) }, - exception: new Exception("Some exception message"), - formatter: (s, e) => string.Empty); - } + allConfigureDelegateCalled = true; + }); + }); + + using var container = testState.Container; + + var factory = testState.LoggerFactory; + + Assert.NotNull(factory); + + Assert.True(configureDelegateCalled); + Assert.True(configureExportProcessorOptionsCalled); + Assert.True(configureBatchOptionsCalled); + + Assert.False(tracingConfigureDelegateCalled); + + Assert.Equal(optionsName == null, unnamedConfigureDelegateCalled); - // Assert. - var logRecord = logRecords.Single(); - var otlpLogRecord = logRecord.ToOtlpLog(DefaultSdkLimitOptions); - var allScopeValues = otlpLogRecord.Attributes - .Where(_ => _.Key == scopeKey1 || _.Key == scopeKey2) - .Select(_ => _.Value.StringValue); - Assert.Equal(7, otlpLogRecord.Attributes.Count); - Assert.Equal(2, allScopeValues.Count()); - Assert.Contains(scopeValue1, allScopeValues); - Assert.Contains(scopeValue2, allScopeValues); + Assert.True(allConfigureDelegateCalled); + } + + private static OtlpCommon.KeyValue TryGetAttribute(OtlpLogs.LogRecord record, string key) + { + return record.Attributes.FirstOrDefault(att => att.Key == key); + } + + private static BaseProcessor GetProcessor(OpenTelemetryLoggerProvider provider) + { + var sdkProvider = typeof(OpenTelemetryLoggerProvider).GetField("Provider", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(provider); + + return (BaseProcessor)sdkProvider.GetType().GetProperty("Processor", BindingFlags.Instance | BindingFlags.Public).GetMethod.Invoke(sdkProvider, null); + } + + private sealed class TestOptionsMonitor : IOptionsMonitor + { + private readonly T instance; + + public TestOptionsMonitor(T instance) + { + this.instance = instance; } - private static OtlpCommon.KeyValue TryGetAttribute(OtlpLogs.LogRecord record, string key) + public T CurrentValue => this.instance; + + public T Get(string name) => this.instance; + + public IDisposable OnChange(Action listener) { - return record.Attributes.FirstOrDefault(att => att.Key == key); + throw new NotImplementedException(); } } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs index 465901e6c01..7322c4d90e5 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs @@ -1,20 +1,11 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics; using System.Diagnostics.Metrics; +using System.Reflection; +using Google.Protobuf; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; using OpenTelemetry.Metrics; @@ -25,694 +16,915 @@ using OtlpCollector = OpenTelemetry.Proto.Collector.Metrics.V1; using OtlpMetrics = OpenTelemetry.Proto.Metrics.V1; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +public class OtlpMetricsExporterTests : Http2UnencryptedSupportTests { - public class OtlpMetricsExporterTests : Http2UnencryptedSupportTests + private static readonly KeyValuePair[] KeyValues = new KeyValuePair[] + { + new KeyValuePair("key1", "value1"), + new KeyValuePair("key2", 123), + }; + + [Fact] + public void TestAddOtlpExporter_SetsCorrectMetricReaderDefaults() { - [Fact] - public void TestAddOtlpExporter_SetsCorrectMetricReaderDefaults() + if (Environment.Version.Major == 3) { - if (Environment.Version.Major == 3) - { - // Adding the OtlpExporter creates a GrpcChannel. - // This switch must be set before creating a GrpcChannel when calling an insecure HTTP/2 endpoint. - // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - } + // Adding the OtlpExporter creates a GrpcChannel. + // This switch must be set before creating a GrpcChannel when calling an insecure HTTP/2 endpoint. + // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + } - var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddOtlpExporter() - .Build(); + var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddOtlpExporter() + .Build(); - CheckMetricReaderDefaults(); + CheckMetricReaderDefaults(); - meterProvider.Dispose(); + meterProvider.Dispose(); - meterProvider = Sdk.CreateMeterProviderBuilder() - .AddOtlpExporter() - .Build(); + meterProvider = Sdk.CreateMeterProviderBuilder() + .AddOtlpExporter() + .Build(); - CheckMetricReaderDefaults(); + CheckMetricReaderDefaults(); - meterProvider.Dispose(); + meterProvider.Dispose(); - void CheckMetricReaderDefaults() - { - var bindingFlags = System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance; + void CheckMetricReaderDefaults() + { + var bindingFlags = BindingFlags.NonPublic | BindingFlags.Instance; - var metricReader = typeof(MetricReader) - .Assembly - .GetType("OpenTelemetry.Metrics.MeterProviderSdk") - .GetField("reader", bindingFlags) - .GetValue(meterProvider) as PeriodicExportingMetricReader; + var metricReader = typeof(MetricReader) + .Assembly + .GetType("OpenTelemetry.Metrics.MeterProviderSdk") + .GetField("reader", bindingFlags) + .GetValue(meterProvider) as PeriodicExportingMetricReader; - Assert.NotNull(metricReader); + Assert.NotNull(metricReader); - var exportIntervalMilliseconds = (int)typeof(PeriodicExportingMetricReader) - .GetField("ExportIntervalMilliseconds", bindingFlags) - .GetValue(metricReader); + var exportIntervalMilliseconds = (int)typeof(PeriodicExportingMetricReader) + .GetField("ExportIntervalMilliseconds", bindingFlags) + .GetValue(metricReader); - Assert.Equal(60000, exportIntervalMilliseconds); - } + Assert.Equal(60000, exportIntervalMilliseconds); } + } + + [Fact] + public void TestAddOtlpExporter_NamedOptions() + { + int defaultExporterOptionsConfigureOptionsInvocations = 0; + int namedExporterOptionsConfigureOptionsInvocations = 0; + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.Configure(o => defaultExporterOptionsConfigureOptionsInvocations++); + services.Configure(o => defaultExporterOptionsConfigureOptionsInvocations++); + + services.Configure("Exporter2", o => namedExporterOptionsConfigureOptionsInvocations++); + services.Configure("Exporter2", o => namedExporterOptionsConfigureOptionsInvocations++); - [Fact] - public void TestAddOtlpExporter_NamedOptions() + services.Configure("Exporter3", o => namedExporterOptionsConfigureOptionsInvocations++); + services.Configure("Exporter3", o => namedExporterOptionsConfigureOptionsInvocations++); + }) + .AddOtlpExporter() + .AddOtlpExporter("Exporter2", o => { }) + .AddOtlpExporter("Exporter3", (eo, ro) => { }) + .Build(); + + Assert.Equal(2, defaultExporterOptionsConfigureOptionsInvocations); + Assert.Equal(4, namedExporterOptionsConfigureOptionsInvocations); + } + + [Fact] + public void UserHttpFactoryCalled() + { + OtlpExporterOptions options = new OtlpExporterOptions(); + + var defaultFactory = options.HttpClientFactory; + + int invocations = 0; + options.Protocol = OtlpExportProtocol.HttpProtobuf; + options.HttpClientFactory = () => { - int defaultExporterOptionsConfigureOptionsInvocations = 0; - int namedExporterOptionsConfigureOptionsInvocations = 0; + invocations++; + return defaultFactory(); + }; - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.Configure(o => defaultExporterOptionsConfigureOptionsInvocations++); - services.Configure(o => defaultExporterOptionsConfigureOptionsInvocations++); - - services.Configure("Exporter2", o => namedExporterOptionsConfigureOptionsInvocations++); - services.Configure("Exporter2", o => namedExporterOptionsConfigureOptionsInvocations++); - - services.Configure("Exporter3", o => namedExporterOptionsConfigureOptionsInvocations++); - services.Configure("Exporter3", o => namedExporterOptionsConfigureOptionsInvocations++); - }) - .AddOtlpExporter() - .AddOtlpExporter("Exporter2", o => { }) - .AddOtlpExporter("Exporter3", (eo, ro) => { }) - .Build(); + using (var exporter = new OtlpMetricExporter(options)) + { + Assert.Equal(1, invocations); + } - Assert.Equal(2, defaultExporterOptionsConfigureOptionsInvocations); - Assert.Equal(4, namedExporterOptionsConfigureOptionsInvocations); + using (var provider = Sdk.CreateMeterProviderBuilder() + .AddOtlpExporter(o => + { + o.Protocol = OtlpExportProtocol.HttpProtobuf; + o.HttpClientFactory = options.HttpClientFactory; + }) + .Build()) + { + Assert.Equal(2, invocations); } - [Fact] - public void UserHttpFactoryCalled() + options.HttpClientFactory = null; + Assert.Throws(() => { - OtlpExporterOptions options = new OtlpExporterOptions(); + using var exporter = new OtlpMetricExporter(options); + }); - var defaultFactory = options.HttpClientFactory; + options.HttpClientFactory = () => null; + Assert.Throws(() => + { + using var exporter = new OtlpMetricExporter(options); + }); + } - int invocations = 0; - options.Protocol = OtlpExportProtocol.HttpProtobuf; - options.HttpClientFactory = () => - { - invocations++; - return defaultFactory(); - }; + [Fact] + public void ServiceProviderHttpClientFactoryInvoked() + { + IServiceCollection services = new ServiceCollection(); - using (var exporter = new OtlpMetricExporter(options)) - { - Assert.Equal(1, invocations); - } + services.AddHttpClient(); - using (var provider = Sdk.CreateMeterProviderBuilder() - .AddOtlpExporter(o => - { - o.Protocol = OtlpExportProtocol.HttpProtobuf; - o.HttpClientFactory = options.HttpClientFactory; - }) - .Build()) - { - Assert.Equal(2, invocations); - } + int invocations = 0; - options.HttpClientFactory = null; - Assert.Throws(() => - { - using var exporter = new OtlpMetricExporter(options); - }); + services.AddHttpClient("OtlpMetricExporter", configureClient: (client) => invocations++); - options.HttpClientFactory = () => null; - Assert.Throws(() => - { - using var exporter = new OtlpMetricExporter(options); - }); - } + services.AddOpenTelemetry().WithMetrics(builder => builder + .AddOtlpExporter(o => o.Protocol = OtlpExportProtocol.HttpProtobuf)); - [Fact] - public void ServiceProviderHttpClientFactoryInvoked() + using var serviceProvider = services.BuildServiceProvider(); + + var meterProvider = serviceProvider.GetRequiredService(); + + Assert.Equal(1, invocations); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ToOtlpResourceMetricsTest(bool includeServiceNameInResource) + { + var resourceBuilder = ResourceBuilder.CreateEmpty(); + if (includeServiceNameInResource) { - IServiceCollection services = new ServiceCollection(); + resourceBuilder.AddAttributes( + new List> + { + new KeyValuePair(ResourceSemanticConventions.AttributeServiceName, "service-name"), + new KeyValuePair(ResourceSemanticConventions.AttributeServiceNamespace, "ns1"), + }); + } - services.AddHttpClient(); + var metrics = new List(); - int invocations = 0; + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{includeServiceNameInResource}", "0.0.1"); + using var provider = Sdk.CreateMeterProviderBuilder() + .SetResourceBuilder(resourceBuilder) + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); - services.AddHttpClient("OtlpMetricExporter", configureClient: (client) => invocations++); + var counter = meter.CreateCounter("counter"); + counter.Add(100); - services.AddOpenTelemetry().WithMetrics(builder => builder - .AddOtlpExporter(o => o.Protocol = OtlpExportProtocol.HttpProtobuf)); + provider.ForceFlush(); - using var serviceProvider = services.BuildServiceProvider(); + var batch = new Batch(metrics.ToArray(), metrics.Count); - var meterProvider = serviceProvider.GetRequiredService(); + var request = new OtlpCollector.ExportMetricsServiceRequest(); + request.AddMetrics(resourceBuilder.Build().ToOtlpResource(), batch); - Assert.Equal(1, invocations); + Assert.Single(request.ResourceMetrics); + var resourceMetric = request.ResourceMetrics.First(); + var otlpResource = resourceMetric.Resource; + + if (includeServiceNameInResource) + { + Assert.Contains(otlpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.StringValue == "service-name"); + Assert.Contains(otlpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceNamespace && kvp.Value.StringValue == "ns1"); + } + else + { + Assert.Contains(otlpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.ToString().Contains("unknown_service:")); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ToOtlpResourceMetricsTest(bool includeServiceNameInResource) + Assert.Single(resourceMetric.ScopeMetrics); + var instrumentationLibraryMetrics = resourceMetric.ScopeMetrics.First(); + Assert.Equal(string.Empty, instrumentationLibraryMetrics.SchemaUrl); + Assert.Equal(meter.Name, instrumentationLibraryMetrics.Scope.Name); + Assert.Equal("0.0.1", instrumentationLibraryMetrics.Scope.Version); + } + + [Theory] + [InlineData("test_gauge", null, null, 123L, null)] + [InlineData("test_gauge", null, null, null, 123.45)] + [InlineData("test_gauge", null, null, 123L, null, true)] + [InlineData("test_gauge", null, null, null, 123.45, true)] + [InlineData("test_gauge", "description", "unit", 123L, null)] + public void TestGaugeToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, bool enableExemplars = false) + { + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .SetExemplarFilter(enableExemplars ? ExemplarFilterType.AlwaysOn : ExemplarFilterType.AlwaysOff) + .AddInMemoryExporter(metrics) + .Build(); + + if (longValue.HasValue) { - var resourceBuilder = ResourceBuilder.CreateEmpty(); - if (includeServiceNameInResource) - { - resourceBuilder.AddAttributes( - new List> - { - new KeyValuePair(ResourceSemanticConventions.AttributeServiceName, "service-name"), - new KeyValuePair(ResourceSemanticConventions.AttributeServiceNamespace, "ns1"), - }); - } + meter.CreateObservableGauge(name, () => longValue.Value, unit, description); + } + else + { + meter.CreateObservableGauge(name, () => doubleValue.Value, unit, description); + } - var metrics = new List(); + provider.ForceFlush(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{includeServiceNameInResource}", "0.0.1"); - using var provider = Sdk.CreateMeterProviderBuilder() - .SetResourceBuilder(resourceBuilder) - .AddMeter(meter.Name) - .AddInMemoryExporter(metrics) - .Build(); + var batch = new Batch(metrics.ToArray(), metrics.Count); - var counter = meter.CreateCounter("counter"); - counter.Add(100); + var request = new OtlpCollector.ExportMetricsServiceRequest(); + request.AddMetrics(ResourceBuilder.CreateEmpty().Build().ToOtlpResource(), batch); - provider.ForceFlush(); + var resourceMetric = request.ResourceMetrics.Single(); + var scopeMetrics = resourceMetric.ScopeMetrics.Single(); + var actual = scopeMetrics.Metrics.Single(); - var batch = new Batch(metrics.ToArray(), metrics.Count); + Assert.Equal(name, actual.Name); + Assert.Equal(description ?? string.Empty, actual.Description); + Assert.Equal(unit ?? string.Empty, actual.Unit); - var request = new OtlpCollector.ExportMetricsServiceRequest(); - request.AddMetrics(resourceBuilder.Build().ToOtlpResource(), batch); + Assert.Equal(OtlpMetrics.Metric.DataOneofCase.Gauge, actual.DataCase); - Assert.Single(request.ResourceMetrics); - var resourceMetric = request.ResourceMetrics.First(); - var otlpResource = resourceMetric.Resource; + Assert.NotNull(actual.Gauge); + Assert.Null(actual.Sum); + Assert.Null(actual.Histogram); + Assert.Null(actual.ExponentialHistogram); + Assert.Null(actual.Summary); - if (includeServiceNameInResource) - { - Assert.Contains(otlpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.StringValue == "service-name"); - Assert.Contains(otlpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceNamespace && kvp.Value.StringValue == "ns1"); - } - else - { - Assert.Contains(otlpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.ToString().Contains("unknown_service:")); - } + Assert.Single(actual.Gauge.DataPoints); + var dataPoint = actual.Gauge.DataPoints.First(); + Assert.True(dataPoint.StartTimeUnixNano > 0); + Assert.True(dataPoint.TimeUnixNano > 0); - Assert.Single(resourceMetric.ScopeMetrics); - var instrumentationLibraryMetrics = resourceMetric.ScopeMetrics.First(); - Assert.Equal(string.Empty, instrumentationLibraryMetrics.SchemaUrl); - Assert.Equal(meter.Name, instrumentationLibraryMetrics.Scope.Name); - Assert.Equal("0.0.1", instrumentationLibraryMetrics.Scope.Version); + if (longValue.HasValue) + { + Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsInt, dataPoint.ValueCase); + Assert.Equal(longValue, dataPoint.AsInt); } - - [Theory] - [InlineData("test_gauge", null, null, 123L, null)] - [InlineData("test_gauge", null, null, null, 123.45)] - [InlineData("test_gauge", "description", "unit", 123L, null)] - public void TestGaugeToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue) + else { - var metrics = new List(); + Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsDouble, dataPoint.ValueCase); + Assert.Equal(doubleValue, dataPoint.AsDouble); + } - using var meter = new Meter(Utils.GetCurrentMethodName()); - using var provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(metrics) - .Build(); + Assert.Empty(dataPoint.Attributes); - if (longValue.HasValue) - { - meter.CreateObservableGauge(name, () => longValue.Value, unit, description); - } - else + VerifyExemplars(longValue, doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); + } + + [Theory] + [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_counter", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative, false, true)] + [InlineData("test_counter", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative, false, true)] + [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta)] + [InlineData("test_counter", null, null, null, 123.45, MetricReaderTemporalityPreference.Delta)] + [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, false, true)] + [InlineData("test_counter", null, null, null, 123.45, MetricReaderTemporalityPreference.Delta, false, true)] + [InlineData("test_counter", "description", "unit", 123L, null, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, true)] + public void TestCounterToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, bool enableKeyValues = false, bool enableExemplars = false) + { + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .SetExemplarFilter(enableExemplars ? ExemplarFilterType.AlwaysOn : ExemplarFilterType.AlwaysOff) + .AddInMemoryExporter(metrics, metricReaderOptions => { - meter.CreateObservableGauge(name, () => doubleValue.Value, unit, description); - } + metricReaderOptions.TemporalityPreference = aggregationTemporality; + }) + .Build(); - provider.ForceFlush(); + var attributes = enableKeyValues ? KeyValues : Array.Empty>(); + if (longValue.HasValue) + { + var counter = meter.CreateCounter(name, unit, description); + counter.Add(longValue.Value, attributes); + } + else + { + var counter = meter.CreateCounter(name, unit, description); + counter.Add(doubleValue.Value, attributes); + } - var batch = new Batch(metrics.ToArray(), metrics.Count); + provider.ForceFlush(); - var request = new OtlpCollector.ExportMetricsServiceRequest(); - request.AddMetrics(ResourceBuilder.CreateEmpty().Build().ToOtlpResource(), batch); + var batch = new Batch(metrics.ToArray(), metrics.Count); - var resourceMetric = request.ResourceMetrics.Single(); - var scopeMetrics = resourceMetric.ScopeMetrics.Single(); - var actual = scopeMetrics.Metrics.Single(); + var request = new OtlpCollector.ExportMetricsServiceRequest(); + request.AddMetrics(ResourceBuilder.CreateEmpty().Build().ToOtlpResource(), batch); - Assert.Equal(name, actual.Name); - Assert.Equal(description ?? string.Empty, actual.Description); - Assert.Equal(unit ?? string.Empty, actual.Unit); + var resourceMetric = request.ResourceMetrics.Single(); + var scopeMetrics = resourceMetric.ScopeMetrics.Single(); + var actual = scopeMetrics.Metrics.Single(); - Assert.Equal(OtlpMetrics.Metric.DataOneofCase.Gauge, actual.DataCase); + Assert.Equal(name, actual.Name); + Assert.Equal(description ?? string.Empty, actual.Description); + Assert.Equal(unit ?? string.Empty, actual.Unit); - Assert.NotNull(actual.Gauge); - Assert.Null(actual.Sum); - Assert.Null(actual.Histogram); - Assert.Null(actual.ExponentialHistogram); - Assert.Null(actual.Summary); + Assert.Equal(OtlpMetrics.Metric.DataOneofCase.Sum, actual.DataCase); - Assert.Single(actual.Gauge.DataPoints); - var dataPoint = actual.Gauge.DataPoints.First(); - Assert.True(dataPoint.StartTimeUnixNano > 0); - Assert.True(dataPoint.TimeUnixNano > 0); + Assert.Null(actual.Gauge); + Assert.NotNull(actual.Sum); + Assert.Null(actual.Histogram); + Assert.Null(actual.ExponentialHistogram); + Assert.Null(actual.Summary); - if (longValue.HasValue) - { - Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsInt, dataPoint.ValueCase); - Assert.Equal(longValue, dataPoint.AsInt); - } - else - { - Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsDouble, dataPoint.ValueCase); - Assert.Equal(doubleValue, dataPoint.AsDouble); - } + Assert.True(actual.Sum.IsMonotonic); - Assert.Empty(dataPoint.Attributes); + var otlpAggregationTemporality = aggregationTemporality == MetricReaderTemporalityPreference.Cumulative + ? OtlpMetrics.AggregationTemporality.Cumulative + : OtlpMetrics.AggregationTemporality.Delta; + Assert.Equal(otlpAggregationTemporality, actual.Sum.AggregationTemporality); - Assert.Empty(dataPoint.Exemplars); + Assert.Single(actual.Sum.DataPoints); + var dataPoint = actual.Sum.DataPoints.First(); + Assert.True(dataPoint.StartTimeUnixNano > 0); + Assert.True(dataPoint.TimeUnixNano > 0); + + if (longValue.HasValue) + { + Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsInt, dataPoint.ValueCase); + Assert.Equal(longValue, dataPoint.AsInt); + } + else + { + Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsDouble, dataPoint.ValueCase); + Assert.Equal(doubleValue, dataPoint.AsDouble); } - [Theory] - [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_counter", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta)] - [InlineData("test_counter", "description", "unit", 123L, null, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, "key1", "value1", "key2", 123)] - public void TestCounterToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, params object[] keysValues) + if (attributes.Length > 0) { - var metrics = new List(); + OtlpTestHelpers.AssertOtlpAttributes(attributes, dataPoint.Attributes); + } + else + { + Assert.Empty(dataPoint.Attributes); + } - using var meter = new Meter(Utils.GetCurrentMethodName()); - using var provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(metrics, metricReaderOptions => - { - metricReaderOptions.TemporalityPreference = aggregationTemporality; - }) - .Build(); + VerifyExemplars(longValue, doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); + } - var attributes = ToAttributes(keysValues).ToArray(); - if (longValue.HasValue) - { - var counter = meter.CreateCounter(name, unit, description); - counter.Add(longValue.Value, attributes); - } - else - { - var counter = meter.CreateCounter(name, unit, description); - counter.Add(doubleValue.Value, attributes); - } + [Theory] + [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_counter", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative, false, true)] + [InlineData("test_counter", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative, false, true)] + [InlineData("test_counter", null, null, -123L, null, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_counter", null, null, null, -123.45, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta)] + [InlineData("test_counter", null, null, null, -123.45, MetricReaderTemporalityPreference.Delta)] + [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, false, true)] + [InlineData("test_counter", null, null, null, -123.45, MetricReaderTemporalityPreference.Delta, false, true)] + [InlineData("test_counter", "description", "unit", 123L, null, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, true)] + public void TestUpDownCounterToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, bool enableKeyValues = false, bool enableExemplars = false) + { + var metrics = new List(); - provider.ForceFlush(); + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .SetExemplarFilter(enableExemplars ? ExemplarFilterType.AlwaysOn : ExemplarFilterType.AlwaysOff) + .AddInMemoryExporter(metrics, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = aggregationTemporality; + }) + .Build(); - var batch = new Batch(metrics.ToArray(), metrics.Count); + var attributes = enableKeyValues ? KeyValues : Array.Empty>(); + if (longValue.HasValue) + { + var counter = meter.CreateUpDownCounter(name, unit, description); + counter.Add(longValue.Value, attributes); + } + else + { + var counter = meter.CreateUpDownCounter(name, unit, description); + counter.Add(doubleValue.Value, attributes); + } - var request = new OtlpCollector.ExportMetricsServiceRequest(); - request.AddMetrics(ResourceBuilder.CreateEmpty().Build().ToOtlpResource(), batch); + provider.ForceFlush(); - var resourceMetric = request.ResourceMetrics.Single(); - var scopeMetrics = resourceMetric.ScopeMetrics.Single(); - var actual = scopeMetrics.Metrics.Single(); + var batch = new Batch(metrics.ToArray(), metrics.Count); - Assert.Equal(name, actual.Name); - Assert.Equal(description ?? string.Empty, actual.Description); - Assert.Equal(unit ?? string.Empty, actual.Unit); + var request = new OtlpCollector.ExportMetricsServiceRequest(); + request.AddMetrics(ResourceBuilder.CreateEmpty().Build().ToOtlpResource(), batch); - Assert.Equal(OtlpMetrics.Metric.DataOneofCase.Sum, actual.DataCase); + var resourceMetric = request.ResourceMetrics.Single(); + var scopeMetrics = resourceMetric.ScopeMetrics.Single(); + var actual = scopeMetrics.Metrics.Single(); - Assert.Null(actual.Gauge); - Assert.NotNull(actual.Sum); - Assert.Null(actual.Histogram); - Assert.Null(actual.ExponentialHistogram); - Assert.Null(actual.Summary); + Assert.Equal(name, actual.Name); + Assert.Equal(description ?? string.Empty, actual.Description); + Assert.Equal(unit ?? string.Empty, actual.Unit); - Assert.True(actual.Sum.IsMonotonic); + Assert.Equal(OtlpMetrics.Metric.DataOneofCase.Sum, actual.DataCase); - var otlpAggregationTemporality = aggregationTemporality == MetricReaderTemporalityPreference.Cumulative - ? OtlpMetrics.AggregationTemporality.Cumulative - : OtlpMetrics.AggregationTemporality.Delta; - Assert.Equal(otlpAggregationTemporality, actual.Sum.AggregationTemporality); + Assert.Null(actual.Gauge); + Assert.NotNull(actual.Sum); + Assert.Null(actual.Histogram); + Assert.Null(actual.ExponentialHistogram); + Assert.Null(actual.Summary); - Assert.Single(actual.Sum.DataPoints); - var dataPoint = actual.Sum.DataPoints.First(); - Assert.True(dataPoint.StartTimeUnixNano > 0); - Assert.True(dataPoint.TimeUnixNano > 0); + Assert.False(actual.Sum.IsMonotonic); - if (longValue.HasValue) - { - Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsInt, dataPoint.ValueCase); - Assert.Equal(longValue, dataPoint.AsInt); - } - else - { - Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsDouble, dataPoint.ValueCase); - Assert.Equal(doubleValue, dataPoint.AsDouble); - } + var otlpAggregationTemporality = aggregationTemporality == MetricReaderTemporalityPreference.Cumulative + ? OtlpMetrics.AggregationTemporality.Cumulative + : OtlpMetrics.AggregationTemporality.Cumulative; + Assert.Equal(otlpAggregationTemporality, actual.Sum.AggregationTemporality); - if (attributes.Length > 0) - { - OtlpTestHelpers.AssertOtlpAttributes(attributes, dataPoint.Attributes); - } - else - { - Assert.Empty(dataPoint.Attributes); - } + Assert.Single(actual.Sum.DataPoints); + var dataPoint = actual.Sum.DataPoints.First(); + Assert.True(dataPoint.StartTimeUnixNano > 0); + Assert.True(dataPoint.TimeUnixNano > 0); - Assert.Empty(dataPoint.Exemplars); + if (longValue.HasValue) + { + Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsInt, dataPoint.ValueCase); + Assert.Equal(longValue, dataPoint.AsInt); + } + else + { + Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsDouble, dataPoint.ValueCase); + Assert.Equal(doubleValue, dataPoint.AsDouble); } - [Theory] - [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_counter", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_counter", null, null, -123L, null, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_counter", null, null, null, -123.45, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta)] - [InlineData("test_counter", "description", "unit", 123L, null, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, "key1", "value1", "key2", 123)] - public void TestUpDownCounterToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, params object[] keysValues) + if (attributes.Length > 0) + { + OtlpTestHelpers.AssertOtlpAttributes(attributes, dataPoint.Attributes); + } + else { - var metrics = new List(); + Assert.Empty(dataPoint.Attributes); + } - using var meter = new Meter(Utils.GetCurrentMethodName()); - using var provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(metrics, metricReaderOptions => - { - metricReaderOptions.TemporalityPreference = aggregationTemporality; - }) - .Build(); + VerifyExemplars(longValue, doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); + } - var attributes = ToAttributes(keysValues).ToArray(); - if (longValue.HasValue) + [Theory] + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative, false, true)] + [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative, false, true)] + [InlineData("test_histogram", null, null, -123L, null, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_histogram", null, null, null, -123.45, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta)] + [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Delta)] + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, false, true)] + [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Delta, false, true)] + [InlineData("test_histogram", "description", "unit", 123L, null, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, true)] + public void TestExponentialHistogramToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, bool enableKeyValues = false, bool enableExemplars = false) + { + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .SetExemplarFilter(enableExemplars ? ExemplarFilterType.AlwaysOn : ExemplarFilterType.AlwaysOff) + .AddInMemoryExporter(metrics, metricReaderOptions => { - var counter = meter.CreateUpDownCounter(name, unit, description); - counter.Add(longValue.Value, attributes); - } - else + metricReaderOptions.TemporalityPreference = aggregationTemporality; + }) + .AddView(instrument => { - var counter = meter.CreateUpDownCounter(name, unit, description); - counter.Add(doubleValue.Value, attributes); - } + return new Base2ExponentialBucketHistogramConfiguration(); + }) + .Build(); - provider.ForceFlush(); + var attributes = enableKeyValues ? KeyValues : Array.Empty>(); + if (longValue.HasValue) + { + var histogram = meter.CreateHistogram(name, unit, description); + histogram.Record(longValue.Value, attributes); + histogram.Record(0, attributes); + } + else + { + var histogram = meter.CreateHistogram(name, unit, description); + histogram.Record(doubleValue.Value, attributes); + histogram.Record(0, attributes); + } - var batch = new Batch(metrics.ToArray(), metrics.Count); + provider.ForceFlush(); - var request = new OtlpCollector.ExportMetricsServiceRequest(); - request.AddMetrics(ResourceBuilder.CreateEmpty().Build().ToOtlpResource(), batch); + var batch = new Batch(metrics.ToArray(), metrics.Count); - var resourceMetric = request.ResourceMetrics.Single(); - var scopeMetrics = resourceMetric.ScopeMetrics.Single(); - var actual = scopeMetrics.Metrics.Single(); + var request = new OtlpCollector.ExportMetricsServiceRequest(); + request.AddMetrics(ResourceBuilder.CreateEmpty().Build().ToOtlpResource(), batch); - Assert.Equal(name, actual.Name); - Assert.Equal(description ?? string.Empty, actual.Description); - Assert.Equal(unit ?? string.Empty, actual.Unit); + var resourceMetric = request.ResourceMetrics.Single(); + var scopeMetrics = resourceMetric.ScopeMetrics.Single(); + var actual = scopeMetrics.Metrics.Single(); - Assert.Equal(OtlpMetrics.Metric.DataOneofCase.Sum, actual.DataCase); + Assert.Equal(name, actual.Name); + Assert.Equal(description ?? string.Empty, actual.Description); + Assert.Equal(unit ?? string.Empty, actual.Unit); - Assert.Null(actual.Gauge); - Assert.NotNull(actual.Sum); - Assert.Null(actual.Histogram); - Assert.Null(actual.ExponentialHistogram); - Assert.Null(actual.Summary); + Assert.Equal(OtlpMetrics.Metric.DataOneofCase.ExponentialHistogram, actual.DataCase); - Assert.False(actual.Sum.IsMonotonic); + Assert.Null(actual.Gauge); + Assert.Null(actual.Sum); + Assert.Null(actual.Histogram); + Assert.NotNull(actual.ExponentialHistogram); + Assert.Null(actual.Summary); - var otlpAggregationTemporality = aggregationTemporality == MetricReaderTemporalityPreference.Cumulative - ? OtlpMetrics.AggregationTemporality.Cumulative - : OtlpMetrics.AggregationTemporality.Cumulative; - Assert.Equal(otlpAggregationTemporality, actual.Sum.AggregationTemporality); + var otlpAggregationTemporality = aggregationTemporality == MetricReaderTemporalityPreference.Cumulative + ? OtlpMetrics.AggregationTemporality.Cumulative + : OtlpMetrics.AggregationTemporality.Delta; + Assert.Equal(otlpAggregationTemporality, actual.ExponentialHistogram.AggregationTemporality); - Assert.Single(actual.Sum.DataPoints); - var dataPoint = actual.Sum.DataPoints.First(); - Assert.True(dataPoint.StartTimeUnixNano > 0); - Assert.True(dataPoint.TimeUnixNano > 0); + Assert.Single(actual.ExponentialHistogram.DataPoints); + var dataPoint = actual.ExponentialHistogram.DataPoints.First(); + Assert.True(dataPoint.StartTimeUnixNano > 0); + Assert.True(dataPoint.TimeUnixNano > 0); - if (longValue.HasValue) + Assert.Equal(20, dataPoint.Scale); + Assert.Equal(1UL, dataPoint.ZeroCount); + if (longValue > 0 || doubleValue > 0) + { + Assert.Equal(2UL, dataPoint.Count); + } + else + { + Assert.Equal(1UL, dataPoint.Count); + } + + if (longValue.HasValue) + { + if (longValue > 0) { - Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsInt, dataPoint.ValueCase); - Assert.Equal(longValue, dataPoint.AsInt); + Assert.Equal((double)longValue, dataPoint.Sum); + Assert.Null(dataPoint.Negative); + Assert.True(dataPoint.Positive.Offset > 0); + Assert.Equal(1UL, dataPoint.Positive.BucketCounts[0]); } else { - Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsDouble, dataPoint.ValueCase); - Assert.Equal(doubleValue, dataPoint.AsDouble); + Assert.Equal(0, dataPoint.Sum); + Assert.Null(dataPoint.Negative); + Assert.True(dataPoint.Positive.Offset == 0); + Assert.Empty(dataPoint.Positive.BucketCounts); } - - if (attributes.Length > 0) + } + else + { + if (doubleValue > 0) { - OtlpTestHelpers.AssertOtlpAttributes(attributes, dataPoint.Attributes); + Assert.Equal(doubleValue, dataPoint.Sum); + Assert.Null(dataPoint.Negative); + Assert.True(dataPoint.Positive.Offset > 0); + Assert.Equal(1UL, dataPoint.Positive.BucketCounts[0]); } else { - Assert.Empty(dataPoint.Attributes); + Assert.Equal(0, dataPoint.Sum); + Assert.Null(dataPoint.Negative); + Assert.True(dataPoint.Positive.Offset == 0); + Assert.Empty(dataPoint.Positive.BucketCounts); } + } - Assert.Empty(dataPoint.Exemplars); + if (attributes.Length > 0) + { + OtlpTestHelpers.AssertOtlpAttributes(attributes, dataPoint.Attributes); + } + else + { + Assert.Empty(dataPoint.Attributes); } - [Theory] - [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_histogram", null, null, -123L, null, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_histogram", null, null, null, -123.45, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta)] - [InlineData("test_histogram", "description", "unit", 123L, null, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, "key1", "value1", "key2", 123)] - public void TestExponentialHistogramToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, params object[] keysValues) + VerifyExemplars(null, longValue ?? doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); + if (enableExemplars) { - var metrics = new List(); + VerifyExemplars(null, 0, enableExemplars, d => d.Exemplars.Skip(1).FirstOrDefault(), dataPoint); + } + } - using var meter = new Meter(Utils.GetCurrentMethodName()); - using var provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(metrics, metricReaderOptions => - { - metricReaderOptions.TemporalityPreference = aggregationTemporality; - }) - .AddView(instrument => - { - return new Base2ExponentialBucketHistogramConfiguration(); - }) - .Build(); + [Theory] + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative, false, true)] + [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative, false, true)] + [InlineData("test_histogram", null, null, -123L, null, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_histogram", null, null, null, -123.45, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta)] + [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Delta)] + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, false, true)] + [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Delta, false, true)] + [InlineData("test_histogram", "description", "unit", 123L, null, MetricReaderTemporalityPreference.Cumulative)] + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, true)] + public void TestHistogramToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, bool enableKeyValues = false, bool enableExemplars = false) + { + var metrics = new List(); - var attributes = ToAttributes(keysValues).ToArray(); - if (longValue.HasValue) - { - var histogram = meter.CreateHistogram(name, unit, description); - histogram.Record(longValue.Value, attributes); - histogram.Record(0, attributes); - } - else + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .SetExemplarFilter(enableExemplars ? ExemplarFilterType.AlwaysOn : ExemplarFilterType.AlwaysOff) + .AddInMemoryExporter(metrics, metricReaderOptions => { - var histogram = meter.CreateHistogram(name, unit, description); - histogram.Record(doubleValue.Value, attributes); - histogram.Record(0, attributes); - } + metricReaderOptions.TemporalityPreference = aggregationTemporality; + }) + .Build(); - provider.ForceFlush(); + var attributes = enableKeyValues ? KeyValues : Array.Empty>(); + if (longValue.HasValue) + { + var histogram = meter.CreateHistogram(name, unit, description); + histogram.Record(longValue.Value, attributes); + } + else + { + var histogram = meter.CreateHistogram(name, unit, description); + histogram.Record(doubleValue.Value, attributes); + } - var batch = new Batch(metrics.ToArray(), metrics.Count); + provider.ForceFlush(); - var request = new OtlpCollector.ExportMetricsServiceRequest(); - request.AddMetrics(ResourceBuilder.CreateEmpty().Build().ToOtlpResource(), batch); + var batch = new Batch(metrics.ToArray(), metrics.Count); - var resourceMetric = request.ResourceMetrics.Single(); - var scopeMetrics = resourceMetric.ScopeMetrics.Single(); - var actual = scopeMetrics.Metrics.Single(); + var request = new OtlpCollector.ExportMetricsServiceRequest(); + request.AddMetrics(ResourceBuilder.CreateEmpty().Build().ToOtlpResource(), batch); - Assert.Equal(name, actual.Name); - Assert.Equal(description ?? string.Empty, actual.Description); - Assert.Equal(unit ?? string.Empty, actual.Unit); + var resourceMetric = request.ResourceMetrics.Single(); + var scopeMetrics = resourceMetric.ScopeMetrics.Single(); + var actual = scopeMetrics.Metrics.Single(); - Assert.Equal(OtlpMetrics.Metric.DataOneofCase.ExponentialHistogram, actual.DataCase); + Assert.Equal(name, actual.Name); + Assert.Equal(description ?? string.Empty, actual.Description); + Assert.Equal(unit ?? string.Empty, actual.Unit); - Assert.Null(actual.Gauge); - Assert.Null(actual.Sum); - Assert.Null(actual.Histogram); - Assert.NotNull(actual.ExponentialHistogram); - Assert.Null(actual.Summary); + Assert.Equal(OtlpMetrics.Metric.DataOneofCase.Histogram, actual.DataCase); - var otlpAggregationTemporality = aggregationTemporality == MetricReaderTemporalityPreference.Cumulative - ? OtlpMetrics.AggregationTemporality.Cumulative - : OtlpMetrics.AggregationTemporality.Delta; - Assert.Equal(otlpAggregationTemporality, actual.ExponentialHistogram.AggregationTemporality); + Assert.Null(actual.Gauge); + Assert.Null(actual.Sum); + Assert.NotNull(actual.Histogram); + Assert.Null(actual.ExponentialHistogram); + Assert.Null(actual.Summary); - Assert.Single(actual.ExponentialHistogram.DataPoints); - var dataPoint = actual.ExponentialHistogram.DataPoints.First(); - Assert.True(dataPoint.StartTimeUnixNano > 0); - Assert.True(dataPoint.TimeUnixNano > 0); + var otlpAggregationTemporality = aggregationTemporality == MetricReaderTemporalityPreference.Cumulative + ? OtlpMetrics.AggregationTemporality.Cumulative + : OtlpMetrics.AggregationTemporality.Delta; + Assert.Equal(otlpAggregationTemporality, actual.Histogram.AggregationTemporality); - Assert.Equal(20, dataPoint.Scale); - Assert.Equal(1UL, dataPoint.ZeroCount); - if (longValue > 0 || doubleValue > 0) - { - Assert.Equal(2UL, dataPoint.Count); - } - else - { - Assert.Equal(1UL, dataPoint.Count); - } + Assert.Single(actual.Histogram.DataPoints); + var dataPoint = actual.Histogram.DataPoints.First(); + Assert.True(dataPoint.StartTimeUnixNano > 0); + Assert.True(dataPoint.TimeUnixNano > 0); - if (longValue.HasValue) - { - if (longValue > 0) - { - Assert.Equal((double)longValue, dataPoint.Sum); - Assert.Null(dataPoint.Negative); - Assert.True(dataPoint.Positive.Offset > 0); - Assert.Equal(1UL, dataPoint.Positive.BucketCounts[0]); - } - else - { - Assert.Equal(0, dataPoint.Sum); - Assert.Null(dataPoint.Negative); - Assert.True(dataPoint.Positive.Offset == 0); - Assert.Empty(dataPoint.Positive.BucketCounts); - } - } - else - { - if (doubleValue > 0) - { - Assert.Equal(doubleValue, dataPoint.Sum); - Assert.Null(dataPoint.Negative); - Assert.True(dataPoint.Positive.Offset > 0); - Assert.Equal(1UL, dataPoint.Positive.BucketCounts[0]); - } - else - { - Assert.Equal(0, dataPoint.Sum); - Assert.Null(dataPoint.Negative); - Assert.True(dataPoint.Positive.Offset == 0); - Assert.Empty(dataPoint.Positive.BucketCounts); - } - } + Assert.Equal(1UL, dataPoint.Count); - if (attributes.Length > 0) - { - OtlpTestHelpers.AssertOtlpAttributes(attributes, dataPoint.Attributes); - } - else + // Known issue: Negative measurements affect the Sum. Per the spec, they should not. + if (longValue.HasValue) + { + Assert.Equal((double)longValue, dataPoint.Sum); + } + else + { + Assert.Equal(doubleValue, dataPoint.Sum); + } + + int bucketIndex; + for (bucketIndex = 0; bucketIndex < dataPoint.ExplicitBounds.Count; ++bucketIndex) + { + if (dataPoint.Sum <= dataPoint.ExplicitBounds[bucketIndex]) { - Assert.Empty(dataPoint.Attributes); + break; } - Assert.Empty(dataPoint.Exemplars); + Assert.Equal(0UL, dataPoint.BucketCounts[bucketIndex]); } - [Theory] - [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_histogram", null, null, -123L, null, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_histogram", null, null, null, -123.45, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta)] - [InlineData("test_histogram", "description", "unit", 123L, null, MetricReaderTemporalityPreference.Cumulative)] - [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, "key1", "value1", "key2", 123)] - public void TestHistogramToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, params object[] keysValues) + Assert.Equal(1UL, dataPoint.BucketCounts[bucketIndex]); + + if (attributes.Length > 0) + { + OtlpTestHelpers.AssertOtlpAttributes(attributes, dataPoint.Attributes); + } + else { - var metrics = new List(); + Assert.Empty(dataPoint.Attributes); + } - using var meter = new Meter(Utils.GetCurrentMethodName()); - using var provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(metrics, metricReaderOptions => - { - metricReaderOptions.TemporalityPreference = aggregationTemporality; - }) - .Build(); + VerifyExemplars(null, longValue ?? doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); + } - var attributes = ToAttributes(keysValues).ToArray(); - if (longValue.HasValue) - { - var histogram = meter.CreateHistogram(name, unit, description); - histogram.Record(longValue.Value, attributes); - } - else + [Theory] + [InlineData("cumulative", MetricReaderTemporalityPreference.Cumulative)] + [InlineData("Cumulative", MetricReaderTemporalityPreference.Cumulative)] + [InlineData("CUMULATIVE", MetricReaderTemporalityPreference.Cumulative)] + [InlineData("delta", MetricReaderTemporalityPreference.Delta)] + [InlineData("Delta", MetricReaderTemporalityPreference.Delta)] + [InlineData("DELTA", MetricReaderTemporalityPreference.Delta)] + public void TestTemporalityPreferenceConfiguration(string configValue, MetricReaderTemporalityPreference expectedTemporality) + { + var configData = new Dictionary { ["OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"] = configValue }; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(configData) + .Build(); + + // Check for both the code paths: + // 1. The final extension method which accepts `Action`. + // 2. The final extension method which accepts `Action`. + + // Test 1st code path + using var meterProvider1 = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => services.AddSingleton(configuration)) + .AddOtlpExporter() // This would in turn call the extension method which accepts `Action` + .Build(); + + var assembly = typeof(Sdk).Assembly; + var type = assembly.GetType("OpenTelemetry.Metrics.MeterProviderSdk"); + var fieldInfo = type.GetField("reader", BindingFlags.Instance | BindingFlags.NonPublic); + var reader = fieldInfo.GetValue(meterProvider1) as MetricReader; + var temporality = reader.TemporalityPreference; + + Assert.Equal(expectedTemporality, temporality); + + // Test 2nd code path + using var meterProvider2 = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => services.AddSingleton(configuration)) + .AddOtlpExporter((_, _) => { }) // This would in turn call the extension method which accepts `Action` + .Build(); + + reader = fieldInfo.GetValue(meterProvider2) as MetricReader; + temporality = reader.TemporalityPreference; + + Assert.Equal(expectedTemporality, temporality); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public void ToOtlpExemplarTests(bool enableTagFiltering, bool enableTracing) + { + ActivitySource activitySource = null; + Activity activity = null; + TracerProvider tracerProvider = null; + + using var meter = new Meter(Utils.GetCurrentMethodName()); + + var exportedItems = new List(); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .SetExemplarFilter(ExemplarFilterType.AlwaysOn) + .AddView(i => { - var histogram = meter.CreateHistogram(name, unit, description); - histogram.Record(doubleValue.Value, attributes); - } + return !enableTagFiltering + ? null + : new MetricStreamConfiguration + { + TagKeys = Array.Empty(), + }; + }) + .AddInMemoryExporter(exportedItems) + .Build(); - provider.ForceFlush(); + if (enableTracing) + { + activitySource = new ActivitySource(Utils.GetCurrentMethodName()); + tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySource.Name) + .SetSampler(new AlwaysOnSampler()) + .Build(); - var batch = new Batch(metrics.ToArray(), metrics.Count); + activity = activitySource.StartActivity("testActivity"); + } - var request = new OtlpCollector.ExportMetricsServiceRequest(); - request.AddMetrics(ResourceBuilder.CreateEmpty().Build().ToOtlpResource(), batch); + var counterDouble = meter.CreateCounter("testCounterDouble"); + var counterLong = meter.CreateCounter("testCounterLong"); - var resourceMetric = request.ResourceMetrics.Single(); - var scopeMetrics = resourceMetric.ScopeMetrics.Single(); - var actual = scopeMetrics.Metrics.Single(); + counterDouble.Add(1.18D, new KeyValuePair("key1", "value1")); + counterLong.Add(18L, new KeyValuePair("key1", "value1")); - Assert.Equal(name, actual.Name); - Assert.Equal(description ?? string.Empty, actual.Description); - Assert.Equal(unit ?? string.Empty, actual.Unit); + meterProvider.ForceFlush(); - Assert.Equal(OtlpMetrics.Metric.DataOneofCase.Histogram, actual.DataCase); + var counterDoubleMetric = exportedItems.FirstOrDefault(m => m.Name == counterDouble.Name); + var counterLongMetric = exportedItems.FirstOrDefault(m => m.Name == counterLong.Name); - Assert.Null(actual.Gauge); - Assert.Null(actual.Sum); - Assert.NotNull(actual.Histogram); - Assert.Null(actual.ExponentialHistogram); - Assert.Null(actual.Summary); + Assert.NotNull(counterDoubleMetric); + Assert.NotNull(counterLongMetric); - var otlpAggregationTemporality = aggregationTemporality == MetricReaderTemporalityPreference.Cumulative - ? OtlpMetrics.AggregationTemporality.Cumulative - : OtlpMetrics.AggregationTemporality.Delta; - Assert.Equal(otlpAggregationTemporality, actual.Histogram.AggregationTemporality); + AssertExemplars(1.18D, counterDoubleMetric); + AssertExemplars(18L, counterLongMetric); - Assert.Single(actual.Histogram.DataPoints); - var dataPoint = actual.Histogram.DataPoints.First(); - Assert.True(dataPoint.StartTimeUnixNano > 0); - Assert.True(dataPoint.TimeUnixNano > 0); + activity?.Dispose(); + tracerProvider?.Dispose(); + activitySource?.Dispose(); - Assert.Equal(1UL, dataPoint.Count); + void AssertExemplars(T value, Metric metric) + where T : struct + { + var metricPointEnumerator = metric.GetMetricPoints().GetEnumerator(); + Assert.True(metricPointEnumerator.MoveNext()); - // Known issue: Negative measurements affect the Sum. Per the spec, they should not. - if (longValue.HasValue) + ref readonly var metricPoint = ref metricPointEnumerator.Current; + + var result = metricPoint.TryGetExemplars(out var exemplars); + Assert.True(result); + + var exemplarEnumerator = exemplars.GetEnumerator(); + Assert.True(exemplarEnumerator.MoveNext()); + + ref readonly var exemplar = ref exemplarEnumerator.Current; + + var otlpExemplar = MetricItemExtensions.ToOtlpExemplar(value, in exemplar); + Assert.NotNull(otlpExemplar); + + Assert.NotEqual(default, otlpExemplar.TimeUnixNano); + if (!enableTracing) { - Assert.Equal((double)longValue, dataPoint.Sum); + Assert.Equal(ByteString.Empty, otlpExemplar.TraceId); + Assert.Equal(ByteString.Empty, otlpExemplar.SpanId); } else { - Assert.Equal(doubleValue, dataPoint.Sum); - } + byte[] traceIdBytes = new byte[16]; + activity.TraceId.CopyTo(traceIdBytes); - int bucketIndex; - for (bucketIndex = 0; bucketIndex < dataPoint.ExplicitBounds.Count; ++bucketIndex) - { - if (dataPoint.Sum <= dataPoint.ExplicitBounds[bucketIndex]) - { - break; - } + byte[] spanIdBytes = new byte[8]; + activity.SpanId.CopyTo(spanIdBytes); - Assert.Equal(0UL, dataPoint.BucketCounts[bucketIndex]); + Assert.Equal(ByteString.CopyFrom(traceIdBytes), otlpExemplar.TraceId); + Assert.Equal(ByteString.CopyFrom(spanIdBytes), otlpExemplar.SpanId); } - Assert.Equal(1UL, dataPoint.BucketCounts[bucketIndex]); - - if (attributes.Length > 0) + if (typeof(T) == typeof(long)) + { + Assert.Equal((long)(object)value, exemplar.LongValue); + } + else if (typeof(T) == typeof(double)) { - OtlpTestHelpers.AssertOtlpAttributes(attributes, dataPoint.Attributes); + Assert.Equal((double)(object)value, exemplar.DoubleValue); } else { - Assert.Empty(dataPoint.Attributes); + Debug.Fail("Unexpected type"); } - Assert.Empty(dataPoint.Exemplars); + if (!enableTagFiltering) + { + var tagEnumerator = exemplar.FilteredTags.GetEnumerator(); + Assert.False(tagEnumerator.MoveNext()); + } + else + { + var tagEnumerator = exemplar.FilteredTags.GetEnumerator(); + Assert.True(tagEnumerator.MoveNext()); + + var tag = tagEnumerator.Current; + Assert.Equal("key1", tag.Key); + Assert.Equal("value1", tag.Value); + } } + } - private static IEnumerable> ToAttributes(object[] keysValues) - { - var keys = keysValues?.Where((_, index) => index % 2 == 0).ToArray(); - var values = keysValues?.Where((_, index) => index % 2 != 0).ToArray(); + private static void VerifyExemplars(long? longValue, double? doubleValue, bool enableExemplars, Func getExemplarFunc, T state) + { + var exemplar = getExemplarFunc(state); - for (var i = 0; keys != null && i < keys.Length; ++i) + if (enableExemplars) + { + Assert.NotNull(exemplar); + Assert.NotEqual(default, exemplar.TimeUnixNano); + if (longValue.HasValue) + { + Assert.Equal(longValue.Value, exemplar.AsInt); + } + else { - yield return new KeyValuePair(keys[i].ToString(), values[i]); + Assert.Equal(doubleValue.Value, exemplar.AsDouble); } } + else + { + Assert.Null(exemplar); + } } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpResourceTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpResourceTests.cs index 0bd09af05df..4a82310103f 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpResourceTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpResourceTests.cs @@ -1,51 +1,37 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; using OpenTelemetry.Resources; using Xunit; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +public class OtlpResourceTests { - public class OtlpResourceTests + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ToOtlpResourceTest(bool includeServiceNameInResource) { - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ToOtlpResourceTest(bool includeServiceNameInResource) + // Targeted test to cover OTel Resource to OTLP Resource + // conversion, independent of signals. + var resourceBuilder = ResourceBuilder.CreateEmpty(); + if (includeServiceNameInResource) { - // Targeted test to cover OTel Resource to OTLP Resource - // conversion, independent of signals. - var resourceBuilder = ResourceBuilder.CreateEmpty(); - if (includeServiceNameInResource) - { - resourceBuilder.AddService("service-name", "ns1"); - } + resourceBuilder.AddService("service-name", "ns1"); + } - var resource = resourceBuilder.Build(); - var otlpResource = resource.ToOtlpResource(); - if (includeServiceNameInResource) - { - Assert.Contains(otlpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.StringValue == "service-name"); - Assert.Contains(otlpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceNamespace && kvp.Value.StringValue == "ns1"); - } - else - { - Assert.Contains(otlpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.ToString().Contains("unknown_service:")); - } + var resource = resourceBuilder.Build(); + var otlpResource = resource.ToOtlpResource(); + if (includeServiceNameInResource) + { + Assert.Contains(otlpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.StringValue == "service-name"); + Assert.Contains(otlpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceNamespace && kvp.Value.StringValue == "ns1"); + } + else + { + Assert.Contains(otlpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.ToString().Contains("unknown_service:")); } } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpRetryTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpRetryTests.cs new file mode 100644 index 00000000000..f8815ed5665 --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpRetryTests.cs @@ -0,0 +1,203 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; +using Xunit; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient.Tests; + +public class OtlpRetryTests +{ + public static IEnumerable GrpcRetryTestData => GrpcRetryTestCase.GetTestCases(); + + [Theory] + [MemberData(nameof(GrpcRetryTestData))] + public void TryGetGrpcRetryResultTest(GrpcRetryTestCase testCase) + { + var attempts = 0; + var nextRetryDelayMilliseconds = OtlpRetry.InitialBackoffMilliseconds; + + foreach (var retryAttempt in testCase.RetryAttempts) + { + ++attempts; + var statusCode = retryAttempt.RpcException.StatusCode; + var deadline = retryAttempt.CallOptions.Deadline; + var trailers = retryAttempt.RpcException.Trailers; + var success = OtlpRetry.TryGetGrpcRetryResult(statusCode, deadline, trailers, nextRetryDelayMilliseconds, out var retryResult); + + Assert.Equal(retryAttempt.ExpectedSuccess, success); + + if (!success) + { + Assert.Equal(testCase.ExpectedRetryAttempts, attempts); + break; + } + + if (retryResult.Throttled) + { + Assert.Equal(retryAttempt.ThrottleDelay, retryResult.RetryDelay); + } + else + { + Assert.True(retryResult.RetryDelay >= TimeSpan.Zero); + Assert.True(retryResult.RetryDelay < TimeSpan.FromMilliseconds(nextRetryDelayMilliseconds)); + } + + Assert.Equal(retryAttempt.ExpectedNextRetryDelayMilliseconds, retryResult.NextRetryDelayMilliseconds); + + nextRetryDelayMilliseconds = retryResult.NextRetryDelayMilliseconds; + } + + Assert.Equal(testCase.ExpectedRetryAttempts, attempts); + } + + public class GrpcRetryTestCase + { + public int ExpectedRetryAttempts; + public GrpcRetryAttempt[] RetryAttempts; + + private string testRunnerName; + + private GrpcRetryTestCase(string testRunnerName, GrpcRetryAttempt[] retryAttempts, int expectedRetryAttempts = 1) + { + this.ExpectedRetryAttempts = expectedRetryAttempts; + this.RetryAttempts = retryAttempts; + this.testRunnerName = testRunnerName; + } + + public static IEnumerable GetTestCases() + { + yield return new[] { new GrpcRetryTestCase("Cancelled", new GrpcRetryAttempt[] { new(StatusCode.Cancelled) }) }; + yield return new[] { new GrpcRetryTestCase("DeadlineExceeded", new GrpcRetryAttempt[] { new(StatusCode.DeadlineExceeded) }) }; + yield return new[] { new GrpcRetryTestCase("Aborted", new GrpcRetryAttempt[] { new(StatusCode.Aborted) }) }; + yield return new[] { new GrpcRetryTestCase("OutOfRange", new GrpcRetryAttempt[] { new(StatusCode.OutOfRange) }) }; + yield return new[] { new GrpcRetryTestCase("DataLoss", new GrpcRetryAttempt[] { new(StatusCode.DataLoss) }) }; + yield return new[] { new GrpcRetryTestCase("Unavailable", new GrpcRetryAttempt[] { new(StatusCode.Unavailable) }) }; + + yield return new[] { new GrpcRetryTestCase("OK", new GrpcRetryAttempt[] { new(StatusCode.OK, expectedSuccess: false) }) }; + yield return new[] { new GrpcRetryTestCase("PermissionDenied", new GrpcRetryAttempt[] { new(StatusCode.PermissionDenied, expectedSuccess: false) }) }; + yield return new[] { new GrpcRetryTestCase("Unknown", new GrpcRetryAttempt[] { new(StatusCode.Unknown, expectedSuccess: false) }) }; + + yield return new[] { new GrpcRetryTestCase("ResourceExhausted w/o RetryInfo", new GrpcRetryAttempt[] { new(StatusCode.ResourceExhausted, expectedSuccess: false) }) }; + yield return new[] { new GrpcRetryTestCase("ResourceExhausted w/ RetryInfo", new GrpcRetryAttempt[] { new(StatusCode.ResourceExhausted, throttleDelay: new Duration { Seconds = 2 }, expectedNextRetryDelayMilliseconds: 3000) }) }; + + yield return new[] { new GrpcRetryTestCase("Unavailable w/ RetryInfo", new GrpcRetryAttempt[] { new(StatusCode.Unavailable, throttleDelay: Duration.FromTimeSpan(TimeSpan.FromMilliseconds(2000)), expectedNextRetryDelayMilliseconds: 3000) }) }; + + yield return new[] { new GrpcRetryTestCase("Expired deadline", new GrpcRetryAttempt[] { new(StatusCode.Unavailable, deadlineExceeded: true, expectedSuccess: false) }) }; + + yield return new[] + { + new GrpcRetryTestCase( + "Exponential backoff", + new GrpcRetryAttempt[] + { + new(StatusCode.Unavailable, expectedNextRetryDelayMilliseconds: 1500), + new(StatusCode.Unavailable, expectedNextRetryDelayMilliseconds: 2250), + new(StatusCode.Unavailable, expectedNextRetryDelayMilliseconds: 3375), + new(StatusCode.Unavailable, expectedNextRetryDelayMilliseconds: 5000), + new(StatusCode.Unavailable, expectedNextRetryDelayMilliseconds: 5000), + }, + expectedRetryAttempts: 5), + }; + + yield return new[] + { + new GrpcRetryTestCase( + "Retry until non-retryable status code encountered", + new GrpcRetryAttempt[] + { + new(StatusCode.Unavailable, expectedNextRetryDelayMilliseconds: 1500), + new(StatusCode.Unavailable, expectedNextRetryDelayMilliseconds: 2250), + new(StatusCode.Unavailable, expectedNextRetryDelayMilliseconds: 3375), + new(StatusCode.PermissionDenied, expectedSuccess: false), + new(StatusCode.Unavailable, expectedNextRetryDelayMilliseconds: 5000), + }, + expectedRetryAttempts: 4), + }; + + // Test throttling affects exponential backoff. + yield return new[] + { + new GrpcRetryTestCase( + "Exponential backoff after throttling", + new GrpcRetryAttempt[] + { + new(StatusCode.Unavailable, expectedNextRetryDelayMilliseconds: 1500), + new(StatusCode.Unavailable, expectedNextRetryDelayMilliseconds: 2250), + new(StatusCode.Unavailable, throttleDelay: Duration.FromTimeSpan(TimeSpan.FromMilliseconds(500)), expectedNextRetryDelayMilliseconds: 750), + new(StatusCode.Unavailable, expectedNextRetryDelayMilliseconds: 1125), + new(StatusCode.Unavailable, expectedNextRetryDelayMilliseconds: 1688), + new(StatusCode.Unavailable, expectedNextRetryDelayMilliseconds: 2532), + new(StatusCode.Unavailable, expectedNextRetryDelayMilliseconds: 3798), + new(StatusCode.Unavailable, expectedNextRetryDelayMilliseconds: 5000), + new(StatusCode.Unavailable, expectedNextRetryDelayMilliseconds: 5000), + }, + expectedRetryAttempts: 9), + }; + + yield return new[] + { + new GrpcRetryTestCase( + "Ridiculous throttling delay", + new GrpcRetryAttempt[] + { + new(StatusCode.Unavailable, throttleDelay: Duration.FromTimeSpan(TimeSpan.FromDays(3000000)), expectedNextRetryDelayMilliseconds: 5000), + }), + }; + } + + public override string ToString() + { + return this.testRunnerName; + } + + private static Metadata GenerateTrailers(Duration throttleDelay) + { + var metadata = new Metadata(); + + var retryInfo = new Google.Rpc.RetryInfo(); + retryInfo.RetryDelay = throttleDelay; + + var status = new Google.Rpc.Status(); + status.Details.Add(Any.Pack(retryInfo)); + + var stream = new MemoryStream(); + status.WriteTo(stream); + + metadata.Add(OtlpRetry.GrpcStatusDetailsHeader, stream.ToArray()); + return metadata; + } + + public struct GrpcRetryAttempt + { + public RpcException RpcException; + public CallOptions CallOptions; + public TimeSpan? ThrottleDelay; + public int? ExpectedNextRetryDelayMilliseconds; + public bool ExpectedSuccess; + + public GrpcRetryAttempt( + StatusCode statusCode, + bool deadlineExceeded = false, + Duration throttleDelay = null, + int expectedNextRetryDelayMilliseconds = 1500, + bool expectedSuccess = true) + { + var status = new Status(statusCode, "Error"); + this.RpcException = throttleDelay != null + ? new RpcException(status, GenerateTrailers(throttleDelay)) + : new RpcException(status); + + this.CallOptions = deadlineExceeded ? new CallOptions(deadline: DateTime.UtcNow.AddSeconds(-1)) : default; + + this.ThrottleDelay = throttleDelay != null ? throttleDelay.ToTimeSpan() : null; + + this.ExpectedNextRetryDelayMilliseconds = expectedNextRetryDelayMilliseconds; + + this.ExpectedSuccess = expectedSuccess; + } + } + } +} diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTestHelpers.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTestHelpers.cs index dc2b0a3d82a..63b90f04c28 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTestHelpers.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTestHelpers.cs @@ -1,116 +1,102 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Google.Protobuf.Collections; using Xunit; using OtlpCommon = OpenTelemetry.Proto.Common.V1; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +internal static class OtlpTestHelpers { - internal static class OtlpTestHelpers + public static void AssertOtlpAttributes( + IEnumerable> expected, + RepeatedField actual) { - public static void AssertOtlpAttributes( - IEnumerable> expected, - RepeatedField actual) + var expectedAttributes = expected.ToList(); + int expectedSize = 0; + for (int i = 0; i < expectedAttributes.Count; i++) { - var expectedAttributes = expected.ToList(); - int expectedSize = 0; - for (int i = 0; i < expectedAttributes.Count; i++) - { - var current = expectedAttributes[i].Value; - Assert.Equal(expectedAttributes[i].Key, actual[i].Key); + var current = expectedAttributes[i].Value; + Assert.Equal(expectedAttributes[i].Key, actual[i].Key); - if (current.GetType().IsArray) + if (current.GetType().IsArray) + { + Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.ArrayValue, actual[i].Value.ValueCase); + if (current is bool[] boolArray) { - Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.ArrayValue, actual[i].Value.ValueCase); - if (current is bool[] boolArray) + Assert.Equal(boolArray.Length, actual[i].Value.ArrayValue.Values.Count); + for (var j = 0; j < boolArray.Length; ++j) { - Assert.Equal(boolArray.Length, actual[i].Value.ArrayValue.Values.Count); - for (var j = 0; j < boolArray.Length; ++j) - { - AssertOtlpAttributeValue(boolArray[j], actual[i].Value.ArrayValue.Values[j]); - } - - expectedSize++; + AssertOtlpAttributeValue(boolArray[j], actual[i].Value.ArrayValue.Values[j]); } - else if (current is int[] intArray) - { - Assert.Equal(intArray.Length, actual[i].Value.ArrayValue.Values.Count); - for (var j = 0; j < intArray.Length; ++j) - { - AssertOtlpAttributeValue(intArray[j], actual[i].Value.ArrayValue.Values[j]); - } - expectedSize++; - } - else if (current is double[] doubleArray) + expectedSize++; + } + else if (current is int[] intArray) + { + Assert.Equal(intArray.Length, actual[i].Value.ArrayValue.Values.Count); + for (var j = 0; j < intArray.Length; ++j) { - Assert.Equal(doubleArray.Length, actual[i].Value.ArrayValue.Values.Count); - for (var j = 0; j < doubleArray.Length; ++j) - { - AssertOtlpAttributeValue(doubleArray[j], actual[i].Value.ArrayValue.Values[j]); - } - - expectedSize++; + AssertOtlpAttributeValue(intArray[j], actual[i].Value.ArrayValue.Values[j]); } - else if (current is string[] stringArray) - { - Assert.Equal(stringArray.Length, actual[i].Value.ArrayValue.Values.Count); - for (var j = 0; j < stringArray.Length; ++j) - { - AssertOtlpAttributeValue(stringArray[j], actual[i].Value.ArrayValue.Values[j]); - } - expectedSize++; + expectedSize++; + } + else if (current is double[] doubleArray) + { + Assert.Equal(doubleArray.Length, actual[i].Value.ArrayValue.Values.Count); + for (var j = 0; j < doubleArray.Length; ++j) + { + AssertOtlpAttributeValue(doubleArray[j], actual[i].Value.ArrayValue.Values[j]); } + + expectedSize++; } - else + else if (current is string[] stringArray) { - Assert.Equal(expectedAttributes[i].Key, actual[i].Key); - AssertOtlpAttributeValue(current, actual[i].Value); + Assert.Equal(stringArray.Length, actual[i].Value.ArrayValue.Values.Count); + for (var j = 0; j < stringArray.Length; ++j) + { + AssertOtlpAttributeValue(stringArray[j], actual[i].Value.ArrayValue.Values[j]); + } + expectedSize++; } } - - Assert.Equal(expectedSize, actual.Count); + else + { + Assert.Equal(expectedAttributes[i].Key, actual[i].Key); + AssertOtlpAttributeValue(current, actual[i].Value); + expectedSize++; + } } - private static void AssertOtlpAttributeValue(object expected, OtlpCommon.AnyValue actual) + Assert.Equal(expectedSize, actual.Count); + } + + private static void AssertOtlpAttributeValue(object expected, OtlpCommon.AnyValue actual) + { + switch (expected) { - switch (expected) - { - case string s: - Assert.Equal(s, actual.StringValue); - break; - case bool b: - Assert.Equal(b, actual.BoolValue); - break; - case long l: - Assert.Equal(l, actual.IntValue); - break; - case double d: - Assert.Equal(d, actual.DoubleValue); - break; - case int i: - Assert.Equal(i, actual.IntValue); - break; - default: - Assert.Equal(expected.ToString(), actual.StringValue); - break; - } + case string s: + Assert.Equal(s, actual.StringValue); + break; + case bool b: + Assert.Equal(b, actual.BoolValue); + break; + case long l: + Assert.Equal(l, actual.IntValue); + break; + case double d: + Assert.Equal(d, actual.DoubleValue); + break; + case int i: + Assert.Equal(i, actual.IntValue); + break; + default: + Assert.Equal(expected.ToString(), actual.StringValue); + break; } } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs index db156c005b7..0c7a5db76e2 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs @@ -1,25 +1,11 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using Google.Protobuf.Collections; using Microsoft.Extensions.DependencyInjection; -using Moq; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; -using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Tests; @@ -30,730 +16,731 @@ using OtlpTrace = OpenTelemetry.Proto.Trace.V1; using Status = OpenTelemetry.Trace.Status; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +[Collection("xUnitCollectionPreventingTestsThatDependOnSdkConfigurationFromRunningInParallel")] +public class OtlpTraceExporterTests : Http2UnencryptedSupportTests { - [Collection("xUnitCollectionPreventingTestsThatDependOnSdkConfigurationFromRunningInParallel")] - public class OtlpTraceExporterTests : Http2UnencryptedSupportTests + private static readonly SdkLimitOptions DefaultSdkLimitOptions = new(); + + static OtlpTraceExporterTests() { - private static readonly SdkLimitOptions DefaultSdkLimitOptions = new(); + Activity.DefaultIdFormat = ActivityIdFormat.W3C; + Activity.ForceDefaultIdFormat = true; - static OtlpTraceExporterTests() + var listener = new ActivityListener { - Activity.DefaultIdFormat = ActivityIdFormat.W3C; - Activity.ForceDefaultIdFormat = true; + ShouldListenTo = _ => true, + Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, + }; + + ActivitySource.AddActivityListener(listener); + } - var listener = new ActivityListener + [Fact] + public void AddOtlpTraceExporterNamedOptionsSupported() + { + int defaultExporterOptionsConfigureOptionsInvocations = 0; + int namedExporterOptionsConfigureOptionsInvocations = 0; + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => { - ShouldListenTo = _ => true, - Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, - }; + services.Configure(o => defaultExporterOptionsConfigureOptionsInvocations++); - ActivitySource.AddActivityListener(listener); - } + services.Configure("Exporter2", o => namedExporterOptionsConfigureOptionsInvocations++); + }) + .AddOtlpExporter() + .AddOtlpExporter("Exporter2", o => { }) + .Build(); - [Fact] - public void AddOtlpTraceExporterNamedOptionsSupported() - { - int defaultExporterOptionsConfigureOptionsInvocations = 0; - int namedExporterOptionsConfigureOptionsInvocations = 0; + Assert.Equal(1, defaultExporterOptionsConfigureOptionsInvocations); + Assert.Equal(1, namedExporterOptionsConfigureOptionsInvocations); + } - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .ConfigureServices(services => - { - services.Configure(o => defaultExporterOptionsConfigureOptionsInvocations++); + [Fact] + public void OtlpExporter_BadArgs() + { + TracerProviderBuilder builder = null; + Assert.Throws(() => builder.AddOtlpExporter()); + } - services.Configure("Exporter2", o => namedExporterOptionsConfigureOptionsInvocations++); - }) - .AddOtlpExporter() - .AddOtlpExporter("Exporter2", o => { }) - .Build(); + [Fact] + public void UserHttpFactoryCalled() + { + OtlpExporterOptions options = new OtlpExporterOptions(); + + var defaultFactory = options.HttpClientFactory; + + int invocations = 0; + options.Protocol = OtlpExportProtocol.HttpProtobuf; + options.HttpClientFactory = () => + { + invocations++; + return defaultFactory(); + }; - Assert.Equal(1, defaultExporterOptionsConfigureOptionsInvocations); - Assert.Equal(1, namedExporterOptionsConfigureOptionsInvocations); + using (var exporter = new OtlpTraceExporter(options)) + { + Assert.Equal(1, invocations); } - [Fact] - public void OtlpExporter_BadArgs() + using (var provider = Sdk.CreateTracerProviderBuilder() + .AddOtlpExporter(o => + { + o.Protocol = OtlpExportProtocol.HttpProtobuf; + o.HttpClientFactory = options.HttpClientFactory; + }) + .Build()) { - TracerProviderBuilder builder = null; - Assert.Throws(() => builder.AddOtlpExporter()); + Assert.Equal(2, invocations); } - [Fact] - public void UserHttpFactoryCalled() + options.HttpClientFactory = null; + Assert.Throws(() => { - OtlpExporterOptions options = new OtlpExporterOptions(); + using var exporter = new OtlpTraceExporter(options); + }); - var defaultFactory = options.HttpClientFactory; + options.HttpClientFactory = () => null; + Assert.Throws(() => + { + using var exporter = new OtlpTraceExporter(options); + }); + } - int invocations = 0; - options.Protocol = OtlpExportProtocol.HttpProtobuf; - options.HttpClientFactory = () => - { - invocations++; - return defaultFactory(); - }; + [Fact] + public void ServiceProviderHttpClientFactoryInvoked() + { + IServiceCollection services = new ServiceCollection(); - using (var exporter = new OtlpTraceExporter(options)) - { - Assert.Equal(1, invocations); - } + services.AddHttpClient(); - using (var provider = Sdk.CreateTracerProviderBuilder() - .AddOtlpExporter(o => - { - o.Protocol = OtlpExportProtocol.HttpProtobuf; - o.HttpClientFactory = options.HttpClientFactory; - }) - .Build()) - { - Assert.Equal(2, invocations); - } + int invocations = 0; - options.HttpClientFactory = null; - Assert.Throws(() => - { - using var exporter = new OtlpTraceExporter(options); - }); + services.AddHttpClient("OtlpTraceExporter", configureClient: (client) => invocations++); - options.HttpClientFactory = () => null; - Assert.Throws(() => - { - using var exporter = new OtlpTraceExporter(options); - }); - } + services.AddOpenTelemetry().WithTracing(builder => builder + .AddOtlpExporter(o => o.Protocol = OtlpExportProtocol.HttpProtobuf)); - [Fact] - public void ServiceProviderHttpClientFactoryInvoked() - { - IServiceCollection services = new ServiceCollection(); + using var serviceProvider = services.BuildServiceProvider(); + + var tracerProvider = serviceProvider.GetRequiredService(); - services.AddHttpClient(); + Assert.Equal(1, invocations); + } - int invocations = 0; + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ToOtlpResourceSpansTest(bool includeServiceNameInResource) + { + var evenTags = new[] { new KeyValuePair("k0", "v0") }; + var oddTags = new[] { new KeyValuePair("k1", "v1") }; + var sources = new[] + { + new ActivitySource("even", "2.4.6"), + new ActivitySource("odd", "1.3.5"), + }; - services.AddHttpClient("OtlpTraceExporter", configureClient: (client) => invocations++); + var resourceBuilder = ResourceBuilder.CreateEmpty(); + if (includeServiceNameInResource) + { + resourceBuilder.AddService("service-name", "ns1"); + } - services.AddOpenTelemetry().WithTracing(builder => builder - .AddOtlpExporter(o => o.Protocol = OtlpExportProtocol.HttpProtobuf)); + var exportedItems = new List(); + var builder = Sdk.CreateTracerProviderBuilder() + .SetResourceBuilder(resourceBuilder) + .AddSource(sources[0].Name) + .AddSource(sources[1].Name) + .AddProcessor(new SimpleActivityExportProcessor(new InMemoryExporter(exportedItems))); - using var serviceProvider = services.BuildServiceProvider(); + using var openTelemetrySdk = builder.Build(); - var tracerProvider = serviceProvider.GetRequiredService(); + const int numOfSpans = 10; + bool isEven; + for (var i = 0; i < numOfSpans; i++) + { + isEven = i % 2 == 0; + var source = sources[i % 2]; + var activityKind = isEven ? ActivityKind.Client : ActivityKind.Server; + var activityTags = isEven ? evenTags : oddTags; - Assert.Equal(1, invocations); + using Activity activity = source.StartActivity($"span-{i}", activityKind, parentContext: default, activityTags); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ToOtlpResourceSpansTest(bool includeServiceNameInResource) + Assert.Equal(10, exportedItems.Count); + var batch = new Batch(exportedItems.ToArray(), exportedItems.Count); + RunTest(DefaultSdkLimitOptions, batch); + + void RunTest(SdkLimitOptions sdkOptions, Batch batch) { - var evenTags = new[] { new KeyValuePair("k0", "v0") }; - var oddTags = new[] { new KeyValuePair("k1", "v1") }; - var sources = new[] - { - new ActivitySource("even", "2.4.6"), - new ActivitySource("odd", "1.3.5"), - }; + var request = new OtlpCollector.ExportTraceServiceRequest(); - var resourceBuilder = ResourceBuilder.CreateEmpty(); + request.AddBatch(sdkOptions, resourceBuilder.Build().ToOtlpResource(), batch); + + Assert.Single(request.ResourceSpans); + var otlpResource = request.ResourceSpans.First().Resource; if (includeServiceNameInResource) { - resourceBuilder.AddService("service-name", "ns1"); + Assert.Contains(otlpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.StringValue == "service-name"); + Assert.Contains(otlpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceNamespace && kvp.Value.StringValue == "ns1"); } - - var exportedItems = new List(); - var builder = Sdk.CreateTracerProviderBuilder() - .SetResourceBuilder(resourceBuilder) - .AddSource(sources[0].Name) - .AddSource(sources[1].Name) - .AddProcessor(new SimpleActivityExportProcessor(new InMemoryExporter(exportedItems))); - - using var openTelemetrySdk = builder.Build(); - - const int numOfSpans = 10; - bool isEven; - for (var i = 0; i < numOfSpans; i++) + else { - isEven = i % 2 == 0; - var source = sources[i % 2]; - var activityKind = isEven ? ActivityKind.Client : ActivityKind.Server; - var activityTags = isEven ? evenTags : oddTags; - - using Activity activity = source.StartActivity($"span-{i}", activityKind, parentContext: default, activityTags); + Assert.Contains(otlpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.ToString().Contains("unknown_service:")); } - Assert.Equal(10, exportedItems.Count); - var batch = new Batch(exportedItems.ToArray(), exportedItems.Count); - RunTest(DefaultSdkLimitOptions, batch); - - void RunTest(SdkLimitOptions sdkOptions, Batch batch) + var scopeSpans = request.ResourceSpans.First().ScopeSpans; + Assert.Equal(2, scopeSpans.Count); + foreach (var scope in scopeSpans) { - var request = new OtlpCollector.ExportTraceServiceRequest(); - - request.AddBatch(sdkOptions, resourceBuilder.Build().ToOtlpResource(), batch); + Assert.Equal(numOfSpans / 2, scope.Spans.Count); + Assert.NotNull(scope.Scope); - Assert.Single(request.ResourceSpans); - var otlpResource = request.ResourceSpans.First().Resource; - if (includeServiceNameInResource) - { - Assert.Contains(otlpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.StringValue == "service-name"); - Assert.Contains(otlpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceNamespace && kvp.Value.StringValue == "ns1"); - } - else + var expectedSpanNames = new List(); + var start = scope.Scope.Name == "even" ? 0 : 1; + for (var i = start; i < numOfSpans; i += 2) { - Assert.Contains(otlpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.ToString().Contains("unknown_service:")); + expectedSpanNames.Add($"span-{i}"); } - var scopeSpans = request.ResourceSpans.First().ScopeSpans; - Assert.Equal(2, scopeSpans.Count); - foreach (var scope in scopeSpans) + var otlpSpans = scope.Spans; + Assert.Equal(expectedSpanNames.Count, otlpSpans.Count); + + var kv0 = new OtlpCommon.KeyValue { Key = "k0", Value = new OtlpCommon.AnyValue { StringValue = "v0" } }; + var kv1 = new OtlpCommon.KeyValue { Key = "k1", Value = new OtlpCommon.AnyValue { StringValue = "v1" } }; + + var expectedTag = scope.Scope.Name == "even" + ? kv0 + : kv1; + + foreach (var otlpSpan in otlpSpans) { - Assert.Equal(numOfSpans / 2, scope.Spans.Count); - Assert.NotNull(scope.Scope); - - var expectedSpanNames = new List(); - var start = scope.Scope.Name == "even" ? 0 : 1; - for (var i = start; i < numOfSpans; i += 2) - { - expectedSpanNames.Add($"span-{i}"); - } - - var otlpSpans = scope.Spans; - Assert.Equal(expectedSpanNames.Count, otlpSpans.Count); - - var kv0 = new OtlpCommon.KeyValue { Key = "k0", Value = new OtlpCommon.AnyValue { StringValue = "v0" } }; - var kv1 = new OtlpCommon.KeyValue { Key = "k1", Value = new OtlpCommon.AnyValue { StringValue = "v1" } }; - - var expectedTag = scope.Scope.Name == "even" - ? kv0 - : kv1; - - foreach (var otlpSpan in otlpSpans) - { - Assert.Contains(otlpSpan.Name, expectedSpanNames); - Assert.Contains(expectedTag, otlpSpan.Attributes); - } + Assert.Contains(otlpSpan.Name, expectedSpanNames); + Assert.Contains(expectedTag, otlpSpan.Attributes); } } } + } - [Fact] - public void SpanLimitsTest() + [Fact] + public void SpanLimitsTest() + { + var sdkOptions = new SdkLimitOptions() { - var sdkOptions = new SdkLimitOptions() - { - AttributeValueLengthLimit = 4, - AttributeCountLimit = 3, - SpanEventCountLimit = 1, - SpanLinkCountLimit = 1, - }; - - var tags = new ActivityTagsCollection() - { - new KeyValuePair("TruncatedTag", "12345"), - new KeyValuePair("TruncatedStringArray", new string[] { "12345", "1234", string.Empty, null }), - new KeyValuePair("TruncatedObjectTag", new object()), - new KeyValuePair("OneTagTooMany", 1), - }; + AttributeValueLengthLimit = 4, + AttributeCountLimit = 3, + SpanEventCountLimit = 1, + SpanLinkCountLimit = 1, + }; - var links = new[] - { - new ActivityLink(default, tags), - new ActivityLink(default, tags), - }; + var tags = new ActivityTagsCollection() + { + new KeyValuePair("TruncatedTag", "12345"), + new KeyValuePair("TruncatedStringArray", new string[] { "12345", "1234", string.Empty, null }), + new KeyValuePair("TruncatedObjectTag", new object()), + new KeyValuePair("OneTagTooMany", 1), + }; - using var activitySource = new ActivitySource(nameof(this.SpanLimitsTest)); - using var activity = activitySource.StartActivity("root", ActivityKind.Server, default(ActivityContext), tags, links); - - var event1 = new ActivityEvent("Event", DateTime.UtcNow, tags); - var event2 = new ActivityEvent("OneEventTooMany", DateTime.Now, tags); - - activity.AddEvent(event1); - activity.AddEvent(event2); - - var otlpSpan = activity.ToOtlpSpan(sdkOptions); - - Assert.NotNull(otlpSpan); - Assert.Equal(3, otlpSpan.Attributes.Count); - Assert.Equal(1u, otlpSpan.DroppedAttributesCount); - Assert.Equal("1234", otlpSpan.Attributes[0].Value.StringValue); - ArrayValueAsserts(otlpSpan.Attributes[1].Value.ArrayValue.Values); - Assert.Equal(new object().ToString().Substring(0, 4), otlpSpan.Attributes[2].Value.StringValue); - - Assert.Single(otlpSpan.Events); - Assert.Equal(1u, otlpSpan.DroppedEventsCount); - Assert.Equal(3, otlpSpan.Events[0].Attributes.Count); - Assert.Equal(1u, otlpSpan.Events[0].DroppedAttributesCount); - Assert.Equal("1234", otlpSpan.Events[0].Attributes[0].Value.StringValue); - ArrayValueAsserts(otlpSpan.Events[0].Attributes[1].Value.ArrayValue.Values); - Assert.Equal(new object().ToString().Substring(0, 4), otlpSpan.Events[0].Attributes[2].Value.StringValue); - - Assert.Single(otlpSpan.Links); - Assert.Equal(1u, otlpSpan.DroppedLinksCount); - Assert.Equal(3, otlpSpan.Links[0].Attributes.Count); - Assert.Equal(1u, otlpSpan.Links[0].DroppedAttributesCount); - Assert.Equal("1234", otlpSpan.Links[0].Attributes[0].Value.StringValue); - ArrayValueAsserts(otlpSpan.Links[0].Attributes[1].Value.ArrayValue.Values); - Assert.Equal(new object().ToString().Substring(0, 4), otlpSpan.Links[0].Attributes[2].Value.StringValue); - - void ArrayValueAsserts(RepeatedField values) + var links = new[] + { + new ActivityLink(default, tags), + new ActivityLink(default, tags), + }; + + using var activitySource = new ActivitySource(nameof(this.SpanLimitsTest)); + using var activity = activitySource.StartActivity("root", ActivityKind.Server, default(ActivityContext), tags, links); + + var event1 = new ActivityEvent("Event", DateTime.UtcNow, tags); + var event2 = new ActivityEvent("OneEventTooMany", DateTime.Now, tags); + + activity.AddEvent(event1); + activity.AddEvent(event2); + + var otlpSpan = activity.ToOtlpSpan(sdkOptions); + + Assert.NotNull(otlpSpan); + Assert.Equal(3, otlpSpan.Attributes.Count); + Assert.Equal(1u, otlpSpan.DroppedAttributesCount); + Assert.Equal("1234", otlpSpan.Attributes[0].Value.StringValue); + ArrayValueAsserts(otlpSpan.Attributes[1].Value.ArrayValue.Values); + Assert.Equal(new object().ToString().Substring(0, 4), otlpSpan.Attributes[2].Value.StringValue); + + Assert.Single(otlpSpan.Events); + Assert.Equal(1u, otlpSpan.DroppedEventsCount); + Assert.Equal(3, otlpSpan.Events[0].Attributes.Count); + Assert.Equal(1u, otlpSpan.Events[0].DroppedAttributesCount); + Assert.Equal("1234", otlpSpan.Events[0].Attributes[0].Value.StringValue); + ArrayValueAsserts(otlpSpan.Events[0].Attributes[1].Value.ArrayValue.Values); + Assert.Equal(new object().ToString().Substring(0, 4), otlpSpan.Events[0].Attributes[2].Value.StringValue); + + Assert.Single(otlpSpan.Links); + Assert.Equal(1u, otlpSpan.DroppedLinksCount); + Assert.Equal(3, otlpSpan.Links[0].Attributes.Count); + Assert.Equal(1u, otlpSpan.Links[0].DroppedAttributesCount); + Assert.Equal("1234", otlpSpan.Links[0].Attributes[0].Value.StringValue); + ArrayValueAsserts(otlpSpan.Links[0].Attributes[1].Value.ArrayValue.Values); + Assert.Equal(new object().ToString().Substring(0, 4), otlpSpan.Links[0].Attributes[2].Value.StringValue); + + void ArrayValueAsserts(RepeatedField values) + { + var expectedStringArray = new string[] { "1234", "1234", string.Empty, null }; + for (var i = 0; i < expectedStringArray.Length; ++i) { - var expectedStringArray = new string[] { "1234", "1234", string.Empty, null }; - for (var i = 0; i < expectedStringArray.Length; ++i) + var expectedValue = expectedStringArray[i]; + var expectedValueCase = expectedValue != null + ? OtlpCommon.AnyValue.ValueOneofCase.StringValue + : OtlpCommon.AnyValue.ValueOneofCase.None; + + var actual = values[i]; + Assert.Equal(expectedValueCase, actual.ValueCase); + if (expectedValueCase != OtlpCommon.AnyValue.ValueOneofCase.None) { - var expectedValue = expectedStringArray[i]; - var expectedValueCase = expectedValue != null - ? OtlpCommon.AnyValue.ValueOneofCase.StringValue - : OtlpCommon.AnyValue.ValueOneofCase.None; - - var actual = values[i]; - Assert.Equal(expectedValueCase, actual.ValueCase); - if (expectedValueCase != OtlpCommon.AnyValue.ValueOneofCase.None) - { - Assert.Equal(expectedValue, actual.StringValue); - } + Assert.Equal(expectedValue, actual.StringValue); } } } + } - [Fact] - public void ToOtlpSpanTest() - { - using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); - - using var rootActivity = activitySource.StartActivity("root", ActivityKind.Producer); - - var attributes = new List> - { - new KeyValuePair("bool", true), - new KeyValuePair("long", 1L), - new KeyValuePair("string", "text"), - new KeyValuePair("double", 3.14), - new KeyValuePair("int", 1), - new KeyValuePair("datetime", DateTime.UtcNow), - new KeyValuePair("bool_array", new bool[] { true, false }), - new KeyValuePair("int_array", new int[] { 1, 2 }), - new KeyValuePair("double_array", new double[] { 1.0, 2.09 }), - new KeyValuePair("string_array", new string[] { "a", "b" }), - }; + [Fact] + public void ToOtlpSpanTest() + { + using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); - foreach (var kvp in attributes) - { - rootActivity.SetTag(kvp.Key, kvp.Value); - } + using var rootActivity = activitySource.StartActivity("root", ActivityKind.Producer); - var startTime = new DateTime(2020, 02, 20, 20, 20, 20, DateTimeKind.Utc); + var attributes = new List> + { + new KeyValuePair("bool", true), + new KeyValuePair("long", 1L), + new KeyValuePair("string", "text"), + new KeyValuePair("double", 3.14), + new KeyValuePair("int", 1), + new KeyValuePair("datetime", DateTime.UtcNow), + new KeyValuePair("bool_array", new bool[] { true, false }), + new KeyValuePair("int_array", new int[] { 1, 2 }), + new KeyValuePair("double_array", new double[] { 1.0, 2.09 }), + new KeyValuePair("string_array", new string[] { "a", "b" }), + }; + + foreach (var kvp in attributes) + { + rootActivity.SetTag(kvp.Key, kvp.Value); + } - DateTimeOffset dateTimeOffset; - dateTimeOffset = DateTimeOffset.FromUnixTimeMilliseconds(0); + var startTime = new DateTime(2020, 02, 20, 20, 20, 20, DateTimeKind.Utc); - var expectedUnixTimeTicks = (ulong)(startTime.Ticks - dateTimeOffset.Ticks); - var duration = TimeSpan.FromMilliseconds(1555); + DateTimeOffset dateTimeOffset; + dateTimeOffset = DateTimeOffset.FromUnixTimeMilliseconds(0); - rootActivity.SetStartTime(startTime); - rootActivity.SetEndTime(startTime + duration); + var expectedUnixTimeTicks = (ulong)(startTime.Ticks - dateTimeOffset.Ticks); + var duration = TimeSpan.FromMilliseconds(1555); - Span traceIdSpan = stackalloc byte[16]; - rootActivity.TraceId.CopyTo(traceIdSpan); - var traceId = traceIdSpan.ToArray(); + rootActivity.SetStartTime(startTime); + rootActivity.SetEndTime(startTime + duration); - var otlpSpan = rootActivity.ToOtlpSpan(DefaultSdkLimitOptions); + Span traceIdSpan = stackalloc byte[16]; + rootActivity.TraceId.CopyTo(traceIdSpan); + var traceId = traceIdSpan.ToArray(); - Assert.NotNull(otlpSpan); - Assert.Equal("root", otlpSpan.Name); - Assert.Equal(OtlpTrace.Span.Types.SpanKind.Producer, otlpSpan.Kind); - Assert.Equal(traceId, otlpSpan.TraceId); - Assert.Empty(otlpSpan.ParentSpanId); - Assert.Null(otlpSpan.Status); - Assert.Empty(otlpSpan.Events); - Assert.Empty(otlpSpan.Links); - OtlpTestHelpers.AssertOtlpAttributes(attributes, otlpSpan.Attributes); + var otlpSpan = rootActivity.ToOtlpSpan(DefaultSdkLimitOptions); - var expectedStartTimeUnixNano = 100 * expectedUnixTimeTicks; - Assert.Equal(expectedStartTimeUnixNano, otlpSpan.StartTimeUnixNano); - var expectedEndTimeUnixNano = expectedStartTimeUnixNano + (duration.TotalMilliseconds * 1_000_000); - Assert.Equal(expectedEndTimeUnixNano, otlpSpan.EndTimeUnixNano); + Assert.NotNull(otlpSpan); + Assert.Equal("root", otlpSpan.Name); + Assert.Equal(OtlpTrace.Span.Types.SpanKind.Producer, otlpSpan.Kind); + Assert.Equal(traceId, otlpSpan.TraceId); + Assert.Empty(otlpSpan.ParentSpanId); + Assert.Null(otlpSpan.Status); + Assert.Empty(otlpSpan.Events); + Assert.Empty(otlpSpan.Links); + OtlpTestHelpers.AssertOtlpAttributes(attributes, otlpSpan.Attributes); - var childLinks = new List { new ActivityLink(rootActivity.Context, new ActivityTagsCollection(attributes)) }; - var childActivity = activitySource.StartActivity( - "child", - ActivityKind.Client, - rootActivity.Context, - links: childLinks); + var expectedStartTimeUnixNano = 100 * expectedUnixTimeTicks; + Assert.Equal(expectedStartTimeUnixNano, otlpSpan.StartTimeUnixNano); + var expectedEndTimeUnixNano = expectedStartTimeUnixNano + (duration.TotalMilliseconds * 1_000_000); + Assert.Equal(expectedEndTimeUnixNano, otlpSpan.EndTimeUnixNano); - childActivity.SetStatus(Status.Error); + var childLinks = new List { new ActivityLink(rootActivity.Context, new ActivityTagsCollection(attributes)) }; + var childActivity = activitySource.StartActivity( + "child", + ActivityKind.Client, + rootActivity.Context, + links: childLinks); - var childEvents = new List { new ActivityEvent("e0"), new ActivityEvent("e1", default, new ActivityTagsCollection(attributes)) }; - childActivity.AddEvent(childEvents[0]); - childActivity.AddEvent(childEvents[1]); + childActivity.SetStatus(Status.Error); - Span parentIdSpan = stackalloc byte[8]; - rootActivity.Context.SpanId.CopyTo(parentIdSpan); - var parentId = parentIdSpan.ToArray(); + var childEvents = new List { new ActivityEvent("e0"), new ActivityEvent("e1", default, new ActivityTagsCollection(attributes)) }; + childActivity.AddEvent(childEvents[0]); + childActivity.AddEvent(childEvents[1]); - otlpSpan = childActivity.ToOtlpSpan(DefaultSdkLimitOptions); + Span parentIdSpan = stackalloc byte[8]; + rootActivity.Context.SpanId.CopyTo(parentIdSpan); + var parentId = parentIdSpan.ToArray(); - Assert.NotNull(otlpSpan); - Assert.Equal("child", otlpSpan.Name); - Assert.Equal(OtlpTrace.Span.Types.SpanKind.Client, otlpSpan.Kind); - Assert.Equal(traceId, otlpSpan.TraceId); - Assert.Equal(parentId, otlpSpan.ParentSpanId); + otlpSpan = childActivity.ToOtlpSpan(DefaultSdkLimitOptions); - // Assert.Equal(OtlpTrace.Status.Types.StatusCode.NotFound, otlpSpan.Status.Code); + Assert.NotNull(otlpSpan); + Assert.Equal("child", otlpSpan.Name); + Assert.Equal(OtlpTrace.Span.Types.SpanKind.Client, otlpSpan.Kind); + Assert.Equal(traceId, otlpSpan.TraceId); + Assert.Equal(parentId, otlpSpan.ParentSpanId); - Assert.Equal(Status.Error.Description ?? string.Empty, otlpSpan.Status.Message); - Assert.Empty(otlpSpan.Attributes); + // Assert.Equal(OtlpTrace.Status.Types.StatusCode.NotFound, otlpSpan.Status.Code); - Assert.Equal(childEvents.Count, otlpSpan.Events.Count); - for (var i = 0; i < childEvents.Count; i++) - { - Assert.Equal(childEvents[i].Name, otlpSpan.Events[i].Name); - OtlpTestHelpers.AssertOtlpAttributes(childEvents[i].Tags.ToList(), otlpSpan.Events[i].Attributes); - } + Assert.Equal(Status.Error.Description ?? string.Empty, otlpSpan.Status.Message); + Assert.Empty(otlpSpan.Attributes); - childLinks.Reverse(); - Assert.Equal(childLinks.Count, otlpSpan.Links.Count); - for (var i = 0; i < childLinks.Count; i++) - { - OtlpTestHelpers.AssertOtlpAttributes(childLinks[i].Tags.ToList(), otlpSpan.Links[i].Attributes); - } + Assert.Equal(childEvents.Count, otlpSpan.Events.Count); + for (var i = 0; i < childEvents.Count; i++) + { + Assert.Equal(childEvents[i].Name, otlpSpan.Events[i].Name); + OtlpTestHelpers.AssertOtlpAttributes(childEvents[i].Tags.ToList(), otlpSpan.Events[i].Attributes); } - [Fact] - public void ToOtlpSpanActivitiesWithNullArrayTest() + childLinks.Reverse(); + Assert.Equal(childLinks.Count, otlpSpan.Links.Count); + for (var i = 0; i < childLinks.Count; i++) { - using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); + OtlpTestHelpers.AssertOtlpAttributes(childLinks[i].Tags.ToList(), otlpSpan.Links[i].Attributes); + } + } - using var rootActivity = activitySource.StartActivity("root", ActivityKind.Client); - Assert.NotNull(rootActivity); + [Fact] + public void ToOtlpSpanActivitiesWithNullArrayTest() + { + using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); - var stringArr = new string[] { "test", string.Empty, null }; - rootActivity.SetTag("stringArray", stringArr); + using var rootActivity = activitySource.StartActivity("root", ActivityKind.Client); + Assert.NotNull(rootActivity); - var otlpSpan = rootActivity.ToOtlpSpan(DefaultSdkLimitOptions); + var stringArr = new string[] { "test", string.Empty, null }; + rootActivity.SetTag("stringArray", stringArr); - Assert.NotNull(otlpSpan); + var otlpSpan = rootActivity.ToOtlpSpan(DefaultSdkLimitOptions); - var stringArray = otlpSpan.Attributes.FirstOrDefault(kvp => kvp.Key == "stringArray"); + Assert.NotNull(otlpSpan); - Assert.NotNull(stringArray); - Assert.Equal("test", stringArray.Value.ArrayValue.Values[0].StringValue); - Assert.Equal(string.Empty, stringArray.Value.ArrayValue.Values[1].StringValue); - Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.None, stringArray.Value.ArrayValue.Values[2].ValueCase); - } + var stringArray = otlpSpan.Attributes.FirstOrDefault(kvp => kvp.Key == "stringArray"); - [Theory] - [InlineData(ActivityStatusCode.Unset, "Description will be ignored if status is Unset.")] - [InlineData(ActivityStatusCode.Ok, "Description will be ignored if status is Okay.")] - [InlineData(ActivityStatusCode.Error, "Description will be kept if status is Error.")] - public void ToOtlpSpanNativeActivityStatusTest(ActivityStatusCode expectedStatusCode, string statusDescription) - { - using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); - using var activity = activitySource.StartActivity("Name"); - activity.SetStatus(expectedStatusCode, statusDescription); + Assert.NotNull(stringArray); + Assert.Equal("test", stringArray.Value.ArrayValue.Values[0].StringValue); + Assert.Equal(string.Empty, stringArray.Value.ArrayValue.Values[1].StringValue); + Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.None, stringArray.Value.ArrayValue.Values[2].ValueCase); + } - var otlpSpan = activity.ToOtlpSpan(DefaultSdkLimitOptions); + [Theory] + [InlineData(ActivityStatusCode.Unset, "Description will be ignored if status is Unset.")] + [InlineData(ActivityStatusCode.Ok, "Description will be ignored if status is Okay.")] + [InlineData(ActivityStatusCode.Error, "Description will be kept if status is Error.")] + public void ToOtlpSpanNativeActivityStatusTest(ActivityStatusCode expectedStatusCode, string statusDescription) + { + using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); + using var activity = activitySource.StartActivity("Name"); + activity.SetStatus(expectedStatusCode, statusDescription); - if (expectedStatusCode == ActivityStatusCode.Unset) - { - Assert.Null(otlpSpan.Status); - } - else - { - Assert.NotNull(otlpSpan.Status); - Assert.Equal((int)expectedStatusCode, (int)otlpSpan.Status.Code); - if (expectedStatusCode == ActivityStatusCode.Error) - { - Assert.Equal(statusDescription, otlpSpan.Status.Message); - } + var otlpSpan = activity.ToOtlpSpan(DefaultSdkLimitOptions); - if (expectedStatusCode == ActivityStatusCode.Ok) - { - Assert.Empty(otlpSpan.Status.Message); - } - } + if (expectedStatusCode == ActivityStatusCode.Unset) + { + Assert.Null(otlpSpan.Status); } - - [Theory] - [InlineData(StatusCode.Unset, "Unset", "Description will be ignored if status is Unset.")] - [InlineData(StatusCode.Ok, "Ok", "Description must only be used with the Error StatusCode.")] - [InlineData(StatusCode.Error, "Error", "Error description.")] - public void ToOtlpSpanStatusTagTest(StatusCode expectedStatusCode, string statusCodeTagValue, string statusDescription) + else { - using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); - using var activity = activitySource.StartActivity("Name"); - activity.SetTag(SpanAttributeConstants.StatusCodeKey, statusCodeTagValue); - activity.SetTag(SpanAttributeConstants.StatusDescriptionKey, statusDescription); - - var otlpSpan = activity.ToOtlpSpan(DefaultSdkLimitOptions); - Assert.NotNull(otlpSpan.Status); Assert.Equal((int)expectedStatusCode, (int)otlpSpan.Status.Code); - - if (expectedStatusCode == StatusCode.Error) + if (expectedStatusCode == ActivityStatusCode.Error) { Assert.Equal(statusDescription, otlpSpan.Status.Message); } - else + + if (expectedStatusCode == ActivityStatusCode.Ok) { Assert.Empty(otlpSpan.Status.Message); } } + } - [Theory] - [InlineData(StatusCode.Unset, "uNsET")] - [InlineData(StatusCode.Ok, "oK")] - [InlineData(StatusCode.Error, "ERROR")] - public void ToOtlpSpanStatusTagIsCaseInsensitiveTest(StatusCode expectedStatusCode, string statusCodeTagValue) - { - using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); - using var activity = activitySource.StartActivity("Name"); - activity.SetTag(SpanAttributeConstants.StatusCodeKey, statusCodeTagValue); + [Theory] + [InlineData(StatusCode.Unset, "Unset", "Description will be ignored if status is Unset.")] + [InlineData(StatusCode.Ok, "Ok", "Description must only be used with the Error StatusCode.")] + [InlineData(StatusCode.Error, "Error", "Error description.")] + public void ToOtlpSpanStatusTagTest(StatusCode expectedStatusCode, string statusCodeTagValue, string statusDescription) + { + using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); + using var activity = activitySource.StartActivity("Name"); + activity.SetTag(SpanAttributeConstants.StatusCodeKey, statusCodeTagValue); + activity.SetTag(SpanAttributeConstants.StatusDescriptionKey, statusDescription); - var otlpSpan = activity.ToOtlpSpan(DefaultSdkLimitOptions); + var otlpSpan = activity.ToOtlpSpan(DefaultSdkLimitOptions); - Assert.NotNull(otlpSpan.Status); - Assert.Equal((int)expectedStatusCode, (int)otlpSpan.Status.Code); - } + Assert.NotNull(otlpSpan.Status); + Assert.Equal((int)expectedStatusCode, (int)otlpSpan.Status.Code); - [Fact] - public void ToOtlpSpanActivityStatusTakesPrecedenceOverStatusTagsWhenActivityStatusCodeIsOk() + if (expectedStatusCode == StatusCode.Error) + { + Assert.Equal(statusDescription, otlpSpan.Status.Message); + } + else { - using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); - using var activity = activitySource.StartActivity("Name"); - const string TagDescriptionOnError = "Description when TagStatusCode is Error."; - activity.SetStatus(ActivityStatusCode.Ok); - activity.SetTag(SpanAttributeConstants.StatusCodeKey, "ERROR"); - activity.SetTag(SpanAttributeConstants.StatusDescriptionKey, TagDescriptionOnError); - - var otlpSpan = activity.ToOtlpSpan(DefaultSdkLimitOptions); - - Assert.NotNull(otlpSpan.Status); - Assert.Equal((int)ActivityStatusCode.Ok, (int)otlpSpan.Status.Code); Assert.Empty(otlpSpan.Status.Message); } + } - [Fact] - public void ToOtlpSpanActivityStatusTakesPrecedenceOverStatusTagsWhenActivityStatusCodeIsError() - { - using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); - using var activity = activitySource.StartActivity("Name"); - const string StatusDescriptionOnError = "Description when ActivityStatusCode is Error."; - activity.SetStatus(ActivityStatusCode.Error, StatusDescriptionOnError); - activity.SetTag(SpanAttributeConstants.StatusCodeKey, "OK"); + [Theory] + [InlineData(StatusCode.Unset, "uNsET")] + [InlineData(StatusCode.Ok, "oK")] + [InlineData(StatusCode.Error, "ERROR")] + public void ToOtlpSpanStatusTagIsCaseInsensitiveTest(StatusCode expectedStatusCode, string statusCodeTagValue) + { + using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); + using var activity = activitySource.StartActivity("Name"); + activity.SetTag(SpanAttributeConstants.StatusCodeKey, statusCodeTagValue); - var otlpSpan = activity.ToOtlpSpan(DefaultSdkLimitOptions); + var otlpSpan = activity.ToOtlpSpan(DefaultSdkLimitOptions); - Assert.NotNull(otlpSpan.Status); - Assert.Equal((int)ActivityStatusCode.Error, (int)otlpSpan.Status.Code); - Assert.Equal(StatusDescriptionOnError, otlpSpan.Status.Message); - } + Assert.NotNull(otlpSpan.Status); + Assert.Equal((int)expectedStatusCode, (int)otlpSpan.Status.Code); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ToOtlpSpanTraceStateTest(bool traceStateWasSet) - { - using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); - using var activity = activitySource.StartActivity("Name"); - string tracestate = "a=b;c=d"; - if (traceStateWasSet) - { - activity.TraceStateString = tracestate; - } + [Fact] + public void ToOtlpSpanActivityStatusTakesPrecedenceOverStatusTagsWhenActivityStatusCodeIsOk() + { + using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); + using var activity = activitySource.StartActivity("Name"); + const string TagDescriptionOnError = "Description when TagStatusCode is Error."; + activity.SetStatus(ActivityStatusCode.Ok); + activity.SetTag(SpanAttributeConstants.StatusCodeKey, "ERROR"); + activity.SetTag(SpanAttributeConstants.StatusDescriptionKey, TagDescriptionOnError); + + var otlpSpan = activity.ToOtlpSpan(DefaultSdkLimitOptions); + + Assert.NotNull(otlpSpan.Status); + Assert.Equal((int)ActivityStatusCode.Ok, (int)otlpSpan.Status.Code); + Assert.Empty(otlpSpan.Status.Message); + } - var otlpSpan = activity.ToOtlpSpan(DefaultSdkLimitOptions); + [Fact] + public void ToOtlpSpanActivityStatusTakesPrecedenceOverStatusTagsWhenActivityStatusCodeIsError() + { + using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); + using var activity = activitySource.StartActivity("Name"); + const string StatusDescriptionOnError = "Description when ActivityStatusCode is Error."; + activity.SetStatus(ActivityStatusCode.Error, StatusDescriptionOnError); + activity.SetTag(SpanAttributeConstants.StatusCodeKey, "OK"); - if (traceStateWasSet) - { - Assert.NotNull(otlpSpan.TraceState); - Assert.Equal(tracestate, otlpSpan.TraceState); - } - else - { - Assert.Equal(string.Empty, otlpSpan.TraceState); - } + var otlpSpan = activity.ToOtlpSpan(DefaultSdkLimitOptions); + + Assert.NotNull(otlpSpan.Status); + Assert.Equal((int)ActivityStatusCode.Error, (int)otlpSpan.Status.Code); + Assert.Equal(StatusDescriptionOnError, otlpSpan.Status.Message); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ToOtlpSpanTraceStateTest(bool traceStateWasSet) + { + using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); + using var activity = activitySource.StartActivity("Name"); + string tracestate = "a=b;c=d"; + if (traceStateWasSet) + { + activity.TraceStateString = tracestate; } - [Fact] - public void ToOtlpSpanPeerServiceTest() + var otlpSpan = activity.ToOtlpSpan(DefaultSdkLimitOptions); + + if (traceStateWasSet) + { + Assert.NotNull(otlpSpan.TraceState); + Assert.Equal(tracestate, otlpSpan.TraceState); + } + else { - using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); + Assert.Equal(string.Empty, otlpSpan.TraceState); + } + } - using var rootActivity = activitySource.StartActivity("root", ActivityKind.Client); + [Fact] + public void ToOtlpSpanPeerServiceTest() + { + using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); - rootActivity.SetTag(SemanticConventions.AttributeHttpHost, "opentelemetry.io"); + using var rootActivity = activitySource.StartActivity("root", ActivityKind.Client); - var otlpSpan = rootActivity.ToOtlpSpan(DefaultSdkLimitOptions); + rootActivity.SetTag(SemanticConventions.AttributeHttpHost, "opentelemetry.io"); - Assert.NotNull(otlpSpan); + var otlpSpan = rootActivity.ToOtlpSpan(DefaultSdkLimitOptions); - var peerService = otlpSpan.Attributes.FirstOrDefault(kvp => kvp.Key == SemanticConventions.AttributePeerService); + Assert.NotNull(otlpSpan); - Assert.NotNull(peerService); - Assert.Equal("opentelemetry.io", peerService.Value.StringValue); - } + var peerService = otlpSpan.Attributes.FirstOrDefault(kvp => kvp.Key == SemanticConventions.AttributePeerService); + + Assert.NotNull(peerService); + Assert.Equal("opentelemetry.io", peerService.Value.StringValue); + } - [Fact] - public void UseOpenTelemetryProtocolActivityExporterWithCustomActivityProcessor() + [Fact] + public void UseOpenTelemetryProtocolActivityExporterWithCustomActivityProcessor() + { + if (Environment.Version.Major == 3) { - if (Environment.Version.Major == 3) - { - // Adding the OtlpExporter creates a GrpcChannel. - // This switch must be set before creating a GrpcChannel when calling an insecure HTTP/2 endpoint. - // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - } + // Adding the OtlpExporter creates a GrpcChannel. + // This switch must be set before creating a GrpcChannel when calling an insecure HTTP/2 endpoint. + // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + } - const string ActivitySourceName = "otlp.test"; - TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); + const string ActivitySourceName = "otlp.test"; + TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); - bool startCalled = false; - bool endCalled = false; + bool startCalled = false; + bool endCalled = false; - testActivityProcessor.StartAction = - (a) => - { - startCalled = true; - }; + testActivityProcessor.StartAction = + (a) => + { + startCalled = true; + }; - testActivityProcessor.EndAction = - (a) => - { - endCalled = true; - }; + testActivityProcessor.EndAction = + (a) => + { + endCalled = true; + }; - var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(ActivitySourceName) - .AddProcessor(testActivityProcessor) - .AddOtlpExporter() - .Build(); + var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(ActivitySourceName) + .AddProcessor(testActivityProcessor) + .AddOtlpExporter() + .Build(); - using var source = new ActivitySource(ActivitySourceName); - var activity = source.StartActivity("Test Otlp Activity"); - activity?.Stop(); + using var source = new ActivitySource(ActivitySourceName); + var activity = source.StartActivity("Test Otlp Activity"); + activity?.Stop(); - Assert.True(startCalled); - Assert.True(endCalled); - } + Assert.True(startCalled); + Assert.True(endCalled); + } - [Fact] - public void Shutdown_ClientShutdownIsCalled() - { - var exportClientMock = new Mock>(); + [Fact] + public void Shutdown_ClientShutdownIsCalled() + { + var exportClientMock = new TestExportClient(); - var exporter = new OtlpTraceExporter(new OtlpExporterOptions(), DefaultSdkLimitOptions, exportClientMock.Object); + var transmissionHandler = new OtlpExporterTransmissionHandler(exportClientMock); - var result = exporter.Shutdown(); + var exporter = new OtlpTraceExporter(new OtlpExporterOptions(), DefaultSdkLimitOptions, transmissionHandler); - exportClientMock.Verify(m => m.Shutdown(It.IsAny()), Times.Once()); - } + exporter.Shutdown(); - [Fact] - public void Null_BatchExportProcessorOptions_SupportedTest() - { - Sdk.CreateTracerProviderBuilder() - .AddOtlpExporter( - o => - { - o.Protocol = OtlpExportProtocol.HttpProtobuf; - o.ExportProcessorType = ExportProcessorType.Batch; - o.BatchExportProcessorOptions = null; - }); - } + Assert.True(exportClientMock.ShutdownCalled); + } - [Fact] - public void NonnamedOptionsMutateSharedInstanceTest() - { - var testOptionsInstance = new OtlpExporterOptions(); + [Fact] + public void Null_BatchExportProcessorOptions_SupportedTest() + { + Sdk.CreateTracerProviderBuilder() + .AddOtlpExporter( + o => + { + o.Protocol = OtlpExportProtocol.HttpProtobuf; + o.ExportProcessorType = ExportProcessorType.Batch; + o.BatchExportProcessorOptions = null; + }); + } - OtlpExporterOptions tracerOptions = null; - OtlpExporterOptions meterOptions = null; + [Fact] + public void NonnamedOptionsMutateSharedInstanceTest() + { + var testOptionsInstance = new OtlpExporterOptions(); - var services = new ServiceCollection(); + OtlpExporterOptions tracerOptions = null; + OtlpExporterOptions meterOptions = null; - services.AddOpenTelemetry() - .WithTracing(builder => builder.AddOtlpExporter(o => - { - Assert.Equal(testOptionsInstance.Endpoint, o.Endpoint); + var services = new ServiceCollection(); - tracerOptions = o; - o.Endpoint = new("http://localhost/traces"); - })) - .WithMetrics(builder => builder.AddOtlpExporter(o => - { - Assert.Equal(testOptionsInstance.Endpoint, o.Endpoint); + services.AddOpenTelemetry() + .WithTracing(builder => builder.AddOtlpExporter(o => + { + Assert.Equal(testOptionsInstance.Endpoint, o.Endpoint); - meterOptions = o; - o.Endpoint = new("http://localhost/metrics"); - })); + tracerOptions = o; + o.Endpoint = new("http://localhost/traces"); + })) + .WithMetrics(builder => builder.AddOtlpExporter(o => + { + Assert.Equal(testOptionsInstance.Endpoint, o.Endpoint); - using var serviceProvider = services.BuildServiceProvider(); + meterOptions = o; + o.Endpoint = new("http://localhost/metrics"); + })); - var tracerProvider = serviceProvider.GetRequiredService(); + using var serviceProvider = services.BuildServiceProvider(); - // Verify the OtlpTraceExporter saw the correct endpoint. + var tracerProvider = serviceProvider.GetRequiredService(); - Assert.NotNull(tracerOptions); - Assert.Null(meterOptions); - Assert.Equal("http://localhost/traces", tracerOptions.Endpoint.OriginalString); + // Verify the OtlpTraceExporter saw the correct endpoint. - var meterProvider = serviceProvider.GetRequiredService(); + Assert.NotNull(tracerOptions); + Assert.Null(meterOptions); + Assert.Equal("http://localhost/traces", tracerOptions.Endpoint.OriginalString); - // Verify the OtlpMetricExporter saw the correct endpoint. + var meterProvider = serviceProvider.GetRequiredService(); - Assert.NotNull(tracerOptions); - Assert.NotNull(meterOptions); - Assert.Equal("http://localhost/metrics", meterOptions.Endpoint.OriginalString); + // Verify the OtlpMetricExporter saw the correct endpoint. - Assert.False(ReferenceEquals(tracerOptions, meterOptions)); - } + Assert.NotNull(tracerOptions); + Assert.NotNull(meterOptions); + Assert.Equal("http://localhost/metrics", meterOptions.Endpoint.OriginalString); - [Fact] - public void NamedOptionsMutateSeparateInstancesTest() - { - OtlpExporterOptions tracerOptions = null; - OtlpExporterOptions meterOptions = null; + Assert.False(ReferenceEquals(tracerOptions, meterOptions)); + } + + [Fact] + public void NamedOptionsMutateSeparateInstancesTest() + { + OtlpExporterOptions tracerOptions = null; + OtlpExporterOptions meterOptions = null; - var services = new ServiceCollection(); + var services = new ServiceCollection(); - services.AddOpenTelemetry() - .WithTracing(builder => builder.AddOtlpExporter("Trace", o => - { - tracerOptions = o; - o.Endpoint = new("http://localhost/traces"); - })) - .WithMetrics(builder => builder.AddOtlpExporter("Metrics", o => - { - meterOptions = o; - o.Endpoint = new("http://localhost/metrics"); - })); + services.AddOpenTelemetry() + .WithTracing(builder => builder.AddOtlpExporter("Trace", o => + { + tracerOptions = o; + o.Endpoint = new("http://localhost/traces"); + })) + .WithMetrics(builder => builder.AddOtlpExporter("Metrics", o => + { + meterOptions = o; + o.Endpoint = new("http://localhost/metrics"); + })); - using var serviceProvider = services.BuildServiceProvider(); + using var serviceProvider = services.BuildServiceProvider(); - var tracerProvider = serviceProvider.GetRequiredService(); + var tracerProvider = serviceProvider.GetRequiredService(); - // Verify the OtlpTraceExporter saw the correct endpoint. + // Verify the OtlpTraceExporter saw the correct endpoint. - Assert.NotNull(tracerOptions); - Assert.Null(meterOptions); - Assert.Equal("http://localhost/traces", tracerOptions.Endpoint.OriginalString); + Assert.NotNull(tracerOptions); + Assert.Null(meterOptions); + Assert.Equal("http://localhost/traces", tracerOptions.Endpoint.OriginalString); - var meterProvider = serviceProvider.GetRequiredService(); + var meterProvider = serviceProvider.GetRequiredService(); - // Verify the OtlpMetricExporter saw the correct endpoint. + // Verify the OtlpMetricExporter saw the correct endpoint. - Assert.NotNull(tracerOptions); - Assert.NotNull(meterOptions); - Assert.Equal("http://localhost/metrics", meterOptions.Endpoint.OriginalString); + Assert.NotNull(tracerOptions); + Assert.NotNull(meterOptions); + Assert.Equal("http://localhost/metrics", meterOptions.Endpoint.OriginalString); - // Verify expected state of instances. + // Verify expected state of instances. - Assert.False(ReferenceEquals(tracerOptions, meterOptions)); - Assert.Equal("http://localhost/traces", tracerOptions.Endpoint.OriginalString); - Assert.Equal("http://localhost/metrics", meterOptions.Endpoint.OriginalString); - } + Assert.False(ReferenceEquals(tracerOptions, meterOptions)); + Assert.Equal("http://localhost/traces", tracerOptions.Endpoint.OriginalString); + Assert.Equal("http://localhost/metrics", meterOptions.Endpoint.OriginalString); } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/SdkLimitOptionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/SdkLimitOptionsTests.cs index 8716fc50d04..dc36def4665 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/SdkLimitOptionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/SdkLimitOptionsTests.cs @@ -1,183 +1,183 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.Configuration; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; using Xunit; -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +public sealed class SdkLimitOptionsTests : IDisposable { - public sealed class SdkLimitOptionsTests : IDisposable + public SdkLimitOptionsTests() { - public SdkLimitOptionsTests() - { - ClearEnvVars(); - } + ClearEnvVars(); + } - public void Dispose() - { - ClearEnvVars(); - } + public void Dispose() + { + ClearEnvVars(); + } - [Fact] - public void SdkLimitOptionsDefaults() - { - var options = new SdkLimitOptions(); - - Assert.Null(options.AttributeValueLengthLimit); - Assert.Equal(128, options.AttributeCountLimit); - Assert.Null(options.SpanAttributeValueLengthLimit); - Assert.Equal(128, options.SpanAttributeCountLimit); - Assert.Equal(128, options.SpanEventCountLimit); - Assert.Equal(128, options.SpanLinkCountLimit); - Assert.Equal(128, options.SpanEventAttributeCountLimit); - Assert.Equal(128, options.SpanLinkAttributeCountLimit); - } - - [Fact] - public void SdkLimitOptionsIsInitializedFromEnvironment() - { - Environment.SetEnvironmentVariable("OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT", "10"); - Environment.SetEnvironmentVariable("OTEL_ATTRIBUTE_COUNT_LIMIT", "10"); - Environment.SetEnvironmentVariable("OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT", "20"); - Environment.SetEnvironmentVariable("OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT", "20"); - Environment.SetEnvironmentVariable("OTEL_SPAN_EVENT_COUNT_LIMIT", "10"); - Environment.SetEnvironmentVariable("OTEL_SPAN_LINK_COUNT_LIMIT", "10"); - Environment.SetEnvironmentVariable("OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT", "30"); - Environment.SetEnvironmentVariable("OTEL_LINK_ATTRIBUTE_COUNT_LIMIT", "30"); - - var options = new SdkLimitOptions(); - - Assert.Equal(10, options.AttributeValueLengthLimit); - Assert.Equal(10, options.AttributeCountLimit); - Assert.Equal(20, options.SpanAttributeValueLengthLimit); - Assert.Equal(20, options.SpanAttributeCountLimit); - Assert.Equal(10, options.SpanEventCountLimit); - Assert.Equal(10, options.SpanLinkCountLimit); - Assert.Equal(30, options.SpanEventAttributeCountLimit); - Assert.Equal(30, options.SpanLinkAttributeCountLimit); - } - - [Fact] - public void SpanAttributeValueLengthLimitFallback() - { - var options = new SdkLimitOptions(); + [Fact] + public void SdkLimitOptionsDefaults() + { + var options = new SdkLimitOptions(); + + Assert.Null(options.AttributeValueLengthLimit); + Assert.Equal(128, options.AttributeCountLimit); + Assert.Null(options.SpanAttributeValueLengthLimit); + Assert.Equal(128, options.SpanAttributeCountLimit); + Assert.Equal(128, options.SpanEventCountLimit); + Assert.Equal(128, options.SpanLinkCountLimit); + Assert.Equal(128, options.SpanEventAttributeCountLimit); + Assert.Equal(128, options.SpanLinkAttributeCountLimit); + Assert.Null(options.LogRecordAttributeValueLengthLimit); + Assert.Equal(128, options.LogRecordAttributeCountLimit); + } - options.AttributeValueLengthLimit = 10; - Assert.Equal(10, options.AttributeValueLengthLimit); - Assert.Equal(10, options.SpanAttributeValueLengthLimit); + [Fact] + public void SdkLimitOptionsIsInitializedFromEnvironment() + { + Environment.SetEnvironmentVariable("OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT", "10"); + Environment.SetEnvironmentVariable("OTEL_ATTRIBUTE_COUNT_LIMIT", "10"); + Environment.SetEnvironmentVariable("OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT", "20"); + Environment.SetEnvironmentVariable("OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT", "20"); + Environment.SetEnvironmentVariable("OTEL_SPAN_EVENT_COUNT_LIMIT", "10"); + Environment.SetEnvironmentVariable("OTEL_SPAN_LINK_COUNT_LIMIT", "10"); + Environment.SetEnvironmentVariable("OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT", "30"); + Environment.SetEnvironmentVariable("OTEL_LINK_ATTRIBUTE_COUNT_LIMIT", "30"); + + var options = new SdkLimitOptions(); + + Assert.Equal(10, options.AttributeValueLengthLimit); + Assert.Equal(10, options.AttributeCountLimit); + Assert.Equal(20, options.SpanAttributeValueLengthLimit); + Assert.Equal(20, options.SpanAttributeCountLimit); + Assert.Equal(10, options.SpanEventCountLimit); + Assert.Equal(10, options.SpanLinkCountLimit); + Assert.Equal(30, options.SpanEventAttributeCountLimit); + Assert.Equal(30, options.SpanLinkAttributeCountLimit); + } - options.SpanAttributeValueLengthLimit = 20; - Assert.Equal(10, options.AttributeValueLengthLimit); - Assert.Equal(20, options.SpanAttributeValueLengthLimit); + [Fact] + public void SpanAttributeValueLengthLimitFallback() + { + var options = new SdkLimitOptions(); + + options.AttributeValueLengthLimit = 10; + Assert.Equal(10, options.AttributeValueLengthLimit); + Assert.Equal(10, options.SpanAttributeValueLengthLimit); + Assert.Equal(10, options.LogRecordAttributeValueLengthLimit); + + options.SpanAttributeValueLengthLimit = 20; + options.LogRecordAttributeValueLengthLimit = 21; + Assert.Equal(10, options.AttributeValueLengthLimit); + Assert.Equal(20, options.SpanAttributeValueLengthLimit); + Assert.Equal(21, options.LogRecordAttributeValueLengthLimit); + + options.SpanAttributeValueLengthLimit = null; + options.LogRecordAttributeValueLengthLimit = null; + Assert.Equal(10, options.AttributeValueLengthLimit); + Assert.Null(options.SpanAttributeValueLengthLimit); + Assert.Null(options.LogRecordAttributeValueLengthLimit); + } - options.SpanAttributeValueLengthLimit = null; - Assert.Equal(10, options.AttributeValueLengthLimit); - Assert.Null(options.SpanAttributeValueLengthLimit); - } + [Fact] + public void SpanAttributeCountLimitFallback() + { + var options = new SdkLimitOptions(); + + options.AttributeCountLimit = 10; + Assert.Equal(10, options.AttributeCountLimit); + Assert.Equal(10, options.SpanAttributeCountLimit); + Assert.Equal(10, options.SpanEventAttributeCountLimit); + Assert.Equal(10, options.SpanLinkAttributeCountLimit); + Assert.Equal(10, options.LogRecordAttributeCountLimit); + + options.SpanAttributeCountLimit = 20; + Assert.Equal(10, options.AttributeCountLimit); + Assert.Equal(20, options.SpanAttributeCountLimit); + Assert.Equal(20, options.SpanEventAttributeCountLimit); + Assert.Equal(20, options.SpanLinkAttributeCountLimit); + + options.SpanEventAttributeCountLimit = 30; + Assert.Equal(10, options.AttributeCountLimit); + Assert.Equal(20, options.SpanAttributeCountLimit); + Assert.Equal(30, options.SpanEventAttributeCountLimit); + Assert.Equal(20, options.SpanLinkAttributeCountLimit); + + options.SpanLinkAttributeCountLimit = 40; + Assert.Equal(10, options.AttributeCountLimit); + Assert.Equal(20, options.SpanAttributeCountLimit); + Assert.Equal(30, options.SpanEventAttributeCountLimit); + Assert.Equal(40, options.SpanLinkAttributeCountLimit); + + options.SpanLinkAttributeCountLimit = null; + Assert.Equal(10, options.AttributeCountLimit); + Assert.Equal(20, options.SpanAttributeCountLimit); + Assert.Equal(30, options.SpanEventAttributeCountLimit); + Assert.Null(options.SpanLinkAttributeCountLimit); + + options.SpanEventAttributeCountLimit = null; + Assert.Equal(10, options.AttributeCountLimit); + Assert.Equal(20, options.SpanAttributeCountLimit); + Assert.Null(options.SpanEventAttributeCountLimit); + Assert.Null(options.SpanLinkAttributeCountLimit); + + options.SpanAttributeCountLimit = null; + Assert.Equal(10, options.AttributeCountLimit); + Assert.Null(options.SpanAttributeCountLimit); + Assert.Null(options.SpanEventAttributeCountLimit); + Assert.Null(options.SpanLinkAttributeCountLimit); + } - [Fact] - public void SpanAttributeCountLimitFallback() - { - var options = new SdkLimitOptions(); - - options.AttributeCountLimit = 10; - Assert.Equal(10, options.AttributeCountLimit); - Assert.Equal(10, options.SpanAttributeCountLimit); - Assert.Equal(10, options.SpanEventAttributeCountLimit); - Assert.Equal(10, options.SpanLinkAttributeCountLimit); - - options.SpanAttributeCountLimit = 20; - Assert.Equal(10, options.AttributeCountLimit); - Assert.Equal(20, options.SpanAttributeCountLimit); - Assert.Equal(20, options.SpanEventAttributeCountLimit); - Assert.Equal(20, options.SpanLinkAttributeCountLimit); - - options.SpanEventAttributeCountLimit = 30; - Assert.Equal(10, options.AttributeCountLimit); - Assert.Equal(20, options.SpanAttributeCountLimit); - Assert.Equal(30, options.SpanEventAttributeCountLimit); - Assert.Equal(20, options.SpanLinkAttributeCountLimit); - - options.SpanLinkAttributeCountLimit = 40; - Assert.Equal(10, options.AttributeCountLimit); - Assert.Equal(20, options.SpanAttributeCountLimit); - Assert.Equal(30, options.SpanEventAttributeCountLimit); - Assert.Equal(40, options.SpanLinkAttributeCountLimit); - - options.SpanLinkAttributeCountLimit = null; - Assert.Equal(10, options.AttributeCountLimit); - Assert.Equal(20, options.SpanAttributeCountLimit); - Assert.Equal(30, options.SpanEventAttributeCountLimit); - Assert.Null(options.SpanLinkAttributeCountLimit); - - options.SpanEventAttributeCountLimit = null; - Assert.Equal(10, options.AttributeCountLimit); - Assert.Equal(20, options.SpanAttributeCountLimit); - Assert.Null(options.SpanEventAttributeCountLimit); - Assert.Null(options.SpanLinkAttributeCountLimit); - - options.SpanAttributeCountLimit = null; - Assert.Equal(10, options.AttributeCountLimit); - Assert.Null(options.SpanAttributeCountLimit); - Assert.Null(options.SpanEventAttributeCountLimit); - Assert.Null(options.SpanLinkAttributeCountLimit); - } - - [Fact] - public void SdkLimitOptionsUsingIConfiguration() - { - var values = new Dictionary - { - ["OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT"] = "23", - ["OTEL_ATTRIBUTE_COUNT_LIMIT"] = "24", - ["OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT"] = "25", - ["OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT"] = "26", - ["OTEL_SPAN_EVENT_COUNT_LIMIT"] = "27", - ["OTEL_SPAN_LINK_COUNT_LIMIT"] = "28", - ["OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT"] = "29", - ["OTEL_LINK_ATTRIBUTE_COUNT_LIMIT"] = "30", - }; - - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(values) - .Build(); - - var options = new SdkLimitOptions(configuration); - - Assert.Equal(23, options.AttributeValueLengthLimit); - Assert.Equal(24, options.AttributeCountLimit); - Assert.Equal(25, options.SpanAttributeValueLengthLimit); - Assert.Equal(26, options.SpanAttributeCountLimit); - Assert.Equal(27, options.SpanEventCountLimit); - Assert.Equal(28, options.SpanLinkCountLimit); - Assert.Equal(29, options.SpanEventAttributeCountLimit); - Assert.Equal(30, options.SpanLinkAttributeCountLimit); - } - - private static void ClearEnvVars() + [Fact] + public void SdkLimitOptionsUsingIConfiguration() + { + var values = new Dictionary { - Environment.SetEnvironmentVariable("OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT", null); - Environment.SetEnvironmentVariable("OTEL_ATTRIBUTE_COUNT_LIMIT", null); - Environment.SetEnvironmentVariable("OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT", null); - Environment.SetEnvironmentVariable("OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT", null); - Environment.SetEnvironmentVariable("OTEL_SPAN_EVENT_COUNT_LIMIT", null); - Environment.SetEnvironmentVariable("OTEL_SPAN_LINK_COUNT_LIMIT", null); - Environment.SetEnvironmentVariable("OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT", null); - Environment.SetEnvironmentVariable("OTEL_LINK_ATTRIBUTE_COUNT_LIMIT", null); - } + ["OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT"] = "23", + ["OTEL_ATTRIBUTE_COUNT_LIMIT"] = "24", + ["OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT"] = "25", + ["OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT"] = "26", + ["OTEL_SPAN_EVENT_COUNT_LIMIT"] = "27", + ["OTEL_SPAN_LINK_COUNT_LIMIT"] = "28", + ["OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT"] = "29", + ["OTEL_LINK_ATTRIBUTE_COUNT_LIMIT"] = "30", + ["OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT"] = "31", + ["OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT"] = "32", + }; + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(values) + .Build(); + + var options = new SdkLimitOptions(configuration); + + Assert.Equal(23, options.AttributeValueLengthLimit); + Assert.Equal(24, options.AttributeCountLimit); + Assert.Equal(25, options.SpanAttributeValueLengthLimit); + Assert.Equal(26, options.SpanAttributeCountLimit); + Assert.Equal(27, options.SpanEventCountLimit); + Assert.Equal(28, options.SpanLinkCountLimit); + Assert.Equal(29, options.SpanEventAttributeCountLimit); + Assert.Equal(30, options.SpanLinkAttributeCountLimit); + Assert.Equal(31, options.LogRecordAttributeValueLengthLimit); + Assert.Equal(32, options.LogRecordAttributeCountLimit); + } + + private static void ClearEnvVars() + { + Environment.SetEnvironmentVariable("OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT", null); + Environment.SetEnvironmentVariable("OTEL_ATTRIBUTE_COUNT_LIMIT", null); + Environment.SetEnvironmentVariable("OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT", null); + Environment.SetEnvironmentVariable("OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT", null); + Environment.SetEnvironmentVariable("OTEL_SPAN_EVENT_COUNT_LIMIT", null); + Environment.SetEnvironmentVariable("OTEL_SPAN_LINK_COUNT_LIMIT", null); + Environment.SetEnvironmentVariable("OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT", null); + Environment.SetEnvironmentVariable("OTEL_LINK_ATTRIBUTE_COUNT_LIMIT", null); + Environment.SetEnvironmentVariable("OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT", null); + Environment.SetEnvironmentVariable("OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT", null); } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestExportClient.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestExportClient.cs new file mode 100644 index 00000000000..c16a4f1665a --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestExportClient.cs @@ -0,0 +1,40 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +internal class TestExportClient(bool throwException = false) : IExportClient +{ + public bool SendExportRequestCalled { get; private set; } + + public bool ShutdownCalled { get; private set; } + + public bool ThrowException { get; set; } = throwException; + + public ExportClientResponse SendExportRequest(T request, CancellationToken cancellationToken = default) + { + if (this.ThrowException) + { + throw new Exception("Exception thrown from SendExportRequest"); + } + + this.SendExportRequestCalled = true; + return new TestExportClientResponse(true, null, null); + } + + public bool Shutdown(int timeoutMilliseconds) + { + this.ShutdownCalled = true; + return true; + } + + private class TestExportClientResponse : ExportClientResponse + { + public TestExportClientResponse(bool success, DateTime? deadline, Exception exception) + : base(success, deadline, exception) + { + } + } +} diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestHttpMessageHandler.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestHttpMessageHandler.cs new file mode 100644 index 00000000000..a15775cb1ac --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestHttpMessageHandler.cs @@ -0,0 +1,34 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if !NET6_0_OR_GREATER +using System.Net.Http; +#endif + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +internal class TestHttpMessageHandler : HttpMessageHandler +{ + public HttpRequestMessage HttpRequestMessage { get; private set; } + + public byte[] HttpRequestContent { get; private set; } + + public virtual HttpResponseMessage InternalSend(HttpRequestMessage request, CancellationToken cancellationToken) + { + this.HttpRequestMessage = request; + this.HttpRequestContent = request.Content.ReadAsByteArrayAsync().Result; + return new HttpResponseMessage(); + } + +#if NET6_0_OR_GREATER + protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) + { + return this.InternalSend(request, cancellationToken); + } +#endif + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(this.InternalSend(request, cancellationToken)); + } +} diff --git a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests.csproj b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests.csproj index eeaca552e42..e3603d4bc50 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests.csproj +++ b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests.csproj @@ -1,8 +1,7 @@ Unit test project for Prometheus Exporter AspNetCore for OpenTelemetry - - net6.0 + $(TargetFrameworksForAspNetCoreTests) $(DefineConstants);PROMETHEUS_ASPNETCORE @@ -10,11 +9,10 @@ + - - - all + runtime; build; native; contentfiles; analyzers @@ -25,14 +23,6 @@ - - - - - - - - @@ -42,5 +32,4 @@ - diff --git a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMeterProviderBuilderExtensionsTests.cs b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMeterProviderBuilderExtensionsTests.cs index a80d2cbc9d2..726b0820d04 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMeterProviderBuilderExtensionsTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMeterProviderBuilderExtensionsTests.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Metrics; diff --git a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs index 6cd618e500e..7e1a6c80bae 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #if !NETFRAMEWORK using System.Diagnostics.Metrics; @@ -20,6 +7,7 @@ using System.Text.RegularExpressions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -27,298 +15,339 @@ using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests +namespace OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests; + +public sealed class PrometheusExporterMiddlewareTests { - public sealed class PrometheusExporterMiddlewareTests + private const string MeterVersion = "1.0.1"; + + private static readonly string MeterName = Utils.GetCurrentMethodName(); + + [Fact] + public Task PrometheusExporterMiddlewareIntegration() { - private static readonly string MeterName = Utils.GetCurrentMethodName(); + return RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics", + app => app.UseOpenTelemetryPrometheusScrapingEndpoint()); + } - [Fact] - public Task PrometheusExporterMiddlewareIntegration() - { - return RunPrometheusExporterMiddlewareIntegrationTest( - "/metrics", - app => app.UseOpenTelemetryPrometheusScrapingEndpoint()); - } + [Fact] + public Task PrometheusExporterMiddlewareIntegration_Options() + { + return RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics_options", + app => app.UseOpenTelemetryPrometheusScrapingEndpoint(), + services => services.Configure(o => o.ScrapeEndpointPath = "metrics_options")); + } - [Fact] - public Task PrometheusExporterMiddlewareIntegration_Options() - { - return RunPrometheusExporterMiddlewareIntegrationTest( - "/metrics_options", - app => app.UseOpenTelemetryPrometheusScrapingEndpoint(), - services => services.Configure(o => o.ScrapeEndpointPath = "metrics_options")); - } + [Fact] + public Task PrometheusExporterMiddlewareIntegration_OptionsFallback() + { + return RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics", + app => app.UseOpenTelemetryPrometheusScrapingEndpoint(), + services => services.Configure(o => o.ScrapeEndpointPath = null)); + } - [Fact] - public Task PrometheusExporterMiddlewareIntegration_OptionsFallback() - { - return RunPrometheusExporterMiddlewareIntegrationTest( - "/metrics", - app => app.UseOpenTelemetryPrometheusScrapingEndpoint(), - services => services.Configure(o => o.ScrapeEndpointPath = null)); - } + [Fact] + public Task PrometheusExporterMiddlewareIntegration_OptionsViaAddPrometheusExporter() + { + return RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics_from_AddPrometheusExporter", + app => app.UseOpenTelemetryPrometheusScrapingEndpoint(), + configureOptions: o => o.ScrapeEndpointPath = "/metrics_from_AddPrometheusExporter"); + } - [Fact] - public Task PrometheusExporterMiddlewareIntegration_OptionsViaAddPrometheusExporter() - { - return RunPrometheusExporterMiddlewareIntegrationTest( - "/metrics_from_AddPrometheusExporter", - app => app.UseOpenTelemetryPrometheusScrapingEndpoint(), - configureOptions: o => o.ScrapeEndpointPath = "/metrics_from_AddPrometheusExporter"); - } + [Fact] + public Task PrometheusExporterMiddlewareIntegration_PathOverride() + { + return RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics_override", + app => app.UseOpenTelemetryPrometheusScrapingEndpoint("/metrics_override")); + } - [Fact] - public Task PrometheusExporterMiddlewareIntegration_PathOverride() - { - return RunPrometheusExporterMiddlewareIntegrationTest( - "/metrics_override", - app => app.UseOpenTelemetryPrometheusScrapingEndpoint("/metrics_override")); - } + [Fact] + public Task PrometheusExporterMiddlewareIntegration_WithPathNamedOptionsOverride() + { + return RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics_override", + app => app.UseOpenTelemetryPrometheusScrapingEndpoint( + meterProvider: null, + predicate: null, + path: null, + configureBranchedPipeline: null, + optionsName: "myOptions"), + services => + { + services.Configure("myOptions", o => o.ScrapeEndpointPath = "/metrics_override"); + }); + } - [Fact] - public Task PrometheusExporterMiddlewareIntegration_WithPathNamedOptionsOverride() - { - return RunPrometheusExporterMiddlewareIntegrationTest( - "/metrics_override", - app => app.UseOpenTelemetryPrometheusScrapingEndpoint( - meterProvider: null, - predicate: null, - path: null, - configureBranchedPipeline: null, - optionsName: "myOptions"), - services => + [Fact] + public Task PrometheusExporterMiddlewareIntegration_Predicate() + { + return RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics_predicate?enabled=true", + app => app.UseOpenTelemetryPrometheusScrapingEndpoint(httpcontext => httpcontext.Request.Path == "/metrics_predicate" && httpcontext.Request.Query["enabled"] == "true")); + } + + [Fact] + public Task PrometheusExporterMiddlewareIntegration_MixedPredicateAndPath() + { + return RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics_predicate", + app => app.UseOpenTelemetryPrometheusScrapingEndpoint( + meterProvider: null, + predicate: httpcontext => httpcontext.Request.Path == "/metrics_predicate", + path: "/metrics_path", + configureBranchedPipeline: branch => branch.Use((context, next) => { - services.Configure("myOptions", o => o.ScrapeEndpointPath = "/metrics_override"); - }); - } + context.Response.Headers.Append("X-MiddlewareExecuted", "true"); + return next(); + }), + optionsName: null), + services => services.Configure(o => o.ScrapeEndpointPath = "/metrics_options"), + validateResponse: rsp => + { + if (!rsp.Headers.TryGetValues("X-MiddlewareExecuted", out IEnumerable headers)) + { + headers = Array.Empty(); + } - [Fact] - public Task PrometheusExporterMiddlewareIntegration_Predicate() - { - return RunPrometheusExporterMiddlewareIntegrationTest( - "/metrics_predicate?enabled=true", - app => app.UseOpenTelemetryPrometheusScrapingEndpoint(httpcontext => httpcontext.Request.Path == "/metrics_predicate" && httpcontext.Request.Query["enabled"] == "true")); - } + Assert.Equal("true", headers.FirstOrDefault()); + }); + } - [Fact] - public Task PrometheusExporterMiddlewareIntegration_MixedPredicateAndPath() - { - return RunPrometheusExporterMiddlewareIntegrationTest( - "/metrics_predicate", - app => app.UseOpenTelemetryPrometheusScrapingEndpoint( - meterProvider: null, - predicate: httpcontext => httpcontext.Request.Path == "/metrics_predicate", - path: "/metrics_path", - configureBranchedPipeline: branch => branch.Use((context, next) => - { - context.Response.Headers.Add("X-MiddlewareExecuted", "true"); - return next(); - }), - optionsName: null), - services => services.Configure(o => o.ScrapeEndpointPath = "/metrics_options"), - validateResponse: rsp => + [Fact] + public Task PrometheusExporterMiddlewareIntegration_MixedPath() + { + return RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics_path", + app => app.UseOpenTelemetryPrometheusScrapingEndpoint( + meterProvider: null, + predicate: null, + path: "/metrics_path", + configureBranchedPipeline: branch => branch.Use((context, next) => + { + context.Response.Headers.Append("X-MiddlewareExecuted", "true"); + return next(); + }), + optionsName: null), + services => services.Configure(o => o.ScrapeEndpointPath = "/metrics_options"), + validateResponse: rsp => + { + if (!rsp.Headers.TryGetValues("X-MiddlewareExecuted", out IEnumerable headers)) { - if (!rsp.Headers.TryGetValues("X-MiddlewareExecuted", out IEnumerable headers)) - { - headers = Array.Empty(); - } + headers = Array.Empty(); + } - Assert.Equal("true", headers.FirstOrDefault()); - }); - } + Assert.Equal("true", headers.FirstOrDefault()); + }); + } - [Fact] - public Task PrometheusExporterMiddlewareIntegration_MixedPath() - { - return RunPrometheusExporterMiddlewareIntegrationTest( - "/metrics_path", - app => app.UseOpenTelemetryPrometheusScrapingEndpoint( - meterProvider: null, - predicate: null, - path: "/metrics_path", - configureBranchedPipeline: branch => branch.Use((context, next) => - { - context.Response.Headers.Add("X-MiddlewareExecuted", "true"); - return next(); - }), - optionsName: null), - services => services.Configure(o => o.ScrapeEndpointPath = "/metrics_options"), - validateResponse: rsp => - { - if (!rsp.Headers.TryGetValues("X-MiddlewareExecuted", out IEnumerable headers)) - { - headers = Array.Empty(); - } + [Fact] + public async Task PrometheusExporterMiddlewareIntegration_MeterProvider() + { + using MeterProvider meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(MeterName) + .AddPrometheusExporter() + .Build(); + + await RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics", + app => app.UseOpenTelemetryPrometheusScrapingEndpoint( + meterProvider: meterProvider, + predicate: null, + path: null, + configureBranchedPipeline: null, + optionsName: null), + registerMeterProvider: false); + } - Assert.Equal("true", headers.FirstOrDefault()); - }); - } + [Fact] + public Task PrometheusExporterMiddlewareIntegration_NoMetrics() + { + return RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics", + app => app.UseOpenTelemetryPrometheusScrapingEndpoint(), + skipMetrics: true); + } - [Fact] - public async Task PrometheusExporterMiddlewareIntegration_MeterProvider() - { - using MeterProvider meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(MeterName) - .AddPrometheusExporter() - .Build(); - - await RunPrometheusExporterMiddlewareIntegrationTest( - "/metrics", - app => app.UseOpenTelemetryPrometheusScrapingEndpoint( - meterProvider: meterProvider, - predicate: null, - path: null, - configureBranchedPipeline: null, - optionsName: null), - registerMeterProvider: false).ConfigureAwait(false); - } + [Fact] + public Task PrometheusExporterMiddlewareIntegration_MapEndpoint() + { + return RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics", + app => app.UseRouting().UseEndpoints(builder => builder.MapPrometheusScrapingEndpoint()), + services => services.AddRouting()); + } - [Fact] - public Task PrometheusExporterMiddlewareIntegration_NoMetrics() - { - return RunPrometheusExporterMiddlewareIntegrationTest( - "/metrics", - app => app.UseOpenTelemetryPrometheusScrapingEndpoint(), - skipMetrics: true); - } + [Fact] + public Task PrometheusExporterMiddlewareIntegration_MapEndpoint_WithPathOverride() + { + return RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics_path", + app => app.UseRouting().UseEndpoints(builder => builder.MapPrometheusScrapingEndpoint("metrics_path")), + services => services.AddRouting()); + } - [Fact] - public Task PrometheusExporterMiddlewareIntegration_MapEndpoint() - { - return RunPrometheusExporterMiddlewareIntegrationTest( - "/metrics", - app => app.UseRouting().UseEndpoints(builder => builder.MapPrometheusScrapingEndpoint()), - services => services.AddRouting()); - } + [Fact] + public Task PrometheusExporterMiddlewareIntegration_MapEndpoint_WithPathNamedOptionsOverride() + { + return RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics_path", + app => app.UseRouting().UseEndpoints(builder => builder.MapPrometheusScrapingEndpoint( + path: null, + meterProvider: null, + configureBranchedPipeline: null, + optionsName: "myOptions")), + services => + { + services.AddRouting(); + services.Configure("myOptions", o => o.ScrapeEndpointPath = "/metrics_path"); + }); + } + + [Fact] + public async Task PrometheusExporterMiddlewareIntegration_MapEndpoint_WithMeterProvider() + { + using MeterProvider meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(MeterName) + .AddPrometheusExporter() + .Build(); + + await RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics", + app => app.UseRouting().UseEndpoints(builder => builder.MapPrometheusScrapingEndpoint( + path: null, + meterProvider: meterProvider, + configureBranchedPipeline: null, + optionsName: null)), + services => services.AddRouting(), + registerMeterProvider: false); + } + + [Fact] + public Task PrometheusExporterMiddlewareIntegration_TextPlainResponse() + { + return RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics", + app => app.UseOpenTelemetryPrometheusScrapingEndpoint(), + acceptHeader: "text/plain"); + } + + [Fact] + public Task PrometheusExporterMiddlewareIntegration_UseOpenMetricsVersionHeader() + { + return RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics", + app => app.UseOpenTelemetryPrometheusScrapingEndpoint(), + acceptHeader: "application/openmetrics-text; version=1.0.0"); + } - [Fact] - public Task PrometheusExporterMiddlewareIntegration_MapEndpoint_WithPathOverride() + private static async Task RunPrometheusExporterMiddlewareIntegrationTest( + string path, + Action configure, + Action configureServices = null, + Action validateResponse = null, + bool registerMeterProvider = true, + Action configureOptions = null, + bool skipMetrics = false, + string acceptHeader = "application/openmetrics-text") + { + var requestOpenMetrics = acceptHeader.StartsWith("application/openmetrics-text"); + + using var host = await new HostBuilder() + .ConfigureWebHost(webBuilder => webBuilder + .UseTestServer() + .ConfigureServices(services => + { + if (registerMeterProvider) + { + services.AddOpenTelemetry().WithMetrics(builder => builder + .AddMeter(MeterName) + .AddPrometheusExporter(o => + { + configureOptions?.Invoke(o); + })); + } + + configureServices?.Invoke(services); + }) + .Configure(configure)) + .StartAsync(); + + var tags = new KeyValuePair[] { - return RunPrometheusExporterMiddlewareIntegrationTest( - "/metrics_path", - app => app.UseRouting().UseEndpoints(builder => builder.MapPrometheusScrapingEndpoint("metrics_path")), - services => services.AddRouting()); - } + new KeyValuePair("key1", "value1"), + new KeyValuePair("key2", "value2"), + }; + + using var meter = new Meter(MeterName, MeterVersion); - [Fact] - public Task PrometheusExporterMiddlewareIntegration_MapEndpoint_WithPathNamedOptionsOverride() + var beginTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + + var counter = meter.CreateCounter("counter_double"); + if (!skipMetrics) { - return RunPrometheusExporterMiddlewareIntegrationTest( - "/metrics_path", - app => app.UseRouting().UseEndpoints(builder => builder.MapPrometheusScrapingEndpoint( - path: null, - meterProvider: null, - configureBranchedPipeline: null, - optionsName: "myOptions")), - services => - { - services.AddRouting(); - services.Configure("myOptions", o => o.ScrapeEndpointPath = "/metrics_path"); - }); + counter.Add(100.18D, tags); + counter.Add(0.99D, tags); } - [Fact] - public async Task PrometheusExporterMiddlewareIntegration_MapEndpoint_WithMeterProvider() + using var client = host.GetTestClient(); + + if (!string.IsNullOrEmpty(acceptHeader)) { - using MeterProvider meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(MeterName) - .AddPrometheusExporter() - .Build(); - - await RunPrometheusExporterMiddlewareIntegrationTest( - "/metrics", - app => app.UseRouting().UseEndpoints(builder => builder.MapPrometheusScrapingEndpoint( - path: null, - meterProvider: meterProvider, - configureBranchedPipeline: null, - optionsName: null)), - services => services.AddRouting(), - registerMeterProvider: false).ConfigureAwait(false); + client.DefaultRequestHeaders.Add("Accept", acceptHeader); } - private static async Task RunPrometheusExporterMiddlewareIntegrationTest( - string path, - Action configure, - Action configureServices = null, - Action validateResponse = null, - bool registerMeterProvider = true, - Action configureOptions = null, - bool skipMetrics = false) - { - using var host = await new HostBuilder() - .ConfigureWebHost(webBuilder => webBuilder - .UseTestServer() - .ConfigureServices(services => - { - if (registerMeterProvider) - { - services.AddOpenTelemetry().WithMetrics(builder => builder - .AddMeter(MeterName) - .AddPrometheusExporter(o => - { - configureOptions?.Invoke(o); - })); - } - - configureServices?.Invoke(services); - }) - .Configure(configure)) - .StartAsync().ConfigureAwait(false); - - var tags = new KeyValuePair[] - { - new KeyValuePair("key1", "value1"), - new KeyValuePair("key2", "value2"), - }; + using var response = await client.GetAsync(path); - using var meter = new Meter(MeterName); + var endTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - var beginTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + if (!skipMetrics) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.True(response.Content.Headers.Contains("Last-Modified")); - var counter = meter.CreateCounter("counter_double"); - if (!skipMetrics) + if (requestOpenMetrics) { - counter.Add(100.18D, tags); - counter.Add(0.99D, tags); + Assert.Equal("application/openmetrics-text; version=1.0.0; charset=utf-8", response.Content.Headers.ContentType.ToString()); } - - using var response = await host.GetTestClient().GetAsync(path).ConfigureAwait(false); - - var endTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - - if (!skipMetrics) + else { - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - 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); - - var matches = Regex.Matches( - content, - ("^" - + "# TYPE counter_double counter\n" - + "counter_double{key1='value1',key2='value2'} 101.17 (\\d+)\n" - + "\n" - + "# EOF\n" - + "$").Replace('\'', '"')); + string content = await response.Content.ReadAsStringAsync(); - Assert.Single(matches); + string expected = requestOpenMetrics + ? "# TYPE otel_scope_info info\n" + + "# HELP otel_scope_info Scope metadata\n" + + $"otel_scope_info{{otel_scope_name='{MeterName}'}} 1\n" + + "# TYPE counter_double_total counter\n" + + $"counter_double_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+\\.\\d{{3}})\n" + + "# EOF\n" + : "# TYPE counter_double_total counter\n" + + $"counter_double_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+)\n" + + "# EOF\n"; - var timestamp = long.Parse(matches[0].Groups[1].Value); + var matches = Regex.Matches(content, ("^" + expected + "$").Replace('\'', '"')); - Assert.True(beginTimestamp <= timestamp && timestamp <= endTimestamp); - } - else - { - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } + Assert.Single(matches); - validateResponse?.Invoke(response); + var timestamp = long.Parse(matches[0].Groups[1].Value.Replace(".", string.Empty)); - await host.StopAsync().ConfigureAwait(false); + Assert.True(beginTimestamp <= timestamp && timestamp <= endTimestamp); + } + else + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); } + + validateResponse?.Invoke(response); + + await host.StopAsync(); } } #endif diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/EventSourceTest.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/EventSourceTest.cs index 05fcbb130c6..baf8dc43377 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/EventSourceTest.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/EventSourceTest.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Tests; using Xunit; diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests.csproj b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests.csproj index 2947acfe440..7ab05160d72 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests.csproj +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests.csproj @@ -1,8 +1,7 @@ Unit test project for Prometheus Exporter HttpListener for OpenTelemetry - - net7.0;net6.0;net462 + $(TargetFrameworksForTests) $(DefineConstants);PROMETHEUS_HTTP_LISTENER @@ -11,10 +10,8 @@ - - - all + runtime; build; native; contentfiles; analyzers @@ -24,6 +21,10 @@ + + + + diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusCollectionManagerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusCollectionManagerTests.cs index 9439a784f68..f4cadff53b5 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusCollectionManagerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusCollectionManagerTests.cs @@ -1,176 +1,161 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Metrics; -#if NETFRAMEWORK -using System.Linq; -#endif using OpenTelemetry.Metrics; using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Exporter.Prometheus.Tests +namespace OpenTelemetry.Exporter.Prometheus.Tests; + +public sealed class PrometheusCollectionManagerTests { - public sealed class PrometheusCollectionManagerTests - { - [Theory] - [InlineData(0)] // disable cache, default value for HttpListener + [Theory] + [InlineData(0, true)] // disable cache, default value for HttpListener + [InlineData(0, false)] // disable cache, default value for HttpListener #if PROMETHEUS_ASPNETCORE - [InlineData(300)] // default value for AspNetCore, no possibility to set on HttpListener + [InlineData(300, true)] // default value for AspNetCore, no possibility to set on HttpListener + [InlineData(300, false)] // default value for AspNetCore, no possibility to set on HttpListener #endif - public async Task EnterExitCollectTest(int scrapeResponseCacheDurationMilliseconds) - { - bool cacheEnabled = scrapeResponseCacheDurationMilliseconds != 0; - using var meter = new Meter(Utils.GetCurrentMethodName()); + public async Task EnterExitCollectTest(int scrapeResponseCacheDurationMilliseconds, bool openMetricsRequested) + { + bool cacheEnabled = scrapeResponseCacheDurationMilliseconds != 0; + using var meter = new Meter(Utils.GetCurrentMethodName()); - using (var provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) + using (var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) #if PROMETHEUS_HTTP_LISTENER - .AddPrometheusHttpListener() + .AddPrometheusHttpListener() #elif PROMETHEUS_ASPNETCORE - .AddPrometheusExporter(x => x.ScrapeResponseCacheDurationMilliseconds = scrapeResponseCacheDurationMilliseconds) + .AddPrometheusExporter(x => x.ScrapeResponseCacheDurationMilliseconds = scrapeResponseCacheDurationMilliseconds) #endif - .Build()) + .Build()) + { + if (!provider.TryFindExporter(out PrometheusExporter exporter)) { - if (!provider.TryFindExporter(out PrometheusExporter exporter)) - { - throw new InvalidOperationException("PrometheusExporter could not be found on MeterProvider."); - } + throw new InvalidOperationException("PrometheusExporter could not be found on MeterProvider."); + } - int runningCollectCount = 0; - var collectFunc = exporter.Collect; - exporter.Collect = (timeout) => - { - bool result = collectFunc(timeout); - runningCollectCount++; - Thread.Sleep(5000); - return result; - }; + int runningCollectCount = 0; + var collectFunc = exporter.Collect; + exporter.Collect = (timeout) => + { + bool result = collectFunc(timeout); + runningCollectCount++; + Thread.Sleep(5000); + return result; + }; - var counter = meter.CreateCounter("counter_int", description: "Prometheus help text goes here \n escaping."); - counter.Add(100); + var counter = meter.CreateCounter("counter_int", description: "Prometheus help text goes here \n escaping."); + counter.Add(100); - Task[] collectTasks = new Task[10]; - for (int i = 0; i < collectTasks.Length; i++) + Task[] collectTasks = new Task[10]; + for (int i = 0; i < collectTasks.Length; i++) + { + collectTasks[i] = Task.Run(async () => { - collectTasks[i] = Task.Run(async () => + var response = await exporter.CollectionManager.EnterCollect(openMetricsRequested); + try { - var response = await exporter.CollectionManager.EnterCollect().ConfigureAwait(false); - try - { - return new Response - { - CollectionResponse = response, - ViewPayload = response.View.ToArray(), - }; - } - finally + return new Response { - exporter.CollectionManager.ExitCollect(); - } - }); - } + CollectionResponse = response, + ViewPayload = response.View.ToArray(), + }; + } + finally + { + exporter.CollectionManager.ExitCollect(); + } + }); + } - await Task.WhenAll(collectTasks).ConfigureAwait(false); + await Task.WhenAll(collectTasks); - Assert.Equal(1, runningCollectCount); + Assert.Equal(1, runningCollectCount); - var firstResponse = collectTasks[0].Result; + var firstResponse = await collectTasks[0]; - Assert.False(firstResponse.CollectionResponse.FromCache); + 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); - } + for (int i = 1; i < collectTasks.Length; i++) + { + Assert.Equal(firstResponse.ViewPayload, (await collectTasks[i]).ViewPayload); + Assert.Equal(firstResponse.CollectionResponse.GeneratedAtUtc, (await collectTasks[i]).CollectionResponse.GeneratedAtUtc); + } - counter.Add(100); + counter.Add(100); - // 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); - try + // This should use the cache and ignore the second counter update. + var task = exporter.CollectionManager.EnterCollect(openMetricsRequested); + Assert.True(task.IsCompleted); + var response = await task; + try + { + if (cacheEnabled) { - if (cacheEnabled) - { - Assert.Equal(1, runningCollectCount); - Assert.True(response.FromCache); - Assert.Equal(firstResponse.CollectionResponse.GeneratedAtUtc, response.GeneratedAtUtc); - } - else - { - Assert.Equal(2, runningCollectCount); - Assert.False(response.FromCache); - Assert.True(firstResponse.CollectionResponse.GeneratedAtUtc < response.GeneratedAtUtc); - } + Assert.Equal(1, runningCollectCount); + Assert.True(response.FromCache); + Assert.Equal(firstResponse.CollectionResponse.GeneratedAtUtc, response.GeneratedAtUtc); } - finally + else { - exporter.CollectionManager.ExitCollect(); + Assert.Equal(2, runningCollectCount); + Assert.False(response.FromCache); + Assert.True(firstResponse.CollectionResponse.GeneratedAtUtc < response.GeneratedAtUtc); } + } + finally + { + exporter.CollectionManager.ExitCollect(); + } - Thread.Sleep(exporter.ScrapeResponseCacheDurationMilliseconds); + Thread.Sleep(exporter.ScrapeResponseCacheDurationMilliseconds); - counter.Add(100); + counter.Add(100); - for (int i = 0; i < collectTasks.Length; i++) + for (int i = 0; i < collectTasks.Length; i++) + { + collectTasks[i] = Task.Run(async () => { - collectTasks[i] = Task.Run(async () => + var response = await exporter.CollectionManager.EnterCollect(openMetricsRequested); + try { - var response = await exporter.CollectionManager.EnterCollect().ConfigureAwait(false); - try + return new Response { - return new Response - { - CollectionResponse = response, - ViewPayload = response.View.ToArray(), - }; - } - finally - { - exporter.CollectionManager.ExitCollect(); - } - }); - } + CollectionResponse = response, + ViewPayload = response.View.ToArray(), + }; + } + finally + { + exporter.CollectionManager.ExitCollect(); + } + }); + } - 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.Equal(cacheEnabled ? 2 : 3, runningCollectCount); + 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); + 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); - } + for (int i = 1; i < collectTasks.Length; i++) + { + Assert.Equal(firstResponse.ViewPayload, (await collectTasks[i]).ViewPayload); + Assert.Equal(firstResponse.CollectionResponse.GeneratedAtUtc, (await collectTasks[i]).CollectionResponse.GeneratedAtUtc); } } + } - private class Response - { - public PrometheusCollectionManager.CollectionResponse CollectionResponse; + private class Response + { + public PrometheusCollectionManager.CollectionResponse CollectionResponse; - public byte[] ViewPayload; - } + public byte[] ViewPayload; } } diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHeadersParserTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHeadersParserTests.cs new file mode 100644 index 00000000000..aa4a1be9bc8 --- /dev/null +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHeadersParserTests.cs @@ -0,0 +1,35 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Xunit; + +namespace OpenTelemetry.Exporter.Prometheus.Tests; + +public class PrometheusHeadersParserTests +{ + [Theory] + [InlineData("application/openmetrics-text")] + [InlineData("application/openmetrics-text; version=1.0.0")] + [InlineData("application/openmetrics-text; version=1.0.0; charset=utf-8")] + [InlineData("text/plain,application/openmetrics-text; version=1.0.0; charset=utf-8")] + [InlineData("text/plain; charset=utf-8,application/openmetrics-text; version=1.0.0; charset=utf-8")] + [InlineData("text/plain, */*;q=0.8,application/openmetrics-text; version=1.0.0; charset=utf-8")] + public void ParseHeader_AcceptHeaders_OpenMetricsValid(string header) + { + var result = PrometheusHeadersParser.AcceptsOpenMetrics(header); + + Assert.True(result); + } + + [Theory] + [InlineData("text/plain")] + [InlineData("text/plain; charset=utf-8")] + [InlineData("text/plain; charset=utf-8; version=0.0.4")] + [InlineData("*/*;q=0.8,text/plain; charset=utf-8; version=0.0.4")] + public void ParseHeader_AcceptHeaders_OtherHeadersInvalid(string header) + { + var result = PrometheusHeadersParser.AcceptsOpenMetrics(header); + + Assert.False(result); + } +} diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerMeterProviderBuilderExtensionsTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerMeterProviderBuilderExtensionsTests.cs index 72cffdc54db..af08063e267 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerMeterProviderBuilderExtensionsTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerMeterProviderBuilderExtensionsTests.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Metrics; @@ -36,7 +23,7 @@ public void TestAddPrometheusHttpListener_NamedOptions() services.Configure("Exporter2", o => namedExporterOptionsConfigureOptionsInvocations++); }) .AddPrometheusHttpListener() - .AddPrometheusHttpListener("Exporter2", o => { }) + .AddPrometheusHttpListener("Exporter2", o => o.ScrapeEndpointPath = "/metrics2") .Build(); Assert.Equal(1, defaultExporterOptionsConfigureOptionsInvocations); diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs index 7e7737919a1..ce7286d8c43 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs @@ -1,144 +1,255 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Metrics; using System.Net; +#if NETFRAMEWORK using System.Net.Http; +#endif using OpenTelemetry.Metrics; using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Exporter.Prometheus.Tests +namespace OpenTelemetry.Exporter.Prometheus.Tests; + +public class PrometheusHttpListenerTests { - public class PrometheusHttpListenerTests + private const string MeterVersion = "1.0.1"; + + private static readonly string MeterName = Utils.GetCurrentMethodName(); + + [Theory] + [InlineData("http://+:9464")] + [InlineData("http://*:9464")] + [InlineData("http://+:9464/")] + [InlineData("http://*:9464/")] + [InlineData("https://example.com")] + [InlineData("http://127.0.0.1")] + [InlineData("http://example.com", "https://example.com", "http://127.0.0.1")] + [InlineData("http://example.com")] + public void UriPrefixesPositiveTest(params string[] uriPrefixes) + { + TestPrometheusHttpListenerUriPrefixOptions(uriPrefixes); + } + + [Fact] + public void UriPrefixesNull() { - private readonly string meterName = Utils.GetCurrentMethodName(); - - [Theory] - [InlineData("http://+:9464")] - [InlineData("http://*:9464")] - [InlineData("http://+:9464/")] - [InlineData("http://*:9464/")] - [InlineData("https://example.com")] - [InlineData("http://127.0.0.1")] - [InlineData("http://example.com", "https://example.com", "http://127.0.0.1")] - [InlineData("http://example.com")] - public void UriPrefixesPositiveTest(params string[] uriPrefixes) + Assert.Throws(() => { - using MeterProvider meterProvider = Sdk.CreateMeterProviderBuilder() - .AddPrometheusHttpListener(options => options.UriPrefixes = uriPrefixes) - .Build(); - } + TestPrometheusHttpListenerUriPrefixOptions(null); + }); + } - [Fact] - public void UriPrefixesNull() + [Fact] + public void UriPrefixesEmptyList() + { + Assert.Throws(() => { - Assert.Throws(() => - { - using MeterProvider meterProvider = Sdk.CreateMeterProviderBuilder() - .AddPrometheusHttpListener(options => options.UriPrefixes = null) - .Build(); - }); - } + TestPrometheusHttpListenerUriPrefixOptions(new string[] { }); + }); + } - [Fact] - public void UriPrefixesEmptyList() + [Fact] + public void UriPrefixesInvalid() + { + Assert.Throws(() => { - Assert.Throws(() => - { - using MeterProvider meterProvider = Sdk.CreateMeterProviderBuilder() - .AddPrometheusHttpListener(options => options.UriPrefixes = new string[] { }) - .Build(); - }); - } + TestPrometheusHttpListenerUriPrefixOptions(new string[] { "ftp://example.com" }); + }); + } + + [Fact] + public async Task PrometheusExporterHttpServerIntegration() + { + await this.RunPrometheusExporterHttpServerIntegrationTest(); + } - [Fact] - public void UriPrefixesInvalid() + [Fact] + public async Task PrometheusExporterHttpServerIntegration_NoMetrics() + { + await this.RunPrometheusExporterHttpServerIntegrationTest(skipMetrics: true); + } + + [Fact] + public async Task PrometheusExporterHttpServerIntegration_NoOpenMetrics() + { + await this.RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: string.Empty); + } + + [Fact] + public async Task PrometheusExporterHttpServerIntegration_UseOpenMetricsVersionHeader() + { + await this.RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: "application/openmetrics-text; version=1.0.0"); + } + + [Fact] + public void PrometheusHttpListenerThrowsOnStart() + { + Random random = new Random(); + int retryAttempts = 5; + int port = 0; + string address = null; + + PrometheusExporter exporter = null; + PrometheusHttpListener listener = null; + + // Step 1: Start a listener on a random port. + while (retryAttempts-- != 0) { - Assert.Throws(() => + port = random.Next(2000, 5000); + address = $"http://localhost:{port}/"; + + try { - using MeterProvider meterProvider = Sdk.CreateMeterProviderBuilder() - .AddPrometheusHttpListener(options => options.UriPrefixes = new string[] { "ftp://example.com" }) - .Build(); - }); - } + exporter = new PrometheusExporter(new()); + listener = new PrometheusHttpListener( + exporter, + new() + { + UriPrefixes = new string[] { address }, + }); - [Fact] - public async Task PrometheusExporterHttpServerIntegration() - { - await this.RunPrometheusExporterHttpServerIntegrationTest().ConfigureAwait(false); + listener.Start(); + + break; + } + catch + { + // ignored + } } - [Fact] - public async Task PrometheusExporterHttpServerIntegration_NoMetrics() + if (retryAttempts == 0) { - await this.RunPrometheusExporterHttpServerIntegrationTest(skipMetrics: true).ConfigureAwait(false); + throw new InvalidOperationException("PrometheusHttpListener could not be started"); } - private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetrics = false) + // Step 2: Make sure if we start a second listener on the same port an exception is thrown. + Assert.Throws(() => { - Random random = new Random(); - int retryAttempts = 5; - int port = 0; - string address = null; - - MeterProvider provider; - using var meter = new Meter(this.meterName); + using var exporter = new PrometheusExporter(new()); + using var listener = new PrometheusHttpListener( + exporter, + new() + { + UriPrefixes = new string[] { address }, + }); + + listener.Start(); + }); + + exporter?.Dispose(); + listener?.Dispose(); + } - while (retryAttempts-- != 0) + private static void TestPrometheusHttpListenerUriPrefixOptions(string[] uriPrefixes) + { + using var exporter = new PrometheusExporter(new()); + using var listener = new PrometheusHttpListener( + exporter, + new() { - port = random.Next(2000, 5000); - address = $"http://localhost:{port}/"; + UriPrefixes = uriPrefixes, + }); + } + private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetrics = false, string acceptHeader = "application/openmetrics-text") + { + var requestOpenMetrics = acceptHeader.StartsWith("application/openmetrics-text"); + + Random random = new Random(); + int retryAttempts = 5; + int port = 0; + string address = null; + + MeterProvider provider = null; + using var meter = new Meter(MeterName, MeterVersion); + + while (retryAttempts-- != 0) + { + port = random.Next(2000, 5000); + address = $"http://localhost:{port}/"; + + try + { provider = Sdk.CreateMeterProviderBuilder() .AddMeter(meter.Name) - .AddPrometheusHttpListener(options => options.UriPrefixes = new string[] { address }) + .AddPrometheusHttpListener(options => + { + options.UriPrefixes = new string[] { address }; + }) .Build(); - } - - var tags = new KeyValuePair[] - { - new KeyValuePair("key1", "value1"), - new KeyValuePair("key2", "value2"), - }; - var counter = meter.CreateCounter("counter_double"); - if (!skipMetrics) + break; + } + catch { - counter.Add(100.18D, tags); - counter.Add(0.99D, tags); + // ignored } + } - using HttpClient client = new HttpClient(); - using var response = await client.GetAsync($"{address}metrics").ConfigureAwait(false); + if (provider == null) + { + throw new InvalidOperationException("HttpListener could not be started"); + } - if (!skipMetrics) - { - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.True(response.Content.Headers.Contains("Last-Modified")); - Assert.Equal("text/plain; charset=utf-8; version=0.0.4", response.Content.Headers.ContentType.ToString()); + var tags = new KeyValuePair[] + { + new KeyValuePair("key1", "value1"), + new KeyValuePair("key2", "value2"), + }; + + var counter = meter.CreateCounter("counter_double"); + if (!skipMetrics) + { + counter.Add(100.18D, tags); + counter.Add(0.99D, tags); + } + + using HttpClient client = new HttpClient(); + + if (!string.IsNullOrEmpty(acceptHeader)) + { + client.DefaultRequestHeaders.Add("Accept", acceptHeader); + } + + using var response = await client.GetAsync($"{address}metrics"); - Assert.Matches( - "^# TYPE counter_double counter\ncounter_double{key1='value1',key2='value2'} 101.17 \\d+\n\n# EOF\n$".Replace('\'', '"'), - await response.Content.ReadAsStringAsync().ConfigureAwait(false)); + if (!skipMetrics) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.True(response.Content.Headers.Contains("Last-Modified")); + + if (requestOpenMetrics) + { + Assert.Equal("application/openmetrics-text; version=1.0.0; charset=utf-8", response.Content.Headers.ContentType.ToString()); } else { - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/plain; charset=utf-8; version=0.0.4", response.Content.Headers.ContentType.ToString()); } + + var content = await response.Content.ReadAsStringAsync(); + + var expected = requestOpenMetrics + ? "# TYPE otel_scope_info info\n" + + "# HELP otel_scope_info Scope metadata\n" + + $"otel_scope_info{{otel_scope_name='{MeterName}'}} 1\n" + + "# TYPE counter_double_total counter\n" + + $"counter_double_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+\\.\\d{{3}})\n" + + "# EOF\n" + : "# TYPE counter_double_total counter\n" + + $"counter_double_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+)\n" + + "# EOF\n"; + + Assert.Matches(("^" + expected + "$").Replace('\'', '"'), content); } + else + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + provider.Dispose(); } } diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusMetricTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusMetricTests.cs new file mode 100644 index 00000000000..81ce3e1f7da --- /dev/null +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusMetricTests.cs @@ -0,0 +1,220 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Xunit; + +namespace OpenTelemetry.Exporter.Prometheus.Tests; + +public sealed class PrometheusMetricTests +{ + [Fact] + public void SanitizeMetricName_Valid() + { + AssertSanitizeMetricName("active_directory_ds_replication_network_io", "active_directory_ds_replication_network_io"); + } + + [Fact] + public void SanitizeMetricName_RemoveConsecutiveUnderscores() + { + AssertSanitizeMetricName("cpu_sp__d_hertz", "cpu_sp_d_hertz"); + } + + [Fact] + public void SanitizeMetricName_SupportLeadingAndTrailingUnderscores() + { + AssertSanitizeMetricName("_cpu_speed_hertz_", "_cpu_speed_hertz_"); + } + + [Fact] + public void SanitizeMetricName_RemoveUnsupportedChracters() + { + AssertSanitizeMetricName("metric_unit_$1000", "metric_unit_1000"); + } + + [Fact] + public void SanitizeMetricName_RemoveWhitespace() + { + AssertSanitizeMetricName("unit include", "unit_include"); + } + + [Fact] + public void SanitizeMetricName_RemoveMultipleUnsupportedChracters() + { + AssertSanitizeMetricName("sample_me%%$$$_count_ !!@unit include", "sample_me_count_unit_include"); + } + + [Fact] + public void SanitizeMetricName_RemoveStartingNumber() + { + AssertSanitizeMetricName("1_some_metric_name", "_some_metric_name"); + } + + [Fact] + public void SanitizeMetricName_SupportColon() + { + AssertSanitizeMetricName("sample_metric_name__:_per_meter", "sample_metric_name_:_per_meter"); + } + + [Fact] + public void Unit_Annotation_None() + { + Assert.Equal("Test", PrometheusMetric.RemoveAnnotations("Test")); + } + + [Fact] + public void Unit_Annotation_RemoveLeading() + { + Assert.Equal("%", PrometheusMetric.RemoveAnnotations("%{percentage}")); + } + + [Fact] + public void Unit_Annotation_RemoveTrailing() + { + Assert.Equal("%", PrometheusMetric.RemoveAnnotations("{percentage}%")); + } + + [Fact] + public void Unit_Annotation_RemoveLeadingAndTrailing() + { + Assert.Equal("%", PrometheusMetric.RemoveAnnotations("{percentage}%{percentage}")); + } + + [Fact] + public void Unit_Annotation_RemoveMiddle() + { + Assert.Equal("startend", PrometheusMetric.RemoveAnnotations("start{percentage}end")); + } + + [Fact] + public void Unit_Annotation_RemoveEverything() + { + Assert.Equal(string.Empty, PrometheusMetric.RemoveAnnotations("{percentage}")); + } + + [Fact] + public void Unit_Annotation_Multiple_RemoveEverything() + { + Assert.Equal(string.Empty, PrometheusMetric.RemoveAnnotations("{one}{two}")); + } + + [Fact] + public void Unit_Annotation_NoClose() + { + Assert.Equal("{one", PrometheusMetric.RemoveAnnotations("{one")); + } + + [Fact] + public void Unit_AnnotationMismatch_NoClose() + { + Assert.Equal("}", PrometheusMetric.RemoveAnnotations("{{one}}")); + } + + [Fact] + public void Unit_AnnotationMismatch_Close() + { + Assert.Equal(string.Empty, PrometheusMetric.RemoveAnnotations("{{one}")); + } + + [Fact] + public void Name_SpecialCaseGuage_AppendRatio() + { + AssertName("sample", "1", PrometheusType.Gauge, false, "sample_ratio"); + } + + [Fact] + public void Name_GuageWithUnit_NoAppendRatio() + { + AssertName("sample", "unit", PrometheusType.Gauge, false, "sample_unit"); + } + + [Fact] + public void Name_SpecialCaseCounter_AppendTotal() + { + AssertName("sample", "unit", PrometheusType.Counter, false, "sample_unit_total"); + } + + [Fact] + public void Name_SpecialCaseCounterWithoutUnit_DropUnitAppendTotal() + { + AssertName("sample", "1", PrometheusType.Counter, false, "sample_total"); + } + + [Fact] + public void Name_DisableTotalSuffixAddition_TotalNotAppended() + { + AssertName("sample", "1", PrometheusType.Counter, true, "sample"); + } + + [Fact] + public void Name_TotalSuffixAlreadyPresent_DisableTotalSuffixAddition_TotalNotRemoved() + { + AssertName("sample_total", "1", PrometheusType.Counter, true, "sample_total"); + } + + [Fact] + public void Name_SpecialCaseCounterWithNumber_AppendTotal() + { + AssertName("sample", "2", PrometheusType.Counter, false, "sample_2_total"); + } + + [Fact] + public void Name_UnsupportedMetricNameChars_Drop() + { + AssertName("s%%ple", "%/m", PrometheusType.Summary, false, "s_ple_percent_per_minute"); + } + + [Fact] + public void Name_UnitOtherThanOne_Normal() + { + AssertName("metric_name", "2", PrometheusType.Summary, false, "metric_name_2"); + } + + [Fact] + public void Name_UnitAlreadyPresentInName_NotAppended() + { + AssertName("metric_name_total", "total", PrometheusType.Counter, false, "metric_name_total"); + } + + [Fact] + public void Name_UnitAlreadyPresentInName_TotalNonCounterType_NotAppended() + { + AssertName("metric_name_total", "total", PrometheusType.Summary, false, "metric_name_total"); + } + + [Fact] + public void Name_UnitAlreadyPresentInName_CustomGauge_NotAppended() + { + AssertName("metric_hertz", "hertz", PrometheusType.Gauge, false, "metric_hertz"); + } + + [Fact] + public void Name_UnitAlreadyPresentInName_CustomCounter_NotAppended() + { + AssertName("metric_hertz_total", "hertz_total", PrometheusType.Counter, false, "metric_hertz_total"); + } + + [Fact] + public void Name_UnitAlreadyPresentInName_OrderMatters_Appended() + { + AssertName("metric_total_hertz", "hertz_total", PrometheusType.Counter, false, "metric_total_hertz_hertz_total"); + } + + [Fact] + public void Name_StartWithNumber_UnderscoreStart() + { + AssertName("2_metric_name", "By", PrometheusType.Summary, false, "_metric_name_bytes"); + } + + private static void AssertName( + string name, string unit, PrometheusType type, bool disableTotalNameSuffixForCounters, string expected) + { + var prometheusMetric = new PrometheusMetric(name, unit, type, disableTotalNameSuffixForCounters); + Assert.Equal(expected, prometheusMetric.Name); + } + + private static void AssertSanitizeMetricName(string name, string expected) + { + var sanatizedName = PrometheusMetric.SanitizeMetricName(name); + Assert.Equal(expected, sanatizedName); + } +} diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs index 4819867c527..7c4a95b05f4 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Metrics; using System.Text; @@ -20,472 +7,654 @@ using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Exporter.Prometheus.Tests +namespace OpenTelemetry.Exporter.Prometheus.Tests; + +public sealed class PrometheusSerializerTests { - public sealed class PrometheusSerializerTests + [Fact] + public void GaugeZeroDimension() { - [Fact] - public void GaugeZeroDimension() - { - var buffer = new byte[85000]; - var metrics = new List(); - - using var meter = new Meter(Utils.GetCurrentMethodName()); - using var provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(metrics) - .Build(); - - meter.CreateObservableGauge("test_gauge", () => 123); - - provider.ForceFlush(); - - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); - Assert.Matches( - ("^" - + "# TYPE test_gauge gauge\n" - + "test_gauge 123 \\d+\n" - + "$").Replace('\'', '"'), - Encoding.UTF8.GetString(buffer, 0, cursor)); - } - - [Fact] - public void GaugeZeroDimensionWithDescription() - { - var buffer = new byte[85000]; - var metrics = new List(); - - using var meter = new Meter(Utils.GetCurrentMethodName()); - using var provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(metrics) - .Build(); - - meter.CreateObservableGauge("test_gauge", () => 123, description: "Hello, world!"); - - provider.ForceFlush(); - - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); - Assert.Matches( - ("^" - + "# TYPE test_gauge gauge\n" - + "# HELP test_gauge Hello, world!\n" - + "test_gauge 123 \\d+\n" - + "$").Replace('\'', '"'), - Encoding.UTF8.GetString(buffer, 0, cursor)); - } - - [Fact] - public void GaugeZeroDimensionWithUnit() - { - var buffer = new byte[85000]; - var metrics = new List(); - - using var meter = new Meter(Utils.GetCurrentMethodName()); - using var provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(metrics) - .Build(); - - meter.CreateObservableGauge("test_gauge", () => 123, unit: "seconds"); - - provider.ForceFlush(); - - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); - Assert.Matches( - ("^" - + "# TYPE test_gauge_seconds gauge\n" - + "# UNIT test_gauge_seconds seconds\n" - + "test_gauge_seconds 123 \\d+\n" - + "$").Replace('\'', '"'), - Encoding.UTF8.GetString(buffer, 0, cursor)); - } - - [Fact] - public void GaugeZeroDimensionWithDescriptionAndUnit() - { - var buffer = new byte[85000]; - var metrics = new List(); - - using var meter = new Meter(Utils.GetCurrentMethodName()); - using var provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(metrics) - .Build(); - - meter.CreateObservableGauge("test_gauge", () => 123, unit: "seconds", description: "Hello, world!"); - - provider.ForceFlush(); - - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); - Assert.Matches( - ("^" - + "# TYPE test_gauge_seconds gauge\n" - + "# UNIT test_gauge_seconds seconds\n" - + "# HELP test_gauge_seconds Hello, world!\n" - + "test_gauge_seconds 123 \\d+\n" - + "$").Replace('\'', '"'), - Encoding.UTF8.GetString(buffer, 0, cursor)); - } - - [Fact] - public void GaugeOneDimension() - { - var buffer = new byte[85000]; - var metrics = new List(); - - using var meter = new Meter(Utils.GetCurrentMethodName()); - using var provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(metrics) - .Build(); - - meter.CreateObservableGauge( - "test_gauge", - () => new Measurement(123, new KeyValuePair("tagKey", "tagValue"))); - - provider.ForceFlush(); - - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); - Assert.Matches( - ("^" - + "# TYPE test_gauge gauge\n" - + "test_gauge{tagKey='tagValue'} 123 \\d+\n" - + "$").Replace('\'', '"'), - Encoding.UTF8.GetString(buffer, 0, cursor)); - } - - [Fact] - public void GaugeDoubleSubnormal() - { - var buffer = new byte[85000]; - var metrics = new List(); - - using var meter = new Meter(Utils.GetCurrentMethodName()); - using var provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(metrics) - .Build(); - - meter.CreateObservableGauge("test_gauge", () => new List> - { - new(double.NegativeInfinity, new("x", "1"), new("y", "2")), - new(double.PositiveInfinity, new("x", "3"), new("y", "4")), - new(double.NaN, new("x", "5"), new("y", "6")), - }); - - provider.ForceFlush(); - - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); - Assert.Matches( - ("^" - + "# TYPE test_gauge gauge\n" - + "test_gauge{x='1',y='2'} -Inf \\d+\n" - + "test_gauge{x='3',y='4'} \\+Inf \\d+\n" - + "test_gauge{x='5',y='6'} Nan \\d+\n" - + "$").Replace('\'', '"'), - Encoding.UTF8.GetString(buffer, 0, cursor)); - } - - [Fact] - public void SumDoubleInfinities() - { - var buffer = new byte[85000]; - var metrics = new List(); - - using var meter = new Meter(Utils.GetCurrentMethodName()); - using var provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(metrics) - .Build(); - - var counter = meter.CreateCounter("test_counter"); - counter.Add(1.0E308); - counter.Add(1.0E308); - - provider.ForceFlush(); - - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); - Assert.Matches( - ("^" - + "# TYPE test_counter counter\n" - + "test_counter \\+Inf \\d+\n" - + "$").Replace('\'', '"'), - Encoding.UTF8.GetString(buffer, 0, cursor)); - } - - [Fact] - public void SumNonMonotonicDouble() - { - var buffer = new byte[85000]; - var metrics = new List(); - - using var meter = new Meter(Utils.GetCurrentMethodName()); - using var provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(metrics) - .Build(); - - var counter = meter.CreateUpDownCounter("test_updown_counter"); - counter.Add(10); - counter.Add(-11); - - provider.ForceFlush(); - - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); - Assert.Matches( - ("^" - + "# TYPE test_updown_counter gauge\n" - + "test_updown_counter -1 \\d+\n" - + "$").Replace('\'', '"'), - Encoding.UTF8.GetString(buffer, 0, cursor)); - } - - [Fact] - public void HistogramZeroDimension() - { - var buffer = new byte[85000]; - var metrics = new List(); - - using var meter = new Meter(Utils.GetCurrentMethodName()); - using var provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(metrics) - .Build(); - - var histogram = meter.CreateHistogram("test_histogram"); - histogram.Record(18); - histogram.Record(100); - - provider.ForceFlush(); - - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); - Assert.Matches( - ("^" - + "# TYPE test_histogram histogram\n" - + "test_histogram_bucket{le='0'} 0 \\d+\n" - + "test_histogram_bucket{le='5'} 0 \\d+\n" - + "test_histogram_bucket{le='10'} 0 \\d+\n" - + "test_histogram_bucket{le='25'} 1 \\d+\n" - + "test_histogram_bucket{le='50'} 1 \\d+\n" - + "test_histogram_bucket{le='75'} 1 \\d+\n" - + "test_histogram_bucket{le='100'} 2 \\d+\n" - + "test_histogram_bucket{le='250'} 2 \\d+\n" - + "test_histogram_bucket{le='500'} 2 \\d+\n" - + "test_histogram_bucket{le='750'} 2 \\d+\n" - + "test_histogram_bucket{le='1000'} 2 \\d+\n" - + "test_histogram_bucket{le='2500'} 2 \\d+\n" - + "test_histogram_bucket{le='5000'} 2 \\d+\n" - + "test_histogram_bucket{le='7500'} 2 \\d+\n" - + "test_histogram_bucket{le='10000'} 2 \\d+\n" - + "test_histogram_bucket{le='\\+Inf'} 2 \\d+\n" - + "test_histogram_sum 118 \\d+\n" - + "test_histogram_count 2 \\d+\n" - + "$").Replace('\'', '"'), - Encoding.UTF8.GetString(buffer, 0, cursor)); - } - - [Fact] - public void HistogramOneDimension() - { - var buffer = new byte[85000]; - var metrics = new List(); - - using var meter = new Meter(Utils.GetCurrentMethodName()); - using var provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(metrics) - .Build(); - - var histogram = meter.CreateHistogram("test_histogram"); - histogram.Record(18, new KeyValuePair("x", "1")); - histogram.Record(100, new KeyValuePair("x", "1")); - - provider.ForceFlush(); - - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); - Assert.Matches( - ("^" - + "# TYPE test_histogram histogram\n" - + "test_histogram_bucket{x='1',le='0'} 0 \\d+\n" - + "test_histogram_bucket{x='1',le='5'} 0 \\d+\n" - + "test_histogram_bucket{x='1',le='10'} 0 \\d+\n" - + "test_histogram_bucket{x='1',le='25'} 1 \\d+\n" - + "test_histogram_bucket{x='1',le='50'} 1 \\d+\n" - + "test_histogram_bucket{x='1',le='75'} 1 \\d+\n" - + "test_histogram_bucket{x='1',le='100'} 2 \\d+\n" - + "test_histogram_bucket{x='1',le='250'} 2 \\d+\n" - + "test_histogram_bucket{x='1',le='500'} 2 \\d+\n" - + "test_histogram_bucket{x='1',le='750'} 2 \\d+\n" - + "test_histogram_bucket{x='1',le='1000'} 2 \\d+\n" - + "test_histogram_bucket{x='1',le='2500'} 2 \\d+\n" - + "test_histogram_bucket{x='1',le='5000'} 2 \\d+\n" - + "test_histogram_bucket{x='1',le='7500'} 2 \\d+\n" - + "test_histogram_bucket{x='1',le='10000'} 2 \\d+\n" - + "test_histogram_bucket{x='1',le='\\+Inf'} 2 \\d+\n" - + "test_histogram_sum{x='1'} 118 \\d+\n" - + "test_histogram_count{x='1'} 2 \\d+\n" - + "$").Replace('\'', '"'), - Encoding.UTF8.GetString(buffer, 0, cursor)); - } - - [Fact] - public void HistogramTwoDimensions() - { - var buffer = new byte[85000]; - var metrics = new List(); - - using var meter = new Meter(Utils.GetCurrentMethodName()); - using var provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(metrics) - .Build(); - - var histogram = meter.CreateHistogram("test_histogram"); - histogram.Record(18, new("x", "1"), new("y", "2")); - histogram.Record(100, new("x", "1"), new("y", "2")); - - provider.ForceFlush(); - - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); - Assert.Matches( - ("^" - + "# TYPE test_histogram histogram\n" - + "test_histogram_bucket{x='1',y='2',le='0'} 0 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='5'} 0 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='10'} 0 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='25'} 1 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='50'} 1 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='75'} 1 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='100'} 2 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='250'} 2 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='500'} 2 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='750'} 2 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='1000'} 2 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='2500'} 2 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='5000'} 2 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='7500'} 2 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='10000'} 2 \\d+\n" - + "test_histogram_bucket{x='1',y='2',le='\\+Inf'} 2 \\d+\n" - + "test_histogram_sum{x='1',y='2'} 118 \\d+\n" - + "test_histogram_count{x='1',y='2'} 2 \\d+\n" - + "$").Replace('\'', '"'), - Encoding.UTF8.GetString(buffer, 0, cursor)); - } - - [Fact] - public void HistogramInfinities() - { - var buffer = new byte[85000]; - var metrics = new List(); - - using var meter = new Meter(Utils.GetCurrentMethodName()); - using var provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(metrics) - .Build(); - - var histogram = meter.CreateHistogram("test_histogram"); - histogram.Record(18); - histogram.Record(double.PositiveInfinity); - histogram.Record(double.PositiveInfinity); - - provider.ForceFlush(); - - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); - Assert.Matches( - ("^" - + "# TYPE test_histogram histogram\n" - + "test_histogram_bucket{le='0'} 0 \\d+\n" - + "test_histogram_bucket{le='5'} 0 \\d+\n" - + "test_histogram_bucket{le='10'} 0 \\d+\n" - + "test_histogram_bucket{le='25'} 1 \\d+\n" - + "test_histogram_bucket{le='50'} 1 \\d+\n" - + "test_histogram_bucket{le='75'} 1 \\d+\n" - + "test_histogram_bucket{le='100'} 1 \\d+\n" - + "test_histogram_bucket{le='250'} 1 \\d+\n" - + "test_histogram_bucket{le='500'} 1 \\d+\n" - + "test_histogram_bucket{le='750'} 1 \\d+\n" - + "test_histogram_bucket{le='1000'} 1 \\d+\n" - + "test_histogram_bucket{le='2500'} 1 \\d+\n" - + "test_histogram_bucket{le='5000'} 1 \\d+\n" - + "test_histogram_bucket{le='7500'} 1 \\d+\n" - + "test_histogram_bucket{le='10000'} 1 \\d+\n" - + "test_histogram_bucket{le='\\+Inf'} 3 \\d+\n" - + "test_histogram_sum \\+Inf \\d+\n" - + "test_histogram_count 3 \\d+\n" - + "$").Replace('\'', '"'), - Encoding.UTF8.GetString(buffer, 0, cursor)); - } - - [Fact] - public void HistogramNaN() - { - var buffer = new byte[85000]; - var metrics = new List(); - - using var meter = new Meter(Utils.GetCurrentMethodName()); - using var provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(metrics) - .Build(); - - var histogram = meter.CreateHistogram("test_histogram"); - histogram.Record(18); - histogram.Record(double.PositiveInfinity); - histogram.Record(double.NaN); - - provider.ForceFlush(); - - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); - Assert.Matches( - ("^" - + "# TYPE test_histogram histogram\n" - + "test_histogram_bucket{le='0'} 0 \\d+\n" - + "test_histogram_bucket{le='5'} 0 \\d+\n" - + "test_histogram_bucket{le='10'} 0 \\d+\n" - + "test_histogram_bucket{le='25'} 1 \\d+\n" - + "test_histogram_bucket{le='50'} 1 \\d+\n" - + "test_histogram_bucket{le='75'} 1 \\d+\n" - + "test_histogram_bucket{le='100'} 1 \\d+\n" - + "test_histogram_bucket{le='250'} 1 \\d+\n" - + "test_histogram_bucket{le='500'} 1 \\d+\n" - + "test_histogram_bucket{le='750'} 1 \\d+\n" - + "test_histogram_bucket{le='1000'} 1 \\d+\n" - + "test_histogram_bucket{le='2500'} 1 \\d+\n" - + "test_histogram_bucket{le='5000'} 1 \\d+\n" - + "test_histogram_bucket{le='7500'} 1 \\d+\n" - + "test_histogram_bucket{le='10000'} 1 \\d+\n" - + "test_histogram_bucket{le='\\+Inf'} 3 \\d+\n" - + "test_histogram_sum Nan \\d+\n" - + "test_histogram_count 3 \\d+\n" - + "$").Replace('\'', '"'), - Encoding.UTF8.GetString(buffer, 0, cursor)); - } - - [Fact] - public void ExponentialHistogramIsIgnoredForNow() + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + meter.CreateObservableGauge("test_gauge", () => 123); + + provider.ForceFlush(); + + var cursor = WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_gauge gauge\n" + + $"test_gauge{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} 123 \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void GaugeZeroDimensionWithDescription() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + meter.CreateObservableGauge("test_gauge", () => 123, description: "Hello, world!"); + + provider.ForceFlush(); + + var cursor = WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_gauge gauge\n" + + "# HELP test_gauge Hello, world!\n" + + $"test_gauge{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} 123 \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void GaugeZeroDimensionWithUnit() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + meter.CreateObservableGauge("test_gauge", () => 123, unit: "seconds"); + + provider.ForceFlush(); + + var cursor = WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_gauge_seconds gauge\n" + + "# UNIT test_gauge_seconds seconds\n" + + $"test_gauge_seconds{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} 123 \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void GaugeZeroDimensionWithDescriptionAndUnit() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + meter.CreateObservableGauge("test_gauge", () => 123, unit: "seconds", description: "Hello, world!"); + + provider.ForceFlush(); + + var cursor = WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_gauge_seconds gauge\n" + + "# UNIT test_gauge_seconds seconds\n" + + "# HELP test_gauge_seconds Hello, world!\n" + + $"test_gauge_seconds{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} 123 \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void GaugeOneDimension() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + meter.CreateObservableGauge( + "test_gauge", + () => new Measurement(123, new KeyValuePair("tagKey", "tagValue"))); + + provider.ForceFlush(); + + var cursor = WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_gauge gauge\n" + + $"test_gauge{{otel_scope_name='{Utils.GetCurrentMethodName()}',tagKey='tagValue'}} 123 \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void GaugeBoolDimension() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + meter.CreateObservableGauge( + "test_gauge", + () => new Measurement(123, new KeyValuePair("tagKey", true))); + + provider.ForceFlush(); + + var cursor = WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_gauge gauge\n" + + $"test_gauge{{otel_scope_name='{Utils.GetCurrentMethodName()}',tagKey='true'}} 123 \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void GaugeDoubleSubnormal() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + meter.CreateObservableGauge("test_gauge", () => new List> { - var buffer = new byte[85000]; - var metrics = new List(); - - using var meter = new Meter(Utils.GetCurrentMethodName()); - using var provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddView(instrument => new Base2ExponentialBucketHistogramConfiguration()) - .AddInMemoryExporter(metrics) - .Build(); - - var histogram = meter.CreateHistogram("test_histogram"); - histogram.Record(18); - histogram.Record(100); - - provider.ForceFlush(); - - var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); - Assert.Matches( - "^$", - Encoding.UTF8.GetString(buffer, 0, cursor)); - } + new(double.NegativeInfinity, new("x", "1"), new("y", "2")), + new(double.PositiveInfinity, new("x", "3"), new("y", "4")), + new(double.NaN, new("x", "5"), new("y", "6")), + }); + + provider.ForceFlush(); + + var cursor = WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_gauge gauge\n" + + $"test_gauge{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2'}} -Inf \\d+\n" + + $"test_gauge{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='3',y='4'}} \\+Inf \\d+\n" + + $"test_gauge{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='5',y='6'}} Nan \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void SumDoubleInfinities() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + var counter = meter.CreateCounter("test_counter"); + counter.Add(1.0E308); + counter.Add(1.0E308); + + provider.ForceFlush(); + + var cursor = WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_counter_total counter\n" + + $"test_counter_total{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} \\+Inf \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void SumNonMonotonicDouble() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + var counter = meter.CreateUpDownCounter("test_updown_counter"); + counter.Add(10); + counter.Add(-11); + + provider.ForceFlush(); + + var cursor = WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_updown_counter gauge\n" + + $"test_updown_counter{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} -1 \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void HistogramZeroDimension() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + var histogram = meter.CreateHistogram("test_histogram"); + histogram.Record(18); + histogram.Record(100); + + provider.ForceFlush(); + + var cursor = WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_histogram histogram\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='0'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='5'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='10'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='25'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='50'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='75'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='100'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='250'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='500'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='750'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='1000'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='2500'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='5000'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='7500'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='10000'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='\\+Inf'}} 2 \\d+\n" + + $"test_histogram_sum{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} 118 \\d+\n" + + $"test_histogram_count{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} 2 \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void HistogramOneDimension() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + var histogram = meter.CreateHistogram("test_histogram"); + histogram.Record(18, new KeyValuePair("x", "1")); + histogram.Record(100, new KeyValuePair("x", "1")); + + provider.ForceFlush(); + + var cursor = WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_histogram histogram\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='0'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='5'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='10'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='25'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='50'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='75'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='100'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='250'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='500'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='750'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='1000'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='2500'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='5000'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='7500'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='10000'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='\\+Inf'}} 2 \\d+\n" + + $"test_histogram_sum{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1'}} 118 \\d+\n" + + $"test_histogram_count{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1'}} 2 \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void HistogramTwoDimensions() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + var histogram = meter.CreateHistogram("test_histogram"); + histogram.Record(18, new("x", "1"), new("y", "2")); + histogram.Record(100, new("x", "1"), new("y", "2")); + + provider.ForceFlush(); + + var cursor = WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_histogram histogram\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='0'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='5'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='10'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='25'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='50'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='75'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='100'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='250'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='500'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='750'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='1000'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='2500'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='5000'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='7500'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='10000'}} 2 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2',le='\\+Inf'}} 2 \\d+\n" + + $"test_histogram_sum{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2'}} 118 \\d+\n" + + $"test_histogram_count{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',y='2'}} 2 \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void HistogramInfinities() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + var histogram = meter.CreateHistogram("test_histogram"); + histogram.Record(18); + histogram.Record(double.PositiveInfinity); + histogram.Record(double.PositiveInfinity); + + provider.ForceFlush(); + + var cursor = WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_histogram histogram\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='0'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='5'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='10'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='25'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='50'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='75'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='100'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='250'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='500'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='750'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='1000'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='2500'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='5000'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='7500'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='10000'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='\\+Inf'}} 3 \\d+\n" + + $"test_histogram_sum{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} \\+Inf \\d+\n" + + $"test_histogram_count{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} 3 \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void HistogramNaN() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + var histogram = meter.CreateHistogram("test_histogram"); + histogram.Record(18); + histogram.Record(double.PositiveInfinity); + histogram.Record(double.NaN); + + provider.ForceFlush(); + + var cursor = WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_histogram histogram\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='0'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='5'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='10'}} 0 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='25'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='50'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='75'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='100'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='250'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='500'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='750'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='1000'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='2500'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='5000'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='7500'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='10000'}} 1 \\d+\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',le='\\+Inf'}} 3 \\d+\n" + + $"test_histogram_sum{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} Nan \\d+\n" + + $"test_histogram_count{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} 3 \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void ExponentialHistogramIsIgnoredForNow() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddView(instrument => new Base2ExponentialBucketHistogramConfiguration()) + .AddInMemoryExporter(metrics) + .Build(); + + var histogram = meter.CreateHistogram("test_histogram"); + histogram.Record(18); + histogram.Record(100); + + provider.ForceFlush(); + + Assert.False(PrometheusSerializer.CanWriteMetric(metrics[0])); + } + + [Fact] + public void SumWithOpenMetricsFormat() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + var counter = meter.CreateUpDownCounter("test_updown_counter"); + counter.Add(10); + counter.Add(-11); + + provider.ForceFlush(); + + var cursor = WriteMetric(buffer, 0, metrics[0], true); + Assert.Matches( + ("^" + + "# TYPE test_updown_counter gauge\n" + + $"test_updown_counter{{otel_scope_name='{Utils.GetCurrentMethodName()}'}} -1 \\d+\\.\\d{{3}}\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void HistogramOneDimensionWithOpenMetricsFormat() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + var histogram = meter.CreateHistogram("test_histogram"); + histogram.Record(18, new KeyValuePair("x", "1")); + histogram.Record(100, new KeyValuePair("x", "1")); + + provider.ForceFlush(); + + var cursor = WriteMetric(buffer, 0, metrics[0], true); + Assert.Matches( + ("^" + + "# TYPE test_histogram histogram\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='0'}} 0 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='5'}} 0 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='10'}} 0 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='25'}} 1 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='50'}} 1 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='75'}} 1 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='100'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='250'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='500'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='750'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='1000'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='2500'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='5000'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='7500'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='10000'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1',le='\\+Inf'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_sum{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1'}} 118 \\d+\\.\\d{{3}}\n" + + $"test_histogram_count{{otel_scope_name='{Utils.GetCurrentMethodName()}',x='1'}} 2 \\d+\\.\\d{{3}}\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void ScopeInfo() + { + var buffer = new byte[85000]; + + var cursor = PrometheusSerializer.WriteScopeInfo(buffer, 0, "test_meter"); + + Assert.Matches( + ("^" + + "# TYPE otel_scope_info info\n" + + "# HELP otel_scope_info Scope metadata\n" + + "otel_scope_info{otel_scope_name='test_meter'} 1\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void SumWithScopeVersion() + { + var buffer = new byte[85000]; + var metrics = new List(); + using var meter = new Meter(Utils.GetCurrentMethodName(), "1.0.0"); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + var counter = meter.CreateUpDownCounter("test_updown_counter"); + counter.Add(10); + counter.Add(-11); + provider.ForceFlush(); + var cursor = WriteMetric(buffer, 0, metrics[0], true); + Assert.Matches( + ("^" + + "# TYPE test_updown_counter gauge\n" + + $"test_updown_counter{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0'}} -1 \\d+\\.\\d{{3}}\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void HistogramOneDimensionWithScopeVersion() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName(), "1.0.0"); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + var histogram = meter.CreateHistogram("test_histogram"); + histogram.Record(18, new KeyValuePair("x", "1")); + histogram.Record(100, new KeyValuePair("x", "1")); + + provider.ForceFlush(); + + var cursor = WriteMetric(buffer, 0, metrics[0], true); + Assert.Matches( + ("^" + + "# TYPE test_histogram histogram\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='0'}} 0 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='5'}} 0 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='10'}} 0 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='25'}} 1 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='50'}} 1 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='75'}} 1 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='100'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='250'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='500'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='750'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='1000'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='2500'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='5000'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='7500'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='10000'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_bucket{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1',le='\\+Inf'}} 2 \\d+\\.\\d{{3}}\n" + + $"test_histogram_sum{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1'}} 118 \\d+\\.\\d{{3}}\n" + + $"test_histogram_count{{otel_scope_name='{Utils.GetCurrentMethodName()}',otel_scope_version='1.0.0',x='1'}} 2 \\d+\\.\\d{{3}}\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + private static int WriteMetric(byte[] buffer, int cursor, Metric metric, bool useOpenMetrics = false) + { + return PrometheusSerializer.WriteMetric(buffer, cursor, metric, PrometheusMetric.Create(metric, false), useOpenMetrics); } } diff --git a/test/OpenTelemetry.Exporter.Zipkin.Tests/EventSourceTest.cs b/test/OpenTelemetry.Exporter.Zipkin.Tests/EventSourceTest.cs index 1dab8fdf74a..0394d1a142d 100644 --- a/test/OpenTelemetry.Exporter.Zipkin.Tests/EventSourceTest.cs +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/EventSourceTest.cs @@ -1,31 +1,17 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Exporter.Zipkin.Implementation; using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Exporter.Zipkin.Tests +namespace OpenTelemetry.Exporter.Zipkin.Tests; + +public class EventSourceTest { - public class EventSourceTest + [Fact] + public void EventSourceTest_ZipkinExporterEventSource() { - [Fact] - public void EventSourceTest_ZipkinExporterEventSource() - { - EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(ZipkinExporterEventSource.Log); - } + EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(ZipkinExporterEventSource.Log); } } diff --git a/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityConversionExtensionsTest.cs b/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityConversionExtensionsTest.cs index 5678b32217c..6e28b18d88c 100644 --- a/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityConversionExtensionsTest.cs +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityConversionExtensionsTest.cs @@ -1,67 +1,53 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Internal; using Xunit; using static OpenTelemetry.Exporter.Zipkin.Implementation.ZipkinActivityConversionExtensions; -namespace OpenTelemetry.Exporter.Zipkin.Implementation.Tests +namespace OpenTelemetry.Exporter.Zipkin.Implementation.Tests; + +public class ZipkinActivityConversionExtensionsTest { - public class ZipkinActivityConversionExtensionsTest + [Theory] + [InlineData("int", 1)] + [InlineData("string", "s")] + [InlineData("bool", true)] + [InlineData("double", 1.0)] + public void CheckProcessTag(string key, object value) { - [Theory] - [InlineData("int", 1)] - [InlineData("string", "s")] - [InlineData("bool", true)] - [InlineData("double", 1.0)] - public void CheckProcessTag(string key, object value) + var attributeEnumerationState = new TagEnumerationState { - var attributeEnumerationState = new TagEnumerationState - { - Tags = PooledList>.Create(), - }; + Tags = PooledList>.Create(), + }; - using var activity = new Activity("TestActivity"); - activity.SetTag(key, value); + using var activity = new Activity("TestActivity"); + activity.SetTag(key, value); - attributeEnumerationState.EnumerateTags(activity); + attributeEnumerationState.EnumerateTags(activity); - Assert.Equal(key, attributeEnumerationState.Tags[0].Key); - Assert.Equal(value, attributeEnumerationState.Tags[0].Value); - } + Assert.Equal(key, attributeEnumerationState.Tags[0].Key); + Assert.Equal(value, attributeEnumerationState.Tags[0].Value); + } - [Theory] - [InlineData("int", null)] - [InlineData("string", null)] - [InlineData("bool", null)] - [InlineData("double", null)] - public void CheckNullValueProcessTag(string key, object value) + [Theory] + [InlineData("int", null)] + [InlineData("string", null)] + [InlineData("bool", null)] + [InlineData("double", null)] + public void CheckNullValueProcessTag(string key, object value) + { + var attributeEnumerationState = new TagEnumerationState { - var attributeEnumerationState = new TagEnumerationState - { - Tags = PooledList>.Create(), - }; + Tags = PooledList>.Create(), + }; - using var activity = new Activity("TestActivity"); - activity.SetTag(key, value); + using var activity = new Activity("TestActivity"); + activity.SetTag(key, value); - attributeEnumerationState.EnumerateTags(activity); + attributeEnumerationState.EnumerateTags(activity); - Assert.Empty(attributeEnumerationState.Tags); - } + Assert.Empty(attributeEnumerationState.Tags); } } diff --git a/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityConversionTest.cs b/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityConversionTest.cs index 7ecfc30fb5e..da6cdcbfcbb 100644 --- a/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityConversionTest.cs +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityConversionTest.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Exporter.Zipkin.Tests; @@ -20,250 +7,249 @@ using OpenTelemetry.Trace; using Xunit; -namespace OpenTelemetry.Exporter.Zipkin.Implementation.Tests -{ - public class ZipkinActivityConversionTest - { - private const string ZipkinSpanName = "Name"; - private static readonly ZipkinEndpoint DefaultZipkinEndpoint = new("TestService"); +namespace OpenTelemetry.Exporter.Zipkin.Implementation.Tests; - [Fact] - public void ToZipkinSpan_AllPropertiesSet() - { - // Arrange - using var activity = ZipkinExporterTests.CreateTestActivity(); +public class ZipkinActivityConversionTest +{ + private const string ZipkinSpanName = "Name"; + private static readonly ZipkinEndpoint DefaultZipkinEndpoint = new("TestService"); - // Act & Assert - var zipkinSpan = activity.ToZipkinSpan(DefaultZipkinEndpoint); + [Fact] + public void ToZipkinSpan_AllPropertiesSet() + { + // Arrange + using var activity = ZipkinExporterTests.CreateTestActivity(); - Assert.Equal(ZipkinSpanName, zipkinSpan.Name); + // Act & Assert + var zipkinSpan = activity.ToZipkinSpan(DefaultZipkinEndpoint); - Assert.Equal(activity.TraceId.ToHexString(), zipkinSpan.TraceId); - Assert.Equal(activity.SpanId.ToHexString(), zipkinSpan.Id); + Assert.Equal(ZipkinSpanName, zipkinSpan.Name); - Assert.Equal(activity.StartTimeUtc.ToEpochMicroseconds(), zipkinSpan.Timestamp); - Assert.Equal((long)(activity.Duration.TotalMilliseconds * 1000), zipkinSpan.Duration); + Assert.Equal(activity.TraceId.ToHexString(), zipkinSpan.TraceId); + Assert.Equal(activity.SpanId.ToHexString(), zipkinSpan.Id); - int counter = 0; - var tagsArray = zipkinSpan.Tags.ToArray(); + Assert.Equal(activity.StartTimeUtc.ToEpochMicroseconds(), zipkinSpan.Timestamp); + Assert.Equal((long)(activity.Duration.TotalMilliseconds * 1000), zipkinSpan.Duration); - foreach (var tags in activity.TagObjects) - { - Assert.Equal(tagsArray[counter].Key, tags.Key); - Assert.Equal(tagsArray[counter++].Value, tags.Value); - } + int counter = 0; + var tagsArray = zipkinSpan.Tags.ToArray(); - foreach (var annotation in zipkinSpan.Annotations) - { - // Timestamp is same in both events - Assert.Equal(activity.Events.First().Timestamp.ToEpochMicroseconds(), annotation.Timestamp); - } + foreach (var tags in activity.TagObjects) + { + Assert.Equal(tagsArray[counter].Key, tags.Key); + Assert.Equal(tagsArray[counter++].Value, tags.Value); } - [Fact] - public void ToZipkinSpan_NoEvents() + foreach (var annotation in zipkinSpan.Annotations) { - // Arrange - using var activity = ZipkinExporterTests.CreateTestActivity(addEvents: false); - - // Act & Assert - var zipkinSpan = activity.ToZipkinSpan(DefaultZipkinEndpoint); + // Timestamp is same in both events + Assert.Equal(activity.Events.First().Timestamp.ToEpochMicroseconds(), annotation.Timestamp); + } + } - Assert.Equal(ZipkinSpanName, zipkinSpan.Name); - Assert.Empty(zipkinSpan.Annotations); - Assert.Equal(activity.TraceId.ToHexString(), zipkinSpan.TraceId); - Assert.Equal(activity.SpanId.ToHexString(), zipkinSpan.Id); + [Fact] + public void ToZipkinSpan_NoEvents() + { + // Arrange + using var activity = ZipkinExporterTests.CreateTestActivity(addEvents: false); - int counter = 0; - var tagsArray = zipkinSpan.Tags.ToArray(); + // Act & Assert + var zipkinSpan = activity.ToZipkinSpan(DefaultZipkinEndpoint); - foreach (var tags in activity.TagObjects) - { - Assert.Equal(tagsArray[counter].Key, tags.Key); - Assert.Equal(tagsArray[counter++].Value, tags.Value); - } + Assert.Equal(ZipkinSpanName, zipkinSpan.Name); + Assert.Empty(zipkinSpan.Annotations); + Assert.Equal(activity.TraceId.ToHexString(), zipkinSpan.TraceId); + Assert.Equal(activity.SpanId.ToHexString(), zipkinSpan.Id); - Assert.Equal(activity.StartTimeUtc.ToEpochMicroseconds(), zipkinSpan.Timestamp); - Assert.Equal((long)activity.Duration.TotalMilliseconds * 1000, zipkinSpan.Duration); - } - - [Theory] - [InlineData(StatusCode.Unset, "unset")] - [InlineData(StatusCode.Ok, "Ok")] - [InlineData(StatusCode.Error, "ERROR")] - [InlineData(StatusCode.Unset, "iNvAlId")] - public void ToZipkinSpan_Status_ErrorFlagTest(StatusCode expectedStatusCode, string statusCodeTagValue) - { - // Arrange - using var activity = ZipkinExporterTests.CreateTestActivity(); - activity.SetTag(SpanAttributeConstants.StatusCodeKey, statusCodeTagValue); - - // Act - var zipkinSpan = activity.ToZipkinSpan(DefaultZipkinEndpoint); - - // Assert - - Assert.Equal(expectedStatusCode, activity.GetStatus().StatusCode); - - if (expectedStatusCode == StatusCode.Unset) - { - Assert.DoesNotContain(zipkinSpan.Tags, t => t.Key == SpanAttributeConstants.StatusCodeKey); - } - else - { - Assert.Equal( - StatusHelper.GetTagValueForStatusCode(expectedStatusCode), - zipkinSpan.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.StatusCodeKey).Value); - } - - if (expectedStatusCode == StatusCode.Error) - { - Assert.Contains(zipkinSpan.Tags, t => t.Key == "error" && (string)t.Value == string.Empty); - } - else - { - Assert.DoesNotContain(zipkinSpan.Tags, t => t.Key == "error"); - } - } + int counter = 0; + var tagsArray = zipkinSpan.Tags.ToArray(); - [Theory] - [InlineData(ActivityStatusCode.Unset)] - [InlineData(ActivityStatusCode.Ok)] - [InlineData(ActivityStatusCode.Error)] - public void ToZipkinSpan_Activity_Status_And_StatusDescription_is_Set(ActivityStatusCode expectedStatusCode) + foreach (var tags in activity.TagObjects) { - // Arrange. - const string description = "Description when ActivityStatusCode is Error."; - using var activity = ZipkinExporterTests.CreateTestActivity(); - activity.SetStatus(expectedStatusCode, description); - - // Act. - var zipkinSpan = activity.ToZipkinSpan(DefaultZipkinEndpoint); - - // Assert. - if (expectedStatusCode == ActivityStatusCode.Unset) - { - Assert.DoesNotContain(zipkinSpan.Tags, t => t.Key == SpanAttributeConstants.StatusCodeKey); - } - else if (expectedStatusCode == ActivityStatusCode.Ok) - { - Assert.Equal("OK", zipkinSpan.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.StatusCodeKey).Value); - } - - // expectedStatusCode is Error - else - { - Assert.Equal("ERROR", zipkinSpan.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.StatusCodeKey).Value); - } - - if (expectedStatusCode == ActivityStatusCode.Error) - { - Assert.Contains( - zipkinSpan.Tags, t => - t.Key == ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName && - (string)t.Value == description); - } - else - { - Assert.DoesNotContain( - zipkinSpan.Tags, t => - t.Key == ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName); - } + Assert.Equal(tagsArray[counter].Key, tags.Key); + Assert.Equal(tagsArray[counter++].Value, tags.Value); } - [Fact] - public void ActivityStatus_Takes_precedence_Over_Status_Tags_ActivityStatusCodeIsOk() - { - // Arrange. - using var activity = ZipkinExporterTests.CreateTestActivity(); - activity.SetStatus(ActivityStatusCode.Ok); - activity.SetTag(SpanAttributeConstants.StatusCodeKey, "ERROR"); + Assert.Equal(activity.StartTimeUtc.ToEpochMicroseconds(), zipkinSpan.Timestamp); + Assert.Equal((long)activity.Duration.TotalMilliseconds * 1000, zipkinSpan.Duration); + } - // Enrich activity with additional tags. - activity.SetTag("myCustomTag", "myCustomTagValue"); + [Theory] + [InlineData(StatusCode.Unset, "unset")] + [InlineData(StatusCode.Ok, "Ok")] + [InlineData(StatusCode.Error, "ERROR")] + [InlineData(StatusCode.Unset, "iNvAlId")] + public void ToZipkinSpan_Status_ErrorFlagTest(StatusCode expectedStatusCode, string statusCodeTagValue) + { + // Arrange + using var activity = ZipkinExporterTests.CreateTestActivity(); + activity.SetTag(SpanAttributeConstants.StatusCodeKey, statusCodeTagValue); - // Act. - var zipkinSpan = activity.ToZipkinSpan(DefaultZipkinEndpoint); + // Act + var zipkinSpan = activity.ToZipkinSpan(DefaultZipkinEndpoint); - // Assert. - Assert.Equal("OK", zipkinSpan.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.StatusCodeKey).Value); + // Assert - Assert.Contains(zipkinSpan.Tags, t => t.Key == "otel.status_code" && (string)t.Value == "OK"); - Assert.DoesNotContain(zipkinSpan.Tags, t => t.Key == "otel.status_code" && (string)t.Value == "ERROR"); + Assert.Equal(expectedStatusCode, activity.GetStatus().StatusCode); - // Ensure additional Activity tags were being converted. - Assert.Contains(zipkinSpan.Tags, t => t.Key == "myCustomTag" && (string)t.Value == "myCustomTagValue"); - Assert.DoesNotContain(zipkinSpan.Tags, t => t.Key == ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName); + if (expectedStatusCode == StatusCode.Unset) + { + Assert.DoesNotContain(zipkinSpan.Tags, t => t.Key == SpanAttributeConstants.StatusCodeKey); + } + else + { + Assert.Equal( + StatusHelper.GetTagValueForStatusCode(expectedStatusCode), + zipkinSpan.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.StatusCodeKey).Value); } - [Fact] - public void ActivityStatus_Takes_precedence_Over_Status_Tags_ActivityStatusCodeIsError() + if (expectedStatusCode == StatusCode.Error) { - // Arrange. - using var activity = ZipkinExporterTests.CreateTestActivity(); + Assert.Contains(zipkinSpan.Tags, t => t.Key == "error" && (string)t.Value == string.Empty); + } + else + { + Assert.DoesNotContain(zipkinSpan.Tags, t => t.Key == "error"); + } + } - const string StatusDescriptionOnError = "Description when ActivityStatusCode is Error."; - const string TagDescriptionOnError = "Description when TagStatusCode is Error."; - activity.SetStatus(ActivityStatusCode.Error, StatusDescriptionOnError); - activity.SetTag(SpanAttributeConstants.StatusCodeKey, "ERROR"); - activity.SetTag(SpanAttributeConstants.StatusDescriptionKey, TagDescriptionOnError); + [Theory] + [InlineData(ActivityStatusCode.Unset)] + [InlineData(ActivityStatusCode.Ok)] + [InlineData(ActivityStatusCode.Error)] + public void ToZipkinSpan_Activity_Status_And_StatusDescription_is_Set(ActivityStatusCode expectedStatusCode) + { + // Arrange. + const string description = "Description when ActivityStatusCode is Error."; + using var activity = ZipkinExporterTests.CreateTestActivity(); + activity.SetStatus(expectedStatusCode, description); - // Enrich activity with additional tags. - activity.SetTag("myCustomTag", "myCustomTagValue"); + // Act. + var zipkinSpan = activity.ToZipkinSpan(DefaultZipkinEndpoint); - // Act. - var zipkinSpan = activity.ToZipkinSpan(DefaultZipkinEndpoint); + // Assert. + if (expectedStatusCode == ActivityStatusCode.Unset) + { + Assert.DoesNotContain(zipkinSpan.Tags, t => t.Key == SpanAttributeConstants.StatusCodeKey); + } + else if (expectedStatusCode == ActivityStatusCode.Ok) + { + Assert.Equal("OK", zipkinSpan.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.StatusCodeKey).Value); + } - // Assert. + // expectedStatusCode is Error + else + { Assert.Equal("ERROR", zipkinSpan.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.StatusCodeKey).Value); + } - // ActivityStatusDescription takes higher precedence. + if (expectedStatusCode == ActivityStatusCode.Error) + { Assert.Contains( zipkinSpan.Tags, t => t.Key == ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName && - (string)t.Value == StatusDescriptionOnError); + (string)t.Value == description); + } + else + { Assert.DoesNotContain( zipkinSpan.Tags, t => - t.Key == ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName && - (string)t.Value == TagDescriptionOnError); - - // Ensure additional Activity tags were being converted. - Assert.Contains(zipkinSpan.Tags, t => t.Key == "myCustomTag" && (string)t.Value == "myCustomTagValue"); + t.Key == ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName); } + } - [Fact] - public void ActivityStatus_Takes_precedence_Over_Status_Tags_ActivityStatusCodeIsError_SettingTagFirst() - { - // Arrange. - using var activity = ZipkinExporterTests.CreateTestActivity(); + [Fact] + public void ActivityStatus_Takes_precedence_Over_Status_Tags_ActivityStatusCodeIsOk() + { + // Arrange. + using var activity = ZipkinExporterTests.CreateTestActivity(); + activity.SetStatus(ActivityStatusCode.Ok); + activity.SetTag(SpanAttributeConstants.StatusCodeKey, "ERROR"); - const string StatusDescriptionOnError = "Description when ActivityStatusCode is Error."; - const string TagDescriptionOnError = "Description when TagStatusCode is Error."; - activity.SetTag(SpanAttributeConstants.StatusCodeKey, "ERROR"); - activity.SetTag(SpanAttributeConstants.StatusDescriptionKey, TagDescriptionOnError); - activity.SetStatus(ActivityStatusCode.Error, StatusDescriptionOnError); + // Enrich activity with additional tags. + activity.SetTag("myCustomTag", "myCustomTagValue"); - // Enrich activity with additional tags. - activity.SetTag("myCustomTag", "myCustomTagValue"); + // Act. + var zipkinSpan = activity.ToZipkinSpan(DefaultZipkinEndpoint); - // Act. - var zipkinSpan = activity.ToZipkinSpan(DefaultZipkinEndpoint); + // Assert. + Assert.Equal("OK", zipkinSpan.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.StatusCodeKey).Value); - // Assert. - Assert.Equal("ERROR", zipkinSpan.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.StatusCodeKey).Value); + Assert.Contains(zipkinSpan.Tags, t => t.Key == "otel.status_code" && (string)t.Value == "OK"); + Assert.DoesNotContain(zipkinSpan.Tags, t => t.Key == "otel.status_code" && (string)t.Value == "ERROR"); - // ActivityStatusDescription takes higher precedence. - Assert.Contains( - zipkinSpan.Tags, t => - t.Key == ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName && - (string)t.Value == StatusDescriptionOnError); - Assert.DoesNotContain( - zipkinSpan.Tags, t => - t.Key == ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName && - (string)t.Value == TagDescriptionOnError); + // Ensure additional Activity tags were being converted. + Assert.Contains(zipkinSpan.Tags, t => t.Key == "myCustomTag" && (string)t.Value == "myCustomTagValue"); + Assert.DoesNotContain(zipkinSpan.Tags, t => t.Key == ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName); + } - // Ensure additional Activity tags were being converted. - Assert.Contains(zipkinSpan.Tags, t => t.Key == "myCustomTag" && (string)t.Value == "myCustomTagValue"); - } + [Fact] + public void ActivityStatus_Takes_precedence_Over_Status_Tags_ActivityStatusCodeIsError() + { + // Arrange. + using var activity = ZipkinExporterTests.CreateTestActivity(); + + const string StatusDescriptionOnError = "Description when ActivityStatusCode is Error."; + const string TagDescriptionOnError = "Description when TagStatusCode is Error."; + activity.SetStatus(ActivityStatusCode.Error, StatusDescriptionOnError); + activity.SetTag(SpanAttributeConstants.StatusCodeKey, "ERROR"); + activity.SetTag(SpanAttributeConstants.StatusDescriptionKey, TagDescriptionOnError); + + // Enrich activity with additional tags. + activity.SetTag("myCustomTag", "myCustomTagValue"); + + // Act. + var zipkinSpan = activity.ToZipkinSpan(DefaultZipkinEndpoint); + + // Assert. + Assert.Equal("ERROR", zipkinSpan.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.StatusCodeKey).Value); + + // ActivityStatusDescription takes higher precedence. + Assert.Contains( + zipkinSpan.Tags, t => + t.Key == ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName && + (string)t.Value == StatusDescriptionOnError); + Assert.DoesNotContain( + zipkinSpan.Tags, t => + t.Key == ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName && + (string)t.Value == TagDescriptionOnError); + + // Ensure additional Activity tags were being converted. + Assert.Contains(zipkinSpan.Tags, t => t.Key == "myCustomTag" && (string)t.Value == "myCustomTagValue"); + } + + [Fact] + public void ActivityStatus_Takes_precedence_Over_Status_Tags_ActivityStatusCodeIsError_SettingTagFirst() + { + // Arrange. + using var activity = ZipkinExporterTests.CreateTestActivity(); + + const string StatusDescriptionOnError = "Description when ActivityStatusCode is Error."; + const string TagDescriptionOnError = "Description when TagStatusCode is Error."; + activity.SetTag(SpanAttributeConstants.StatusCodeKey, "ERROR"); + activity.SetTag(SpanAttributeConstants.StatusDescriptionKey, TagDescriptionOnError); + activity.SetStatus(ActivityStatusCode.Error, StatusDescriptionOnError); + + // Enrich activity with additional tags. + activity.SetTag("myCustomTag", "myCustomTagValue"); + + // Act. + var zipkinSpan = activity.ToZipkinSpan(DefaultZipkinEndpoint); + + // Assert. + Assert.Equal("ERROR", zipkinSpan.Tags.FirstOrDefault(t => t.Key == SpanAttributeConstants.StatusCodeKey).Value); + + // ActivityStatusDescription takes higher precedence. + Assert.Contains( + zipkinSpan.Tags, t => + t.Key == ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName && + (string)t.Value == StatusDescriptionOnError); + Assert.DoesNotContain( + zipkinSpan.Tags, t => + t.Key == ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName && + (string)t.Value == TagDescriptionOnError); + + // Ensure additional Activity tags were being converted. + Assert.Contains(zipkinSpan.Tags, t => t.Key == "myCustomTag" && (string)t.Value == "myCustomTagValue"); } } diff --git a/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityExporterRemoteEndpointTests.cs b/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityExporterRemoteEndpointTests.cs index fa21b83ed21..ddbd059c0cf 100644 --- a/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityExporterRemoteEndpointTests.cs +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityExporterRemoteEndpointTests.cs @@ -1,196 +1,182 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Exporter.Zipkin.Tests; using OpenTelemetry.Trace; using Xunit; -namespace OpenTelemetry.Exporter.Zipkin.Implementation.Tests +namespace OpenTelemetry.Exporter.Zipkin.Implementation.Tests; + +public class ZipkinActivityExporterRemoteEndpointTests { - public class ZipkinActivityExporterRemoteEndpointTests - { - private static readonly ZipkinEndpoint DefaultZipkinEndpoint = new("TestService"); + private static readonly ZipkinEndpoint DefaultZipkinEndpoint = new("TestService"); - [Fact] - public void GenerateActivity_RemoteEndpointOmittedByDefault() - { - // Arrange - using var activity = ZipkinExporterTests.CreateTestActivity(); + [Fact] + public void GenerateActivity_RemoteEndpointOmittedByDefault() + { + // Arrange + using var activity = ZipkinExporterTests.CreateTestActivity(); - // Act & Assert - var zipkinSpan = ZipkinActivityConversionExtensions.ToZipkinSpan(activity, DefaultZipkinEndpoint); + // Act & Assert + var zipkinSpan = ZipkinActivityConversionExtensions.ToZipkinSpan(activity, DefaultZipkinEndpoint); - Assert.NotNull(zipkinSpan.RemoteEndpoint); - } + Assert.NotNull(zipkinSpan.RemoteEndpoint); + } - [Fact] - public void GenerateActivity_RemoteEndpointResolution() - { - // Arrange - using var activity = ZipkinExporterTests.CreateTestActivity( - additionalAttributes: new Dictionary - { - ["net.peer.name"] = "RemoteServiceName", - }); + [Fact] + public void GenerateActivity_RemoteEndpointResolution() + { + // Arrange + using var activity = ZipkinExporterTests.CreateTestActivity( + additionalAttributes: new Dictionary + { + ["net.peer.name"] = "RemoteServiceName", + }); - // Act & Assert - var zipkinSpan = ZipkinActivityConversionExtensions.ToZipkinSpan(activity, DefaultZipkinEndpoint); + // Act & Assert + var zipkinSpan = ZipkinActivityConversionExtensions.ToZipkinSpan(activity, DefaultZipkinEndpoint); - Assert.NotNull(zipkinSpan.RemoteEndpoint); - Assert.Equal("RemoteServiceName", zipkinSpan.RemoteEndpoint.ServiceName); - } + Assert.NotNull(zipkinSpan.RemoteEndpoint); + Assert.Equal("RemoteServiceName", zipkinSpan.RemoteEndpoint.ServiceName); + } - [Theory] - [MemberData(nameof(RemoteEndpointPriorityTestCase.GetTestCases), MemberType = typeof(RemoteEndpointPriorityTestCase))] - public void GenerateActivity_RemoteEndpointResolutionPriority(RemoteEndpointPriorityTestCase testCase) - { - // Arrange - using var activity = ZipkinExporterTests.CreateTestActivity(additionalAttributes: testCase.RemoteEndpointAttributes); + [Theory] + [MemberData(nameof(RemoteEndpointPriorityTestCase.GetTestCases), MemberType = typeof(RemoteEndpointPriorityTestCase))] + public void GenerateActivity_RemoteEndpointResolutionPriority(RemoteEndpointPriorityTestCase testCase) + { + // Arrange + using var activity = ZipkinExporterTests.CreateTestActivity(additionalAttributes: testCase.RemoteEndpointAttributes); - // Act & Assert - var zipkinSpan = ZipkinActivityConversionExtensions.ToZipkinSpan(activity, DefaultZipkinEndpoint); + // Act & Assert + var zipkinSpan = ZipkinActivityConversionExtensions.ToZipkinSpan(activity, DefaultZipkinEndpoint); - Assert.NotNull(zipkinSpan.RemoteEndpoint); - Assert.Equal(testCase.ExpectedResult, zipkinSpan.RemoteEndpoint.ServiceName); - } + Assert.NotNull(zipkinSpan.RemoteEndpoint); + Assert.Equal(testCase.ExpectedResult, zipkinSpan.RemoteEndpoint.ServiceName); + } - public class RemoteEndpointPriorityTestCase - { - public string Name { get; set; } + public class RemoteEndpointPriorityTestCase + { + public string Name { get; set; } - public string ExpectedResult { get; set; } + public string ExpectedResult { get; set; } - public Dictionary RemoteEndpointAttributes { get; set; } + public Dictionary RemoteEndpointAttributes { get; set; } - public static IEnumerable GetTestCases() + public static IEnumerable GetTestCases() + { + yield return new object[] { - yield return new object[] + new RemoteEndpointPriorityTestCase { - new RemoteEndpointPriorityTestCase + Name = "Highest priority name = net.peer.name", + ExpectedResult = "RemoteServiceName", + RemoteEndpointAttributes = new Dictionary { - Name = "Highest priority name = net.peer.name", - ExpectedResult = "RemoteServiceName", - RemoteEndpointAttributes = new Dictionary - { - ["http.host"] = "DiscardedRemoteServiceName", - ["net.peer.name"] = "RemoteServiceName", - ["peer.hostname"] = "DiscardedRemoteServiceName", - }, + ["http.host"] = "DiscardedRemoteServiceName", + ["net.peer.name"] = "RemoteServiceName", + ["peer.hostname"] = "DiscardedRemoteServiceName", }, - }; + }, + }; - yield return new object[] + yield return new object[] + { + new RemoteEndpointPriorityTestCase { - new RemoteEndpointPriorityTestCase + Name = "Highest priority name = SemanticConventions.AttributePeerService", + ExpectedResult = "RemoteServiceName", + RemoteEndpointAttributes = new Dictionary { - Name = "Highest priority name = SemanticConventions.AttributePeerService", - ExpectedResult = "RemoteServiceName", - RemoteEndpointAttributes = new Dictionary - { - [SemanticConventions.AttributePeerService] = "RemoteServiceName", - ["http.host"] = "DiscardedRemoteServiceName", - ["net.peer.name"] = "DiscardedRemoteServiceName", - ["net.peer.port"] = "1234", - ["peer.hostname"] = "DiscardedRemoteServiceName", - }, + [SemanticConventions.AttributePeerService] = "RemoteServiceName", + ["http.host"] = "DiscardedRemoteServiceName", + ["net.peer.name"] = "DiscardedRemoteServiceName", + ["net.peer.port"] = "1234", + ["peer.hostname"] = "DiscardedRemoteServiceName", }, - }; + }, + }; - yield return new object[] + yield return new object[] + { + new RemoteEndpointPriorityTestCase { - new RemoteEndpointPriorityTestCase + Name = "Only has net.peer.name and net.peer.port", + ExpectedResult = "RemoteServiceName:1234", + RemoteEndpointAttributes = new Dictionary { - Name = "Only has net.peer.name and net.peer.port", - ExpectedResult = "RemoteServiceName:1234", - RemoteEndpointAttributes = new Dictionary - { - ["net.peer.name"] = "RemoteServiceName", - ["net.peer.port"] = "1234", - }, + ["net.peer.name"] = "RemoteServiceName", + ["net.peer.port"] = "1234", }, - }; + }, + }; - yield return new object[] + yield return new object[] + { + new RemoteEndpointPriorityTestCase { - new RemoteEndpointPriorityTestCase + Name = "net.peer.port is an int", + ExpectedResult = "RemoteServiceName:1234", + RemoteEndpointAttributes = new Dictionary { - Name = "net.peer.port is an int", - ExpectedResult = "RemoteServiceName:1234", - RemoteEndpointAttributes = new Dictionary - { - ["net.peer.name"] = "RemoteServiceName", - ["net.peer.port"] = 1234, - }, + ["net.peer.name"] = "RemoteServiceName", + ["net.peer.port"] = 1234, }, - }; + }, + }; - yield return new object[] + yield return new object[] + { + new RemoteEndpointPriorityTestCase { - new RemoteEndpointPriorityTestCase + Name = "Has net.peer.name and net.peer.port", + ExpectedResult = "RemoteServiceName:1234", + RemoteEndpointAttributes = new Dictionary { - Name = "Has net.peer.name and net.peer.port", - ExpectedResult = "RemoteServiceName:1234", - RemoteEndpointAttributes = new Dictionary - { - ["http.host"] = "DiscardedRemoteServiceName", - ["net.peer.name"] = "RemoteServiceName", - ["net.peer.port"] = "1234", - ["peer.hostname"] = "DiscardedRemoteServiceName", - }, + ["http.host"] = "DiscardedRemoteServiceName", + ["net.peer.name"] = "RemoteServiceName", + ["net.peer.port"] = "1234", + ["peer.hostname"] = "DiscardedRemoteServiceName", }, - }; + }, + }; - yield return new object[] + yield return new object[] + { + new RemoteEndpointPriorityTestCase { - new RemoteEndpointPriorityTestCase + Name = "Has net.peer.ip and net.peer.port", + ExpectedResult = "1.2.3.4:1234", + RemoteEndpointAttributes = new Dictionary { - Name = "Has net.peer.ip and net.peer.port", - ExpectedResult = "1.2.3.4:1234", - RemoteEndpointAttributes = new Dictionary - { - ["http.host"] = "DiscardedRemoteServiceName", - ["net.peer.ip"] = "1.2.3.4", - ["net.peer.port"] = "1234", - ["peer.hostname"] = "DiscardedRemoteServiceName", - }, + ["http.host"] = "DiscardedRemoteServiceName", + ["net.peer.ip"] = "1.2.3.4", + ["net.peer.port"] = "1234", + ["peer.hostname"] = "DiscardedRemoteServiceName", }, - }; + }, + }; - yield return new object[] + yield return new object[] + { + new RemoteEndpointPriorityTestCase { - new RemoteEndpointPriorityTestCase + Name = "Has net.peer.name, net.peer.ip, and net.peer.port", + ExpectedResult = "RemoteServiceName:1234", + RemoteEndpointAttributes = new Dictionary { - Name = "Has net.peer.name, net.peer.ip, and net.peer.port", - ExpectedResult = "RemoteServiceName:1234", - RemoteEndpointAttributes = new Dictionary - { - ["http.host"] = "DiscardedRemoteServiceName", - ["net.peer.name"] = "RemoteServiceName", - ["net.peer.ip"] = "1.2.3.4", - ["net.peer.port"] = "1234", - ["peer.hostname"] = "DiscardedRemoteServiceName", - }, + ["http.host"] = "DiscardedRemoteServiceName", + ["net.peer.name"] = "RemoteServiceName", + ["net.peer.ip"] = "1.2.3.4", + ["net.peer.port"] = "1234", + ["peer.hostname"] = "DiscardedRemoteServiceName", }, - }; - } + }, + }; + } - public override string ToString() - { - return this.Name; - } + public override string ToString() + { + return this.Name; } } } diff --git a/test/OpenTelemetry.Exporter.Zipkin.Tests/OpenTelemetry.Exporter.Zipkin.Tests.csproj b/test/OpenTelemetry.Exporter.Zipkin.Tests/OpenTelemetry.Exporter.Zipkin.Tests.csproj index 66a1161a1fd..d277eb4d15e 100644 --- a/test/OpenTelemetry.Exporter.Zipkin.Tests/OpenTelemetry.Exporter.Zipkin.Tests.csproj +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/OpenTelemetry.Exporter.Zipkin.Tests.csproj @@ -1,29 +1,24 @@ Unit test project for Zipkin Exporter for OpenTelemetry - - net7.0;net6.0 - $(TargetFrameworks);net462 - + $(TargetFrameworksForTests) disable - - - - + + + + - - - all + runtime; build; native; contentfiles; analyzers diff --git a/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinExporterTests.cs b/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinExporterTests.cs index 6c4b422ee61..9ed6a4d3dfc 100644 --- a/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinExporterTests.cs +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinExporterTests.cs @@ -1,23 +1,12 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Collections.Concurrent; using System.Diagnostics; using System.Net; +#if NETFRAMEWORK using System.Net.Http; +#endif using System.Text; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -27,516 +16,515 @@ using OpenTelemetry.Trace; using Xunit; -namespace OpenTelemetry.Exporter.Zipkin.Tests +namespace OpenTelemetry.Exporter.Zipkin.Tests; + +public class ZipkinExporterTests : IDisposable { - public class ZipkinExporterTests : IDisposable + private const string TraceId = "e8ea7e9ac72de94e91fabc613f9686b2"; + private static readonly ConcurrentDictionary Responses = new(); + + private readonly IDisposable testServer; + private readonly string testServerHost; + private readonly int testServerPort; + + static ZipkinExporterTests() { - private const string TraceId = "e8ea7e9ac72de94e91fabc613f9686b2"; - private static readonly ConcurrentDictionary Responses = new(); + Activity.DefaultIdFormat = ActivityIdFormat.W3C; + Activity.ForceDefaultIdFormat = true; - private readonly IDisposable testServer; - private readonly string testServerHost; - private readonly int testServerPort; + var listener = new ActivityListener + { + ShouldListenTo = _ => true, + Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, + }; + + ActivitySource.AddActivityListener(listener); + } - static ZipkinExporterTests() + public ZipkinExporterTests() + { + this.testServer = TestHttpServer.RunServer( + ctx => ProcessServerRequest(ctx), + out this.testServerHost, + out this.testServerPort); + + static void ProcessServerRequest(HttpListenerContext context) { - Activity.DefaultIdFormat = ActivityIdFormat.W3C; - Activity.ForceDefaultIdFormat = true; + context.Response.StatusCode = 200; - var listener = new ActivityListener - { - ShouldListenTo = _ => true, - Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, - }; + using StreamReader readStream = new StreamReader(context.Request.InputStream); + + string requestContent = readStream.ReadToEnd(); - ActivitySource.AddActivityListener(listener); + Responses.TryAdd( + Guid.Parse(context.Request.QueryString["requestId"]), + requestContent); + + context.Response.OutputStream.Close(); } + } - public ZipkinExporterTests() - { - this.testServer = TestHttpServer.RunServer( - ctx => ProcessServerRequest(ctx), - out this.testServerHost, - out this.testServerPort); + public void Dispose() + { + this.testServer.Dispose(); + GC.SuppressFinalize(this); + } - static void ProcessServerRequest(HttpListenerContext context) + [Fact] + public void AddAddZipkinExporterNamedOptionsSupported() + { + int defaultExporterOptionsConfigureOptionsInvocations = 0; + int namedExporterOptionsConfigureOptionsInvocations = 0; + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => { - context.Response.StatusCode = 200; + services.Configure(o => defaultExporterOptionsConfigureOptionsInvocations++); - using StreamReader readStream = new StreamReader(context.Request.InputStream); + services.Configure("Exporter2", o => namedExporterOptionsConfigureOptionsInvocations++); + }) + .AddZipkinExporter() + .AddZipkinExporter("Exporter2", o => { }) + .Build(); - string requestContent = readStream.ReadToEnd(); + Assert.Equal(1, defaultExporterOptionsConfigureOptionsInvocations); + Assert.Equal(1, namedExporterOptionsConfigureOptionsInvocations); + } - Responses.TryAdd( - Guid.Parse(context.Request.QueryString["requestId"]), - requestContent); + [Fact] + public void BadArgs() + { + TracerProviderBuilder builder = null; + Assert.Throws(() => builder.AddZipkinExporter()); + } - context.Response.OutputStream.Close(); - } - } + [Fact] + public void SuppressesInstrumentation() + { + const string ActivitySourceName = "zipkin.test"; + Guid requestId = Guid.NewGuid(); + TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); - public void Dispose() - { - this.testServer.Dispose(); - GC.SuppressFinalize(this); - } + int endCalledCount = 0; - [Fact] - public void AddAddZipkinExporterNamedOptionsSupported() + testActivityProcessor.EndAction = + (a) => + { + endCalledCount++; + }; + + var exporterOptions = new ZipkinExporterOptions { - int defaultExporterOptionsConfigureOptionsInvocations = 0; - int namedExporterOptionsConfigureOptionsInvocations = 0; + Endpoint = new Uri($"http://{this.testServerHost}:{this.testServerPort}/api/v2/spans?requestId={requestId}"), + }; + using var zipkinExporter = new ZipkinExporter(exporterOptions); + using var exportActivityProcessor = new BatchActivityExportProcessor(zipkinExporter); + + var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(ActivitySourceName) + .AddProcessor(testActivityProcessor) + .AddProcessor(exportActivityProcessor) + .AddHttpClientInstrumentation() + .Build(); + + using var source = new ActivitySource(ActivitySourceName); + using var activity = source.StartActivity("Test Zipkin Activity"); + activity?.Stop(); + + // We call ForceFlush on the exporter twice, so that in the event + // of a regression, this should give any operations performed in + // the Zipkin exporter itself enough time to be instrumented and + // loop back through the exporter. + exportActivityProcessor.ForceFlush(); + exportActivityProcessor.ForceFlush(); + + Assert.Equal(1, endCalledCount); + } - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .ConfigureServices(services => - { - services.Configure(o => defaultExporterOptionsConfigureOptionsInvocations++); + [Fact] + public void EndpointConfigurationUsingEnvironmentVariable() + { + try + { + Environment.SetEnvironmentVariable(ZipkinExporterOptions.ZipkinEndpointEnvVar, "http://urifromenvironmentvariable"); - services.Configure("Exporter2", o => namedExporterOptionsConfigureOptionsInvocations++); - }) - .AddZipkinExporter() - .AddZipkinExporter("Exporter2", o => { }) - .Build(); + var exporterOptions = new ZipkinExporterOptions(); - Assert.Equal(1, defaultExporterOptionsConfigureOptionsInvocations); - Assert.Equal(1, namedExporterOptionsConfigureOptionsInvocations); + Assert.Equal(new Uri(Environment.GetEnvironmentVariable(ZipkinExporterOptions.ZipkinEndpointEnvVar)).AbsoluteUri, exporterOptions.Endpoint.AbsoluteUri); } - - [Fact] - public void BadArgs() + finally { - TracerProviderBuilder builder = null; - Assert.Throws(() => builder.AddZipkinExporter()); + Environment.SetEnvironmentVariable(ZipkinExporterOptions.ZipkinEndpointEnvVar, null); } + } - [Fact] - public void SuppressesInstrumentation() + [Fact] + public void IncodeEndpointConfigTakesPrecedenceOverEnvironmentVariable() + { + try { - const string ActivitySourceName = "zipkin.test"; - Guid requestId = Guid.NewGuid(); - TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); - - int endCalledCount = 0; - - testActivityProcessor.EndAction = - (a) => - { - endCalledCount++; - }; + Environment.SetEnvironmentVariable(ZipkinExporterOptions.ZipkinEndpointEnvVar, "http://urifromenvironmentvariable"); var exporterOptions = new ZipkinExporterOptions { - Endpoint = new Uri($"http://{this.testServerHost}:{this.testServerPort}/api/v2/spans?requestId={requestId}"), + Endpoint = new Uri("http://urifromcode"), }; - using var zipkinExporter = new ZipkinExporter(exporterOptions); - using var exportActivityProcessor = new BatchActivityExportProcessor(zipkinExporter); - - var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(ActivitySourceName) - .AddProcessor(testActivityProcessor) - .AddProcessor(exportActivityProcessor) - .AddHttpClientInstrumentation() - .Build(); - - using var source = new ActivitySource(ActivitySourceName); - using var activity = source.StartActivity("Test Zipkin Activity"); - activity?.Stop(); - - // We call ForceFlush on the exporter twice, so that in the event - // of a regression, this should give any operations performed in - // the Zipkin exporter itself enough time to be instrumented and - // loop back through the exporter. - exportActivityProcessor.ForceFlush(); - exportActivityProcessor.ForceFlush(); - - Assert.Equal(1, endCalledCount); - } - [Fact] - public void EndpointConfigurationUsingEnvironmentVariable() + Assert.Equal(new Uri("http://urifromcode").AbsoluteUri, exporterOptions.Endpoint.AbsoluteUri); + } + finally { - try - { - Environment.SetEnvironmentVariable(ZipkinExporterOptions.ZipkinEndpointEnvVar, "http://urifromenvironmentvariable"); - - var exporterOptions = new ZipkinExporterOptions(); - - Assert.Equal(new Uri(Environment.GetEnvironmentVariable(ZipkinExporterOptions.ZipkinEndpointEnvVar)).AbsoluteUri, exporterOptions.Endpoint.AbsoluteUri); - } - finally - { - Environment.SetEnvironmentVariable(ZipkinExporterOptions.ZipkinEndpointEnvVar, null); - } + Environment.SetEnvironmentVariable(ZipkinExporterOptions.ZipkinEndpointEnvVar, null); } + } - [Fact] - public void IncodeEndpointConfigTakesPrecedenceOverEnvironmentVariable() + [Fact(Skip = "https://github.com/open-telemetry/opentelemetry-dotnet/issues/3690")] + public void ErrorGettingUriFromEnvVarSetsDefaultEndpointValue() + { + try { - try - { - Environment.SetEnvironmentVariable(ZipkinExporterOptions.ZipkinEndpointEnvVar, "http://urifromenvironmentvariable"); + Environment.SetEnvironmentVariable(ZipkinExporterOptions.ZipkinEndpointEnvVar, "InvalidUri"); - var exporterOptions = new ZipkinExporterOptions - { - Endpoint = new Uri("http://urifromcode"), - }; + var options = new ZipkinExporterOptions(); - Assert.Equal(new Uri("http://urifromcode").AbsoluteUri, exporterOptions.Endpoint.AbsoluteUri); - } - finally - { - Environment.SetEnvironmentVariable(ZipkinExporterOptions.ZipkinEndpointEnvVar, null); - } + Assert.Equal(new Uri(ZipkinExporterOptions.DefaultZipkinEndpoint), options.Endpoint); } - - [Fact(Skip = "https://github.com/open-telemetry/opentelemetry-dotnet/issues/3690")] - public void ErrorGettingUriFromEnvVarSetsDefaultEndpointValue() + finally { - try - { - Environment.SetEnvironmentVariable(ZipkinExporterOptions.ZipkinEndpointEnvVar, "InvalidUri"); - - var options = new ZipkinExporterOptions(); - - Assert.Equal(new Uri(ZipkinExporterOptions.DefaultZipkinEndpoint), options.Endpoint); - } - finally - { - Environment.SetEnvironmentVariable(ZipkinExporterOptions.ZipkinEndpointEnvVar, null); - } + Environment.SetEnvironmentVariable(ZipkinExporterOptions.ZipkinEndpointEnvVar, null); } + } - [Fact] - public void EndpointConfigurationUsingIConfiguration() + [Fact] + public void EndpointConfigurationUsingIConfiguration() + { + var values = new Dictionary() { - var values = new Dictionary() - { - [ZipkinExporterOptions.ZipkinEndpointEnvVar] = "http://custom-endpoint:12345", - }; - - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(values) - .Build(); + [ZipkinExporterOptions.ZipkinEndpointEnvVar] = "http://custom-endpoint:12345", + }; - var options = new ZipkinExporterOptions(configuration, new()); + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(values) + .Build(); - Assert.Equal(new Uri("http://custom-endpoint:12345"), options.Endpoint); - } + var options = new ZipkinExporterOptions(configuration, new()); - [Fact] - public void UserHttpFactoryCalled() - { - ZipkinExporterOptions options = new ZipkinExporterOptions(); - - var defaultFactory = options.HttpClientFactory; + Assert.Equal(new Uri("http://custom-endpoint:12345"), options.Endpoint); + } - int invocations = 0; - options.HttpClientFactory = () => - { - invocations++; - return defaultFactory(); - }; + [Fact] + public void UserHttpFactoryCalled() + { + ZipkinExporterOptions options = new ZipkinExporterOptions(); - using (var exporter = new ZipkinExporter(options)) - { - Assert.Equal(1, invocations); - } + var defaultFactory = options.HttpClientFactory; - using (var provider = Sdk.CreateTracerProviderBuilder() - .AddZipkinExporter(o => o.HttpClientFactory = options.HttpClientFactory) - .Build()) - { - Assert.Equal(2, invocations); - } + int invocations = 0; + options.HttpClientFactory = () => + { + invocations++; + return defaultFactory(); + }; - using var client = new HttpClient(); + using (var exporter = new ZipkinExporter(options)) + { + Assert.Equal(1, invocations); + } - using (var exporter = new ZipkinExporter(options, client)) - { - // Factory not called when client is passed as a param. - Assert.Equal(2, invocations); - } + using (var provider = Sdk.CreateTracerProviderBuilder() + .AddZipkinExporter(o => o.HttpClientFactory = options.HttpClientFactory) + .Build()) + { + Assert.Equal(2, invocations); + } - options.HttpClientFactory = null; - Assert.Throws(() => - { - using var exporter = new ZipkinExporter(options); - }); + using var client = new HttpClient(); - options.HttpClientFactory = () => null; - Assert.Throws(() => - { - using var exporter = new ZipkinExporter(options); - }); + using (var exporter = new ZipkinExporter(options, client)) + { + // Factory not called when client is passed as a param. + Assert.Equal(2, invocations); } - [Fact] - public void ServiceProviderHttpClientFactoryInvoked() + options.HttpClientFactory = null; + Assert.Throws(() => { - IServiceCollection services = new ServiceCollection(); + using var exporter = new ZipkinExporter(options); + }); - services.AddHttpClient(); + options.HttpClientFactory = () => null; + Assert.Throws(() => + { + using var exporter = new ZipkinExporter(options); + }); + } - int invocations = 0; + [Fact] + public void ServiceProviderHttpClientFactoryInvoked() + { + IServiceCollection services = new ServiceCollection(); - services.AddHttpClient("ZipkinExporter", configureClient: (client) => invocations++); + services.AddHttpClient(); - services.AddOpenTelemetry().WithTracing(builder => builder - .AddZipkinExporter()); + int invocations = 0; - using var serviceProvider = services.BuildServiceProvider(); + services.AddHttpClient("ZipkinExporter", configureClient: (client) => invocations++); - var tracerProvider = serviceProvider.GetRequiredService(); + services.AddOpenTelemetry().WithTracing(builder => builder + .AddZipkinExporter()); - Assert.Equal(1, invocations); - } + using var serviceProvider = services.BuildServiceProvider(); - [Fact] - public void UpdatesServiceNameFromDefaultResource() - { - var zipkinExporter = new ZipkinExporter(new ZipkinExporterOptions()); + var tracerProvider = serviceProvider.GetRequiredService(); - zipkinExporter.SetLocalEndpointFromResource(Resource.Empty); + Assert.Equal(1, invocations); + } - Assert.StartsWith("unknown_service:", zipkinExporter.LocalEndpoint.ServiceName); - } + [Fact] + public void UpdatesServiceNameFromDefaultResource() + { + var zipkinExporter = new ZipkinExporter(new ZipkinExporterOptions()); - [Fact] - public void UpdatesServiceNameFromIConfiguration() - { - var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder() - .ConfigureServices(services => + zipkinExporter.SetLocalEndpointFromResource(Resource.Empty); + + Assert.StartsWith("unknown_service:", zipkinExporter.LocalEndpoint.ServiceName); + } + + [Fact] + public void UpdatesServiceNameFromIConfiguration() + { + var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => + { + Dictionary configuration = new() { - Dictionary configuration = new() - { - ["OTEL_SERVICE_NAME"] = "myservicename", - }; + ["OTEL_SERVICE_NAME"] = "myservicename", + }; - services.AddSingleton( - new ConfigurationBuilder().AddInMemoryCollection(configuration).Build()); - }); + services.AddSingleton( + new ConfigurationBuilder().AddInMemoryCollection(configuration).Build()); + }); - var zipkinExporter = new ZipkinExporter(new ZipkinExporterOptions()); + var zipkinExporter = new ZipkinExporter(new ZipkinExporterOptions()); - tracerProviderBuilder.AddProcessor(new BatchActivityExportProcessor(zipkinExporter)); + tracerProviderBuilder.AddProcessor(new BatchActivityExportProcessor(zipkinExporter)); - using var provider = tracerProviderBuilder.Build(); + using var provider = tracerProviderBuilder.Build(); - zipkinExporter.SetLocalEndpointFromResource(Resource.Empty); + zipkinExporter.SetLocalEndpointFromResource(Resource.Empty); - Assert.Equal("myservicename", zipkinExporter.LocalEndpoint.ServiceName); - } + Assert.Equal("myservicename", zipkinExporter.LocalEndpoint.ServiceName); + } - [Theory] - [InlineData(true, false, false)] - [InlineData(false, false, false)] - [InlineData(false, true, false)] - [InlineData(false, false, true)] - [InlineData(false, false, false, StatusCode.Ok)] - [InlineData(false, false, false, StatusCode.Ok, null, true)] - [InlineData(false, false, false, StatusCode.Error)] - [InlineData(false, false, false, StatusCode.Error, "Error description")] - public void IntegrationTest( - bool useShortTraceIds, - bool useTestResource, - bool isRootSpan, - StatusCode statusCode = StatusCode.Unset, - string statusDescription = null, - bool addErrorTag = false) + [Theory] + [InlineData(true, false, false)] + [InlineData(false, false, false)] + [InlineData(false, true, false)] + [InlineData(false, false, true)] + [InlineData(false, false, false, StatusCode.Ok)] + [InlineData(false, false, false, StatusCode.Ok, null, true)] + [InlineData(false, false, false, StatusCode.Error)] + [InlineData(false, false, false, StatusCode.Error, "Error description")] + public void IntegrationTest( + bool useShortTraceIds, + bool useTestResource, + bool isRootSpan, + StatusCode statusCode = StatusCode.Unset, + string statusDescription = null, + bool addErrorTag = false) + { + var status = statusCode switch { - var status = statusCode switch - { - StatusCode.Unset => Status.Unset, - StatusCode.Ok => Status.Ok, - StatusCode.Error => Status.Error, - _ => throw new InvalidOperationException(), - }; + StatusCode.Unset => Status.Unset, + StatusCode.Ok => Status.Ok, + StatusCode.Error => Status.Error, + _ => throw new InvalidOperationException(), + }; - if (!string.IsNullOrEmpty(statusDescription)) - { - status = status.WithDescription(statusDescription); - } + if (!string.IsNullOrEmpty(statusDescription)) + { + status = status.WithDescription(statusDescription); + } - Guid requestId = Guid.NewGuid(); + Guid requestId = Guid.NewGuid(); - ZipkinExporter exporter = new ZipkinExporter( - new ZipkinExporterOptions - { - Endpoint = new Uri($"http://{this.testServerHost}:{this.testServerPort}/api/v2/spans?requestId={requestId}"), - UseShortTraceIds = useShortTraceIds, - }); - - var serviceName = (string)exporter.ParentProvider.GetDefaultResource().Attributes - .Where(pair => pair.Key == ResourceSemanticConventions.AttributeServiceName).FirstOrDefault().Value; - var resourceTags = string.Empty; - var activity = CreateTestActivity(isRootSpan: isRootSpan, status: status); - if (useTestResource) + ZipkinExporter exporter = new ZipkinExporter( + new ZipkinExporterOptions { - serviceName = "MyService"; + Endpoint = new Uri($"http://{this.testServerHost}:{this.testServerPort}/api/v2/spans?requestId={requestId}"), + UseShortTraceIds = useShortTraceIds, + }); - exporter.SetLocalEndpointFromResource(ResourceBuilder.CreateEmpty().AddAttributes(new Dictionary - { - [ResourceSemanticConventions.AttributeServiceName] = serviceName, - ["service.tag"] = "hello world", - }).Build()); - } - else - { - exporter.SetLocalEndpointFromResource(Resource.Empty); - } + var serviceName = (string)exporter.ParentProvider.GetDefaultResource().Attributes + .Where(pair => pair.Key == ResourceSemanticConventions.AttributeServiceName).FirstOrDefault().Value; + var resourceTags = string.Empty; + var activity = CreateTestActivity(isRootSpan: isRootSpan, status: status); + if (useTestResource) + { + serviceName = "MyService"; - if (addErrorTag) + exporter.SetLocalEndpointFromResource(ResourceBuilder.CreateEmpty().AddAttributes(new Dictionary { - activity.SetTag(ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName, "This should be removed."); - } + [ResourceSemanticConventions.AttributeServiceName] = serviceName, + ["service.tag"] = "hello world", + }).Build()); + } + else + { + exporter.SetLocalEndpointFromResource(Resource.Empty); + } - var processor = new SimpleActivityExportProcessor(exporter); + if (addErrorTag) + { + activity.SetTag(ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName, "This should be removed."); + } - processor.OnEnd(activity); + var processor = new SimpleActivityExportProcessor(exporter); - var context = activity.Context; + processor.OnEnd(activity); - var timestamp = activity.StartTimeUtc.ToEpochMicroseconds(); - var eventTimestamp = activity.Events.First().Timestamp.ToEpochMicroseconds(); + var context = activity.Context; - StringBuilder ipInformation = new StringBuilder(); - if (!string.IsNullOrEmpty(exporter.LocalEndpoint.Ipv4)) - { - ipInformation.Append($@",""ipv4"":""{exporter.LocalEndpoint.Ipv4}"""); - } + var timestamp = activity.StartTimeUtc.ToEpochMicroseconds(); + var eventTimestamp = activity.Events.First().Timestamp.ToEpochMicroseconds(); - if (!string.IsNullOrEmpty(exporter.LocalEndpoint.Ipv6)) - { - ipInformation.Append($@",""ipv6"":""{exporter.LocalEndpoint.Ipv6}"""); - } + StringBuilder ipInformation = new StringBuilder(); + if (!string.IsNullOrEmpty(exporter.LocalEndpoint.Ipv4)) + { + ipInformation.Append($@",""ipv4"":""{exporter.LocalEndpoint.Ipv4}"""); + } - var parentId = isRootSpan ? string.Empty : $@"""parentId"":""{ZipkinActivityConversionExtensions.EncodeSpanId(activity.ParentSpanId)}"","; + if (!string.IsNullOrEmpty(exporter.LocalEndpoint.Ipv6)) + { + ipInformation.Append($@",""ipv6"":""{exporter.LocalEndpoint.Ipv6}"""); + } - var traceId = useShortTraceIds ? TraceId.Substring(TraceId.Length - 16, 16) : TraceId; + var parentId = isRootSpan ? string.Empty : $@"""parentId"":""{ZipkinActivityConversionExtensions.EncodeSpanId(activity.ParentSpanId)}"","; - string statusTag; - string errorTag = string.Empty; - switch (statusCode) - { - case StatusCode.Ok: - statusTag = $@"""{SpanAttributeConstants.StatusCodeKey}"":""OK"","; - break; - case StatusCode.Unset: - statusTag = string.Empty; - break; - case StatusCode.Error: - statusTag = $@"""{SpanAttributeConstants.StatusCodeKey}"":""ERROR"","; - errorTag = $@"""{ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName}"":""{statusDescription}"","; - break; - default: - throw new NotSupportedException(); - } + var traceId = useShortTraceIds ? TraceId.Substring(TraceId.Length - 16, 16) : TraceId; - Assert.Equal( - $@"[{{""traceId"":""{traceId}"",""name"":""Name"",{parentId}""id"":""{ZipkinActivityConversionExtensions.EncodeSpanId(context.SpanId)}"",""kind"":""CLIENT"",""timestamp"":{timestamp},""duration"":60000000,""localEndpoint"":{{""serviceName"":""{serviceName}""{ipInformation}}},""remoteEndpoint"":{{""serviceName"":""http://localhost:44312/""}},""annotations"":[{{""timestamp"":{eventTimestamp},""value"":""Event1""}},{{""timestamp"":{eventTimestamp},""value"":""Event2""}}],""tags"":{{{resourceTags}""stringKey"":""value"",""longKey"":""1"",""longKey2"":""1"",""doubleKey"":""1"",""doubleKey2"":""1"",""longArrayKey"":""[1,2]"",""boolKey"":""true"",""boolArrayKey"":""[true,false]"",""http.host"":""http://localhost:44312/"",{statusTag}{errorTag}""otel.library.name"":""CreateTestActivity"",""peer.service"":""http://localhost:44312/""}}}}]", - Responses[requestId]); + string statusTag; + string errorTag = string.Empty; + switch (statusCode) + { + case StatusCode.Ok: + statusTag = $@"""{SpanAttributeConstants.StatusCodeKey}"":""OK"","; + break; + case StatusCode.Unset: + statusTag = string.Empty; + break; + case StatusCode.Error: + statusTag = $@"""{SpanAttributeConstants.StatusCodeKey}"":""ERROR"","; + errorTag = $@"""{ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName}"":""{statusDescription}"","; + break; + default: + throw new NotSupportedException(); } - internal static Activity CreateTestActivity( - bool isRootSpan = false, - bool setAttributes = true, - Dictionary additionalAttributes = null, - bool addEvents = true, - bool addLinks = true, - Resource resource = null, - ActivityKind kind = ActivityKind.Client, - Status? status = null) - { - var startTimestamp = DateTime.UtcNow; - var endTimestamp = startTimestamp.AddSeconds(60); - var eventTimestamp = DateTime.UtcNow; - var traceId = ActivityTraceId.CreateFromString("e8ea7e9ac72de94e91fabc613f9686b2".AsSpan()); + Assert.Equal( + $@"[{{""traceId"":""{traceId}"",""name"":""Name"",{parentId}""id"":""{ZipkinActivityConversionExtensions.EncodeSpanId(context.SpanId)}"",""kind"":""CLIENT"",""timestamp"":{timestamp},""duration"":60000000,""localEndpoint"":{{""serviceName"":""{serviceName}""{ipInformation}}},""remoteEndpoint"":{{""serviceName"":""http://localhost:44312/""}},""annotations"":[{{""timestamp"":{eventTimestamp},""value"":""Event1""}},{{""timestamp"":{eventTimestamp},""value"":""Event2""}}],""tags"":{{{resourceTags}""stringKey"":""value"",""longKey"":""1"",""longKey2"":""1"",""doubleKey"":""1"",""doubleKey2"":""1"",""longArrayKey"":""[1,2]"",""boolKey"":""true"",""boolArrayKey"":""[true,false]"",""http.host"":""http://localhost:44312/"",{statusTag}{errorTag}""otel.library.name"":""CreateTestActivity"",""peer.service"":""http://localhost:44312/""}}}}]", + Responses[requestId]); + } - var parentSpanId = isRootSpan ? default : ActivitySpanId.CreateFromBytes(new byte[] { 12, 23, 34, 45, 56, 67, 78, 89 }); + internal static Activity CreateTestActivity( + bool isRootSpan = false, + bool setAttributes = true, + Dictionary additionalAttributes = null, + bool addEvents = true, + bool addLinks = true, + Resource resource = null, + ActivityKind kind = ActivityKind.Client, + Status? status = null) + { + var startTimestamp = DateTime.UtcNow; + var endTimestamp = startTimestamp.AddSeconds(60); + var eventTimestamp = DateTime.UtcNow; + var traceId = ActivityTraceId.CreateFromString("e8ea7e9ac72de94e91fabc613f9686b2".AsSpan()); - var attributes = new Dictionary - { - { "stringKey", "value" }, - { "longKey", 1L }, - { "longKey2", 1 }, - { "doubleKey", 1D }, - { "doubleKey2", 1F }, - { "longArrayKey", new long[] { 1, 2 } }, - { "boolKey", true }, - { "boolArrayKey", new bool[] { true, false } }, - { "http.host", "http://localhost:44312/" }, // simulating instrumentation tag adding http.host - }; - if (additionalAttributes != null) + var parentSpanId = isRootSpan ? default : ActivitySpanId.CreateFromBytes(new byte[] { 12, 23, 34, 45, 56, 67, 78, 89 }); + + var attributes = new Dictionary + { + { "stringKey", "value" }, + { "longKey", 1L }, + { "longKey2", 1 }, + { "doubleKey", 1D }, + { "doubleKey2", 1F }, + { "longArrayKey", new long[] { 1, 2 } }, + { "boolKey", true }, + { "boolArrayKey", new bool[] { true, false } }, + { "http.host", "http://localhost:44312/" }, // simulating instrumentation tag adding http.host + }; + if (additionalAttributes != null) + { + foreach (var attribute in additionalAttributes) { - foreach (var attribute in additionalAttributes) + if (!attributes.ContainsKey(attribute.Key)) { - if (!attributes.ContainsKey(attribute.Key)) - { - attributes.Add(attribute.Key, attribute.Value); - } + attributes.Add(attribute.Key, attribute.Value); } } + } - var events = new List - { - new ActivityEvent( - "Event1", - eventTimestamp, - new ActivityTagsCollection(new Dictionary - { - { "key", "value" }, - })), - new ActivityEvent( - "Event2", - eventTimestamp, - new ActivityTagsCollection(new Dictionary - { - { "key", "value" }, - })), - }; + var events = new List + { + new ActivityEvent( + "Event1", + eventTimestamp, + new ActivityTagsCollection(new Dictionary + { + { "key", "value" }, + })), + new ActivityEvent( + "Event2", + eventTimestamp, + new ActivityTagsCollection(new Dictionary + { + { "key", "value" }, + })), + }; - var linkedSpanId = ActivitySpanId.CreateFromString("888915b6286b9c41".AsSpan()); - - var activitySource = new ActivitySource(nameof(CreateTestActivity)); - - var tags = setAttributes ? - attributes.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value)) - : null; - var links = addLinks ? - new[] - { - new ActivityLink(new ActivityContext( - traceId, - linkedSpanId, - ActivityTraceFlags.Recorded)), - } - : null; - - var activity = activitySource.StartActivity( - "Name", - kind, - parentContext: new ActivityContext(traceId, parentSpanId, ActivityTraceFlags.Recorded), - tags, - links, - startTime: startTimestamp); - - if (addEvents) - { - foreach (var evnt in events) + var linkedSpanId = ActivitySpanId.CreateFromString("888915b6286b9c41".AsSpan()); + + var activitySource = new ActivitySource(nameof(CreateTestActivity)); + + var tags = setAttributes ? + attributes.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value)) + : null; + var links = addLinks ? + new[] { - activity.AddEvent(evnt); + new ActivityLink(new ActivityContext( + traceId, + linkedSpanId, + ActivityTraceFlags.Recorded)), } - } + : null; - if (status.HasValue) + var activity = activitySource.StartActivity( + "Name", + kind, + parentContext: new ActivityContext(traceId, parentSpanId, ActivityTraceFlags.Recorded), + tags, + links, + startTime: startTimestamp); + + if (addEvents) + { + foreach (var evnt in events) { - activity.SetStatus(status.Value); + activity.AddEvent(evnt); } + } - activity.SetEndTime(endTimestamp); - activity.Stop(); - - return activity; + if (status.HasValue) + { + activity.SetStatus(status.Value); } + + activity.SetEndTime(endTimestamp); + activity.Stop(); + + return activity; } } diff --git a/test/OpenTelemetry.Extensions.Hosting.Tests/EventSourceTest.cs b/test/OpenTelemetry.Extensions.Hosting.Tests/EventSourceTest.cs index 64b4ab634bf..83d475bfec0 100644 --- a/test/OpenTelemetry.Extensions.Hosting.Tests/EventSourceTest.cs +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/EventSourceTest.cs @@ -1,31 +1,17 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Extensions.Hosting.Implementation; using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Extensions.Hosting.Tests +namespace OpenTelemetry.Extensions.Hosting.Tests; + +public class EventSourceTest { - public class EventSourceTest + [Fact] + public void EventSourceTest_HostingExtensionsEventSource() { - [Fact] - public void EventSourceTest_HostingExtensionsEventSource() - { - EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(HostingExtensionsEventSource.Log); - } + EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(HostingExtensionsEventSource.Log); } } diff --git a/test/OpenTelemetry.Extensions.Hosting.Tests/InMemoryExporterMetricsExtensionsTests.cs b/test/OpenTelemetry.Extensions.Hosting.Tests/InMemoryExporterMetricsExtensionsTests.cs index c68b494b702..99db85423b4 100644 --- a/test/OpenTelemetry.Extensions.Hosting.Tests/InMemoryExporterMetricsExtensionsTests.cs +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/InMemoryExporterMetricsExtensionsTests.cs @@ -1,26 +1,10 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #if NET6_0_OR_GREATER -using System; -using System.Collections.Generic; using System.Diagnostics.Metrics; using System.Net; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -33,80 +17,79 @@ using Xunit; -namespace OpenTelemetry.Extensions.Hosting.Tests +namespace OpenTelemetry.Extensions.Hosting.Tests; + +/// +/// These tests verify that works with . +/// +public class InMemoryExporterMetricsExtensionsTests { - /// - /// These tests verify that works with . - /// - public class InMemoryExporterMetricsExtensionsTests + [Fact] + public async Task DeferredMeterProviderBuilder_WithMetric() + { + var meterName = Utils.GetCurrentMethodName(); + var exportedItems = new List(); + + await RunMetricsTest( + configure: builder => builder + .AddMeter(meterName) + .AddInMemoryExporter(exportedItems), + testAction: () => + { + using var meter = new Meter(meterName); + var counter = meter.CreateCounter("meter"); + counter.Add(10); + }); + + Assert.Single(exportedItems); + var metricPointsEnumerator = exportedItems[0].GetMetricPoints().GetEnumerator(); + Assert.True(metricPointsEnumerator.MoveNext()); + Assert.Equal(10, metricPointsEnumerator.Current.GetSumLong()); + } + + [Fact] + public async Task DeferredMeterProviderBuilder_WithMetricSnapshot() + { + var meterName = Utils.GetCurrentMethodName(); + var exportedItems = new List(); + + await RunMetricsTest( + configure: builder => builder + .AddMeter(meterName) + .AddInMemoryExporter(exportedItems), + testAction: () => + { + using var meter = new Meter(meterName); + var counter = meter.CreateCounter("meter"); + counter.Add(10); + }); + + Assert.Single(exportedItems); + Assert.Equal(10, exportedItems[0].MetricPoints[0].GetSumLong()); + } + + private static async Task RunMetricsTest(Action configure, Action testAction) { - [Fact] - public async Task DeferredMeterProviderBuilder_WithMetric() - { - var meterName = Utils.GetCurrentMethodName(); - var exportedItems = new List(); - - await RunMetricsTest( - configure: builder => builder - .AddMeter(meterName) - .AddInMemoryExporter(exportedItems), - testAction: () => - { - 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(); - Assert.True(metricPointsEnumerator.MoveNext()); - Assert.Equal(10, metricPointsEnumerator.Current.GetSumLong()); - } - - [Fact] - public async Task DeferredMeterProviderBuilder_WithMetricSnapshot() - { - var meterName = Utils.GetCurrentMethodName(); - var exportedItems = new List(); - - await RunMetricsTest( - configure: builder => builder - .AddMeter(meterName) - .AddInMemoryExporter(exportedItems), - testAction: () => - { - 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()); - } - - private static async Task RunMetricsTest(Action configure, Action testAction) - { - using var host = await new HostBuilder() - .ConfigureWebHost(webBuilder => webBuilder - .UseTestServer() - .ConfigureServices(services => services.AddOpenTelemetry().WithMetrics(configure)) - .Configure(app => app.Run(httpContext => - { - testAction.Invoke(); - - var meterProvider = app.ApplicationServices.GetRequiredService(); - meterProvider.ForceFlush(); - - return Task.CompletedTask; - }))) - .StartAsync().ConfigureAwait(false); - - using var response = await host.GetTestClient().GetAsync($"/{nameof(RunMetricsTest)}").ConfigureAwait(false); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - await host.StopAsync().ConfigureAwait(false); - } + using var host = await new HostBuilder() + .ConfigureWebHost(webBuilder => webBuilder + .UseTestServer() + .ConfigureServices(services => services.AddOpenTelemetry().WithMetrics(configure)) + .Configure(app => app.Run(httpContext => + { + testAction.Invoke(); + + var meterProvider = app.ApplicationServices.GetRequiredService(); + meterProvider.ForceFlush(); + + return Task.CompletedTask; + }))) + .StartAsync(); + + using var response = await host.GetTestClient().GetAsync($"/{nameof(RunMetricsTest)}"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + 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 38c51130b8c..1ea17a421de 100644 --- a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetry.Extensions.Hosting.Tests.csproj +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetry.Extensions.Hosting.Tests.csproj @@ -1,12 +1,10 @@ Unit test project for OpenTelemetry .NET Core hosting library - - net7.0;net6.0 - $(TargetFrameworks);net462 - + $(TargetFrameworksForTests) disable + $(DefineConstants);BUILDING_HOSTING_TESTS @@ -16,27 +14,32 @@ + + + + + + + + + + + + + + - - - all + runtime; build; native; contentfiles; analyzers - - - - - - - - - diff --git a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryBuilderTests.cs b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryBuilderTests.cs index 4d31768c2ff..e5160e3b79e 100644 --- a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryBuilderTests.cs +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryBuilderTests.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Logs; @@ -75,4 +62,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..d34dc060f1e --- /dev/null +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryMetricsBuilderExtensionsTests.cs @@ -0,0 +1,246 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +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 c6a3b0a8108..0753220b6cc 100644 --- a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryServicesExtensionsTests.cs +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryServicesExtensionsTests.cs @@ -1,19 +1,7 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -36,9 +24,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] @@ -74,9 +62,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); } @@ -171,11 +159,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(); } @@ -294,11 +282,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(); } @@ -380,7 +368,7 @@ public void AddOpenTelemetry_WithLogging_DisposalTest() } [Fact] - public async Task AddOpenTelemetry_WithLogging_HostConfigurationHonoredTest() + public void AddOpenTelemetry_WithLogging_HostConfigurationHonoredTest() { bool configureBuilderCalled = false; @@ -415,14 +403,8 @@ public async Task AddOpenTelemetry_WithLogging_HostConfigurationHonoredTest() var host = builder.Build(); - Assert.False(configureBuilderCalled); - - await host.StartAsync().ConfigureAwait(false); - Assert.True(configureBuilderCalled); - await host.StopAsync().ConfigureAwait(false); - host.Dispose(); } @@ -450,9 +432,48 @@ public void AddOpenTelemetry_WithLogging_NestedResolutionUsingConfigureTest() Assert.True(innerTestExecuted); } + [Fact] + public async Task AddOpenTelemetry_HostedServiceOrder_DoesNotMatter() + { + var exportedItems = new List(); + + var builder = new HostBuilder().ConfigureServices(services => + { + services.AddHostedService(); + services.AddOpenTelemetry() + .WithTracing(builder => + { + builder.SetSampler(new AlwaysOnSampler()); + builder.AddSource(nameof(TestHostedService)); + builder.AddInMemoryExporter(exportedItems); + }); + }); + + var host = builder.Build(); + await host.StartAsync(); + await host.StopAsync(); + host.Dispose(); + + Assert.Single(exportedItems); + } + private sealed class MySampler : Sampler { public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) => new(SamplingDecision.RecordAndSample); } + + private sealed class TestHostedService : BackgroundService + { + private readonly ActivitySource activitySource = new ActivitySource(nameof(TestHostedService)); + + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + using (var activity = this.activitySource.StartActivity("test")) + { + } + + return Task.CompletedTask; + } + } } diff --git a/test/OpenTelemetry.Extensions.Propagators.Tests/B3PropagatorTest.cs b/test/OpenTelemetry.Extensions.Propagators.Tests/B3PropagatorTest.cs index e0f20cbc2b4..5373573ad6c 100644 --- a/test/OpenTelemetry.Extensions.Propagators.Tests/B3PropagatorTest.cs +++ b/test/OpenTelemetry.Extensions.Propagators.Tests/B3PropagatorTest.cs @@ -1,397 +1,384 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + using System.Diagnostics; using OpenTelemetry.Context.Propagation; using Xunit; using Xunit.Abstractions; -namespace OpenTelemetry.Extensions.Propagators.Tests +namespace OpenTelemetry.Extensions.Propagators.Tests; + +public class B3PropagatorTest { - public class B3PropagatorTest - { - private const string TraceIdBase16 = "ff000000000000000000000000000041"; - private const string TraceIdBase16EightBytes = "0000000000000041"; - private const string SpanIdBase16 = "ff00000000000041"; - private const string InvalidId = "abcdefghijklmnop"; - private const string InvalidSizeId = "0123456789abcdef00"; - private const ActivityTraceFlags TraceOptions = ActivityTraceFlags.Recorded; + private const string TraceIdBase16 = "ff000000000000000000000000000041"; + private const string TraceIdBase16EightBytes = "0000000000000041"; + private const string SpanIdBase16 = "ff00000000000041"; + private const string InvalidId = "abcdefghijklmnop"; + private const string InvalidSizeId = "0123456789abcdef00"; + private const ActivityTraceFlags TraceOptions = ActivityTraceFlags.Recorded; - private static readonly ActivityTraceId TraceId = ActivityTraceId.CreateFromString(TraceIdBase16.AsSpan()); - private static readonly ActivityTraceId TraceIdEightBytes = ActivityTraceId.CreateFromString(("0000000000000000" + TraceIdBase16EightBytes).AsSpan()); - private static readonly ActivitySpanId SpanId = ActivitySpanId.CreateFromString(SpanIdBase16.AsSpan()); + private static readonly ActivityTraceId TraceId = ActivityTraceId.CreateFromString(TraceIdBase16.AsSpan()); + private static readonly ActivityTraceId TraceIdEightBytes = ActivityTraceId.CreateFromString(("0000000000000000" + TraceIdBase16EightBytes).AsSpan()); + private static readonly ActivitySpanId SpanId = ActivitySpanId.CreateFromString(SpanIdBase16.AsSpan()); - private static readonly Action, string, string> Setter = (d, k, v) => d[k] = v; - private static readonly Func, string, IEnumerable> Getter = - (d, k) => - { - d.TryGetValue(k, out var v); - return new string[] { v }; - }; + private static readonly Action, string, string> Setter = (d, k, v) => d[k] = v; + private static readonly Func, string, IEnumerable> Getter = + (d, k) => + { + d.TryGetValue(k, out var v); + return new string[] { v }; + }; - private readonly B3Propagator b3propagator = new(); - private readonly B3Propagator b3PropagatorSingleHeader = new(true); + private readonly B3Propagator b3propagator = new(); + private readonly B3Propagator b3PropagatorSingleHeader = new(true); - private readonly ITestOutputHelper output; + private readonly ITestOutputHelper output; - public B3PropagatorTest(ITestOutputHelper output) - { - this.output = output; - } + public B3PropagatorTest(ITestOutputHelper output) + { + this.output = output; + } - [Fact] - public void Serialize_SampledContext() - { - var carrier = new Dictionary(); - this.b3propagator.Inject(new PropagationContext(new ActivityContext(TraceId, SpanId, TraceOptions), default), carrier, Setter); - this.ContainsExactly(carrier, new Dictionary { { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Sampled, "1" } }); - } + [Fact] + public void Serialize_SampledContext() + { + var carrier = new Dictionary(); + this.b3propagator.Inject(new PropagationContext(new ActivityContext(TraceId, SpanId, TraceOptions), default), carrier, Setter); + this.ContainsExactly(carrier, new Dictionary { { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Sampled, "1" } }); + } - [Fact] - public void Serialize_NotSampledContext() - { - var carrier = new Dictionary(); - var context = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None); - this.output.WriteLine(context.ToString()); - this.b3propagator.Inject(new PropagationContext(context, default), carrier, Setter); - this.ContainsExactly(carrier, new Dictionary { { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 } }); - } + [Fact] + public void Serialize_NotSampledContext() + { + var carrier = new Dictionary(); + var context = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None); + this.output.WriteLine(context.ToString()); + this.b3propagator.Inject(new PropagationContext(context, default), carrier, Setter); + this.ContainsExactly(carrier, new Dictionary { { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 } }); + } - [Fact] - public void ParseMissingSampledAndMissingFlag() + [Fact] + public void ParseMissingSampledAndMissingFlag() + { + var headersNotSampled = new Dictionary { - var headersNotSampled = new Dictionary - { - { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, - }; - var spanContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true); - Assert.Equal(new PropagationContext(spanContext, default), this.b3propagator.Extract(default, headersNotSampled, Getter)); - } + { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, + }; + var spanContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true); + Assert.Equal(new PropagationContext(spanContext, default), this.b3propagator.Extract(default, headersNotSampled, Getter)); + } - [Theory] - [InlineData("1")] - [InlineData("true")] - public void ParseSampled(string sampledValue) + [Theory] + [InlineData("1")] + [InlineData("true")] + public void ParseSampled(string sampledValue) + { + var headersSampled = new Dictionary { - var headersSampled = new Dictionary - { - { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Sampled, sampledValue }, - }; - var activityContext = new ActivityContext(TraceId, SpanId, TraceOptions, isRemote: true); - Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersSampled, Getter)); - } + { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Sampled, sampledValue }, + }; + var activityContext = new ActivityContext(TraceId, SpanId, TraceOptions, isRemote: true); + Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersSampled, Getter)); + } - [Theory] - [InlineData("0")] - [InlineData("false")] - [InlineData("something_else")] - public void ParseNotSampled(string sampledValue) + [Theory] + [InlineData("0")] + [InlineData("false")] + [InlineData("something_else")] + public void ParseNotSampled(string sampledValue) + { + var headersNotSampled = new Dictionary { - var headersNotSampled = new Dictionary - { - { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Sampled, sampledValue }, - }; - var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true); - Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersNotSampled, Getter)); - } + { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Sampled, sampledValue }, + }; + var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true); + Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersNotSampled, Getter)); + } - [Fact] - public void ParseFlag() + [Fact] + public void ParseFlag() + { + var headersFlagSampled = new Dictionary { - var headersFlagSampled = new Dictionary - { - { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Flags, "1" }, - }; - var activityContext = new ActivityContext(TraceId, SpanId, TraceOptions, isRemote: true); - Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersFlagSampled, Getter)); - } + { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Flags, "1" }, + }; + var activityContext = new ActivityContext(TraceId, SpanId, TraceOptions, isRemote: true); + Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersFlagSampled, Getter)); + } - [Fact] - public void ParseZeroFlag() + [Fact] + public void ParseZeroFlag() + { + var headersFlagNotSampled = new Dictionary { - var headersFlagNotSampled = new Dictionary - { - { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Flags, "0" }, - }; - var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true); - Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersFlagNotSampled, Getter)); - } + { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Flags, "0" }, + }; + var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true); + Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersFlagNotSampled, Getter)); + } - [Fact] - public void ParseEightBytesTraceId() - { - var headersEightBytes = new Dictionary - { - { B3Propagator.XB3TraceId, TraceIdBase16EightBytes }, - { B3Propagator.XB3SpanId, SpanIdBase16 }, - { B3Propagator.XB3Sampled, "1" }, - }; - var activityContext = new ActivityContext(TraceIdEightBytes, SpanId, TraceOptions, isRemote: true); - Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersEightBytes, Getter)); - } + [Fact] + public void ParseEightBytesTraceId() + { + var headersEightBytes = new Dictionary + { + { B3Propagator.XB3TraceId, TraceIdBase16EightBytes }, + { B3Propagator.XB3SpanId, SpanIdBase16 }, + { B3Propagator.XB3Sampled, "1" }, + }; + var activityContext = new ActivityContext(TraceIdEightBytes, SpanId, TraceOptions, isRemote: true); + Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersEightBytes, Getter)); + } - [Fact] - public void ParseEightBytesTraceId_NotSampledSpanContext() + [Fact] + public void ParseEightBytesTraceId_NotSampledSpanContext() + { + var headersEightBytes = new Dictionary { - var headersEightBytes = new Dictionary - { - { B3Propagator.XB3TraceId, TraceIdBase16EightBytes }, { B3Propagator.XB3SpanId, SpanIdBase16 }, - }; - var activityContext = new ActivityContext(TraceIdEightBytes, SpanId, ActivityTraceFlags.None, isRemote: true); - Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersEightBytes, Getter)); - } + { B3Propagator.XB3TraceId, TraceIdBase16EightBytes }, { B3Propagator.XB3SpanId, SpanIdBase16 }, + }; + var activityContext = new ActivityContext(TraceIdEightBytes, SpanId, ActivityTraceFlags.None, isRemote: true); + Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersEightBytes, Getter)); + } - [Fact] - public void ParseInvalidTraceId() + [Fact] + public void ParseInvalidTraceId() + { + var invalidHeaders = new Dictionary { - var invalidHeaders = new Dictionary - { - { B3Propagator.XB3TraceId, InvalidId }, { B3Propagator.XB3SpanId, SpanIdBase16 }, - }; - Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter)); - } + { B3Propagator.XB3TraceId, InvalidId }, { B3Propagator.XB3SpanId, SpanIdBase16 }, + }; + Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter)); + } - [Fact] - public void ParseInvalidTraceId_Size() + [Fact] + public void ParseInvalidTraceId_Size() + { + var invalidHeaders = new Dictionary { - var invalidHeaders = new Dictionary - { - { B3Propagator.XB3TraceId, InvalidSizeId }, { B3Propagator.XB3SpanId, SpanIdBase16 }, - }; + { B3Propagator.XB3TraceId, InvalidSizeId }, { B3Propagator.XB3SpanId, SpanIdBase16 }, + }; - Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter)); - } + Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter)); + } - [Fact] - public void ParseMissingTraceId() - { - var invalidHeaders = new Dictionary { { B3Propagator.XB3SpanId, SpanIdBase16 }, }; - Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter)); - } + [Fact] + public void ParseMissingTraceId() + { + var invalidHeaders = new Dictionary { { B3Propagator.XB3SpanId, SpanIdBase16 }, }; + Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter)); + } - [Fact] - public void ParseInvalidSpanId() + [Fact] + public void ParseInvalidSpanId() + { + var invalidHeaders = new Dictionary { - var invalidHeaders = new Dictionary - { - { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, InvalidId }, - }; - Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter)); - } + { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, InvalidId }, + }; + Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter)); + } - [Fact] - public void ParseInvalidSpanId_Size() + [Fact] + public void ParseInvalidSpanId_Size() + { + var invalidHeaders = new Dictionary { - var invalidHeaders = new Dictionary - { - { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, InvalidSizeId }, - }; - Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter)); - } + { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, InvalidSizeId }, + }; + Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter)); + } - [Fact] - public void ParseMissingSpanId() - { - var invalidHeaders = new Dictionary { { B3Propagator.XB3TraceId, TraceIdBase16 } }; - Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter)); - } + [Fact] + public void ParseMissingSpanId() + { + var invalidHeaders = new Dictionary { { B3Propagator.XB3TraceId, TraceIdBase16 } }; + Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter)); + } - [Fact] - public void Serialize_SampledContext_SingleHeader() - { - var carrier = new Dictionary(); - var activityContext = new ActivityContext(TraceId, SpanId, TraceOptions); - this.b3PropagatorSingleHeader.Inject(new PropagationContext(activityContext, default), carrier, Setter); - this.ContainsExactly(carrier, new Dictionary { { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-1" } }); - } + [Fact] + public void Serialize_SampledContext_SingleHeader() + { + var carrier = new Dictionary(); + var activityContext = new ActivityContext(TraceId, SpanId, TraceOptions); + this.b3PropagatorSingleHeader.Inject(new PropagationContext(activityContext, default), carrier, Setter); + this.ContainsExactly(carrier, new Dictionary { { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-1" } }); + } - [Fact] - public void Serialize_NotSampledContext_SingleHeader() - { - var carrier = new Dictionary(); - var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None); - this.output.WriteLine(activityContext.ToString()); - this.b3PropagatorSingleHeader.Inject(new PropagationContext(activityContext, default), carrier, Setter); - this.ContainsExactly(carrier, new Dictionary { { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}" } }); - } + [Fact] + public void Serialize_NotSampledContext_SingleHeader() + { + var carrier = new Dictionary(); + var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None); + this.output.WriteLine(activityContext.ToString()); + this.b3PropagatorSingleHeader.Inject(new PropagationContext(activityContext, default), carrier, Setter); + this.ContainsExactly(carrier, new Dictionary { { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}" } }); + } - [Fact] - public void ParseMissingSampledAndMissingFlag_SingleHeader() + [Fact] + public void ParseMissingSampledAndMissingFlag_SingleHeader() + { + var headersNotSampled = new Dictionary { - var headersNotSampled = new Dictionary - { - { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}" }, - }; - var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true); - Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersNotSampled, Getter)); - } + { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}" }, + }; + var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true); + Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersNotSampled, Getter)); + } - [Fact] - public void ParseSampled_SingleHeader() + [Fact] + public void ParseSampled_SingleHeader() + { + var headersSampled = new Dictionary { - var headersSampled = new Dictionary - { - { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-1" }, - }; - - Assert.Equal( - new PropagationContext(new ActivityContext(TraceId, SpanId, TraceOptions, isRemote: true), default), - this.b3PropagatorSingleHeader.Extract(default, headersSampled, Getter)); - } + { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-1" }, + }; + + Assert.Equal( + new PropagationContext(new ActivityContext(TraceId, SpanId, TraceOptions, isRemote: true), default), + this.b3PropagatorSingleHeader.Extract(default, headersSampled, Getter)); + } - [Fact] - public void ParseZeroSampled_SingleHeader() + [Fact] + public void ParseZeroSampled_SingleHeader() + { + var headersNotSampled = new Dictionary { - var headersNotSampled = new Dictionary - { - { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-0" }, - }; - - Assert.Equal( - new PropagationContext(new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true), default), - this.b3PropagatorSingleHeader.Extract(default, headersNotSampled, Getter)); - } + { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-0" }, + }; + + Assert.Equal( + new PropagationContext(new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true), default), + this.b3PropagatorSingleHeader.Extract(default, headersNotSampled, Getter)); + } - [Fact] - public void ParseFlag_SingleHeader() + [Fact] + public void ParseFlag_SingleHeader() + { + var headersFlagSampled = new Dictionary { - var headersFlagSampled = new Dictionary - { - { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-1" }, - }; - var activityContext = new ActivityContext(TraceId, SpanId, TraceOptions, isRemote: true); - Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersFlagSampled, Getter)); - } + { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-1" }, + }; + var activityContext = new ActivityContext(TraceId, SpanId, TraceOptions, isRemote: true); + Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersFlagSampled, Getter)); + } - [Fact] - public void ParseZeroFlag_SingleHeader() + [Fact] + public void ParseZeroFlag_SingleHeader() + { + var headersFlagNotSampled = new Dictionary { - var headersFlagNotSampled = new Dictionary - { - { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-0" }, - }; - var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true); - Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersFlagNotSampled, Getter)); - } + { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-0" }, + }; + var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true); + Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersFlagNotSampled, Getter)); + } - [Fact] - public void ParseEightBytesTraceId_SingleHeader() + [Fact] + public void ParseEightBytesTraceId_SingleHeader() + { + var headersEightBytes = new Dictionary { - var headersEightBytes = new Dictionary - { - { B3Propagator.XB3Combined, $"{TraceIdBase16EightBytes}-{SpanIdBase16}-1" }, - }; - var activityContext = new ActivityContext(TraceIdEightBytes, SpanId, TraceOptions, isRemote: true); - Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersEightBytes, Getter)); - } + { B3Propagator.XB3Combined, $"{TraceIdBase16EightBytes}-{SpanIdBase16}-1" }, + }; + var activityContext = new ActivityContext(TraceIdEightBytes, SpanId, TraceOptions, isRemote: true); + Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersEightBytes, Getter)); + } - [Fact] - public void ParseEightBytesTraceId_NotSampledSpanContext_SingleHeader() + [Fact] + public void ParseEightBytesTraceId_NotSampledSpanContext_SingleHeader() + { + var headersEightBytes = new Dictionary { - var headersEightBytes = new Dictionary - { - { B3Propagator.XB3Combined, $"{TraceIdBase16EightBytes}-{SpanIdBase16}" }, - }; - var activityContext = new ActivityContext(TraceIdEightBytes, SpanId, ActivityTraceFlags.None, isRemote: true); - Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersEightBytes, Getter)); - } + { B3Propagator.XB3Combined, $"{TraceIdBase16EightBytes}-{SpanIdBase16}" }, + }; + var activityContext = new ActivityContext(TraceIdEightBytes, SpanId, ActivityTraceFlags.None, isRemote: true); + Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersEightBytes, Getter)); + } - [Fact] - public void ParseInvalidTraceId_SingleHeader() + [Fact] + public void ParseInvalidTraceId_SingleHeader() + { + var invalidHeaders = new Dictionary { - var invalidHeaders = new Dictionary - { - { B3Propagator.XB3Combined, $"{InvalidId}-{SpanIdBase16}" }, - }; - Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter)); - } + { B3Propagator.XB3Combined, $"{InvalidId}-{SpanIdBase16}" }, + }; + Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter)); + } - [Fact] - public void ParseInvalidTraceId_Size_SingleHeader() + [Fact] + public void ParseInvalidTraceId_Size_SingleHeader() + { + var invalidHeaders = new Dictionary { - var invalidHeaders = new Dictionary - { - { B3Propagator.XB3Combined, $"{InvalidSizeId}-{SpanIdBase16}" }, - }; + { B3Propagator.XB3Combined, $"{InvalidSizeId}-{SpanIdBase16}" }, + }; - Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter)); - } + Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter)); + } - [Fact] - public void ParseMissingTraceId_SingleHeader() - { - var invalidHeaders = new Dictionary { { B3Propagator.XB3Combined, $"-{SpanIdBase16}" } }; - Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter)); - } + [Fact] + public void ParseMissingTraceId_SingleHeader() + { + var invalidHeaders = new Dictionary { { B3Propagator.XB3Combined, $"-{SpanIdBase16}" } }; + Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter)); + } - [Fact] - public void ParseInvalidSpanId_SingleHeader() + [Fact] + public void ParseInvalidSpanId_SingleHeader() + { + var invalidHeaders = new Dictionary { - var invalidHeaders = new Dictionary - { - { B3Propagator.XB3Combined, $"{TraceIdBase16}-{InvalidId}" }, - }; - Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter)); - } + { B3Propagator.XB3Combined, $"{TraceIdBase16}-{InvalidId}" }, + }; + Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter)); + } - [Fact] - public void ParseInvalidSpanId_Size_SingleHeader() + [Fact] + public void ParseInvalidSpanId_Size_SingleHeader() + { + var invalidHeaders = new Dictionary { - var invalidHeaders = new Dictionary - { - { B3Propagator.XB3Combined, $"{TraceIdBase16}-{InvalidSizeId}" }, - }; - Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter)); - } + { B3Propagator.XB3Combined, $"{TraceIdBase16}-{InvalidSizeId}" }, + }; + Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter)); + } - [Fact] - public void ParseMissingSpanId_SingleHeader() - { - var invalidHeaders = new Dictionary { { B3Propagator.XB3Combined, $"{TraceIdBase16}-" } }; - Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter)); - } + [Fact] + public void ParseMissingSpanId_SingleHeader() + { + var invalidHeaders = new Dictionary { { B3Propagator.XB3Combined, $"{TraceIdBase16}-" } }; + Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter)); + } + + [Fact] + public void Fields_list() + { + ContainsExactly( + this.b3propagator.Fields, + new List { B3Propagator.XB3TraceId, B3Propagator.XB3SpanId, B3Propagator.XB3ParentSpanId, B3Propagator.XB3Sampled, B3Propagator.XB3Flags }); + } - [Fact] - public void Fields_list() + private static void ContainsExactly(ISet list, List items) + { + Assert.Equal(items.Count, list.Count); + foreach (var item in items) { - ContainsExactly( - this.b3propagator.Fields, - new List { B3Propagator.XB3TraceId, B3Propagator.XB3SpanId, B3Propagator.XB3ParentSpanId, B3Propagator.XB3Sampled, B3Propagator.XB3Flags }); + Assert.Contains(item, list); } + } - private static void ContainsExactly(ISet list, List items) + private void ContainsExactly(IDictionary dict, IDictionary items) + { + foreach (var d in dict) { - Assert.Equal(items.Count, list.Count); - foreach (var item in items) - { - Assert.Contains(item, list); - } + this.output.WriteLine(d.Key + "=" + d.Value); } - private void ContainsExactly(IDictionary dict, IDictionary items) + Assert.Equal(items.Count, dict.Count); + foreach (var item in items) { - foreach (var d in dict) - { - this.output.WriteLine(d.Key + "=" + d.Value); - } - - Assert.Equal(items.Count, dict.Count); - foreach (var item in items) - { - Assert.Contains(item, dict); - } + Assert.Contains(item, dict); } } } diff --git a/test/OpenTelemetry.Extensions.Propagators.Tests/EventSourceTest.cs b/test/OpenTelemetry.Extensions.Propagators.Tests/EventSourceTest.cs index fdc75a02c75..65e606cc872 100644 --- a/test/OpenTelemetry.Extensions.Propagators.Tests/EventSourceTest.cs +++ b/test/OpenTelemetry.Extensions.Propagators.Tests/EventSourceTest.cs @@ -1,31 +1,17 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Internal; using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Instrumentation.SqlClient.Tests +namespace OpenTelemetry.Instrumentation.SqlClient.Tests; + +public class EventSourceTest { - public class EventSourceTest + [Fact] + public void EventSourceTest_PropagatorsEventSource() { - [Fact] - public void EventSourceTest_PropagatorsEventSource() - { - EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(OpenTelemetryPropagatorsEventSource.Log); - } + EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(OpenTelemetryPropagatorsEventSource.Log); } } diff --git a/test/OpenTelemetry.Extensions.Propagators.Tests/JaegerPropagatorTest.cs b/test/OpenTelemetry.Extensions.Propagators.Tests/JaegerPropagatorTest.cs index 9c2e7cbe89e..9e040746dd3 100644 --- a/test/OpenTelemetry.Extensions.Propagators.Tests/JaegerPropagatorTest.cs +++ b/test/OpenTelemetry.Extensions.Propagators.Tests/JaegerPropagatorTest.cs @@ -1,233 +1,220 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + using System.Diagnostics; using OpenTelemetry.Context.Propagation; using Xunit; -namespace OpenTelemetry.Extensions.Propagators.Tests +namespace OpenTelemetry.Extensions.Propagators.Tests; + +public class JaegerPropagatorTest { - public class JaegerPropagatorTest + private const string JaegerHeader = "uber-trace-id"; + private const string JaegerDelimiter = ":"; + private const string JaegerDelimiterEncoded = "%3A"; + + private const string TraceId = "0007651916cd43dd8448eb211c803177"; + private const string TraceIdShort = "7651916cd43dd8448eb211c803177"; + private const string SpanId = "0007c989f9791877"; + private const string SpanIdShort = "7c989f9791877"; + private const string ParentSpanId = "0"; + private const string FlagSampled = "1"; + private const string FlagNotSampled = "0"; + + private static readonly Func, string, IEnumerable> Getter = (headers, name) => { - private const string JaegerHeader = "uber-trace-id"; - private const string JaegerDelimiter = ":"; - private const string JaegerDelimiterEncoded = "%3A"; - - private const string TraceId = "0007651916cd43dd8448eb211c803177"; - private const string TraceIdShort = "7651916cd43dd8448eb211c803177"; - private const string SpanId = "0007c989f9791877"; - private const string SpanIdShort = "7c989f9791877"; - private const string ParentSpanId = "0"; - private const string FlagSampled = "1"; - private const string FlagNotSampled = "0"; - - private static readonly Func, string, IEnumerable> Getter = (headers, name) => + if (headers.TryGetValue(name, out var value)) { - if (headers.TryGetValue(name, out var value)) - { - return value; - } + return value; + } - return Array.Empty(); - }; + return Array.Empty(); + }; - private static readonly Action, string, string> Setter = (carrier, name, value) => - { - carrier[name] = value; - }; + private static readonly Action, string, string> Setter = (carrier, name, value) => + { + carrier[name] = value; + }; - [Fact] - public void ExtractReturnsOriginalContextIfContextIsAlreadyValid() - { - // arrange - var traceId = ActivityTraceId.CreateFromString(TraceId.AsSpan()); - var spanId = ActivitySpanId.CreateFromString(SpanId.AsSpan()); - var propagationContext = new PropagationContext( - new ActivityContext(traceId, spanId, ActivityTraceFlags.Recorded, isRemote: true), - default); + [Fact] + public void ExtractReturnsOriginalContextIfContextIsAlreadyValid() + { + // arrange + var traceId = ActivityTraceId.CreateFromString(TraceId.AsSpan()); + var spanId = ActivitySpanId.CreateFromString(SpanId.AsSpan()); + var propagationContext = new PropagationContext( + new ActivityContext(traceId, spanId, ActivityTraceFlags.Recorded, isRemote: true), + default); - var headers = new Dictionary(); + var headers = new Dictionary(); - // act - var result = new JaegerPropagator().Extract(propagationContext, headers, Getter); + // act + var result = new JaegerPropagator().Extract(propagationContext, headers, Getter); - // assert - Assert.Equal(propagationContext, result); - } + // assert + Assert.Equal(propagationContext, result); + } - [Fact] - public void ExtractReturnsOriginalContextIfCarrierIsNull() - { - // arrange - var propagationContext = default(PropagationContext); + [Fact] + public void ExtractReturnsOriginalContextIfCarrierIsNull() + { + // arrange + var propagationContext = default(PropagationContext); - // act - var result = new JaegerPropagator().Extract(propagationContext, null, Getter!); + // act + var result = new JaegerPropagator().Extract(propagationContext, null, Getter!); - // assert - Assert.Equal(propagationContext, result); - } + // assert + Assert.Equal(propagationContext, result); + } - [Fact] - public void ExtractReturnsOriginalContextIfGetterIsNull() - { - // arrange - var propagationContext = default(PropagationContext); + [Fact] + public void ExtractReturnsOriginalContextIfGetterIsNull() + { + // arrange + var propagationContext = default(PropagationContext); - var headers = new Dictionary(); + var headers = new Dictionary(); - // act - var result = new JaegerPropagator().Extract(propagationContext, headers, null); + // act + var result = new JaegerPropagator().Extract(propagationContext, headers, null); - // assert - Assert.Equal(propagationContext, result); - } + // assert + Assert.Equal(propagationContext, result); + } - [Theory] - [InlineData("", SpanId, ParentSpanId, FlagSampled, JaegerDelimiter)] - [InlineData(TraceId, "", ParentSpanId, FlagSampled, JaegerDelimiter)] - [InlineData(TraceId, SpanId, "", FlagSampled, JaegerDelimiter)] - [InlineData(TraceId, SpanId, ParentSpanId, "", JaegerDelimiter)] - [InlineData(TraceId, SpanId, ParentSpanId, FlagSampled, "")] - [InlineData("invalid trace id", SpanId, ParentSpanId, FlagSampled, JaegerDelimiter)] - [InlineData(TraceId, "invalid span id", ParentSpanId, FlagSampled, JaegerDelimiter)] - [InlineData(TraceId, SpanId, $"too many {JaegerDelimiter} records", FlagSampled, JaegerDelimiter)] - public void ExtractReturnsOriginalContextIfHeaderIsNotValid(string traceId, string spanId, string parentSpanId, string flags, string delimiter) - { - // arrange - var propagationContext = default(PropagationContext); + [Theory] + [InlineData("", SpanId, ParentSpanId, FlagSampled, JaegerDelimiter)] + [InlineData(TraceId, "", ParentSpanId, FlagSampled, JaegerDelimiter)] + [InlineData(TraceId, SpanId, "", FlagSampled, JaegerDelimiter)] + [InlineData(TraceId, SpanId, ParentSpanId, "", JaegerDelimiter)] + [InlineData(TraceId, SpanId, ParentSpanId, FlagSampled, "")] + [InlineData("invalid trace id", SpanId, ParentSpanId, FlagSampled, JaegerDelimiter)] + [InlineData(TraceId, "invalid span id", ParentSpanId, FlagSampled, JaegerDelimiter)] + [InlineData(TraceId, SpanId, $"too many {JaegerDelimiter} records", FlagSampled, JaegerDelimiter)] + public void ExtractReturnsOriginalContextIfHeaderIsNotValid(string traceId, string spanId, string parentSpanId, string flags, string delimiter) + { + // arrange + var propagationContext = default(PropagationContext); - var formattedHeader = string.Join( - delimiter, - traceId, - spanId, - parentSpanId, - flags); + var formattedHeader = string.Join( + delimiter, + traceId, + spanId, + parentSpanId, + flags); - var headers = new Dictionary { { JaegerHeader, new[] { formattedHeader } } }; + var headers = new Dictionary { { JaegerHeader, new[] { formattedHeader } } }; - // act - var result = new JaegerPropagator().Extract(propagationContext, headers, Getter); + // act + var result = new JaegerPropagator().Extract(propagationContext, headers, Getter); - // assert - Assert.Equal(propagationContext, result); - } + // assert + Assert.Equal(propagationContext, result); + } - [Theory] - [InlineData(TraceId, SpanId, ParentSpanId, FlagSampled, JaegerDelimiter)] - [InlineData(TraceIdShort, SpanIdShort, ParentSpanId, FlagNotSampled, JaegerDelimiterEncoded)] - public void ExtractReturnsNewContextIfHeaderIsValid(string traceId, string spanId, string parentSpanId, string flags, string delimiter) - { - // arrange - var propagationContext = default(PropagationContext); + [Theory] + [InlineData(TraceId, SpanId, ParentSpanId, FlagSampled, JaegerDelimiter)] + [InlineData(TraceIdShort, SpanIdShort, ParentSpanId, FlagNotSampled, JaegerDelimiterEncoded)] + public void ExtractReturnsNewContextIfHeaderIsValid(string traceId, string spanId, string parentSpanId, string flags, string delimiter) + { + // arrange + var propagationContext = default(PropagationContext); - var formattedHeader = string.Join( - delimiter, - traceId, - spanId, - parentSpanId, - flags); + var formattedHeader = string.Join( + delimiter, + traceId, + spanId, + parentSpanId, + flags); - var headers = new Dictionary { { JaegerHeader, new[] { formattedHeader } } }; + var headers = new Dictionary { { JaegerHeader, new[] { formattedHeader } } }; - // act - var result = new JaegerPropagator().Extract(propagationContext, headers, Getter); + // act + var result = new JaegerPropagator().Extract(propagationContext, headers, Getter); - // assert - Assert.Equal(traceId.PadLeft(TraceId.Length, '0'), result.ActivityContext.TraceId.ToString()); - Assert.Equal(spanId.PadLeft(SpanId.Length, '0'), result.ActivityContext.SpanId.ToString()); - Assert.Equal(flags == "1" ? ActivityTraceFlags.Recorded : ActivityTraceFlags.None, result.ActivityContext.TraceFlags); - } + // assert + Assert.Equal(traceId.PadLeft(TraceId.Length, '0'), result.ActivityContext.TraceId.ToString()); + Assert.Equal(spanId.PadLeft(SpanId.Length, '0'), result.ActivityContext.SpanId.ToString()); + Assert.Equal(flags == "1" ? ActivityTraceFlags.Recorded : ActivityTraceFlags.None, result.ActivityContext.TraceFlags); + } - [Fact] - public void InjectDoesNoopIfContextIsInvalid() - { - // arrange - var propagationContext = default(PropagationContext); + [Fact] + public void InjectDoesNoopIfContextIsInvalid() + { + // arrange + var propagationContext = default(PropagationContext); - var headers = new Dictionary(); + var headers = new Dictionary(); - // act - new JaegerPropagator().Inject(propagationContext, headers, Setter); + // act + new JaegerPropagator().Inject(propagationContext, headers, Setter); - // assert - Assert.Empty(headers); - } + // assert + Assert.Empty(headers); + } - [Fact] - public void InjectDoesNoopIfCarrierIsNull() - { - // arrange - var traceId = ActivityTraceId.CreateFromString(TraceId.AsSpan()); - var spanId = ActivitySpanId.CreateFromString(SpanId.AsSpan()); - var propagationContext = new PropagationContext( - new ActivityContext(traceId, spanId, ActivityTraceFlags.Recorded, isRemote: true), - default); + [Fact] + public void InjectDoesNoopIfCarrierIsNull() + { + // arrange + var traceId = ActivityTraceId.CreateFromString(TraceId.AsSpan()); + var spanId = ActivitySpanId.CreateFromString(SpanId.AsSpan()); + var propagationContext = new PropagationContext( + new ActivityContext(traceId, spanId, ActivityTraceFlags.Recorded, isRemote: true), + default); - // act - new JaegerPropagator().Inject(propagationContext, null, Setter!); + // act + new JaegerPropagator().Inject(propagationContext, null, Setter!); - // assert - } + // assert + } - [Fact] - public void InjectDoesNoopIfSetterIsNull() - { - // arrange - var traceId = ActivityTraceId.CreateFromString(TraceId.AsSpan()); - var spanId = ActivitySpanId.CreateFromString(SpanId.AsSpan()); - var propagationContext = new PropagationContext( - new ActivityContext(traceId, spanId, ActivityTraceFlags.Recorded, isRemote: true), - default); + [Fact] + public void InjectDoesNoopIfSetterIsNull() + { + // arrange + var traceId = ActivityTraceId.CreateFromString(TraceId.AsSpan()); + var spanId = ActivitySpanId.CreateFromString(SpanId.AsSpan()); + var propagationContext = new PropagationContext( + new ActivityContext(traceId, spanId, ActivityTraceFlags.Recorded, isRemote: true), + default); - var headers = new Dictionary(); + var headers = new Dictionary(); - // act - new JaegerPropagator().Inject(propagationContext, headers, null); + // act + new JaegerPropagator().Inject(propagationContext, headers, null); - // assert - Assert.Empty(headers); - } + // assert + Assert.Empty(headers); + } - [Theory] - [InlineData(FlagSampled)] - [InlineData(FlagNotSampled)] - public void InjectWillAddJaegerFormattedTraceToCarrier(string sampledFlag) - { - // arrange - var traceId = ActivityTraceId.CreateFromString(TraceId.AsSpan()); - var spanId = ActivitySpanId.CreateFromString(SpanId.AsSpan()); - var flags = sampledFlag == "1" ? ActivityTraceFlags.Recorded : ActivityTraceFlags.None; + [Theory] + [InlineData(FlagSampled)] + [InlineData(FlagNotSampled)] + public void InjectWillAddJaegerFormattedTraceToCarrier(string sampledFlag) + { + // arrange + var traceId = ActivityTraceId.CreateFromString(TraceId.AsSpan()); + var spanId = ActivitySpanId.CreateFromString(SpanId.AsSpan()); + var flags = sampledFlag == "1" ? ActivityTraceFlags.Recorded : ActivityTraceFlags.None; - var propagationContext = new PropagationContext(new ActivityContext(traceId, spanId, flags, isRemote: true), default); + var propagationContext = new PropagationContext(new ActivityContext(traceId, spanId, flags, isRemote: true), default); - var expectedValue = string.Join( - JaegerDelimiter, - traceId, - spanId, - ParentSpanId, - sampledFlag); + var expectedValue = string.Join( + JaegerDelimiter, + traceId, + spanId, + ParentSpanId, + sampledFlag); - var headers = new Dictionary(); + var headers = new Dictionary(); - // act - new JaegerPropagator().Inject(propagationContext, headers, Setter); + // act + new JaegerPropagator().Inject(propagationContext, headers, Setter); - // assert - Assert.Single(headers); - Assert.Equal(expectedValue, headers[JaegerHeader]); - } + // assert + Assert.Single(headers); + Assert.Equal(expectedValue, headers[JaegerHeader]); } } diff --git a/test/OpenTelemetry.Extensions.Propagators.Tests/OpenTelemetry.Extensions.Propagators.Tests.csproj b/test/OpenTelemetry.Extensions.Propagators.Tests/OpenTelemetry.Extensions.Propagators.Tests.csproj index 0f8aae96226..77eaa8a1f68 100644 --- a/test/OpenTelemetry.Extensions.Propagators.Tests/OpenTelemetry.Extensions.Propagators.Tests.csproj +++ b/test/OpenTelemetry.Extensions.Propagators.Tests/OpenTelemetry.Extensions.Propagators.Tests.csproj @@ -1,24 +1,20 @@ - - net7.0;net6.0;net462 - $(TARGET_FRAMEWORK) - + $(TargetFrameworksForTests) disable - - + + - - all + runtime; build; native; contentfiles; analyzers diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/AttributesExtensions.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/AttributesExtensions.cs index 2bfbd067e98..7ad43bdaea1 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/AttributesExtensions.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/AttributesExtensions.cs @@ -1,26 +1,12 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Instrumentation.AspNetCore.Tests +namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; + +internal static class AttributesExtensions { - internal static class AttributesExtensions + public static object GetValue(this IEnumerable> attributes, string key) { - public static object GetValue(this IEnumerable> attributes, string key) - { - return attributes.FirstOrDefault(kvp => kvp.Key == key).Value; - } + return attributes.FirstOrDefault(kvp => kvp.Key == key).Value; } } diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs index d8b3737ead0..80033de3711 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Text.Json; @@ -22,8 +9,8 @@ using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Moq; using OpenTelemetry.Context.Propagation; using OpenTelemetry.Instrumentation.AspNetCore.Implementation; using OpenTelemetry.Tests; @@ -32,151 +19,209 @@ using TestApp.AspNetCore.Filters; using Xunit; -namespace OpenTelemetry.Instrumentation.AspNetCore.Tests +namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; + +// See https://github.com/aspnet/Docs/tree/master/aspnetcore/test/integration-tests/samples/2.x/IntegrationTestsSample +public sealed class BasicTests + : IClassFixture>, IDisposable { - // See https://github.com/aspnet/Docs/tree/master/aspnetcore/test/integration-tests/samples/2.x/IntegrationTestsSample - public sealed class BasicTests - : IClassFixture>, IDisposable + private readonly WebApplicationFactory factory; + private TracerProvider tracerProvider = null; + + public BasicTests(WebApplicationFactory factory) + { + this.factory = factory; + } + + [Fact] + public void AddAspNetCoreInstrumentation_BadArgs() { - private readonly WebApplicationFactory factory; - private TracerProvider tracerProvider = null; + TracerProviderBuilder builder = null; + Assert.Throws(() => builder.AddAspNetCoreInstrumentation()); + } - public BasicTests(WebApplicationFactory factory) + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task StatusIsUnsetOn200Response(bool disableLogging) + { + var exportedItems = new List(); + void ConfigureTestServices(IServiceCollection services) { - this.factory = factory; + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build(); } - [Fact] - public void AddAspNetCoreInstrumentation_BadArgs() + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(ConfigureTestServices); + if (disableLogging) + { + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + } + }) + .CreateClient()) { - TracerProviderBuilder builder = null; - Assert.Throws(() => builder.AddAspNetCoreInstrumentation()); + // Act + using var response = await client.GetAsync("/api/values"); + + // Assert + response.EnsureSuccessStatusCode(); // Status Code 200-299 + + WaitForActivityExport(exportedItems, 1); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task StatusIsUnsetOn200Response(bool disableLogging) - { - var exportedItems = new List(); - void ConfigureTestServices(IServiceCollection services) - { - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation() - .AddInMemoryExporter(exportedItems) - .Build(); - } + Assert.Single(exportedItems); + var activity = exportedItems[0]; - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => + Assert.Equal(200, activity.GetTagValue(SemanticConventions.AttributeHttpResponseStatusCode)); + Assert.Equal(ActivityStatusCode.Unset, activity.Status); + ValidateAspNetCoreActivity(activity, "/api/values"); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task SuccessfulTemplateControllerCallGeneratesASpan(bool shouldEnrich) + { + var exportedItems = new List(); + void ConfigureTestServices(IServiceCollection services) + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation(options => { - builder.ConfigureTestServices(ConfigureTestServices); - if (disableLogging) + if (shouldEnrich) { - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + options.EnrichWithHttpRequest = (activity, request) => { activity.SetTag("enrichedOnStart", "yes"); }; + options.EnrichWithHttpResponse = (activity, response) => { activity.SetTag("enrichedOnStop", "yes"); }; } }) - .CreateClient()) + .AddInMemoryExporter(exportedItems) + .Build(); + } + + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => { - // Act - using var response = await client.GetAsync("/api/values").ConfigureAwait(false); + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + // Act + using var response = await client.GetAsync("/api/values"); - // Assert - response.EnsureSuccessStatusCode(); // Status Code 200-299 + // Assert + response.EnsureSuccessStatusCode(); // Status Code 200-299 - WaitForActivityExport(exportedItems, 1); - } + WaitForActivityExport(exportedItems, 1); + } - Assert.Single(exportedItems); - var activity = exportedItems[0]; + Assert.Single(exportedItems); + var activity = exportedItems[0]; - Assert.Equal(200, activity.GetTagValue(SemanticConventions.AttributeHttpStatusCode)); - Assert.Equal(ActivityStatusCode.Unset, activity.Status); - ValidateAspNetCoreActivity(activity, "/api/values"); + if (shouldEnrich) + { + Assert.NotEmpty(activity.Tags.Where(tag => tag.Key == "enrichedOnStart" && tag.Value == "yes")); + Assert.NotEmpty(activity.Tags.Where(tag => tag.Key == "enrichedOnStop" && tag.Value == "yes")); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task SuccessfulTemplateControllerCallGeneratesASpan(bool shouldEnrich) - { - var exportedItems = new List(); - void ConfigureTestServices(IServiceCollection services) + ValidateAspNetCoreActivity(activity, "/api/values"); + } + + [Fact] + public async Task SuccessfulTemplateControllerCallUsesParentContext() + { + var exportedItems = new List(); + var expectedTraceId = ActivityTraceId.CreateRandom(); + var expectedSpanId = ActivitySpanId.CreateRandom(); + + // Arrange + using (var testFactory = this.factory + .WithWebHostBuilder(builder => { - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation(options => - { - if (shouldEnrich) - { - options.EnrichWithHttpRequest = (activity, request) => { activity.SetTag("enrichedOnStart", "yes"); }; - options.EnrichWithHttpResponse = (activity, response) => { activity.SetTag("enrichedOnStop", "yes"); }; - } - }) + builder.ConfigureTestServices(services => + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() .AddInMemoryExporter(exportedItems) .Build(); - } + }); - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(ConfigureTestServices); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }) - .CreateClient()) - { - // Act - using var response = await client.GetAsync("/api/values").ConfigureAwait(false); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + })) + { + using var client = testFactory.CreateClient(); + var request = new HttpRequestMessage(HttpMethod.Get, "/api/values/2"); + request.Headers.Add("traceparent", $"00-{expectedTraceId}-{expectedSpanId}-01"); - // Assert - response.EnsureSuccessStatusCode(); // Status Code 200-299 + // Act + var response = await client.SendAsync(request); - WaitForActivityExport(exportedItems, 1); - } + // Assert + response.EnsureSuccessStatusCode(); // Status Code 200-299 - Assert.Single(exportedItems); - var activity = exportedItems[0]; + WaitForActivityExport(exportedItems, 1); + } - if (shouldEnrich) - { - Assert.NotEmpty(activity.Tags.Where(tag => tag.Key == "enrichedOnStart" && tag.Value == "yes")); - Assert.NotEmpty(activity.Tags.Where(tag => tag.Key == "enrichedOnStop" && tag.Value == "yes")); - } + Assert.Single(exportedItems); + var activity = exportedItems[0]; - ValidateAspNetCoreActivity(activity, "/api/values"); - } + Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", activity.OperationName); + + Assert.Equal(expectedTraceId, activity.Context.TraceId); + Assert.Equal(expectedSpanId, activity.ParentSpanId); + + ValidateAspNetCoreActivity(activity, "/api/values/2"); + } - [Fact] - public async Task SuccessfulTemplateControllerCallUsesParentContext() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CustomPropagator(bool addSampler) + { + try { var exportedItems = new List(); var expectedTraceId = ActivityTraceId.CreateRandom(); var expectedSpanId = ActivitySpanId.CreateRandom(); + var propagator = new CustomTextMapPropagator + { + TraceId = expectedTraceId, + SpanId = expectedSpanId, + }; + // Arrange using (var testFactory = this.factory .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(services => { - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation() - .AddInMemoryExporter(exportedItems) - .Build(); - }); + builder.ConfigureTestServices(services => + { + Sdk.SetDefaultTextMapPropagator(propagator); + var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder(); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - })) + if (addSampler) + { + tracerProviderBuilder + .SetSampler(new TestSampler(SamplingDecision.RecordAndSample, new Dictionary { { "SomeTag", "SomeKey" }, })); + } + + this.tracerProvider = tracerProviderBuilder + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build(); + }); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + })) { using var client = testFactory.CreateClient(); - var request = new HttpRequestMessage(HttpMethod.Get, "/api/values/2"); - request.Headers.Add("traceparent", $"00-{expectedTraceId}-{expectedSpanId}-01"); - - // Act - var response = await client.SendAsync(request).ConfigureAwait(false); - - // Assert + using var response = await client.GetAsync("/api/values/2"); response.EnsureSuccessStatusCode(); // Status Code 200-299 WaitForActivityExport(exportedItems, 1); @@ -185,1033 +230,980 @@ public async Task SuccessfulTemplateControllerCallUsesParentContext() Assert.Single(exportedItems); var activity = exportedItems[0]; - Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", activity.OperationName); - Assert.Equal("api/Values/{id}", activity.DisplayName); + Assert.True(activity.Duration != TimeSpan.Zero); Assert.Equal(expectedTraceId, activity.Context.TraceId); Assert.Equal(expectedSpanId, activity.ParentSpanId); ValidateAspNetCoreActivity(activity, "/api/values/2"); } - - [Fact] - public async Task CustomPropagator() + finally { - try - { - var exportedItems = new List(); - var expectedTraceId = ActivityTraceId.CreateRandom(); - var expectedSpanId = ActivitySpanId.CreateRandom(); - - var propagator = new Mock(); - propagator.Setup(m => m.Extract(It.IsAny(), It.IsAny(), It.IsAny>>())).Returns( - new PropagationContext( - new ActivityContext( - expectedTraceId, - expectedSpanId, - ActivityTraceFlags.Recorded), - default)); - - // Arrange - using (var testFactory = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(services => - { - Sdk.SetDefaultTextMapPropagator(propagator.Object); - this.tracerProvider = Sdk.CreateTracerProviderBuilder().AddAspNetCoreInstrumentation().AddInMemoryExporter(exportedItems).Build(); - }); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - })) - { - using var client = testFactory.CreateClient(); - using var response = await client.GetAsync("/api/values/2").ConfigureAwait(false); - response.EnsureSuccessStatusCode(); // Status Code 200-299 - - WaitForActivityExport(exportedItems, 1); - } - - Assert.Single(exportedItems); - 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); - - ValidateAspNetCoreActivity(activity, "/api/values/2"); - } - finally + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] { - Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] - { - new TraceContextPropagator(), - new BaggagePropagator(), - })); - } + new TraceContextPropagator(), + new BaggagePropagator(), + })); } + } + + [Fact] + public async Task RequestNotCollectedWhenFilterIsApplied() + { + var exportedItems = new List(); - [Fact] - public async Task RequestNotCollectedWhenFilterIsApplied() + void ConfigureTestServices(IServiceCollection services) { - var exportedItems = new List(); + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation((opt) => opt.Filter = (ctx) => ctx.Request.Path != "/api/values/2") + .AddInMemoryExporter(exportedItems) + .Build(); + } - void ConfigureTestServices(IServiceCollection services) + // Arrange + using (var testFactory = this.factory + .WithWebHostBuilder(builder => { - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation((opt) => opt.Filter = (ctx) => ctx.Request.Path != "/api/values/2") - .AddInMemoryExporter(exportedItems) - .Build(); - } + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + })) + { + using var client = testFactory.CreateClient(); - // Arrange - using (var testFactory = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(ConfigureTestServices); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - })) - { - using var client = testFactory.CreateClient(); + // Act + using var response1 = await client.GetAsync("/api/values"); + using var response2 = await client.GetAsync("/api/values/2"); - // Act - using var response1 = await client.GetAsync("/api/values").ConfigureAwait(false); - using var response2 = await client.GetAsync("/api/values/2").ConfigureAwait(false); + // Assert + response1.EnsureSuccessStatusCode(); // Status Code 200-299 + response2.EnsureSuccessStatusCode(); // Status Code 200-299 - // Assert - response1.EnsureSuccessStatusCode(); // Status Code 200-299 - response2.EnsureSuccessStatusCode(); // Status Code 200-299 + WaitForActivityExport(exportedItems, 1); + } - WaitForActivityExport(exportedItems, 1); - } + Assert.Single(exportedItems); + var activity = exportedItems[0]; - Assert.Single(exportedItems); - var activity = exportedItems[0]; + ValidateAspNetCoreActivity(activity, "/api/values"); + } - ValidateAspNetCoreActivity(activity, "/api/values"); - } + [Fact] + public async Task RequestNotCollectedWhenFilterThrowException() + { + var exportedItems = new List(); - [Fact] - public async Task RequestNotCollectedWhenFilterThrowException() + void ConfigureTestServices(IServiceCollection services) { - var exportedItems = new List(); - - void ConfigureTestServices(IServiceCollection services) - { - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation((opt) => opt.Filter = (ctx) => + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation((opt) => opt.Filter = (ctx) => + { + if (ctx.Request.Path == "/api/values/2") { - if (ctx.Request.Path == "/api/values/2") - { - throw new Exception("from InstrumentationFilter"); - } - else - { - return true; - } - }) - .AddInMemoryExporter(exportedItems) - .Build(); - } + throw new Exception("from InstrumentationFilter"); + } + else + { + return true; + } + }) + .AddInMemoryExporter(exportedItems) + .Build(); + } - // Arrange - using (var testFactory = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(ConfigureTestServices); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - })) + // Arrange + using (var testFactory = this.factory + .WithWebHostBuilder(builder => { - using var client = testFactory.CreateClient(); - - // 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); + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + })) + { + using var client = testFactory.CreateClient(); - response1.EnsureSuccessStatusCode(); // Status Code 200-299 - response2.EnsureSuccessStatusCode(); // Status Code 200-299 - Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 3)); - } + // Act + using (var inMemoryEventListener = new InMemoryEventListener(AspNetCoreInstrumentationEventSource.Log)) + { + using var response1 = await client.GetAsync("/api/values"); + using var response2 = await client.GetAsync("/api/values/2"); - WaitForActivityExport(exportedItems, 1); + response1.EnsureSuccessStatusCode(); // Status Code 200-299 + response2.EnsureSuccessStatusCode(); // Status Code 200-299 + Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 3)); } - // As InstrumentationFilter threw, we continue as if the - // InstrumentationFilter did not exist. - - Assert.Single(exportedItems); - var activity = exportedItems[0]; - ValidateAspNetCoreActivity(activity, "/api/values"); + WaitForActivityExport(exportedItems, 1); } - [Theory] - [InlineData(SamplingDecision.Drop)] - [InlineData(SamplingDecision.RecordOnly)] - [InlineData(SamplingDecision.RecordAndSample)] - public async Task ExtractContextIrrespectiveOfSamplingDecision(SamplingDecision samplingDecision) + // As InstrumentationFilter threw, we continue as if the + // InstrumentationFilter did not exist. + + Assert.Single(exportedItems); + var activity = exportedItems[0]; + ValidateAspNetCoreActivity(activity, "/api/values"); + } + + [Theory] + [InlineData(SamplingDecision.Drop)] + [InlineData(SamplingDecision.RecordOnly)] + [InlineData(SamplingDecision.RecordAndSample)] + public async Task ExtractContextIrrespectiveOfSamplingDecision(SamplingDecision samplingDecision) + { + try { - try - { - var expectedTraceId = ActivityTraceId.CreateRandom(); - var expectedParentSpanId = ActivitySpanId.CreateRandom(); - var expectedTraceState = "rojo=1,congo=2"; - var activityContext = new ActivityContext(expectedTraceId, expectedParentSpanId, ActivityTraceFlags.Recorded, expectedTraceState, true); - var expectedBaggage = Baggage.SetBaggage("key1", "value1").SetBaggage("key2", "value2"); - Sdk.SetDefaultTextMapPropagator(new ExtractOnlyPropagator(activityContext, expectedBaggage)); - - // Arrange - using var testFactory = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(services => { this.tracerProvider = Sdk.CreateTracerProviderBuilder().SetSampler(new TestSampler(samplingDecision)).AddAspNetCoreInstrumentation().Build(); }); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }); - using var client = testFactory.CreateClient(); + var expectedTraceId = ActivityTraceId.CreateRandom(); + var expectedParentSpanId = ActivitySpanId.CreateRandom(); + var expectedTraceState = "rojo=1,congo=2"; + var activityContext = new ActivityContext(expectedTraceId, expectedParentSpanId, ActivityTraceFlags.Recorded, expectedTraceState, true); + var expectedBaggage = Baggage.SetBaggage("key1", "value1").SetBaggage("key2", "value2"); + Sdk.SetDefaultTextMapPropagator(new ExtractOnlyPropagator(activityContext, expectedBaggage)); + + // Arrange + using var testFactory = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(services => { this.tracerProvider = Sdk.CreateTracerProviderBuilder().SetSampler(new TestSampler(samplingDecision)).AddAspNetCoreInstrumentation().Build(); }); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }); + using var client = testFactory.CreateClient(); - // 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); + // Test TraceContext Propagation + var request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityTraceContext"); + var response = await client.SendAsync(request); + var childActivityTraceContext = JsonSerializer.Deserialize>(await response.Content.ReadAsStringAsync()); - response.EnsureSuccessStatusCode(); + response.EnsureSuccessStatusCode(); - Assert.Equal(expectedTraceId.ToString(), childActivityTraceContext["TraceId"]); - Assert.Equal(expectedTraceState, childActivityTraceContext["TraceState"]); - Assert.NotEqual(expectedParentSpanId.ToString(), childActivityTraceContext["ParentSpanId"]); // there is a new activity created in instrumentation therefore the ParentSpanId is different that what is provided in the headers + Assert.Equal(expectedTraceId.ToString(), childActivityTraceContext["TraceId"]); + Assert.Equal(expectedTraceState, childActivityTraceContext["TraceState"]); + Assert.NotEqual(expectedParentSpanId.ToString(), childActivityTraceContext["ParentSpanId"]); // there is a new activity created in instrumentation therefore the ParentSpanId is different that what is provided in the headers - // Test Baggage Context Propagation - request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityBaggageContext"); + // 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(); + response.EnsureSuccessStatusCode(); - Assert.Single(childActivityBaggageContext, item => item.Key == "key1" && item.Value == "value1"); - Assert.Single(childActivityBaggageContext, item => item.Key == "key2" && item.Value == "value2"); - } - finally + Assert.Single(childActivityBaggageContext, item => item.Key == "key1" && item.Value == "value1"); + Assert.Single(childActivityBaggageContext, item => item.Key == "key2" && item.Value == "value2"); + } + finally + { + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] { - Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] - { - new TraceContextPropagator(), - new BaggagePropagator(), - })); - } + new TraceContextPropagator(), + new BaggagePropagator(), + })); } + } - [Fact] - public async Task ExtractContextIrrespectiveOfTheFilterApplied() + [Fact] + public async Task ExtractContextIrrespectiveOfTheFilterApplied() + { + try { - try - { - var expectedTraceId = ActivityTraceId.CreateRandom(); - var expectedParentSpanId = ActivitySpanId.CreateRandom(); - var expectedTraceState = "rojo=1,congo=2"; - var activityContext = new ActivityContext(expectedTraceId, expectedParentSpanId, ActivityTraceFlags.Recorded, expectedTraceState); - var expectedBaggage = Baggage.SetBaggage("key1", "value1").SetBaggage("key2", "value2"); - Sdk.SetDefaultTextMapPropagator(new ExtractOnlyPropagator(activityContext, expectedBaggage)); - - // Arrange - bool isFilterCalled = false; - using var testFactory = this.factory - .WithWebHostBuilder(builder => + var expectedTraceId = ActivityTraceId.CreateRandom(); + var expectedParentSpanId = ActivitySpanId.CreateRandom(); + var expectedTraceState = "rojo=1,congo=2"; + var activityContext = new ActivityContext(expectedTraceId, expectedParentSpanId, ActivityTraceFlags.Recorded, expectedTraceState); + var expectedBaggage = Baggage.SetBaggage("key1", "value1").SetBaggage("key2", "value2"); + Sdk.SetDefaultTextMapPropagator(new ExtractOnlyPropagator(activityContext, expectedBaggage)); + + // Arrange + bool isFilterCalled = false; + using var testFactory = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(services => { - builder.ConfigureTestServices(services => - { - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation(options => + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation(options => + { + options.Filter = context => { - options.Filter = context => - { - isFilterCalled = true; - return false; - }; - }) - .Build(); - }); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + isFilterCalled = true; + return false; + }; + }) + .Build(); }); - using var client = testFactory.CreateClient(); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }); + using var client = testFactory.CreateClient(); - // Test TraceContext Propagation - var request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityTraceContext"); - var response = await client.SendAsync(request).ConfigureAwait(false); + // Test TraceContext Propagation + var request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityTraceContext"); + var response = await client.SendAsync(request); - // Ensure that filter was called - Assert.True(isFilterCalled); + // 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(); + response.EnsureSuccessStatusCode(); - Assert.Equal(expectedTraceId.ToString(), childActivityTraceContext["TraceId"]); - Assert.Equal(expectedTraceState, childActivityTraceContext["TraceState"]); - Assert.NotEqual(expectedParentSpanId.ToString(), childActivityTraceContext["ParentSpanId"]); // there is a new activity created in instrumentation therefore the ParentSpanId is different that what is provided in the headers + Assert.Equal(expectedTraceId.ToString(), childActivityTraceContext["TraceId"]); + Assert.Equal(expectedTraceState, childActivityTraceContext["TraceState"]); + Assert.NotEqual(expectedParentSpanId.ToString(), childActivityTraceContext["ParentSpanId"]); // there is a new activity created in instrumentation therefore the ParentSpanId is different that what is provided in the headers - // Test Baggage Context Propagation - request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityBaggageContext"); + // 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(); + response.EnsureSuccessStatusCode(); - Assert.Single(childActivityBaggageContext, item => item.Key == "key1" && item.Value == "value1"); - Assert.Single(childActivityBaggageContext, item => item.Key == "key2" && item.Value == "value2"); - } - finally + Assert.Single(childActivityBaggageContext, item => item.Key == "key1" && item.Value == "value1"); + Assert.Single(childActivityBaggageContext, item => item.Key == "key2" && item.Value == "value2"); + } + finally + { + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] { - Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] - { - new TraceContextPropagator(), - new BaggagePropagator(), - })); - } + new TraceContextPropagator(), + new BaggagePropagator(), + })); } + } - [Fact] - public async Task BaggageIsNotClearedWhenActivityStopped() - { - int? baggageCountAfterStart = null; - int? baggageCountAfterStop = null; - using EventWaitHandle stopSignal = new EventWaitHandle(false, EventResetMode.ManualReset); + [Fact] + public async Task BaggageIsNotClearedWhenActivityStopped() + { + int? baggageCountAfterStart = null; + int? baggageCountAfterStop = null; + using EventWaitHandle stopSignal = new EventWaitHandle(false, EventResetMode.ManualReset); - void ConfigureTestServices(IServiceCollection services) - { - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation( - new TestHttpInListener(new AspNetCoreInstrumentationOptions()) + void ConfigureTestServices(IServiceCollection services) + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation( + new TestHttpInListener(new AspNetCoreTraceInstrumentationOptions()) + { + OnEventWrittenCallback = (name, payload) => { - OnEventWrittenCallback = (name, payload) => + switch (name) { - switch (name) - { - case HttpInListener.OnStartEvent: - { - baggageCountAfterStart = Baggage.Current.Count; - } - - break; - case HttpInListener.OnStopEvent: - { - baggageCountAfterStop = Baggage.Current.Count; - stopSignal.Set(); - } - - break; - } - }, - }) - .Build(); - } + case HttpInListener.OnStartEvent: + { + baggageCountAfterStart = Baggage.Current.Count; + } - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(ConfigureTestServices); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }) - .CreateClient()) - { - using var request = new HttpRequestMessage(HttpMethod.Get, "/api/values"); + break; + case HttpInListener.OnStopEvent: + { + baggageCountAfterStop = Baggage.Current.Count; + stopSignal.Set(); + } - request.Headers.TryAddWithoutValidation("baggage", "TestKey1=123,TestKey2=456"); + break; + } + }, + }) + .Build(); + } - // Act - using var response = await client.SendAsync(request).ConfigureAwait(false); - } + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + using var request = new HttpRequestMessage(HttpMethod.Get, "/api/values"); - stopSignal.WaitOne(5000); + request.Headers.TryAddWithoutValidation("baggage", "TestKey1=123,TestKey2=456"); - // Assert - Assert.NotNull(baggageCountAfterStart); - Assert.Equal(2, baggageCountAfterStart); - Assert.NotNull(baggageCountAfterStop); - Assert.Equal(2, baggageCountAfterStop); + // Act + using var response = await client.SendAsync(request); } - [Theory] - [InlineData(SamplingDecision.Drop, false, false)] - [InlineData(SamplingDecision.RecordOnly, true, true)] - [InlineData(SamplingDecision.RecordAndSample, true, true)] - public async Task FilterAndEnrichAreOnlyCalledWhenSampled(SamplingDecision samplingDecision, bool shouldFilterBeCalled, bool shouldEnrichBeCalled) - { - bool filterCalled = false; - bool enrichWithHttpRequestCalled = false; - bool enrichWithHttpResponseCalled = false; - void ConfigureTestServices(IServiceCollection services) - { - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .SetSampler(new TestSampler(samplingDecision)) - .AddAspNetCoreInstrumentation(options => - { - options.Filter = (context) => - { - filterCalled = true; - return true; - }; - options.EnrichWithHttpRequest = (activity, request) => - { - enrichWithHttpRequestCalled = true; - }; - options.EnrichWithHttpResponse = (activity, request) => - { - enrichWithHttpResponseCalled = true; - }; - }) - .Build(); - } + stopSignal.WaitOne(5000); - // Arrange - using var client = this.factory - .WithWebHostBuilder(builder => + // Assert + Assert.NotNull(baggageCountAfterStart); + Assert.Equal(2, baggageCountAfterStart); + Assert.NotNull(baggageCountAfterStop); + Assert.Equal(2, baggageCountAfterStop); + } + + [Theory] + [InlineData(SamplingDecision.Drop, false, false)] + [InlineData(SamplingDecision.RecordOnly, true, true)] + [InlineData(SamplingDecision.RecordAndSample, true, true)] + public async Task FilterAndEnrichAreOnlyCalledWhenSampled(SamplingDecision samplingDecision, bool shouldFilterBeCalled, bool shouldEnrichBeCalled) + { + bool filterCalled = false; + bool enrichWithHttpRequestCalled = false; + bool enrichWithHttpResponseCalled = false; + void ConfigureTestServices(IServiceCollection services) + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(new TestSampler(samplingDecision)) + .AddAspNetCoreInstrumentation(options => { - builder.ConfigureTestServices(ConfigureTestServices); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + options.Filter = (context) => + { + filterCalled = true; + return true; + }; + options.EnrichWithHttpRequest = (activity, request) => + { + enrichWithHttpRequestCalled = true; + }; + options.EnrichWithHttpResponse = (activity, request) => + { + enrichWithHttpResponseCalled = true; + }; }) - .CreateClient(); + .Build(); + } - // Act - using var response = await client.GetAsync("/api/values").ConfigureAwait(false); + // Arrange + using var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient(); + + // Act + using var response = await client.GetAsync("/api/values"); + + // Assert + Assert.Equal(shouldFilterBeCalled, filterCalled); + Assert.Equal(shouldEnrichBeCalled, enrichWithHttpRequestCalled); + Assert.Equal(shouldEnrichBeCalled, enrichWithHttpResponseCalled); + } - // Assert - Assert.Equal(shouldFilterBeCalled, filterCalled); - Assert.Equal(shouldEnrichBeCalled, enrichWithHttpRequestCalled); - Assert.Equal(shouldEnrichBeCalled, enrichWithHttpResponseCalled); + [Fact] + public async Task ActivitiesStartedInMiddlewareShouldNotBeUpdated() + { + var exportedItems = new List(); + + var activitySourceName = "TestMiddlewareActivitySource"; + var activityName = "TestMiddlewareActivity"; + + void ConfigureTestServices(IServiceCollection services) + { + services.AddSingleton(new TestActivityMiddlewareImpl(activitySourceName, activityName)); + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddSource(activitySourceName) + .AddInMemoryExporter(exportedItems) + .Build(); } - [Fact] - public async Task ActivitiesStartedInMiddlewareShouldNotBeUpdated() + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) { - var exportedItems = new List(); + using var response = await client.GetAsync("/api/values/2"); + response.EnsureSuccessStatusCode(); + WaitForActivityExport(exportedItems, 2); + } - var activitySourceName = "TestMiddlewareActivitySource"; - var activityName = "TestMiddlewareActivity"; + Assert.Equal(2, exportedItems.Count); - void ConfigureTestServices(IServiceCollection services) - { - services.AddSingleton(new TestActivityMiddlewareImpl(activitySourceName, activityName)); - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation() - .AddSource(activitySourceName) - .AddInMemoryExporter(exportedItems) - .Build(); - } + var middlewareActivity = exportedItems[0]; - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(ConfigureTestServices); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }) - .CreateClient()) - { - using var response = await client.GetAsync("/api/values/2").ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - WaitForActivityExport(exportedItems, 2); - } + var aspnetcoreframeworkactivity = exportedItems[1]; - Assert.Equal(2, exportedItems.Count); + // Middleware activity name should not be changed + Assert.Equal(ActivityKind.Internal, middlewareActivity.Kind); + Assert.Equal(activityName, middlewareActivity.OperationName); + Assert.Equal(activityName, middlewareActivity.DisplayName); - var middlewareActivity = exportedItems[0]; + // 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); + } - var aspnetcoreframeworkactivity = exportedItems[1]; + [Theory] + [InlineData("CONNECT", "CONNECT")] + [InlineData("DELETE", "DELETE")] + [InlineData("GET", "GET")] + [InlineData("PUT", "PUT")] + [InlineData("HEAD", "HEAD")] + [InlineData("OPTIONS", "OPTIONS")] + [InlineData("PATCH", "PATCH")] + [InlineData("Get", "GET")] + [InlineData("POST", "POST")] + [InlineData("TRACE", "TRACE")] + [InlineData("CUSTOM", "_OTHER")] + public async Task HttpRequestMethodIsSetAsPerSpec(string originalMethod, string expectedMethod) + { + var exportedItems = new List(); - // Middleware activity name should not be changed - Assert.Equal(ActivityKind.Internal, middlewareActivity.Kind); - Assert.Equal(activityName, middlewareActivity.OperationName); - Assert.Equal(activityName, middlewareActivity.DisplayName); + void ConfigureTestServices(IServiceCollection services) + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build(); + } - // tag http.route should be added on activity started by asp.net core - Assert.Equal("api/Values/{id}", aspnetcoreframeworkactivity.GetTagValue(SemanticConventions.AttributeHttpRoute) as string); - Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", aspnetcoreframeworkactivity.OperationName); - Assert.Equal("api/Values/{id}", aspnetcoreframeworkactivity.DisplayName); + // Arrange + using var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient(); + + var message = new HttpRequestMessage(); + + message.Method = new HttpMethod(originalMethod); + + try + { + using var response = await client.SendAsync(message); + response.EnsureSuccessStatusCode(); } + catch + { + // ignore error. + } + + WaitForActivityExport(exportedItems, 1); + + Assert.Single(exportedItems); + + var activity = exportedItems[0]; + + Assert.Contains(activity.TagObjects, t => t.Key == SemanticConventions.AttributeHttpRequestMethod); - [Fact] - public async Task ActivitiesStartedInMiddlewareBySettingHostActivityToNullShouldNotBeUpdated() + if (originalMethod.Equals(expectedMethod, StringComparison.OrdinalIgnoreCase)) { - var exportedItems = new List(); + Assert.DoesNotContain(activity.TagObjects, t => t.Key == SemanticConventions.AttributeHttpRequestMethodOriginal); + } + else + { + Assert.Equal(originalMethod, activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethodOriginal) as string); + } + + Assert.Equal(expectedMethod, activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod) as string); + } - var activitySourceName = "TestMiddlewareActivitySource"; - var activityName = "TestMiddlewareActivity"; + [Fact] + public async Task ActivitiesStartedInMiddlewareBySettingHostActivityToNullShouldNotBeUpdated() + { + var exportedItems = new List(); - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices((IServiceCollection services) => - { - services.AddSingleton(new TestNullHostActivityMiddlewareImpl(activitySourceName, activityName)); - services.AddOpenTelemetry() - .WithTracing(builder => builder - .AddAspNetCoreInstrumentation() - .AddSource(activitySourceName) - .AddInMemoryExporter(exportedItems)); - }); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }) - .CreateClient()) + var activitySourceName = "TestMiddlewareActivitySource"; + var activityName = "TestMiddlewareActivity"; + + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => { - using var response = await client.GetAsync("/api/values/2").ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - WaitForActivityExport(exportedItems, 2); - } + builder.ConfigureTestServices((IServiceCollection services) => + { + services.AddSingleton(new TestNullHostActivityMiddlewareImpl(activitySourceName, activityName)); + services.AddOpenTelemetry() + .WithTracing(builder => builder + .AddAspNetCoreInstrumentation() + .AddSource(activitySourceName) + .AddInMemoryExporter(exportedItems)); + }); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + using var response = await client.GetAsync("/api/values/2"); + response.EnsureSuccessStatusCode(); + WaitForActivityExport(exportedItems, 2); + } - Assert.Equal(2, exportedItems.Count); + Assert.Equal(2, exportedItems.Count); - var middlewareActivity = exportedItems[0]; + var middlewareActivity = exportedItems[0]; - var aspnetcoreframeworkactivity = exportedItems[1]; + var aspnetcoreframeworkactivity = exportedItems[1]; - // Middleware activity name should not be changed - Assert.Equal(ActivityKind.Internal, middlewareActivity.Kind); - Assert.Equal(activityName, middlewareActivity.OperationName); - Assert.Equal(activityName, middlewareActivity.DisplayName); + // Middleware activity name should not be changed + Assert.Equal(ActivityKind.Internal, middlewareActivity.Kind); + 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); - Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", aspnetcoreframeworkactivity.OperationName); - Assert.Equal("/api/values/2", aspnetcoreframeworkactivity.DisplayName); - } + // 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); + } #if NET7_0_OR_GREATER - [Fact] - public async Task UserRegisteredActivitySourceIsUsedForActivityCreationByAspNetCore() + [Fact] + public async Task UserRegisteredActivitySourceIsUsedForActivityCreationByAspNetCore() + { + var exportedItems = new List(); + void ConfigureTestServices(IServiceCollection services) { - var exportedItems = new List(); - void ConfigureTestServices(IServiceCollection services) - { - services.AddOpenTelemetry() - .WithTracing(builder => builder - .AddAspNetCoreInstrumentation() - .AddInMemoryExporter(exportedItems)); - - // Register ActivitySource here so that it will be used - // by ASP.NET Core to create activities - // https://github.com/dotnet/aspnetcore/blob/0e5cbf447d329a1e7d69932c3decd1c70a00fbba/src/Hosting/Hosting/src/Internal/WebHost.cs#L152 - services.AddSingleton(sp => new ActivitySource("UserRegisteredActivitySource")); - } + services.AddOpenTelemetry() + .WithTracing(builder => builder + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems)); - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(ConfigureTestServices); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }) - .CreateClient()) + // Register ActivitySource here so that it will be used + // by ASP.NET Core to create activities + // https://github.com/dotnet/aspnetcore/blob/0e5cbf447d329a1e7d69932c3decd1c70a00fbba/src/Hosting/Hosting/src/Internal/WebHost.cs#L152 + services.AddSingleton(sp => new ActivitySource("UserRegisteredActivitySource")); + } + + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => { - // Act - using var response = await client.GetAsync("/api/values").ConfigureAwait(false); + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + // Act + using var response = await client.GetAsync("/api/values"); - // Assert - response.EnsureSuccessStatusCode(); // Status Code 200-299 + // Assert + response.EnsureSuccessStatusCode(); // Status Code 200-299 - WaitForActivityExport(exportedItems, 1); - } + WaitForActivityExport(exportedItems, 1); + } - Assert.Single(exportedItems); - var activity = exportedItems[0]; + Assert.Single(exportedItems); + var activity = exportedItems[0]; - Assert.Equal("UserRegisteredActivitySource", activity.Source.Name); - } + Assert.Equal("UserRegisteredActivitySource", activity.Source.Name); + } #endif - [Theory] - [InlineData(1)] - [InlineData(2)] - public async Task ShouldExportActivityWithOneOrMoreExceptionFilters(int mode) - { - var exportedItems = new List(); + [Theory] + [InlineData(1)] + [InlineData(2)] + public async Task ShouldExportActivityWithOneOrMoreExceptionFilters(int mode) + { + var exportedItems = new List(); - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices( - (s) => this.ConfigureExceptionFilters(s, mode, ref exportedItems)); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }) - .CreateClient()) + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => { - // Act - using var response = await client.GetAsync("/api/error").ConfigureAwait(false); - - WaitForActivityExport(exportedItems, 1); - } + builder.ConfigureTestServices( + (s) => this.ConfigureExceptionFilters(s, mode, ref exportedItems)); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + // Act + using var response = await client.GetAsync("/api/error"); - // Assert - AssertException(exportedItems); + WaitForActivityExport(exportedItems, 1); } - [Fact] - public async Task DiagnosticSourceCallbacksAreReceivedOnlyForSubscribedEvents() - { - int numberOfUnSubscribedEvents = 0; - int numberofSubscribedEvents = 0; - void ConfigureTestServices(IServiceCollection services) - { - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation( - new TestHttpInListener(new AspNetCoreInstrumentationOptions()) + // Assert + AssertException(exportedItems); + } + + [Fact] + public async Task DiagnosticSourceCallbacksAreReceivedOnlyForSubscribedEvents() + { + int numberOfUnSubscribedEvents = 0; + int numberofSubscribedEvents = 0; + + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation( + new TestHttpInListener(new AspNetCoreTraceInstrumentationOptions()) + { + OnEventWrittenCallback = (name, payload) => + { + switch (name) { - OnEventWrittenCallback = (name, payload) => - { - switch (name) + case HttpInListener.OnStartEvent: { - case HttpInListener.OnStartEvent: - { - numberofSubscribedEvents++; - } - - break; - case HttpInListener.OnStopEvent: - { - numberofSubscribedEvents++; - } - - break; - case HttpInListener.OnMvcBeforeActionEvent: - { - numberofSubscribedEvents++; - } - - break; - default: - { - numberOfUnSubscribedEvents++; - } - - break; + numberofSubscribedEvents++; } - }, - }) - .Build(); - } - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(ConfigureTestServices); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + break; + case HttpInListener.OnStopEvent: + { + numberofSubscribedEvents++; + } + + break; + default: + { + numberOfUnSubscribedEvents++; + } + + break; + } + }, }) - .CreateClient()) - { - using var request = new HttpRequestMessage(HttpMethod.Get, "/api/values"); + .Build(); - // Act - using var response = await client.SendAsync(request).ConfigureAwait(false); - } + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + using var request = new HttpRequestMessage(HttpMethod.Get, "/api/values"); - Assert.Equal(0, numberOfUnSubscribedEvents); - Assert.Equal(3, numberofSubscribedEvents); + // Act + using var response = await client.SendAsync(request); } - [Fact] - public async Task DiagnosticSourceExceptionCallbackIsReceivedForUnHandledException() - { - int numberOfUnSubscribedEvents = 0; - int numberofSubscribedEvents = 0; - int numberOfExceptionCallbacks = 0; - void ConfigureTestServices(IServiceCollection services) - { - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation( - new TestHttpInListener(new AspNetCoreInstrumentationOptions()) + Assert.Equal(0, numberOfUnSubscribedEvents); + Assert.Equal(2, numberofSubscribedEvents); + } + + [Fact] + public async Task DiagnosticSourceExceptionCallbackIsReceivedForUnHandledException() + { + int numberOfUnSubscribedEvents = 0; + int numberofSubscribedEvents = 0; + int numberOfExceptionCallbacks = 0; + + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation( + new TestHttpInListener(new AspNetCoreTraceInstrumentationOptions()) + { + OnEventWrittenCallback = (name, payload) => + { + switch (name) { - OnEventWrittenCallback = (name, payload) => - { - switch (name) + case HttpInListener.OnStartEvent: { - case HttpInListener.OnStartEvent: - { - numberofSubscribedEvents++; - } - - break; - case HttpInListener.OnStopEvent: - { - numberofSubscribedEvents++; - } - - break; - case HttpInListener.OnMvcBeforeActionEvent: - { - numberofSubscribedEvents++; - } - - break; - - // 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; + numberofSubscribedEvents++; } - }, - }) - .Build(); - } - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(ConfigureTestServices); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + break; + case HttpInListener.OnStopEvent: + { + numberofSubscribedEvents++; + } + + break; + + // 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; + } + }, }) - .CreateClient()) + .Build(); + + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => { - try - { - using var request = new HttpRequestMessage(HttpMethod.Get, "/api/error"); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + try + { + using var request = new HttpRequestMessage(HttpMethod.Get, "/api/error"); - // Act - using var response = await client.SendAsync(request).ConfigureAwait(false); - } - catch - { - // ignore exception - } + // Act + using var response = await client.SendAsync(request); + } + catch + { + // ignore exception } - - Assert.Equal(1, numberOfExceptionCallbacks); - Assert.Equal(0, numberOfUnSubscribedEvents); - Assert.Equal(4, numberofSubscribedEvents); } - [Fact] - public async Task DiagnosticSourceExceptionCallBackIsNotReceivedForExceptionsHandledInMiddleware() - { - int numberOfUnSubscribedEvents = 0; - int numberofSubscribedEvents = 0; - int numberOfExceptionCallbacks = 0; + Assert.Equal(1, numberOfExceptionCallbacks); + Assert.Equal(0, numberOfUnSubscribedEvents); + Assert.Equal(3, numberofSubscribedEvents); + } - // configure SDK - using var tracerprovider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation( - new TestHttpInListener(new AspNetCoreInstrumentationOptions()) + [Fact] + public async Task DiagnosticSourceExceptionCallBackIsNotReceivedForExceptionsHandledInMiddleware() + { + int numberOfUnSubscribedEvents = 0; + int numberOfSubscribedEvents = 0; + int numberOfExceptionCallbacks = 0; + bool exceptionHandled = false; + + // configure SDK + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation( + new TestHttpInListener(new AspNetCoreTraceInstrumentationOptions()) + { + OnEventWrittenCallback = (name, payload) => { - OnEventWrittenCallback = (name, payload) => + switch (name) { - switch (name) - { - case HttpInListener.OnStartEvent: - { - numberofSubscribedEvents++; - } - - break; - case HttpInListener.OnStopEvent: - { - numberofSubscribedEvents++; - } + case HttpInListener.OnStartEvent: + { + numberOfSubscribedEvents++; + } - break; + break; + case HttpInListener.OnStopEvent: + { + numberOfSubscribedEvents++; + } - // TODO: Add test case for validating name for both the types - // of exception event. - case HttpInListener.OnUnhandledHostingExceptionEvent: - case HttpInListener.OnUnHandledDiagnosticsExceptionEvent: - { - numberofSubscribedEvents++; - numberOfExceptionCallbacks++; - } + break; - break; - default: - { - numberOfUnSubscribedEvents++; - } + // TODO: Add test case for validating name for both the types + // of exception event. + case HttpInListener.OnUnhandledHostingExceptionEvent: + case HttpInListener.OnUnHandledDiagnosticsExceptionEvent: + { + numberOfSubscribedEvents++; + numberOfExceptionCallbacks++; + } - break; - } - }, - }) - .Build(); + break; + default: + { + numberOfUnSubscribedEvents++; + } - var builder = WebApplication.CreateBuilder(); - builder.Logging.ClearProviders(); - var app = builder.Build(); + break; + } + }, + }) + .Build(); - app.UseExceptionHandler(handler => - { + TestMiddleware.Create(builder => builder + .UseExceptionHandler(handler => handler.Run(async (ctx) => { - await ctx.Response.WriteAsync("handled").ConfigureAwait(false); - }); - }); - - app.Map("/error", ThrowException); + exceptionHandled = true; + await ctx.Response.WriteAsync("handled"); + }))); - static void ThrowException(IApplicationBuilder app) + using (var client = this.factory + .WithWebHostBuilder(builder => { - app.Run(context => - { - throw new Exception("CustomException"); - }); - } - - _ = app.RunAsync(); - - using var client = new HttpClient(); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { try { - await client.GetStringAsync("http://localhost:5000/error").ConfigureAwait(false); + using var request = new HttpRequestMessage(HttpMethod.Get, "/api/error"); + using var response = await client.SendAsync(request); } catch { - // ignore 500 error. + // ignore exception } - - 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(); + Assert.Equal(0, numberOfExceptionCallbacks); + Assert.Equal(0, numberOfUnSubscribedEvents); + Assert.Equal(2, numberOfSubscribedEvents); + Assert.True(exceptionHandled); + } - using var client = new HttpClient(); - var res = await client.GetStringAsync("http://localhost:5000/custom/abc").ConfigureAwait(false); - Assert.NotNull(res); +#if NET6_0_OR_GREATER + [Fact] + public async Task NoSiblingActivityCreatedWhenTraceFlagsNone() + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddAspNetCoreInstrumentation() + .Build(); - tracerprovider.ForceFlush(); - for (var i = 0; i < 10; i++) + using var testFactory = this.factory + .WithWebHostBuilder(builder => { - if (exportedItems.Count > 0) + builder.ConfigureTestServices(services => { - 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); - } - - var activity = exportedItems[0]; + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .Build(); + }); - Assert.NotNull(activity); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }); + using var client = testFactory.CreateClient(); + var request = new HttpRequestMessage(HttpMethod.Get, "/api/GetActivityEquality"); + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + request.Headers.Add("traceparent", $"00-{traceId}-{spanId}-00"); - // After fix update to Contains http.route - Assert.DoesNotContain(activity.TagObjects, t => t.Key == SemanticConventions.AttributeHttpRoute); - Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", activity.OperationName); + var response = await client.SendAsync(request); + var result = bool.Parse(await response.Content.ReadAsStringAsync()); - // After fix this should be /custom/{name:alpha} - Assert.Equal("/custom/abc", activity.DisplayName); + Assert.True(response.IsSuccessStatusCode); - await app.DisposeAsync().ConfigureAwait(false); - } + // Confirm that Activity.Current and IHttpActivityFeature activity are same + Assert.True(result); + } +#endif - public void Dispose() - { - this.tracerProvider?.Dispose(); - } + public void Dispose() + { + this.tracerProvider?.Dispose(); + } - private static void WaitForActivityExport(List exportedItems, int count) - { - // 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 - Assert.True(SpinWait.SpinUntil( - () => - { - Thread.Sleep(10); - return exportedItems.Count >= count; - }, - TimeSpan.FromSeconds(1))); - } + private static void WaitForActivityExport(List exportedItems, int count) + { + // 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 + Assert.True(SpinWait.SpinUntil( + () => + { + Thread.Sleep(10); + return exportedItems.Count >= count; + }, + TimeSpan.FromSeconds(1))); + } - private static void ValidateAspNetCoreActivity(Activity activityToValidate, string expectedHttpPath) - { - Assert.Equal(ActivityKind.Server, activityToValidate.Kind); + private static void ValidateAspNetCoreActivity(Activity activityToValidate, string expectedHttpPath) + { + Assert.Equal(ActivityKind.Server, activityToValidate.Kind); #if NET7_0_OR_GREATER - Assert.Equal(HttpInListener.AspNetCoreActivitySourceName, activityToValidate.Source.Name); - Assert.Empty(activityToValidate.Source.Version); + Assert.Equal(HttpInListener.AspNetCoreActivitySourceName, activityToValidate.Source.Name); + Assert.Empty(activityToValidate.Source.Version); #else - Assert.Equal(HttpInListener.ActivitySourceName, activityToValidate.Source.Name); - Assert.Equal(HttpInListener.Version.ToString(), activityToValidate.Source.Version); + 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) - { - Assert.Single(exportedItems); - var activity = exportedItems[0]; + private static void AssertException(List exportedItems) + { + Assert.Single(exportedItems); + var activity = exportedItems[0]; - var exMessage = "something's wrong!"; - Assert.Single(activity.Events); - Assert.Equal("System.Exception", activity.Events.First().Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionType).Value); - Assert.Equal(exMessage, activity.Events.First().Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionMessage).Value); + var exMessage = "something's wrong!"; + Assert.Single(activity.Events); + Assert.Equal("System.Exception", activity.Events.First().Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionType).Value); + Assert.Equal(exMessage, activity.Events.First().Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionMessage).Value); - ValidateAspNetCoreActivity(activity, "/api/error"); - } + ValidateAspNetCoreActivity(activity, "/api/error"); + } - private void ConfigureExceptionFilters(IServiceCollection services, int mode, ref List exportedItems) + private void ConfigureExceptionFilters(IServiceCollection services, int mode, ref List exportedItems) + { + switch (mode) { - switch (mode) - { - case 1: - services.AddMvc(x => x.Filters.Add()); - break; - case 2: - services.AddMvc(x => x.Filters.Add()); - services.AddMvc(x => x.Filters.Add()); - break; - default: - break; - } - - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation(x => x.RecordException = true) - .AddInMemoryExporter(exportedItems) - .Build(); + case 1: + services.AddMvc(x => x.Filters.Add()); + break; + case 2: + services.AddMvc(x => x.Filters.Add()); + services.AddMvc(x => x.Filters.Add()); + break; + default: + break; } - private class ExtractOnlyPropagator : TextMapPropagator - { - private readonly ActivityContext activityContext; - private readonly Baggage baggage; - - public ExtractOnlyPropagator(ActivityContext activityContext, Baggage baggage) - { - this.activityContext = activityContext; - this.baggage = baggage; - } + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation(x => x.RecordException = true) + .AddInMemoryExporter(exportedItems) + .Build(); + } - public override ISet Fields => throw new NotImplementedException(); + private class ExtractOnlyPropagator(ActivityContext activityContext, Baggage baggage) : TextMapPropagator + { + private readonly ActivityContext activityContext = activityContext; + private readonly Baggage baggage = baggage; - public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) - { - return new PropagationContext(this.activityContext, this.baggage); - } + public override ISet Fields => throw new NotImplementedException(); - public override void Inject(PropagationContext context, T carrier, Action setter) - { - throw new NotImplementedException(); - } + public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) + { + return new PropagationContext(this.activityContext, this.baggage); } - private class TestSampler : Sampler + public override void Inject(PropagationContext context, T carrier, Action setter) { - private readonly SamplingDecision samplingDecision; + throw new NotImplementedException(); + } + } - public TestSampler(SamplingDecision samplingDecision) - { - this.samplingDecision = samplingDecision; - } + private class TestSampler(SamplingDecision samplingDecision, IEnumerable> attributes = null) : Sampler + { + private readonly SamplingDecision samplingDecision = samplingDecision; + private readonly IEnumerable> attributes = attributes; - public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) - { - return new SamplingResult(this.samplingDecision); - } + public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) + { + return new SamplingResult(this.samplingDecision, this.attributes); } + } - private class TestHttpInListener : HttpInListener + private class TestHttpInListener(AspNetCoreTraceInstrumentationOptions options) : HttpInListener(options) + { + public Action OnEventWrittenCallback; + + public override void OnEventWritten(string name, object payload) { - public Action OnEventWrittenCallback; + base.OnEventWritten(name, payload); - public TestHttpInListener(AspNetCoreInstrumentationOptions options) - : base(options) - { - } + this.OnEventWrittenCallback?.Invoke(name, payload); + } + } - public override void OnEventWritten(string name, object payload) - { - base.OnEventWritten(name, payload); + private class TestNullHostActivityMiddlewareImpl(string activitySourceName, string activityName) : ActivityMiddleware.ActivityMiddlewareImpl + { + private readonly ActivitySource activitySource = new(activitySourceName); + private readonly string activityName = activityName; + private Activity activity; - this.OnEventWrittenCallback?.Invoke(name, payload); - } + public override void PreProcess(HttpContext context) + { + // Setting the host activity i.e. activity started by asp.net core + // to null here will have no impact on middleware activity. + // This also means that asp.net core activity will not be found + // during OnEventWritten event. + Activity.Current = null; + this.activity = this.activitySource.StartActivity(this.activityName); } - private class TestNullHostActivityMiddlewareImpl : ActivityMiddleware.ActivityMiddlewareImpl + public override void PostProcess(HttpContext context) { - private ActivitySource activitySource; - private Activity activity; - private string activityName; - - public TestNullHostActivityMiddlewareImpl(string activitySourceName, string activityName) - { - this.activitySource = new ActivitySource(activitySourceName); - this.activityName = activityName; - } + this.activity?.Stop(); + } + } - public override void PreProcess(HttpContext context) - { - // Setting the host activity i.e. activity started by asp.net core - // to null here will have no impact on middleware activity. - // This also means that asp.net core activity will not be found - // during OnEventWritten event. - Activity.Current = null; - this.activity = this.activitySource.StartActivity(this.activityName); - } + private class TestActivityMiddlewareImpl(string activitySourceName, string activityName) : ActivityMiddleware.ActivityMiddlewareImpl + { + private readonly ActivitySource activitySource = new(activitySourceName); + private readonly string activityName = activityName; + private Activity activity; - public override void PostProcess(HttpContext context) - { - this.activity?.Stop(); - } + public override void PreProcess(HttpContext context) + { + this.activity = this.activitySource.StartActivity(this.activityName); } - private class TestActivityMiddlewareImpl : ActivityMiddleware.ActivityMiddlewareImpl + public override void PostProcess(HttpContext context) { - private ActivitySource activitySource; - 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) - { - this.activity = this.activitySource.StartActivity(this.activityName); - } - - public override void PostProcess(HttpContext context) - { - this.activity?.Stop(); - } + this.activity?.Stop(); } } } diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs index 3631ca9a1ef..766a81b40dd 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; @@ -22,78 +9,46 @@ using OpenTelemetry.Trace; using Xunit; -namespace OpenTelemetry.Instrumentation.AspNetCore.Tests +namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; + +public class DependencyInjectionConfigTests + : IClassFixture> { - public class DependencyInjectionConfigTests - : IClassFixture> + private readonly WebApplicationFactory factory; + + public DependencyInjectionConfigTests(WebApplicationFactory factory) { - private readonly WebApplicationFactory factory; + this.factory = factory; + } - public DependencyInjectionConfigTests(WebApplicationFactory factory) - { - this.factory = factory; - } + [Theory] + [InlineData(null)] + [InlineData("CustomName")] + public void TestTracingOptionsDIConfig(string name) + { + name ??= Options.DefaultName; - [Theory] - [InlineData(null)] - [InlineData("CustomName")] - public void TestTracingOptionsDIConfig(string name) + bool optionsPickedFromDI = false; + void ConfigureTestServices(IServiceCollection services) { - name ??= Options.DefaultName; - - bool optionsPickedFromDI = false; - void ConfigureTestServices(IServiceCollection services) - { - services.AddOpenTelemetry() - .WithTracing(builder => builder - .AddAspNetCoreInstrumentation(name, configureAspNetCoreInstrumentationOptions: null)); - - services.Configure(name, options => - { - optionsPickedFromDI = true; - }); - } + services.AddOpenTelemetry() + .WithTracing(builder => builder + .AddAspNetCoreInstrumentation(name, configureAspNetCoreTraceInstrumentationOptions: null)); - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - builder.ConfigureTestServices(ConfigureTestServices)) - .CreateClient()) + services.Configure(name, options => { - } - - Assert.True(optionsPickedFromDI); + optionsPickedFromDI = true; + }); } - [Theory] - [InlineData(null)] - [InlineData("CustomName")] - public void TestMetricsOptionsDIConfig(string name) + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => + builder.ConfigureTestServices(ConfigureTestServices)) + .CreateClient()) { - name ??= Options.DefaultName; - - bool optionsPickedFromDI = false; - void ConfigureTestServices(IServiceCollection services) - { - services.AddOpenTelemetry() - .WithMetrics(builder => builder - .AddAspNetCoreInstrumentation(name, configureAspNetCoreInstrumentationOptions: null)); - - services.Configure(name, options => - { - optionsPickedFromDI = true; - }); - } - - // Arrange - using (var client = this.factory - .WithWebHostBuilder(builder => - builder.ConfigureTestServices(ConfigureTestServices)) - .CreateClient()) - { - } - - Assert.True(optionsPickedFromDI); } + + Assert.True(optionsPickedFromDI); } } diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/EventSourceTest.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/EventSourceTest.cs index e347a9b63d0..5bae1e25f4d 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/EventSourceTest.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/EventSourceTest.cs @@ -1,31 +1,17 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Instrumentation.AspNetCore.Implementation; using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Instrumentation.AspNetCore.Tests +namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; + +public class EventSourceTest { - public class EventSourceTest + [Fact] + public void EventSourceTest_AspNetCoreInstrumentationEventSource() { - [Fact] - public void EventSourceTest_AspNetCoreInstrumentationEventSource() - { - EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(AspNetCoreInstrumentationEventSource.Log); - } + EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(AspNetCoreInstrumentationEventSource.Log); } } diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/InProcServerTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/InProcServerTests.cs deleted file mode 100644 index 74c0cced5d2..00000000000 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/InProcServerTests.cs +++ /dev/null @@ -1,89 +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. -// - -#if NET6_0_OR_GREATER -using System.Diagnostics; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Logging; -using OpenTelemetry.Trace; -using Xunit; - -namespace OpenTelemetry.Instrumentation.AspNetCore.Tests -{ - public sealed class InProcServerTests : IDisposable - { - private TracerProvider tracerProvider; - private WebApplication app; - private HttpClient client; - private List exportedItems; - - public InProcServerTests() - { - this.exportedItems = new List(); - var builder = WebApplication.CreateBuilder(); - builder.Logging.ClearProviders(); - var app = builder.Build(); - - this.tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation() - .AddInMemoryExporter(this.exportedItems).Build(); - app.MapGet("/", () => "Hello World!"); - app.RunAsync(); - - this.app = app; - this.client = new HttpClient(); - } - - [Fact] - public async void ExampleTest() - { - var res = await this.client.GetStringAsync("http://localhost:5000").ConfigureAwait(false); - Assert.NotNull(res); - - this.tracerProvider.ForceFlush(); - for (var i = 0; i < 10; i++) - { - if (this.exportedItems.Count > 0) - { - 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); - } - - var activity = this.exportedItems[0]; - Assert.Equal(ActivityKind.Server, activity.Kind); - Assert.Equal("localhost", activity.GetTagValue(SemanticConventions.AttributeNetHostName)); - Assert.Equal(5000, activity.GetTagValue(SemanticConventions.AttributeNetHostPort)); - Assert.Equal("GET", activity.GetTagValue(SemanticConventions.AttributeHttpMethod)); - Assert.Equal("1.1", activity.GetTagValue(SemanticConventions.AttributeHttpFlavor)); - Assert.Equal(200, activity.GetTagValue(SemanticConventions.AttributeHttpStatusCode)); - Assert.True(activity.Status == ActivityStatusCode.Unset); - Assert.True(activity.StatusDescription is null); - } - - public async void Dispose() - { - this.tracerProvider.Dispose(); - this.client.Dispose(); - await this.app.DisposeAsync().ConfigureAwait(false); - } - } -} -#endif diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs index af9e63e2677..ee2195c6a4c 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using Microsoft.AspNetCore.Hosting; @@ -26,172 +13,156 @@ using TestApp.AspNetCore; using Xunit; -namespace OpenTelemetry.Instrumentation.AspNetCore.Tests +namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; + +public class IncomingRequestsCollectionsIsAccordingToTheSpecTests + : IClassFixture> { - public class IncomingRequestsCollectionsIsAccordingToTheSpecTests - : IClassFixture> + private readonly WebApplicationFactory factory; + + public IncomingRequestsCollectionsIsAccordingToTheSpecTests(WebApplicationFactory factory) { - private readonly WebApplicationFactory factory; + this.factory = factory; + } - public IncomingRequestsCollectionsIsAccordingToTheSpecTests(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) + { + var exportedItems = new List(); - [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_Old( - string urlPath, - string query, - string userAgent, - int statusCode, - string reasonPhrase, - bool recordException = false) + // 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 { - Environment.SetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN", "none"); - - 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()) + if (!string.IsNullOrEmpty(userAgent)) { - 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); - } + client.DefaultRequestHeaders.Add("User-Agent", userAgent); } - 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) + // Act + var path = urlPath; + if (query != null) { - Assert.Equal(ActivityStatusCode.Error, activity.Status); - } - else - { - Assert.Equal(ActivityStatusCode.Unset, activity.Status); + path += query; } - // Instrumentation is not expected to set status description - // as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode - if (!urlPath.EndsWith("exception")) - { - Assert.True(string.IsNullOrEmpty(activity.StatusDescription)); - } - else - { - Assert.Equal("exception description", activity.StatusDescription); - } + using var response = await client.GetAsync(path); + } + catch (Exception) + { + // ignore errors + } - if (recordException) + for (var i = 0; i < 10; i++) + { + if (exportedItems.Count == 1) { - Assert.Single(activity.Events); - Assert.Equal("exception", activity.Events.First().Name); + break; } - ValidateTagValue(activity, SemanticConventions.AttributeHttpUserAgent, userAgent); - - activity.Dispose(); - } - finally - { - Environment.SetEnvironmentVariable("OTEL_SEMCONV_STABILITY_OPT_IN", null); + // 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)); } } - private static void ValidateTagValue(Activity activity, string attribute, string expectedValue) + 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) { - if (string.IsNullOrEmpty(expectedValue)) - { - Assert.Null(activity.GetTagValue(attribute)); - } - else - { - Assert.Equal(expectedValue, activity.GetTagValue(attribute)); - } + Assert.Equal(ActivityStatusCode.Error, activity.Status); + Assert.Equal("System.Exception", activity.GetTagValue(SemanticConventions.AttributeErrorType)); + } + else + { + Assert.Equal(ActivityStatusCode.Unset, activity.Status); } - public class TestCallbackMiddlewareImpl : CallbackMiddleware.CallbackMiddlewareImpl + // Instrumentation is not expected to set status description + // as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode + Assert.Null(activity.StatusDescription); + + if (recordException) { - private readonly int statusCode; - private readonly string reasonPhrase; + Assert.Single(activity.Events); + Assert.Equal("exception", activity.Events.First().Name); + } - public TestCallbackMiddlewareImpl(int statusCode, string reasonPhrase) - { - this.statusCode = statusCode; - this.reasonPhrase = reasonPhrase; - } + ValidateTagValue(activity, SemanticConventions.AttributeUserAgentOriginal, userAgent); - 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); + activity.Dispose(); + } - if (context.Request.Path.Value.EndsWith("exception")) - { - throw new Exception("exception description"); - } + 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)); + } + } - return false; + 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"); + + if (context.Request.Path.Value.EndsWith("exception")) + { + throw new Exception("exception description"); } + + return false; } } } 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 01e3cb8ce93..00000000000 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests_Dupe.cs +++ /dev/null @@ -1,204 +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 - if (!urlPath.EndsWith("exception")) - { - Assert.True(string.IsNullOrEmpty(activity.StatusDescription)); - } - else - { - Assert.Equal("exception description", 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 47895a14754..00000000000 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests_New.cs +++ /dev/null @@ -1,197 +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", 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_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); - } - else - { - Assert.Equal(ActivityStatusCode.Unset, activity.Status); - } - - // Instrumentation is not expected to set status description - // as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode - if (!urlPath.EndsWith("exception")) - { - Assert.True(string.IsNullOrEmpty(activity.StatusDescription)); - } - else - { - Assert.Equal("exception description", 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 ac7f6610e24..ddec198dc4c 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs @@ -1,250 +1,419 @@ -// // 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; +// SPDX-License-Identifier: Apache-2.0 + +#if NET8_0_OR_GREATER +using System.Threading.RateLimiting; +using Microsoft.AspNetCore.Builder; +#endif using Microsoft.AspNetCore.Hosting; +#if NET8_0_OR_GREATER using Microsoft.AspNetCore.Http; +#endif using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.AspNetCore.TestHost; +#if NET8_0_OR_GREATER +using Microsoft.AspNetCore.RateLimiting; +#endif +#if NET8_0_OR_GREATER using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +#endif using Microsoft.Extensions.Logging; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; using Xunit; -namespace OpenTelemetry.Instrumentation.AspNetCore.Tests -{ - public class MetricTests +namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; + +public class MetricTests(WebApplicationFactory factory) : IClassFixture>, IDisposable +{ + private const int StandardTagsCount = 6; + + private readonly WebApplicationFactory factory = factory; + private MeterProvider meterProvider; + + [Fact] + public void AddAspNetCoreInstrumentation_BadArgs() { - private const int StandardTagsCount = 6; + MeterProviderBuilder builder = null; + Assert.Throws(builder.AddAspNetCoreInstrumentation); + } - private readonly WebApplicationFactory factory; - private MeterProvider meterProvider; +#if NET8_0_OR_GREATER + [Fact] + public async Task ValidateNet8MetricsAsync() + { + var exportedItems = new List(); + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build(); - public MetricTests(WebApplicationFactory factory) - { - this.factory = factory; - } + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseUrls("http://*:0"); + var app = builder.Build(); - [Fact] - public void AddAspNetCoreInstrumentation_BadArgs() - { - MeterProviderBuilder builder = null; - Assert.Throws(() => builder.AddAspNetCoreInstrumentation()); - } + app.MapGet("/", () => "Hello"); - [Fact] - public async Task RequestMetricIsCaptured() - { - var metricItems = new List(); + _ = app.RunAsync(); + + var url = app.Urls.ToArray()[0]; + var portNumber = url.Substring(url.LastIndexOf(':') + 1); + + using var client = new HttpClient(); + 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)); + + this.meterProvider.Dispose(); + + var requestDurationMetric = exportedItems + .Count(item => item.Name == "http.server.request.duration"); + + var activeRequestsMetric = exportedItems. + Count(item => item.Name == "http.server.active_requests"); + + var routeMatchingMetric = exportedItems. + Count(item => item.Name == "aspnetcore.routing.match_attempts"); + + var kestrelActiveConnectionsMetric = exportedItems. + Count(item => item.Name == "kestrel.active_connections"); + + var kestrelQueuedConnectionMetric = exportedItems. + Count(item => item.Name == "kestrel.queued_connections"); + + Assert.Equal(1, requestDurationMetric); + Assert.Equal(1, activeRequestsMetric); + Assert.Equal(1, routeMatchingMetric); + Assert.Equal(1, kestrelActiveConnectionsMetric); + Assert.Equal(1, kestrelQueuedConnectionMetric); + + // TODO + // kestrel.queued_requests + // kestrel.upgraded_connections + // kestrel.rejected_connections + // kestrel.tls_handshake.duration + // kestrel.active_tls_handshakes + await app.DisposeAsync(); + } + + [Fact] + public async Task ValidateNet8RateLimitingMetricsAsync() + { + var exportedItems = new List(); + + void ConfigureTestServices(IServiceCollection services) + { this.meterProvider = Sdk.CreateMeterProviderBuilder() .AddAspNetCoreInstrumentation() - .AddInMemoryExporter(metricItems) + .AddInMemoryExporter(exportedItems) .Build(); - using (var client = this.factory - .WithWebHostBuilder(builder => + services.AddRateLimiter(_ => _ + .AddFixedWindowLimiter(policyName: "fixed", options => { - 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); + options.PermitLimit = 4; + options.Window = TimeSpan.FromSeconds(12); + options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; + options.QueueLimit = 2; + })); + } - response1.EnsureSuccessStatusCode(); - response2.EnsureSuccessStatusCode(); - } + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseUrls("http://*:0"); + ConfigureTestServices(builder.Services); - // 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); + builder.Logging.ClearProviders(); + var app = builder.Build(); - this.meterProvider.Dispose(); + app.UseRateLimiter(); - var requestMetrics = metricItems - .Where(item => item.Name == "http.server.duration") - .ToArray(); + static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000"); - var metric = Assert.Single(requestMetrics); - var metricPoints = GetMetricPoints(metric); - Assert.Equal(2, metricPoints.Count); + app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}")) + .RequireRateLimiting("fixed"); - AssertMetricPoint(metricPoints[0], expectedRoute: "api/Values"); - AssertMetricPoint(metricPoints[1], expectedRoute: "api/Values/{id}"); - } + _ = app.RunAsync(); - [Fact] - public async Task MetricNotCollectedWhenFilterIsApplied() - { - var metricItems = new List(); + var url = app.Urls.ToArray()[0]; + var portNumber = url.Substring(url.LastIndexOf(':') + 1); - void ConfigureTestServices(IServiceCollection services) - { - this.meterProvider = Sdk.CreateMeterProviderBuilder() - .AddAspNetCoreInstrumentation(opt => opt.Filter = (name, ctx) => ctx.Request.Path != "/api/values/2") - .AddInMemoryExporter(metricItems) - .Build(); - } + using var client = new HttpClient(); + var res = await client.GetAsync($"http://localhost:{portNumber}/"); + Assert.NotNull(res); - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(ConfigureTestServices); - 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); + // 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)); - response1.EnsureSuccessStatusCode(); - response2.EnsureSuccessStatusCode(); - } + this.meterProvider.Dispose(); - // 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 activeRequestLeasesMetric = exportedItems + .Where(item => item.Name == "aspnetcore.rate_limiting.active_request_leases") + .ToArray(); - this.meterProvider.Dispose(); + var requestLeaseDurationMetric = exportedItems. + Where(item => item.Name == "aspnetcore.rate_limiting.request_lease.duration") + .ToArray(); - var requestMetrics = metricItems - .Where(item => item.Name == "http.server.duration") - .ToArray(); + var limitingRequestsMetric = exportedItems. + Where(item => item.Name == "aspnetcore.rate_limiting.requests") + .ToArray(); - var metric = Assert.Single(requestMetrics); + Assert.Single(activeRequestLeasesMetric); + Assert.Single(requestLeaseDurationMetric); + Assert.Single(limitingRequestsMetric); - // Assert single because we filtered out one route - var metricPoint = Assert.Single(GetMetricPoints(metric)); - AssertMetricPoint(metricPoint); - } + // TODO + // aspnetcore.rate_limiting.request.time_in_queue + // aspnetcore.rate_limiting.queued_requests - [Fact] - public async Task MetricEnrichedWithCustomTags() - { - var tagsToAdd = new KeyValuePair[] - { - new("custom_tag_1", 1), - new("custom_tag_2", "one"), - }; + await app.DisposeAsync(); + } +#endif - var metricItems = new List(); + [Theory] + [InlineData("/api/values/2", "api/Values/{id}", null, 200)] + [InlineData("/api/Error", "api/Error", "System.Exception", 500)] + public async Task RequestMetricIsCaptured(string api, string expectedRoute, string expectedErrorType, int expectedStatusCode) + { + var metricItems = new List(); - void ConfigureTestServices(IServiceCollection services) - { - this.meterProvider = Sdk.CreateMeterProviderBuilder() - .AddAspNetCoreInstrumentation(opt => opt.Enrich = (string _, HttpContext _, ref TagList tags) => - { - foreach (var keyValuePair in tagsToAdd) - { - tags.Add(keyValuePair); - } - }) - .AddInMemoryExporter(metricItems) - .Build(); - } + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(metricItems) + .Build(); - using (var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(ConfigureTestServices); - builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); - }) - .CreateClient()) + using (var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + try { - using var response = await client.GetAsync("/api/values").ConfigureAwait(false); + using var response = await client.GetAsync(api); response.EnsureSuccessStatusCode(); } + catch + { + // ignore error. + } + } + + // 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)); + + this.meterProvider.Dispose(); + + var requestMetrics = metricItems + .Where(item => item.Name == "http.server.request.duration") + .ToArray(); + + var metric = Assert.Single(requestMetrics); + + Assert.Equal("s", metric.Unit); + var metricPoints = GetMetricPoints(metric); + Assert.Single(metricPoints); + + AssertMetricPoints( + metricPoints: metricPoints, + expectedRoutes: new List { expectedRoute }, + expectedErrorType, + expectedStatusCode, + expectedTagsCount: expectedErrorType == null ? 5 : 6); + } + + [Theory] + [InlineData("CONNECT", "CONNECT")] + [InlineData("DELETE", "DELETE")] + [InlineData("GET", "GET")] + [InlineData("PUT", "PUT")] + [InlineData("HEAD", "HEAD")] + [InlineData("OPTIONS", "OPTIONS")] + [InlineData("PATCH", "PATCH")] + [InlineData("Get", "GET")] + [InlineData("POST", "POST")] + [InlineData("TRACE", "TRACE")] + [InlineData("CUSTOM", "_OTHER")] + public async Task HttpRequestMethodIsCapturedAsPerSpec(string originalMethod, string expectedMethod) + { + var metricItems = new List(); + + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(metricItems) + .Build(); + + using var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient(); - // 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 message = new HttpRequestMessage(); + message.Method = new HttpMethod(originalMethod); - this.meterProvider.Dispose(); + try + { + using var response = await client.SendAsync(message); + } + catch + { + // ignore error. + } + + // 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)); + + this.meterProvider.Dispose(); + + var requestMetrics = metricItems + .Where(item => item.Name == "http.server.request.duration") + .ToArray(); - var requestMetrics = metricItems - .Where(item => item.Name == "http.server.duration") - .ToArray(); + var metric = Assert.Single(requestMetrics); - var metric = Assert.Single(requestMetrics); - var metricPoint = Assert.Single(GetMetricPoints(metric)); + Assert.Equal("s", metric.Unit); + var metricPoints = GetMetricPoints(metric); + Assert.Single(metricPoints); - var tags = AssertMetricPoint(metricPoint, expectedTagsCount: StandardTagsCount + 2); + var mp = metricPoints[0]; - Assert.Contains(tagsToAdd[0], tags); - Assert.Contains(tagsToAdd[1], tags); + // Inspect Metric Attributes + var attributes = new Dictionary(); + foreach (var tag in mp.Tags) + { + attributes[tag.Key] = tag.Value; } - public void Dispose() + Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpRequestMethod && kvp.Value.ToString() == expectedMethod); + + Assert.DoesNotContain(attributes, t => t.Key == SemanticConventions.AttributeHttpRequestMethodOriginal); + } + + public void Dispose() + { + this.meterProvider?.Dispose(); + GC.SuppressFinalize(this); + } + + private static List GetMetricPoints(Metric metric) + { + Assert.NotNull(metric); + Assert.True(metric.MetricType == MetricType.Histogram); + var metricPoints = new List(); + foreach (var p in metric.GetMetricPoints()) { - this.meterProvider?.Dispose(); - GC.SuppressFinalize(this); + metricPoints.Add(p); } - private static List GetMetricPoints(Metric metric) + return metricPoints; + } + + private static void AssertMetricPoints( + List metricPoints, + List expectedRoutes, + string expectedErrorType, + int expectedStatusCode, + int expectedTagsCount) + { + // Assert that one MetricPoint exists for each ExpectedRoute + foreach (var expectedRoute in expectedRoutes) { - Assert.NotNull(metric); - Assert.True(metric.MetricType == MetricType.Histogram); - var metricPoints = new List(); - foreach (var p in metric.GetMetricPoints()) + MetricPoint? metricPoint = null; + + foreach (var mp in metricPoints) { - metricPoints.Add(p); + foreach (var tag in mp.Tags) + { + if (tag.Key == SemanticConventions.AttributeHttpRoute && tag.Value.ToString() == expectedRoute) + { + metricPoint = mp; + } + } } - return metricPoints; + if (metricPoint.HasValue) + { + AssertMetricPoint(metricPoint.Value, expectedStatusCode, expectedRoute, expectedErrorType, expectedTagsCount); + } + else + { + Assert.Fail($"A metric for route '{expectedRoute}' was not found"); + } } + } - private static KeyValuePair[] AssertMetricPoint( - MetricPoint metricPoint, - string expectedRoute = "api/Values", - int expectedTagsCount = StandardTagsCount) + private static void AssertMetricPoint( + MetricPoint metricPoint, + int expectedStatusCode, + string expectedRoute, + string expectedErrorType, + int expectedTagsCount) + { + 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) { - var count = metricPoint.GetHistogramCount(); - var sum = metricPoint.GetHistogramSum(); + attributes[i++] = tag; + } - Assert.Equal(1L, count); - Assert.True(sum > 0); + // Inspect Attributes + Assert.Equal(expectedTagsCount, attributes.Length); + + var method = new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, "GET"); + var scheme = new KeyValuePair(SemanticConventions.AttributeUrlScheme, "http"); + var statusCode = new KeyValuePair(SemanticConventions.AttributeHttpResponseStatusCode, expectedStatusCode); + var flavor = new KeyValuePair(SemanticConventions.AttributeNetworkProtocolVersion, "1.1"); + 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(route, attributes); + + if (expectedErrorType != null) + { + var errorType = new KeyValuePair(SemanticConventions.AttributeErrorType, expectedErrorType); - var attributes = new KeyValuePair[metricPoint.Tags.Count]; - int i = 0; - foreach (var tag in metricPoint.Tags) - { - attributes[i++] = tag; - } + Assert.Contains(errorType, attributes); + } - 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); - Assert.Equal(expectedTagsCount, attributes.Length); - - return attributes; + // 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. + + 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); + + Assert.True(histogramBoundsMatchCorrectly); } } diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj index 67835116b86..26722a5dd51 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj @@ -1,41 +1,47 @@ Unit test project for OpenTelemetry ASP.NET Core instrumentation - - net7.0;net6.0 - + $(TargetFrameworksForAspNetCoreTests) disable + + - - - all + runtime; build; native; contentfiles; analyzers - - - - - + + + - - + + + - - + + + + + + + + RoutingTestCases.json + Always + + diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.md b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.md new file mode 100644 index 00000000000..38ae9f93fd9 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.md @@ -0,0 +1,204 @@ +# ASP.NET Core `http.route` tests + +This folder contains a test suite that validates the instrumentation produces +the expected `http.route` attribute on both the activity and metric it emits. +When available, the `http.route` is also a required component of the +`Activity.DisplayName`. + +The test suite covers a variety of different routing scenarios available for +ASP.NET Core: + +* [Conventional routing](https://learn.microsoft.com/aspnet/core/mvc/controllers/routing#conventional-routing) +* [Conventional routing using areas](https://learn.microsoft.com/aspnet/core/mvc/controllers/routing#areas) +* [Attribute routing](https://learn.microsoft.com/aspnet/core/mvc/controllers/routing#attribute-routing-for-rest-apis) +* [Razor pages](https://learn.microsoft.com/aspnet/core/razor-pages/razor-pages-conventions) +* [Minimal APIs](https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis/route-handlers) + +The individual test cases are defined in RoutingTestCases.json. + +The test suite is unique in that, when run, it generates README files for each +target framework which aids in documenting how the instrumentation behaves for +each test case. These files are source-controlled, so if the behavior of the +instrumentation changes, the README files will be updated to reflect the change. + +* [.NET 6](./README.net6.0.md) +* [.NET 7](./README.net7.0.md) +* [.NET 8](./README.net8.0.md) + +For each test case a request is made to an ASP.NET Core application with a +particular routing configuration. ASP.NET Core offers a +[variety of APIs](#aspnet-core-apis-for-retrieving-route-information) for +retrieving the route information of a given request. The README files include +detailed information documenting the route information available using the +various APIs in each test case. For example, here is the detailed result +generated for a test case: + +```json +{ + "IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}", + "ActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter/2", + "ActivityHttpRoute": "", + "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/ConventionalRoute/ActionWithStringParameter/2?num=3", + "RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "controller": "ConventionalRoute", + "action": "ActionWithStringParameter", + "id": "2" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": null, + "Parameters": [ + "id", + "num" + ], + "ControllerActionDescriptor": { + "ControllerName": "ConventionalRoute", + "ActionName": "ActionWithStringParameter" + }, + "PageActionDescriptor": null + } + } +} +``` + +> [!NOTE] +> The test result currently includes an `IdealHttpRoute` property. This is +> temporary, and is meant to drive a conversation to determine the best way +> for generating the `http.route` attribute under different routing scenarios. +> In the example above, the path invoked is +> `/ConventionalRoute/ActionWithStringParameter/2?num=3`. Currently, we see +> that the `http.route` attribute on the metric emitted is +> `{controller=ConventionalRoute}/{action=Default}/{id?}` which was derived +> using `RoutePattern.RawText`. This is not ideal +> because the route template does not include the actual action that was +> invoked `ActionWithStringParameter`. The invoked action could be derived +> using either the `ControllerActionDescriptor` +> or `HttpContext.GetRouteData()`. + +## ASP.NET Core APIs for retrieving route information + +Included below are short snippets illustrating the use of the various +APIs available for retrieving route information. + +### Retrieving the route template + +The route template can be obtained from `HttpContext` by retrieving the +`RouteEndpoint` using the following two APIs. + +For attribute routing and minimal API scenarios, using the route template alone +is sufficient for deriving `http.route` in all test cases. + +The route template does not well describe the `http.route` in conventional +routing and some Razor page scenarios. + +#### [RoutePattern.RawText](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.routing.patterns.routepattern.rawtext) + +```csharp +(httpContext.GetEndpoint() as RouteEndpoint)?.RoutePattern.RawText; +``` + +#### [IRouteDiagnosticsMetadata.Route](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.metadata.iroutediagnosticsmetadata.route) + +This API was introduced in .NET 8. + +```csharp +httpContext.GetEndpoint()?.Metadata.GetMetadata()?.Route; +``` + +### RouteData + +`RouteData` can be retrieved from `HttpContext` using the `GetRouteData()` +extension method. The values obtained from `RouteData` identify the controller/ +action or Razor page invoked by the request. + +#### [HttpContext.GetRouteData()](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.routing.routinghttpcontextextensions.getroutedata) + +```csharp +foreach (var value in httpContext.GetRouteData().Values) +{ + Console.WriteLine($"{value.Key} = {value.Value?.ToString()}"); +} +``` + +For example, the above code produces something like: + +```text +controller = ConventionalRoute +action = ActionWithStringParameter +id = 2 +``` + +### Information from the ActionDescriptor + +For requests that invoke an action or Razor page, the `ActionDescriptor` can +be used to access route information. + +#### [AttributeRouteInfo.Template](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.mvc.routing.attributerouteinfo.template) + +The `AttributeRouteInfo.Template` is equivalent to using +[other APIs for retrieving the route template](#retrieving-the-route-template) +when using attribute routing. For conventional routing and Razor pages it will +be `null`. + +```csharp +actionDescriptor.AttributeRouteInfo?.Template; +``` + +#### [ControllerActionDescriptor](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.mvc.controllers.controlleractiondescriptor) + +For requests that invoke an action on a controller, the `ActionDescriptor` +will be of type `ControllerActionDescriptor` which includes the controller and +action name. + +```csharp +(actionDescriptor as ControllerActionDescriptor)?.ControllerName; +(actionDescriptor as ControllerActionDescriptor)?.ActionName; +``` + +#### [PageActionDescriptor](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pageactiondescriptor) + +For requests that invoke a Razor page, the `ActionDescriptor` +will be of type `PageActionDescriptor` which includes the path to the invoked +page. + +```csharp +(actionDescriptor as PageActionDescriptor)?.RelativePath; +(actionDescriptor as PageActionDescriptor)?.ViewEnginePath; +``` + +#### [Parameters](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.mvc.abstractions.actiondescriptor.parameters#microsoft-aspnetcore-mvc-abstractions-actiondescriptor-parameters) + +The `ActionDescriptor.Parameters` property is interesting because it describes +the actual parameters (type and name) of an invoked action method. Some APM +products use `ActionDescriptor.Parameters` to more precisely describe the +method an endpoint invokes since not all parameters may be present in the +route template. + +Consider the following action method: + +```csharp +public IActionResult SomeActionMethod(string id, int num) { ... } +``` + +Using conventional routing assuming a default route template +`{controller=ConventionalRoute}/{action=Default}/{id?}`, the `SomeActionMethod` +may match this route template. The route template describes the `id` parameter +but not the `num` parameter. + +```csharp +foreach (var parameter in actionDescriptor.Parameters) +{ + Console.WriteLine($"{parameter.Name}"); +} +``` + +The above code produces: + +```text +id +num +``` diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net6.0.md b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net6.0.md new file mode 100644 index 00000000000..6582c757155 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net6.0.md @@ -0,0 +1,612 @@ +# Test results for ASP.NET Core 6 + +| 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: | ExceptionMiddleware | [Exception Handled by Exception Handler Middleware](#exceptionmiddleware-exception-handled-by-exception-handler-middleware) | + +## ConventionalRouting: Root path + +```json +{ + "IdealHttpRoute": "ConventionalRoute/Default/{id?}", + "ActivityDisplayName": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/", + "RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "controller": "ConventionalRoute", + "action": "Default" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": null, + "Parameters": [], + "ControllerActionDescriptor": { + "ControllerName": "ConventionalRoute", + "ActionName": "Default" + }, + "PageActionDescriptor": null + } + } +} +``` + +## ConventionalRouting: Non-default action with route parameter and query string + +```json +{ + "IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}", + "ActivityDisplayName": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/ConventionalRoute/ActionWithStringParameter/2?num=3", + "RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "controller": "ConventionalRoute", + "action": "ActionWithStringParameter", + "id": "2" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": null, + "Parameters": [ + "id", + "num" + ], + "ControllerActionDescriptor": { + "ControllerName": "ConventionalRoute", + "ActionName": "ActionWithStringParameter" + }, + "PageActionDescriptor": null + } + } +} +``` + +## ConventionalRouting: Non-default action with query string + +```json +{ + "IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}", + "ActivityDisplayName": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/ConventionalRoute/ActionWithStringParameter?num=3", + "RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "controller": "ConventionalRoute", + "action": "ActionWithStringParameter" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": null, + "Parameters": [ + "id", + "num" + ], + "ControllerActionDescriptor": { + "ControllerName": "ConventionalRoute", + "ActionName": "ActionWithStringParameter" + }, + "PageActionDescriptor": null + } + } +} +``` + +## ConventionalRouting: Not Found (404) + +```json +{ + "IdealHttpRoute": "", + "ActivityDisplayName": "GET", + "ActivityHttpRoute": "", + "MetricHttpRoute": "", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/ConventionalRoute/NotFound", + "RoutePattern.RawText": null, + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": {}, + "ActionDescriptor": null + } +} +``` + +## ConventionalRouting: Route template with parameter constraint + +```json +{ + "IdealHttpRoute": "SomePath/{id}/{num:int}", + "ActivityDisplayName": "GET SomePath/{id}/{num:int}", + "ActivityHttpRoute": "SomePath/{id}/{num:int}", + "MetricHttpRoute": "SomePath/{id}/{num:int}", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/SomePath/SomeString/2", + "RoutePattern.RawText": "SomePath/{id}/{num:int}", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "controller": "ConventionalRoute", + "action": "ActionWithStringParameter", + "id": "SomeString", + "num": "2" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": null, + "Parameters": [ + "id", + "num" + ], + "ControllerActionDescriptor": { + "ControllerName": "ConventionalRoute", + "ActionName": "ActionWithStringParameter" + }, + "PageActionDescriptor": null + } + } +} +``` + +## ConventionalRouting: Path that does not match parameter constraint + +```json +{ + "IdealHttpRoute": "", + "ActivityDisplayName": "GET", + "ActivityHttpRoute": "", + "MetricHttpRoute": "", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/SomePath/SomeString/NotAnInt", + "RoutePattern.RawText": null, + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": {}, + "ActionDescriptor": null + } +} +``` + +## ConventionalRouting: Area using `area:exists`, default controller/action + +```json +{ + "IdealHttpRoute": "{area:exists}/ControllerForMyArea/Default/{id?}", + "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", + "Path": "/MyArea", + "RoutePattern.RawText": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "controller": "ControllerForMyArea", + "action": "Default", + "area": "MyArea" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": null, + "Parameters": [], + "ControllerActionDescriptor": { + "ControllerName": "ControllerForMyArea", + "ActionName": "Default" + }, + "PageActionDescriptor": null + } + } +} +``` + +## ConventionalRouting: Area using `area:exists`, non-default action + +```json +{ + "IdealHttpRoute": "{area:exists}/ControllerForMyArea/NonDefault/{id?}", + "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", + "Path": "/MyArea/ControllerForMyArea/NonDefault", + "RoutePattern.RawText": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "controller": "ControllerForMyArea", + "area": "MyArea", + "action": "NonDefault" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": null, + "Parameters": [], + "ControllerActionDescriptor": { + "ControllerName": "ControllerForMyArea", + "ActionName": "NonDefault" + }, + "PageActionDescriptor": null + } + } +} +``` + +## ConventionalRouting: Area w/o `area:exists`, default controller/action + +```json +{ + "IdealHttpRoute": "SomePrefix/AnotherArea/Index/{id?}", + "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", + "Path": "/SomePrefix", + "RoutePattern.RawText": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "area": "AnotherArea", + "controller": "AnotherArea", + "action": "Index" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": null, + "Parameters": [], + "ControllerActionDescriptor": { + "ControllerName": "AnotherArea", + "ActionName": "Index" + }, + "PageActionDescriptor": null + } + } +} +``` + +## AttributeRouting: Default action + +```json +{ + "IdealHttpRoute": "AttributeRoute", + "ActivityDisplayName": "GET AttributeRoute", + "ActivityHttpRoute": "AttributeRoute", + "MetricHttpRoute": "AttributeRoute", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/AttributeRoute", + "RoutePattern.RawText": "AttributeRoute", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "action": "Get", + "controller": "AttributeRoute" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": "AttributeRoute", + "Parameters": [], + "ControllerActionDescriptor": { + "ControllerName": "AttributeRoute", + "ActionName": "Get" + }, + "PageActionDescriptor": null + } + } +} +``` + +## AttributeRouting: Action without parameter + +```json +{ + "IdealHttpRoute": "AttributeRoute/Get", + "ActivityDisplayName": "GET AttributeRoute/Get", + "ActivityHttpRoute": "AttributeRoute/Get", + "MetricHttpRoute": "AttributeRoute/Get", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/AttributeRoute/Get", + "RoutePattern.RawText": "AttributeRoute/Get", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "action": "Get", + "controller": "AttributeRoute" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": "AttributeRoute/Get", + "Parameters": [], + "ControllerActionDescriptor": { + "ControllerName": "AttributeRoute", + "ActionName": "Get" + }, + "PageActionDescriptor": null + } + } +} +``` + +## AttributeRouting: Action with parameter + +```json +{ + "IdealHttpRoute": "AttributeRoute/Get/{id}", + "ActivityDisplayName": "GET AttributeRoute/Get/{id}", + "ActivityHttpRoute": "AttributeRoute/Get/{id}", + "MetricHttpRoute": "AttributeRoute/Get/{id}", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/AttributeRoute/Get/12", + "RoutePattern.RawText": "AttributeRoute/Get/{id}", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "action": "Get", + "controller": "AttributeRoute", + "id": "12" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": "AttributeRoute/Get/{id}", + "Parameters": [ + "id" + ], + "ControllerActionDescriptor": { + "ControllerName": "AttributeRoute", + "ActionName": "Get" + }, + "PageActionDescriptor": null + } + } +} +``` + +## AttributeRouting: Action with parameter before action name in template + +```json +{ + "IdealHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "ActivityDisplayName": "GET AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "ActivityHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "MetricHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/AttributeRoute/12/GetWithActionNameInDifferentSpotInTemplate", + "RoutePattern.RawText": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "action": "GetWithActionNameInDifferentSpotInTemplate", + "controller": "AttributeRoute", + "id": "12" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "Parameters": [ + "id" + ], + "ControllerActionDescriptor": { + "ControllerName": "AttributeRoute", + "ActionName": "GetWithActionNameInDifferentSpotInTemplate" + }, + "PageActionDescriptor": null + } + } +} +``` + +## AttributeRouting: Action invoked resulting in 400 Bad Request + +```json +{ + "IdealHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "ActivityDisplayName": "GET AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "ActivityHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "MetricHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/AttributeRoute/NotAnInt/GetWithActionNameInDifferentSpotInTemplate", + "RoutePattern.RawText": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "action": "GetWithActionNameInDifferentSpotInTemplate", + "controller": "AttributeRoute", + "id": "NotAnInt" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "Parameters": [ + "id" + ], + "ControllerActionDescriptor": { + "ControllerName": "AttributeRoute", + "ActionName": "GetWithActionNameInDifferentSpotInTemplate" + }, + "PageActionDescriptor": null + } + } +} +``` + +## RazorPages: Root path + +```json +{ + "IdealHttpRoute": "/Index", + "ActivityDisplayName": "GET", + "ActivityHttpRoute": "", + "MetricHttpRoute": "", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/", + "RoutePattern.RawText": "", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "page": "/Index" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": "", + "Parameters": [], + "ControllerActionDescriptor": null, + "PageActionDescriptor": { + "RelativePath": "/Pages/Index.cshtml", + "ViewEnginePath": "/Index" + } + } + } +} +``` + +## RazorPages: Index page + +```json +{ + "IdealHttpRoute": "/Index", + "ActivityDisplayName": "GET Index", + "ActivityHttpRoute": "Index", + "MetricHttpRoute": "Index", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/Index", + "RoutePattern.RawText": "Index", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "page": "/Index" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": "Index", + "Parameters": [], + "ControllerActionDescriptor": null, + "PageActionDescriptor": { + "RelativePath": "/Pages/Index.cshtml", + "ViewEnginePath": "/Index" + } + } + } +} +``` + +## RazorPages: Throws exception + +```json +{ + "IdealHttpRoute": "/PageThatThrowsException", + "ActivityDisplayName": "GET PageThatThrowsException", + "ActivityHttpRoute": "PageThatThrowsException", + "MetricHttpRoute": "PageThatThrowsException", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/PageThatThrowsException", + "RoutePattern.RawText": "PageThatThrowsException", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "page": "/PageThatThrowsException" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": "PageThatThrowsException", + "Parameters": [], + "ControllerActionDescriptor": null, + "PageActionDescriptor": { + "RelativePath": "/Pages/PageThatThrowsException.cshtml", + "ViewEnginePath": "/PageThatThrowsException" + } + } + } +} +``` + +## RazorPages: Static content + +```json +{ + "IdealHttpRoute": "", + "ActivityDisplayName": "GET", + "ActivityHttpRoute": "", + "MetricHttpRoute": "", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/js/site.js", + "RoutePattern.RawText": null, + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": {}, + "ActionDescriptor": null + } +} +``` + +## MinimalApi: Action without parameter + +```json +{ + "IdealHttpRoute": "/MinimalApi", + "ActivityDisplayName": "GET /MinimalApi", + "ActivityHttpRoute": "/MinimalApi", + "MetricHttpRoute": "/MinimalApi", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/MinimalApi", + "RoutePattern.RawText": "/MinimalApi", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": {}, + "ActionDescriptor": null + } +} +``` + +## MinimalApi: Action with parameter + +```json +{ + "IdealHttpRoute": "/MinimalApi/{id}", + "ActivityDisplayName": "GET /MinimalApi/{id}", + "ActivityHttpRoute": "/MinimalApi/{id}", + "MetricHttpRoute": "/MinimalApi/{id}", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/MinimalApi/123", + "RoutePattern.RawText": "/MinimalApi/{id}", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "id": "123" + }, + "ActionDescriptor": null + } +} +``` + +## ExceptionMiddleware: Exception Handled by Exception Handler Middleware + +```json +{ + "IdealHttpRoute": "/Exception", + "ActivityDisplayName": "GET /Exception", + "ActivityHttpRoute": "/Exception", + "MetricHttpRoute": "/Exception", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/Exception", + "RoutePattern.RawText": null, + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": {}, + "ActionDescriptor": null + } +} +``` diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net7.0.md b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net7.0.md new file mode 100644 index 00000000000..49d8224155c --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net7.0.md @@ -0,0 +1,654 @@ +# Test results for ASP.NET Core 7 + +| 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) | +| :green_heart: | ExceptionMiddleware | [Exception Handled by Exception Handler Middleware](#exceptionmiddleware-exception-handled-by-exception-handler-middleware) | + +## ConventionalRouting: Root path + +```json +{ + "IdealHttpRoute": "ConventionalRoute/Default/{id?}", + "ActivityDisplayName": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/", + "RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "controller": "ConventionalRoute", + "action": "Default" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": null, + "Parameters": [], + "ControllerActionDescriptor": { + "ControllerName": "ConventionalRoute", + "ActionName": "Default" + }, + "PageActionDescriptor": null + } + } +} +``` + +## ConventionalRouting: Non-default action with route parameter and query string + +```json +{ + "IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}", + "ActivityDisplayName": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/ConventionalRoute/ActionWithStringParameter/2?num=3", + "RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "controller": "ConventionalRoute", + "action": "ActionWithStringParameter", + "id": "2" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": null, + "Parameters": [ + "id", + "num" + ], + "ControllerActionDescriptor": { + "ControllerName": "ConventionalRoute", + "ActionName": "ActionWithStringParameter" + }, + "PageActionDescriptor": null + } + } +} +``` + +## ConventionalRouting: Non-default action with query string + +```json +{ + "IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}", + "ActivityDisplayName": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/ConventionalRoute/ActionWithStringParameter?num=3", + "RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "controller": "ConventionalRoute", + "action": "ActionWithStringParameter" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": null, + "Parameters": [ + "id", + "num" + ], + "ControllerActionDescriptor": { + "ControllerName": "ConventionalRoute", + "ActionName": "ActionWithStringParameter" + }, + "PageActionDescriptor": null + } + } +} +``` + +## ConventionalRouting: Not Found (404) + +```json +{ + "IdealHttpRoute": "", + "ActivityDisplayName": "GET", + "ActivityHttpRoute": "", + "MetricHttpRoute": "", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/ConventionalRoute/NotFound", + "RoutePattern.RawText": null, + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": {}, + "ActionDescriptor": null + } +} +``` + +## ConventionalRouting: Route template with parameter constraint + +```json +{ + "IdealHttpRoute": "SomePath/{id}/{num:int}", + "ActivityDisplayName": "GET SomePath/{id}/{num:int}", + "ActivityHttpRoute": "SomePath/{id}/{num:int}", + "MetricHttpRoute": "SomePath/{id}/{num:int}", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/SomePath/SomeString/2", + "RoutePattern.RawText": "SomePath/{id}/{num:int}", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "controller": "ConventionalRoute", + "action": "ActionWithStringParameter", + "id": "SomeString", + "num": "2" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": null, + "Parameters": [ + "id", + "num" + ], + "ControllerActionDescriptor": { + "ControllerName": "ConventionalRoute", + "ActionName": "ActionWithStringParameter" + }, + "PageActionDescriptor": null + } + } +} +``` + +## ConventionalRouting: Path that does not match parameter constraint + +```json +{ + "IdealHttpRoute": "", + "ActivityDisplayName": "GET", + "ActivityHttpRoute": "", + "MetricHttpRoute": "", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/SomePath/SomeString/NotAnInt", + "RoutePattern.RawText": null, + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": {}, + "ActionDescriptor": null + } +} +``` + +## ConventionalRouting: Area using `area:exists`, default controller/action + +```json +{ + "IdealHttpRoute": "{area:exists}/ControllerForMyArea/Default/{id?}", + "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", + "Path": "/MyArea", + "RoutePattern.RawText": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "controller": "ControllerForMyArea", + "action": "Default", + "area": "MyArea" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": null, + "Parameters": [], + "ControllerActionDescriptor": { + "ControllerName": "ControllerForMyArea", + "ActionName": "Default" + }, + "PageActionDescriptor": null + } + } +} +``` + +## ConventionalRouting: Area using `area:exists`, non-default action + +```json +{ + "IdealHttpRoute": "{area:exists}/ControllerForMyArea/NonDefault/{id?}", + "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", + "Path": "/MyArea/ControllerForMyArea/NonDefault", + "RoutePattern.RawText": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "controller": "ControllerForMyArea", + "area": "MyArea", + "action": "NonDefault" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": null, + "Parameters": [], + "ControllerActionDescriptor": { + "ControllerName": "ControllerForMyArea", + "ActionName": "NonDefault" + }, + "PageActionDescriptor": null + } + } +} +``` + +## ConventionalRouting: Area w/o `area:exists`, default controller/action + +```json +{ + "IdealHttpRoute": "SomePrefix/AnotherArea/Index/{id?}", + "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", + "Path": "/SomePrefix", + "RoutePattern.RawText": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "area": "AnotherArea", + "controller": "AnotherArea", + "action": "Index" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": null, + "Parameters": [], + "ControllerActionDescriptor": { + "ControllerName": "AnotherArea", + "ActionName": "Index" + }, + "PageActionDescriptor": null + } + } +} +``` + +## AttributeRouting: Default action + +```json +{ + "IdealHttpRoute": "AttributeRoute", + "ActivityDisplayName": "GET AttributeRoute", + "ActivityHttpRoute": "AttributeRoute", + "MetricHttpRoute": "AttributeRoute", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/AttributeRoute", + "RoutePattern.RawText": "AttributeRoute", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "action": "Get", + "controller": "AttributeRoute" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": "AttributeRoute", + "Parameters": [], + "ControllerActionDescriptor": { + "ControllerName": "AttributeRoute", + "ActionName": "Get" + }, + "PageActionDescriptor": null + } + } +} +``` + +## AttributeRouting: Action without parameter + +```json +{ + "IdealHttpRoute": "AttributeRoute/Get", + "ActivityDisplayName": "GET AttributeRoute/Get", + "ActivityHttpRoute": "AttributeRoute/Get", + "MetricHttpRoute": "AttributeRoute/Get", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/AttributeRoute/Get", + "RoutePattern.RawText": "AttributeRoute/Get", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "action": "Get", + "controller": "AttributeRoute" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": "AttributeRoute/Get", + "Parameters": [], + "ControllerActionDescriptor": { + "ControllerName": "AttributeRoute", + "ActionName": "Get" + }, + "PageActionDescriptor": null + } + } +} +``` + +## AttributeRouting: Action with parameter + +```json +{ + "IdealHttpRoute": "AttributeRoute/Get/{id}", + "ActivityDisplayName": "GET AttributeRoute/Get/{id}", + "ActivityHttpRoute": "AttributeRoute/Get/{id}", + "MetricHttpRoute": "AttributeRoute/Get/{id}", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/AttributeRoute/Get/12", + "RoutePattern.RawText": "AttributeRoute/Get/{id}", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "action": "Get", + "controller": "AttributeRoute", + "id": "12" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": "AttributeRoute/Get/{id}", + "Parameters": [ + "id" + ], + "ControllerActionDescriptor": { + "ControllerName": "AttributeRoute", + "ActionName": "Get" + }, + "PageActionDescriptor": null + } + } +} +``` + +## AttributeRouting: Action with parameter before action name in template + +```json +{ + "IdealHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "ActivityDisplayName": "GET AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "ActivityHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "MetricHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/AttributeRoute/12/GetWithActionNameInDifferentSpotInTemplate", + "RoutePattern.RawText": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "action": "GetWithActionNameInDifferentSpotInTemplate", + "controller": "AttributeRoute", + "id": "12" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "Parameters": [ + "id" + ], + "ControllerActionDescriptor": { + "ControllerName": "AttributeRoute", + "ActionName": "GetWithActionNameInDifferentSpotInTemplate" + }, + "PageActionDescriptor": null + } + } +} +``` + +## AttributeRouting: Action invoked resulting in 400 Bad Request + +```json +{ + "IdealHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "ActivityDisplayName": "GET AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "ActivityHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "MetricHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/AttributeRoute/NotAnInt/GetWithActionNameInDifferentSpotInTemplate", + "RoutePattern.RawText": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "action": "GetWithActionNameInDifferentSpotInTemplate", + "controller": "AttributeRoute", + "id": "NotAnInt" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "Parameters": [ + "id" + ], + "ControllerActionDescriptor": { + "ControllerName": "AttributeRoute", + "ActionName": "GetWithActionNameInDifferentSpotInTemplate" + }, + "PageActionDescriptor": null + } + } +} +``` + +## RazorPages: Root path + +```json +{ + "IdealHttpRoute": "/Index", + "ActivityDisplayName": "GET", + "ActivityHttpRoute": "", + "MetricHttpRoute": "", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/", + "RoutePattern.RawText": "", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "page": "/Index" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": "", + "Parameters": [], + "ControllerActionDescriptor": null, + "PageActionDescriptor": { + "RelativePath": "/Pages/Index.cshtml", + "ViewEnginePath": "/Index" + } + } + } +} +``` + +## RazorPages: Index page + +```json +{ + "IdealHttpRoute": "/Index", + "ActivityDisplayName": "GET Index", + "ActivityHttpRoute": "Index", + "MetricHttpRoute": "Index", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/Index", + "RoutePattern.RawText": "Index", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "page": "/Index" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": "Index", + "Parameters": [], + "ControllerActionDescriptor": null, + "PageActionDescriptor": { + "RelativePath": "/Pages/Index.cshtml", + "ViewEnginePath": "/Index" + } + } + } +} +``` + +## RazorPages: Throws exception + +```json +{ + "IdealHttpRoute": "/PageThatThrowsException", + "ActivityDisplayName": "GET PageThatThrowsException", + "ActivityHttpRoute": "PageThatThrowsException", + "MetricHttpRoute": "PageThatThrowsException", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/PageThatThrowsException", + "RoutePattern.RawText": "PageThatThrowsException", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "page": "/PageThatThrowsException" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": "PageThatThrowsException", + "Parameters": [], + "ControllerActionDescriptor": null, + "PageActionDescriptor": { + "RelativePath": "/Pages/PageThatThrowsException.cshtml", + "ViewEnginePath": "/PageThatThrowsException" + } + } + } +} +``` + +## RazorPages: Static content + +```json +{ + "IdealHttpRoute": "", + "ActivityDisplayName": "GET", + "ActivityHttpRoute": "", + "MetricHttpRoute": "", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/js/site.js", + "RoutePattern.RawText": null, + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": {}, + "ActionDescriptor": null + } +} +``` + +## MinimalApi: Action without parameter + +```json +{ + "IdealHttpRoute": "/MinimalApi", + "ActivityDisplayName": "GET /MinimalApi", + "ActivityHttpRoute": "/MinimalApi", + "MetricHttpRoute": "/MinimalApi", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/MinimalApi", + "RoutePattern.RawText": "/MinimalApi", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": {}, + "ActionDescriptor": null + } +} +``` + +## MinimalApi: Action with parameter + +```json +{ + "IdealHttpRoute": "/MinimalApi/{id}", + "ActivityDisplayName": "GET /MinimalApi/{id}", + "ActivityHttpRoute": "/MinimalApi/{id}", + "MetricHttpRoute": "/MinimalApi/{id}", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/MinimalApi/123", + "RoutePattern.RawText": "/MinimalApi/{id}", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "id": "123" + }, + "ActionDescriptor": null + } +} +``` + +## MinimalApi: Action without parameter (MapGroup) + +```json +{ + "IdealHttpRoute": "/MinimalApiUsingMapGroup/", + "ActivityDisplayName": "GET /MinimalApiUsingMapGroup/", + "ActivityHttpRoute": "/MinimalApiUsingMapGroup/", + "MetricHttpRoute": "/MinimalApiUsingMapGroup/", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/MinimalApiUsingMapGroup", + "RoutePattern.RawText": "/MinimalApiUsingMapGroup/", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": {}, + "ActionDescriptor": null + } +} +``` + +## MinimalApi: Action with parameter (MapGroup) + +```json +{ + "IdealHttpRoute": "/MinimalApiUsingMapGroup/{id}", + "ActivityDisplayName": "GET /MinimalApiUsingMapGroup/{id}", + "ActivityHttpRoute": "/MinimalApiUsingMapGroup/{id}", + "MetricHttpRoute": "/MinimalApiUsingMapGroup/{id}", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/MinimalApiUsingMapGroup/123", + "RoutePattern.RawText": "/MinimalApiUsingMapGroup/{id}", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "id": "123" + }, + "ActionDescriptor": null + } +} +``` + +## ExceptionMiddleware: Exception Handled by Exception Handler Middleware + +```json +{ + "IdealHttpRoute": "/Exception", + "ActivityDisplayName": "GET /Exception", + "ActivityHttpRoute": "/Exception", + "MetricHttpRoute": "/Exception", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/Exception", + "RoutePattern.RawText": null, + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": {}, + "ActionDescriptor": null + } +} +``` diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net8.0.md b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net8.0.md new file mode 100644 index 00000000000..40b63a1ca45 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net8.0.md @@ -0,0 +1,654 @@ +# Test results for ASP.NET Core 8 + +| 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) | +| :green_heart: | ExceptionMiddleware | [Exception Handled by Exception Handler Middleware](#exceptionmiddleware-exception-handled-by-exception-handler-middleware) | + +## ConventionalRouting: Root path + +```json +{ + "IdealHttpRoute": "ConventionalRoute/Default/{id?}", + "ActivityDisplayName": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/", + "RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "IRouteDiagnosticsMetadata.Route": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "HttpContext.GetRouteData()": { + "controller": "ConventionalRoute", + "action": "Default" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": null, + "Parameters": [], + "ControllerActionDescriptor": { + "ControllerName": "ConventionalRoute", + "ActionName": "Default" + }, + "PageActionDescriptor": null + } + } +} +``` + +## ConventionalRouting: Non-default action with route parameter and query string + +```json +{ + "IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}", + "ActivityDisplayName": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/ConventionalRoute/ActionWithStringParameter/2?num=3", + "RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "IRouteDiagnosticsMetadata.Route": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "HttpContext.GetRouteData()": { + "controller": "ConventionalRoute", + "action": "ActionWithStringParameter", + "id": "2" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": null, + "Parameters": [ + "id", + "num" + ], + "ControllerActionDescriptor": { + "ControllerName": "ConventionalRoute", + "ActionName": "ActionWithStringParameter" + }, + "PageActionDescriptor": null + } + } +} +``` + +## ConventionalRouting: Non-default action with query string + +```json +{ + "IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}", + "ActivityDisplayName": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/ConventionalRoute/ActionWithStringParameter?num=3", + "RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "IRouteDiagnosticsMetadata.Route": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "HttpContext.GetRouteData()": { + "controller": "ConventionalRoute", + "action": "ActionWithStringParameter" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": null, + "Parameters": [ + "id", + "num" + ], + "ControllerActionDescriptor": { + "ControllerName": "ConventionalRoute", + "ActionName": "ActionWithStringParameter" + }, + "PageActionDescriptor": null + } + } +} +``` + +## ConventionalRouting: Not Found (404) + +```json +{ + "IdealHttpRoute": "", + "ActivityDisplayName": "GET", + "ActivityHttpRoute": "", + "MetricHttpRoute": "", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/ConventionalRoute/NotFound", + "RoutePattern.RawText": null, + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": {}, + "ActionDescriptor": null + } +} +``` + +## ConventionalRouting: Route template with parameter constraint + +```json +{ + "IdealHttpRoute": "SomePath/{id}/{num:int}", + "ActivityDisplayName": "GET SomePath/{id}/{num:int}", + "ActivityHttpRoute": "SomePath/{id}/{num:int}", + "MetricHttpRoute": "SomePath/{id}/{num:int}", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/SomePath/SomeString/2", + "RoutePattern.RawText": "SomePath/{id}/{num:int}", + "IRouteDiagnosticsMetadata.Route": "SomePath/{id}/{num:int}", + "HttpContext.GetRouteData()": { + "controller": "ConventionalRoute", + "action": "ActionWithStringParameter", + "id": "SomeString", + "num": "2" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": null, + "Parameters": [ + "id", + "num" + ], + "ControllerActionDescriptor": { + "ControllerName": "ConventionalRoute", + "ActionName": "ActionWithStringParameter" + }, + "PageActionDescriptor": null + } + } +} +``` + +## ConventionalRouting: Path that does not match parameter constraint + +```json +{ + "IdealHttpRoute": "", + "ActivityDisplayName": "GET", + "ActivityHttpRoute": "", + "MetricHttpRoute": "", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/SomePath/SomeString/NotAnInt", + "RoutePattern.RawText": null, + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": {}, + "ActionDescriptor": null + } +} +``` + +## ConventionalRouting: Area using `area:exists`, default controller/action + +```json +{ + "IdealHttpRoute": "{area:exists}/ControllerForMyArea/Default/{id?}", + "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", + "Path": "/MyArea", + "RoutePattern.RawText": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "IRouteDiagnosticsMetadata.Route": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "HttpContext.GetRouteData()": { + "controller": "ControllerForMyArea", + "action": "Default", + "area": "MyArea" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": null, + "Parameters": [], + "ControllerActionDescriptor": { + "ControllerName": "ControllerForMyArea", + "ActionName": "Default" + }, + "PageActionDescriptor": null + } + } +} +``` + +## ConventionalRouting: Area using `area:exists`, non-default action + +```json +{ + "IdealHttpRoute": "{area:exists}/ControllerForMyArea/NonDefault/{id?}", + "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", + "Path": "/MyArea/ControllerForMyArea/NonDefault", + "RoutePattern.RawText": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "IRouteDiagnosticsMetadata.Route": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "HttpContext.GetRouteData()": { + "controller": "ControllerForMyArea", + "area": "MyArea", + "action": "NonDefault" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": null, + "Parameters": [], + "ControllerActionDescriptor": { + "ControllerName": "ControllerForMyArea", + "ActionName": "NonDefault" + }, + "PageActionDescriptor": null + } + } +} +``` + +## ConventionalRouting: Area w/o `area:exists`, default controller/action + +```json +{ + "IdealHttpRoute": "SomePrefix/AnotherArea/Index/{id?}", + "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", + "Path": "/SomePrefix", + "RoutePattern.RawText": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}", + "IRouteDiagnosticsMetadata.Route": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}", + "HttpContext.GetRouteData()": { + "area": "AnotherArea", + "controller": "AnotherArea", + "action": "Index" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": null, + "Parameters": [], + "ControllerActionDescriptor": { + "ControllerName": "AnotherArea", + "ActionName": "Index" + }, + "PageActionDescriptor": null + } + } +} +``` + +## AttributeRouting: Default action + +```json +{ + "IdealHttpRoute": "AttributeRoute", + "ActivityDisplayName": "GET AttributeRoute", + "ActivityHttpRoute": "AttributeRoute", + "MetricHttpRoute": "AttributeRoute", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/AttributeRoute", + "RoutePattern.RawText": "AttributeRoute", + "IRouteDiagnosticsMetadata.Route": "AttributeRoute", + "HttpContext.GetRouteData()": { + "action": "Get", + "controller": "AttributeRoute" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": "AttributeRoute", + "Parameters": [], + "ControllerActionDescriptor": { + "ControllerName": "AttributeRoute", + "ActionName": "Get" + }, + "PageActionDescriptor": null + } + } +} +``` + +## AttributeRouting: Action without parameter + +```json +{ + "IdealHttpRoute": "AttributeRoute/Get", + "ActivityDisplayName": "GET AttributeRoute/Get", + "ActivityHttpRoute": "AttributeRoute/Get", + "MetricHttpRoute": "AttributeRoute/Get", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/AttributeRoute/Get", + "RoutePattern.RawText": "AttributeRoute/Get", + "IRouteDiagnosticsMetadata.Route": "AttributeRoute/Get", + "HttpContext.GetRouteData()": { + "action": "Get", + "controller": "AttributeRoute" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": "AttributeRoute/Get", + "Parameters": [], + "ControllerActionDescriptor": { + "ControllerName": "AttributeRoute", + "ActionName": "Get" + }, + "PageActionDescriptor": null + } + } +} +``` + +## AttributeRouting: Action with parameter + +```json +{ + "IdealHttpRoute": "AttributeRoute/Get/{id}", + "ActivityDisplayName": "GET AttributeRoute/Get/{id}", + "ActivityHttpRoute": "AttributeRoute/Get/{id}", + "MetricHttpRoute": "AttributeRoute/Get/{id}", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/AttributeRoute/Get/12", + "RoutePattern.RawText": "AttributeRoute/Get/{id}", + "IRouteDiagnosticsMetadata.Route": "AttributeRoute/Get/{id}", + "HttpContext.GetRouteData()": { + "action": "Get", + "controller": "AttributeRoute", + "id": "12" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": "AttributeRoute/Get/{id}", + "Parameters": [ + "id" + ], + "ControllerActionDescriptor": { + "ControllerName": "AttributeRoute", + "ActionName": "Get" + }, + "PageActionDescriptor": null + } + } +} +``` + +## AttributeRouting: Action with parameter before action name in template + +```json +{ + "IdealHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "ActivityDisplayName": "GET AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "ActivityHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "MetricHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/AttributeRoute/12/GetWithActionNameInDifferentSpotInTemplate", + "RoutePattern.RawText": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "IRouteDiagnosticsMetadata.Route": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "HttpContext.GetRouteData()": { + "action": "GetWithActionNameInDifferentSpotInTemplate", + "controller": "AttributeRoute", + "id": "12" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "Parameters": [ + "id" + ], + "ControllerActionDescriptor": { + "ControllerName": "AttributeRoute", + "ActionName": "GetWithActionNameInDifferentSpotInTemplate" + }, + "PageActionDescriptor": null + } + } +} +``` + +## AttributeRouting: Action invoked resulting in 400 Bad Request + +```json +{ + "IdealHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "ActivityDisplayName": "GET AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "ActivityHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "MetricHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/AttributeRoute/NotAnInt/GetWithActionNameInDifferentSpotInTemplate", + "RoutePattern.RawText": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "IRouteDiagnosticsMetadata.Route": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "HttpContext.GetRouteData()": { + "action": "GetWithActionNameInDifferentSpotInTemplate", + "controller": "AttributeRoute", + "id": "NotAnInt" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "Parameters": [ + "id" + ], + "ControllerActionDescriptor": { + "ControllerName": "AttributeRoute", + "ActionName": "GetWithActionNameInDifferentSpotInTemplate" + }, + "PageActionDescriptor": null + } + } +} +``` + +## RazorPages: Root path + +```json +{ + "IdealHttpRoute": "/Index", + "ActivityDisplayName": "GET", + "ActivityHttpRoute": "", + "MetricHttpRoute": "", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/", + "RoutePattern.RawText": "", + "IRouteDiagnosticsMetadata.Route": "", + "HttpContext.GetRouteData()": { + "page": "/Index" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": "", + "Parameters": [], + "ControllerActionDescriptor": null, + "PageActionDescriptor": { + "RelativePath": "/Pages/Index.cshtml", + "ViewEnginePath": "/Index" + } + } + } +} +``` + +## RazorPages: Index page + +```json +{ + "IdealHttpRoute": "/Index", + "ActivityDisplayName": "GET Index", + "ActivityHttpRoute": "Index", + "MetricHttpRoute": "Index", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/Index", + "RoutePattern.RawText": "Index", + "IRouteDiagnosticsMetadata.Route": "Index", + "HttpContext.GetRouteData()": { + "page": "/Index" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": "Index", + "Parameters": [], + "ControllerActionDescriptor": null, + "PageActionDescriptor": { + "RelativePath": "/Pages/Index.cshtml", + "ViewEnginePath": "/Index" + } + } + } +} +``` + +## RazorPages: Throws exception + +```json +{ + "IdealHttpRoute": "/PageThatThrowsException", + "ActivityDisplayName": "GET PageThatThrowsException", + "ActivityHttpRoute": "PageThatThrowsException", + "MetricHttpRoute": "PageThatThrowsException", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/PageThatThrowsException", + "RoutePattern.RawText": "PageThatThrowsException", + "IRouteDiagnosticsMetadata.Route": "PageThatThrowsException", + "HttpContext.GetRouteData()": { + "page": "/PageThatThrowsException" + }, + "ActionDescriptor": { + "AttributeRouteInfo.Template": "PageThatThrowsException", + "Parameters": [], + "ControllerActionDescriptor": null, + "PageActionDescriptor": { + "RelativePath": "/Pages/PageThatThrowsException.cshtml", + "ViewEnginePath": "/PageThatThrowsException" + } + } + } +} +``` + +## RazorPages: Static content + +```json +{ + "IdealHttpRoute": "", + "ActivityDisplayName": "GET", + "ActivityHttpRoute": "", + "MetricHttpRoute": "", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/js/site.js", + "RoutePattern.RawText": null, + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": {}, + "ActionDescriptor": null + } +} +``` + +## MinimalApi: Action without parameter + +```json +{ + "IdealHttpRoute": "/MinimalApi", + "ActivityDisplayName": "GET /MinimalApi", + "ActivityHttpRoute": "/MinimalApi", + "MetricHttpRoute": "/MinimalApi", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/MinimalApi", + "RoutePattern.RawText": "/MinimalApi", + "IRouteDiagnosticsMetadata.Route": "/MinimalApi", + "HttpContext.GetRouteData()": {}, + "ActionDescriptor": null + } +} +``` + +## MinimalApi: Action with parameter + +```json +{ + "IdealHttpRoute": "/MinimalApi/{id}", + "ActivityDisplayName": "GET /MinimalApi/{id}", + "ActivityHttpRoute": "/MinimalApi/{id}", + "MetricHttpRoute": "/MinimalApi/{id}", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/MinimalApi/123", + "RoutePattern.RawText": "/MinimalApi/{id}", + "IRouteDiagnosticsMetadata.Route": "/MinimalApi/{id}", + "HttpContext.GetRouteData()": { + "id": "123" + }, + "ActionDescriptor": null + } +} +``` + +## MinimalApi: Action without parameter (MapGroup) + +```json +{ + "IdealHttpRoute": "/MinimalApiUsingMapGroup/", + "ActivityDisplayName": "GET /MinimalApiUsingMapGroup/", + "ActivityHttpRoute": "/MinimalApiUsingMapGroup/", + "MetricHttpRoute": "/MinimalApiUsingMapGroup/", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/MinimalApiUsingMapGroup", + "RoutePattern.RawText": "/MinimalApiUsingMapGroup/", + "IRouteDiagnosticsMetadata.Route": "/MinimalApiUsingMapGroup/", + "HttpContext.GetRouteData()": {}, + "ActionDescriptor": null + } +} +``` + +## MinimalApi: Action with parameter (MapGroup) + +```json +{ + "IdealHttpRoute": "/MinimalApiUsingMapGroup/{id}", + "ActivityDisplayName": "GET /MinimalApiUsingMapGroup/{id}", + "ActivityHttpRoute": "/MinimalApiUsingMapGroup/{id}", + "MetricHttpRoute": "/MinimalApiUsingMapGroup/{id}", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/MinimalApiUsingMapGroup/123", + "RoutePattern.RawText": "/MinimalApiUsingMapGroup/{id}", + "IRouteDiagnosticsMetadata.Route": "/MinimalApiUsingMapGroup/{id}", + "HttpContext.GetRouteData()": { + "id": "123" + }, + "ActionDescriptor": null + } +} +``` + +## ExceptionMiddleware: Exception Handled by Exception Handler Middleware + +```json +{ + "IdealHttpRoute": "/Exception", + "ActivityDisplayName": "GET /Exception", + "ActivityHttpRoute": "/Exception", + "MetricHttpRoute": "/Exception", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/Exception", + "RoutePattern.RawText": "/Exception", + "IRouteDiagnosticsMetadata.Route": "/Exception", + "HttpContext.GetRouteData()": {}, + "ActionDescriptor": null + } +} +``` diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs new file mode 100644 index 00000000000..a3ec3818886 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs @@ -0,0 +1,69 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using RouteTests.TestApplication; + +namespace RouteTests; + +public static class RoutingTestCases +{ + public static IEnumerable GetTestCases() + { + var assembly = Assembly.GetExecutingAssembly(); + var input = JsonSerializer.Deserialize( + assembly.GetManifestResourceStream("RoutingTestCases.json")!, + new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter() }, + }); + return GetArgumentsFromTestCaseObject(input!); + } + + private static IEnumerable GetArgumentsFromTestCaseObject(IEnumerable input) + { + var result = new List(); + + foreach (var testCase in input) + { + if (testCase.MinimumDotnetVersion.HasValue && Environment.Version.Major < testCase.MinimumDotnetVersion.Value) + { + continue; + } + + result.Add(new object[] { testCase }); + } + + return result; + } + + public class TestCase + { + public string Name { get; set; } = string.Empty; + + public int? MinimumDotnetVersion { get; set; } + + public TestApplicationScenario TestApplicationScenario { get; set; } + + public string? HttpMethod { get; set; } + + public string Path { get; set; } = string.Empty; + + public int ExpectedStatusCode { get; set; } + + public string? ExpectedHttpRoute { get; set; } + + public string? CurrentHttpRoute { get; set; } + + public override string ToString() + { + // This is used by Visual Studio's test runner to identify the test case. + return $"{this.TestApplicationScenario}: {this.Name}"; + } + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.json b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.json new file mode 100644 index 00000000000..2d1fa584ee8 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.json @@ -0,0 +1,211 @@ +[ + { + "name": "Root path", + "testApplicationScenario": "ConventionalRouting", + "httpMethod": "GET", + "path": "/", + "expectedStatusCode": 200, + "currentHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "expectedHttpRoute": "ConventionalRoute/Default/{id?}" + }, + { + "name": "Non-default action with route parameter and query string", + "testApplicationScenario": "ConventionalRouting", + "httpMethod": "GET", + "path": "/ConventionalRoute/ActionWithStringParameter/2?num=3", + "expectedStatusCode": 200, + "currentHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "expectedHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}" + }, + { + "name": "Non-default action with query string", + "testApplicationScenario": "ConventionalRouting", + "httpMethod": "GET", + "path": "/ConventionalRoute/ActionWithStringParameter?num=3", + "expectedStatusCode": 200, + "currentHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "expectedHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}" + }, + { + "name": "Not Found (404)", + "testApplicationScenario": "ConventionalRouting", + "httpMethod": "GET", + "path": "/ConventionalRoute/NotFound", + "expectedStatusCode": 404, + "currentHttpRoute": null, + "expectedHttpRoute": "" + }, + { + "name": "Route template with parameter constraint", + "testApplicationScenario": "ConventionalRouting", + "httpMethod": "GET", + "path": "/SomePath/SomeString/2", + "expectedStatusCode": 200, + "currentHttpRoute": null, + "expectedHttpRoute": "SomePath/{id}/{num:int}" + }, + { + "name": "Path that does not match parameter constraint", + "testApplicationScenario": "ConventionalRouting", + "httpMethod": "GET", + "path": "/SomePath/SomeString/NotAnInt", + "expectedStatusCode": 404, + "currentHttpRoute": null, + "expectedHttpRoute": "" + }, + { + "name": "Area using `area:exists`, default controller/action", + "testApplicationScenario": "ConventionalRouting", + "httpMethod": "GET", + "path": "/MyArea", + "expectedStatusCode": 200, + "currentHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "expectedHttpRoute": "{area:exists}/ControllerForMyArea/Default/{id?}" + }, + { + "name": "Area using `area:exists`, non-default action", + "testApplicationScenario": "ConventionalRouting", + "httpMethod": "GET", + "path": "/MyArea/ControllerForMyArea/NonDefault", + "expectedStatusCode": 200, + "currentHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "expectedHttpRoute": "{area:exists}/ControllerForMyArea/NonDefault/{id?}" + }, + { + "name": "Area w/o `area:exists`, default controller/action", + "testApplicationScenario": "ConventionalRouting", + "httpMethod": "GET", + "path": "/SomePrefix", + "expectedStatusCode": 200, + "currentHttpRoute": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}", + "expectedHttpRoute": "SomePrefix/AnotherArea/Index/{id?}" + }, + { + "name": "Default action", + "testApplicationScenario": "AttributeRouting", + "httpMethod": "GET", + "path": "/AttributeRoute", + "expectedStatusCode": 200, + "currentHttpRoute": null, + "expectedHttpRoute": "AttributeRoute" + }, + { + "name": "Action without parameter", + "testApplicationScenario": "AttributeRouting", + "httpMethod": "GET", + "path": "/AttributeRoute/Get", + "expectedStatusCode": 200, + "currentHttpRoute": null, + "expectedHttpRoute": "AttributeRoute/Get" + }, + { + "name": "Action with parameter", + "testApplicationScenario": "AttributeRouting", + "httpMethod": "GET", + "path": "/AttributeRoute/Get/12", + "expectedStatusCode": 200, + "currentHttpRoute": null, + "expectedHttpRoute": "AttributeRoute/Get/{id}" + }, + { + "name": "Action with parameter before action name in template", + "testApplicationScenario": "AttributeRouting", + "httpMethod": "GET", + "path": "/AttributeRoute/12/GetWithActionNameInDifferentSpotInTemplate", + "expectedStatusCode": 200, + "currentHttpRoute": null, + "expectedHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate" + }, + { + "name": "Action invoked resulting in 400 Bad Request", + "testApplicationScenario": "AttributeRouting", + "httpMethod": "GET", + "path": "/AttributeRoute/NotAnInt/GetWithActionNameInDifferentSpotInTemplate", + "expectedStatusCode": 400, + "currentHttpRoute": null, + "expectedHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate" + }, + { + "name": "Root path", + "testApplicationScenario": "RazorPages", + "httpMethod": "GET", + "path": "/", + "expectedStatusCode": 200, + "currentHttpRoute": "", + "expectedHttpRoute": "/Index" + }, + { + "name": "Index page", + "testApplicationScenario": "RazorPages", + "httpMethod": "GET", + "path": "/Index", + "expectedStatusCode": 200, + "currentHttpRoute": "Index", + "expectedHttpRoute": "/Index" + }, + { + "name": "Throws exception", + "testApplicationScenario": "RazorPages", + "httpMethod": "GET", + "path": "/PageThatThrowsException", + "expectedStatusCode": 500, + "currentHttpRoute": "PageThatThrowsException", + "expectedHttpRoute": "/PageThatThrowsException" + }, + { + "name": "Static content", + "testApplicationScenario": "RazorPages", + "httpMethod": "GET", + "path": "/js/site.js", + "expectedStatusCode": 200, + "currentHttpRoute": null, + "expectedHttpRoute": "" + }, + { + "name": "Action without parameter", + "testApplicationScenario": "MinimalApi", + "httpMethod": "GET", + "path": "/MinimalApi", + "expectedStatusCode": 200, + "currentHttpRoute": null, + "expectedHttpRoute": "/MinimalApi" + }, + { + "name": "Action with parameter", + "testApplicationScenario": "MinimalApi", + "httpMethod": "GET", + "path": "/MinimalApi/123", + "expectedStatusCode": 200, + "currentHttpRoute": null, + "expectedHttpRoute": "/MinimalApi/{id}" + }, + { + "name": "Action without parameter (MapGroup)", + "minimumDotnetVersion": 7, + "testApplicationScenario": "MinimalApi", + "httpMethod": "GET", + "path": "/MinimalApiUsingMapGroup", + "expectedStatusCode": 200, + "currentHttpRoute": null, + "expectedHttpRoute": "/MinimalApiUsingMapGroup/" + }, + { + "name": "Action with parameter (MapGroup)", + "minimumDotnetVersion": 7, + "testApplicationScenario": "MinimalApi", + "httpMethod": "GET", + "path": "/MinimalApiUsingMapGroup/123", + "expectedStatusCode": 200, + "currentHttpRoute": null, + "expectedHttpRoute": "/MinimalApiUsingMapGroup/{id}" + }, + { + "name": "Exception Handled by Exception Handler Middleware", + "testApplicationScenario": "ExceptionMiddleware", + "httpMethod": "GET", + "path": "/Exception", + "expectedStatusCode": 500, + "currentHttpRoute": null, + "expectedHttpRoute": "/Exception" + } +] diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestFixture.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestFixture.cs new file mode 100644 index 00000000000..1b949e26740 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestFixture.cs @@ -0,0 +1,108 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Text; +using Microsoft.AspNetCore.Builder; +using RouteTests.TestApplication; + +namespace RouteTests; + +public class RoutingTestFixture : IDisposable +{ + private static readonly HttpClient HttpClient = new(); + private readonly Dictionary apps = new(); + private readonly RouteInfoDiagnosticObserver diagnostics = new(); + private readonly List testResults = new(); + + public RoutingTestFixture() + { + foreach (var scenario in Enum.GetValues()) + { + var app = TestApplicationFactory.CreateApplication(scenario); + if (app != null) + { + this.apps.Add(scenario, app); + } + } + + foreach (var app in this.apps) + { + app.Value.RunAsync(); + } + } + + 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); + } + + public void AddTestResult(RoutingTestResult result) + { + this.testResults.Add(result); + } + + public void Dispose() + { + foreach (var app in this.apps) + { + app.Value.DisposeAsync().GetAwaiter().GetResult(); + } + + HttpClient.Dispose(); + this.diagnostics.Dispose(); + + this.GenerateReadme(); + } + + private void GenerateReadme() + { + var sb = new StringBuilder(); + sb.AppendLine($"# Test results for ASP.NET Core {Environment.Version.Major}"); + 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 emoji = result.TestCase.CurrentHttpRoute == null ? ":green_heart:" : ":broken_heart:"; + sb.AppendLine($"| {emoji} | {result.TestCase.TestApplicationScenario} | [{result.TestCase.Name}]({GenerateLinkFragment(result.TestCase.TestApplicationScenario, result.TestCase.Name)}) |"); + } + + for (var i = 0; i < this.testResults.Count; ++i) + { + var result = this.testResults[i]; + sb.AppendLine(); + sb.AppendLine($"## {result.TestCase.TestApplicationScenario}: {result.TestCase.Name}"); + sb.AppendLine(); + sb.AppendLine("```json"); + sb.AppendLine(result.ToString()); + sb.AppendLine("```"); + } + + var readmeFileName = $"README.net{Environment.Version.Major}.0.md"; + File.WriteAllText(Path.Combine("..", "..", "..", "RouteTests", readmeFileName), sb.ToString()); + + // Generates a link fragment that should comply with markdownlint rule MD051 + // https://github.com/DavidAnson/markdownlint/blob/main/doc/md051.md + static string GenerateLinkFragment(TestApplicationScenario scenario, string name) + { + var chars = name.ToCharArray() + .Where(c => (!char.IsPunctuation(c) && c != '`') || c == '-') + .Select(c => c switch + { + '-' => '-', + ' ' => '-', + _ => char.ToLower(c), + }) + .ToArray(); + + return $"#{scenario.ToString().ToLower()}-{new string(chars)}"; + } + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestResult.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestResult.cs new file mode 100644 index 00000000000..8217fa09872 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestResult.cs @@ -0,0 +1,33 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Text.Json; +using System.Text.Json.Serialization; +using RouteTests.TestApplication; + +namespace RouteTests; + +public class RoutingTestResult +{ + private static readonly JsonSerializerOptions JsonSerializerOptions = new() { WriteIndented = true }; + + public string? IdealHttpRoute { get; set; } + + public string ActivityDisplayName { get; set; } = string.Empty; + + public string? ActivityHttpRoute { get; set; } + + public string? MetricHttpRoute { get; set; } + + public RouteInfo RouteInfo { get; set; } = new RouteInfo(); + + [JsonIgnore] + public RoutingTestCases.TestCase TestCase { get; set; } = new RoutingTestCases.TestCase(); + + public override string ToString() + { + return JsonSerializer.Serialize(this, JsonSerializerOptions); + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTests.cs new file mode 100644 index 00000000000..b62bee47a67 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTests.cs @@ -0,0 +1,141 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Diagnostics; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; +using RouteTests.TestApplication; +using Xunit; + +namespace RouteTests; + +public class RoutingTests : IClassFixture +{ + private const string OldHttpStatusCode = "http.status_code"; + private const string OldHttpMethod = "http.method"; + private const string HttpStatusCode = "http.response.status_code"; + private const string HttpMethod = "http.request.method"; + private const string HttpRoute = "http.route"; + + private readonly RoutingTestFixture fixture; + private readonly List exportedActivities = new(); + private readonly List exportedMetrics = new(); + + public RoutingTests(RoutingTestFixture fixture) + { + this.fixture = fixture; + } + + public static IEnumerable TestData => RoutingTestCases.GetTestCases(); + + [Theory] + [MemberData(nameof(TestData))] + public async Task TestHttpRoute(RoutingTestCases.TestCase testCase) + { + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(this.exportedActivities) + .Build()!; + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(this.exportedMetrics) + .Build()!; + + await this.fixture.MakeRequest(testCase.TestApplicationScenario, testCase.Path); + + for (var i = 0; i < 10; i++) + { + if (this.exportedActivities.Count > 0) + { + break; + } + + await Task.Delay(TimeSpan.FromSeconds(1)); + } + + meterProvider.ForceFlush(); + + var durationMetric = this.exportedMetrics.Single(x => x.Name == "http.server.request.duration" || x.Name == "http.server.duration"); + var metricPoints = new List(); + foreach (var mp in durationMetric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + var activity = Assert.Single(this.exportedActivities); + var metricPoint = Assert.Single(metricPoints); + + 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 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); + + // 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}"; + + Assert.Equal(expectedActivityDisplayName, activity.DisplayName); + + var testResult = new RoutingTestResult + { + IdealHttpRoute = testCase.ExpectedHttpRoute, + ActivityDisplayName = activity.DisplayName, + ActivityHttpRoute = activityHttpRoute, + MetricHttpRoute = metricHttpRoute, + TestCase = testCase, + RouteInfo = RouteInfo.Current, + }; + + this.fixture.AddTestResult(testResult); + } + + private static void GetTagsFromActivity(Activity activity, out int httpStatusCode, out string httpMethod, out string? httpRoute) + { + 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; + } + + private static void GetTagsFromMetricPoint(bool useLegacyConventions, MetricPoint metricPoint, out int httpStatusCode, out string httpMethod, out string? httpRoute) + { + var expectedStatusCodeKey = HttpStatusCode; + var expectedHttpMethodKey = HttpMethod; + + httpStatusCode = 0; + httpMethod = string.Empty; + httpRoute = string.Empty; + + foreach (var tag in metricPoint.Tags) + { + if (tag.Key.Equals(expectedStatusCodeKey)) + { + httpStatusCode = Convert.ToInt32(tag.Value); + } + else if (tag.Key.Equals(expectedHttpMethodKey)) + { + httpMethod = (tag.Value as string)!; + } + else if (tag.Key.Equals(HttpRoute)) + { + httpRoute = tag.Value as string; + } + } + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Areas/AnotherArea/Controllers/AnotherAreaController.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Areas/AnotherArea/Controllers/AnotherAreaController.cs new file mode 100644 index 00000000000..7754255edfd --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Areas/AnotherArea/Controllers/AnotherAreaController.cs @@ -0,0 +1,14 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable disable + +using Microsoft.AspNetCore.Mvc; + +namespace RouteTests.Controllers; + +[Area("AnotherArea")] +public class AnotherAreaController : Controller +{ + public IActionResult Index() => this.Ok(); +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Areas/MyArea/Controllers/ControllerForMyAreaController.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Areas/MyArea/Controllers/ControllerForMyAreaController.cs new file mode 100644 index 00000000000..762f4a95e82 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Areas/MyArea/Controllers/ControllerForMyAreaController.cs @@ -0,0 +1,16 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable disable + +using Microsoft.AspNetCore.Mvc; + +namespace RouteTests.Controllers; + +[Area("MyArea")] +public class ControllerForMyAreaController : Controller +{ + public IActionResult Default() => this.Ok(); + + public IActionResult NonDefault() => this.Ok(); +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Controllers/AttributeRouteController.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Controllers/AttributeRouteController.cs new file mode 100644 index 00000000000..b1e5783b0af --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Controllers/AttributeRouteController.cs @@ -0,0 +1,23 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable disable + +using Microsoft.AspNetCore.Mvc; + +namespace RouteTests.Controllers; + +[ApiController] +[Route("[controller]")] +public class AttributeRouteController : ControllerBase +{ + [HttpGet] + [HttpGet("[action]")] + public IActionResult Get() => this.Ok(); + + [HttpGet("[action]/{id}")] + public IActionResult Get(int id) => this.Ok(); + + [HttpGet("{id}/[action]")] + public IActionResult GetWithActionNameInDifferentSpotInTemplate(int id) => this.Ok(); +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Controllers/ConventionalRouteController.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Controllers/ConventionalRouteController.cs new file mode 100644 index 00000000000..977ee36a136 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Controllers/ConventionalRouteController.cs @@ -0,0 +1,17 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable disable + +using Microsoft.AspNetCore.Mvc; + +namespace RouteTests.Controllers; + +public class ConventionalRouteController : Controller +{ + public IActionResult Default() => this.Ok(); + + public IActionResult ActionWithParameter(int id) => this.Ok(); + + public IActionResult ActionWithStringParameter(string id, int num) => this.Ok(); +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Pages/Index.cshtml b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Pages/Index.cshtml new file mode 100644 index 00000000000..51c350f9565 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Pages/Index.cshtml @@ -0,0 +1,2 @@ +@page +Hello, OpenTelemetry! diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Pages/PageThatThrowsException.cshtml b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Pages/PageThatThrowsException.cshtml new file mode 100644 index 00000000000..cf6ac0d5b81 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Pages/PageThatThrowsException.cshtml @@ -0,0 +1,4 @@ +@page +@{ + throw new Exception("Oops."); +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfo.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfo.cs new file mode 100644 index 00000000000..78587210394 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfo.cs @@ -0,0 +1,138 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Http; +#if NET8_0_OR_GREATER +using Microsoft.AspNetCore.Http.Metadata; +#endif +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Routing; + +namespace RouteTests.TestApplication; + +public class RouteInfo +{ + public static RouteInfo Current { get; set; } = new(); + + public string? HttpMethod { get; set; } + + public string? Path { get; set; } + + [JsonPropertyName("RoutePattern.RawText")] + public string? RawText { get; set; } + + [JsonPropertyName("IRouteDiagnosticsMetadata.Route")] + public string? RouteDiagnosticMetadata { get; set; } + + [JsonPropertyName("HttpContext.GetRouteData()")] + public IDictionary? RouteData { get; set; } + + public ActionDescriptorInfo? ActionDescriptor { get; set; } + + public void SetValues(HttpContext context) + { + this.HttpMethod = context.Request.Method; + this.Path = $"{context.Request.Path}{context.Request.QueryString}"; + var endpoint = context.GetEndpoint(); + this.RawText = (endpoint as RouteEndpoint)?.RoutePattern.RawText; +#if NET8_0_OR_GREATER + this.RouteDiagnosticMetadata = endpoint?.Metadata.GetMetadata()?.Route; +#endif + this.RouteData = new Dictionary(); + foreach (var value in context.GetRouteData().Values) + { + this.RouteData[value.Key] = value.Value?.ToString(); + } + } + + public void SetValues(ActionDescriptor actionDescriptor) + { + if (this.ActionDescriptor == null) + { + this.ActionDescriptor = new ActionDescriptorInfo(actionDescriptor); + } + } + + public class ActionDescriptorInfo + { + public ActionDescriptorInfo() + { + } + + public ActionDescriptorInfo(ActionDescriptor actionDescriptor) + { + this.AttributeRouteInfo = actionDescriptor.AttributeRouteInfo?.Template; + + this.ActionParameters = new List(); + foreach (var item in actionDescriptor.Parameters) + { + this.ActionParameters.Add(item.Name); + } + + if (actionDescriptor is PageActionDescriptor pad) + { + this.PageActionDescriptorSummary = new PageActionDescriptorInfo(pad.RelativePath, pad.ViewEnginePath); + } + + if (actionDescriptor is ControllerActionDescriptor cad) + { + this.ControllerActionDescriptorSummary = new ControllerActionDescriptorInfo(cad.ControllerName, cad.ActionName); + } + } + + [JsonPropertyName("AttributeRouteInfo.Template")] + public string? AttributeRouteInfo { get; set; } + + [JsonPropertyName("Parameters")] + public IList? ActionParameters { get; set; } + + [JsonPropertyName("ControllerActionDescriptor")] + public ControllerActionDescriptorInfo? ControllerActionDescriptorSummary { get; set; } + + [JsonPropertyName("PageActionDescriptor")] + public PageActionDescriptorInfo? PageActionDescriptorSummary { get; set; } + } + + public class ControllerActionDescriptorInfo + { + public ControllerActionDescriptorInfo() + { + } + + public ControllerActionDescriptorInfo(string controllerName, string actionName) + { + this.ControllerActionDescriptorControllerName = controllerName; + this.ControllerActionDescriptorActionName = actionName; + } + + [JsonPropertyName("ControllerName")] + public string ControllerActionDescriptorControllerName { get; set; } = string.Empty; + + [JsonPropertyName("ActionName")] + public string ControllerActionDescriptorActionName { get; set; } = string.Empty; + } + + public class PageActionDescriptorInfo + { + public PageActionDescriptorInfo() + { + } + + public PageActionDescriptorInfo(string relativePath, string viewEnginePath) + { + this.PageActionDescriptorRelativePath = relativePath; + this.PageActionDescriptorViewEnginePath = viewEnginePath; + } + + [JsonPropertyName("RelativePath")] + public string PageActionDescriptorRelativePath { get; set; } = string.Empty; + + [JsonPropertyName("ViewEnginePath")] + public string PageActionDescriptorViewEnginePath { get; set; } = string.Empty; + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfoDiagnosticObserver.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfoDiagnosticObserver.cs new file mode 100644 index 00000000000..3b3feb59e15 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfoDiagnosticObserver.cs @@ -0,0 +1,110 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Diagnostics; + +namespace RouteTests.TestApplication; + +/// +/// This observer captures all the available route information for a request. +/// This route information is used for generating a README file for analyzing +/// what information is available in different scenarios. +/// +internal sealed class RouteInfoDiagnosticObserver : IDisposable, IObserver, IObserver> +{ + 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"; + + private readonly List listenerSubscriptions = new(); + private IDisposable? allSourcesSubscription; + private long disposed; + + public RouteInfoDiagnosticObserver() + { + this.allSourcesSubscription = DiagnosticListener.AllListeners.Subscribe(this); + } + + public void OnNext(DiagnosticListener value) + { + if (value.Name == "Microsoft.AspNetCore") + { + var subscription = value.Subscribe(this); + + lock (this.listenerSubscriptions) + { + this.listenerSubscriptions.Add(subscription); + } + } + } + + public void OnNext(KeyValuePair value) + { + HttpContext? context; + BeforeActionEventData? actionMethodEventData; + RouteInfo? info; + + switch (value.Key) + { + case OnStartEvent: + context = value.Value as HttpContext; + Debug.Assert(context != null, "HttpContext was null"); + info = new RouteInfo(); + info.SetValues(context); + RouteInfo.Current = info; + break; + case OnMvcBeforeActionEvent: + actionMethodEventData = value.Value as BeforeActionEventData; + Debug.Assert(actionMethodEventData != null, $"expected {nameof(BeforeActionEventData)}"); + RouteInfo.Current.SetValues(actionMethodEventData.HttpContext); + RouteInfo.Current.SetValues(actionMethodEventData.ActionDescriptor); + break; + case OnStopEvent: + context = value.Value as HttpContext; + Debug.Assert(context != null, "HttpContext was null"); + RouteInfo.Current.SetValues(context); + break; + default: + break; + } + } + + public void OnCompleted() + { + } + + public void OnError(Exception error) + { + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (Interlocked.CompareExchange(ref this.disposed, 1, 0) == 1) + { + return; + } + + lock (this.listenerSubscriptions) + { + foreach (var listenerSubscription in this.listenerSubscriptions) + { + listenerSubscription?.Dispose(); + } + + this.listenerSubscriptions.Clear(); + } + + this.allSourcesSubscription?.Dispose(); + this.allSourcesSubscription = null; + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/TestApplicationFactory.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/TestApplicationFactory.cs new file mode 100644 index 00000000000..b030ab7f42b --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/TestApplicationFactory.cs @@ -0,0 +1,199 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Diagnostics; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; + +namespace RouteTests.TestApplication; + +public enum TestApplicationScenario +{ + /// + /// An application that uses conventional routing. + /// + ConventionalRouting, + + /// + /// An application that uses attribute routing. + /// + AttributeRouting, + + /// + /// A Minimal API application. + /// + MinimalApi, + + /// + /// An Razor Pages application. + /// + RazorPages, + + /// + /// Application with Exception Handling Middleware. + /// + ExceptionMiddleware, +} + +internal class TestApplicationFactory +{ + private static readonly string AspNetCoreTestsPath = new FileInfo(typeof(RoutingTests)!.Assembly!.Location)!.Directory!.Parent!.Parent!.Parent!.FullName; + private static readonly string ContentRootPath = Path.Combine(AspNetCoreTestsPath, "RouteTests", "TestApplication"); + + public static WebApplication? CreateApplication(TestApplicationScenario config) + { + Debug.Assert(Directory.Exists(ContentRootPath), $"Cannot find ContentRootPath: {ContentRootPath}"); + switch (config) + { + case TestApplicationScenario.ConventionalRouting: + return CreateConventionalRoutingApplication(); + case TestApplicationScenario.AttributeRouting: + return CreateAttributeRoutingApplication(); + case TestApplicationScenario.MinimalApi: + return CreateMinimalApiApplication(); + case TestApplicationScenario.RazorPages: + return CreateRazorPagesApplication(); + case TestApplicationScenario.ExceptionMiddleware: + return CreateExceptionHandlerApplication(); + default: + throw new ArgumentException($"Invalid {nameof(TestApplicationScenario)}"); + } + } + + private static WebApplication CreateConventionalRoutingApplication() + { + var builder = WebApplication.CreateBuilder(new WebApplicationOptions { ContentRootPath = ContentRootPath }); + builder.Logging.ClearProviders(); + + builder.Services + .AddControllersWithViews() + .AddApplicationPart(typeof(RoutingTests).Assembly); + + var app = builder.Build(); + app.Urls.Clear(); + app.Urls.Add("http://[::1]:0"); + app.UseStaticFiles(); + app.UseRouting(); + + app.MapAreaControllerRoute( + name: "AnotherArea", + areaName: "AnotherArea", + pattern: "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}"); + + app.MapControllerRoute( + name: "MyArea", + pattern: "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}"); + + app.MapControllerRoute( + name: "FixedRouteWithConstraints", + pattern: "SomePath/{id}/{num:int}", + defaults: new { controller = "ConventionalRoute", action = "ActionWithStringParameter" }); + + app.MapControllerRoute( + name: "default", + pattern: "{controller=ConventionalRoute}/{action=Default}/{id?}"); + + return app; + } + + private static WebApplication CreateAttributeRoutingApplication() + { + var builder = WebApplication.CreateBuilder(); + builder.Logging.ClearProviders(); + + builder.Services + .AddControllers() + .AddApplicationPart(typeof(RoutingTests).Assembly); + + var app = builder.Build(); + app.Urls.Clear(); + app.Urls.Add("http://[::1]:0"); + app.MapControllers(); + + return app; + } + + private static WebApplication CreateMinimalApiApplication() + { + var builder = WebApplication.CreateBuilder(); + builder.Logging.ClearProviders(); + + var app = builder.Build(); + app.Urls.Clear(); + app.Urls.Add("http://[::1]:0"); + + app.MapGet("/MinimalApi", () => Results.Ok()); + app.MapGet("/MinimalApi/{id}", (int id) => Results.Ok()); + +#if NET7_0_OR_GREATER + var api = app.MapGroup("/MinimalApiUsingMapGroup"); + api.MapGet("/", () => Results.Ok()); + api.MapGet("/{id}", (int id) => Results.Ok()); +#endif + + return app; + } + + private static WebApplication CreateRazorPagesApplication() + { + var builder = WebApplication.CreateBuilder(new WebApplicationOptions { ContentRootPath = ContentRootPath }); + builder.Logging.ClearProviders(); + + builder.Services + .AddRazorPages() + .AddRazorRuntimeCompilation(options => + { + options.FileProviders.Add(new PhysicalFileProvider(ContentRootPath)); + }) + .AddApplicationPart(typeof(RoutingTests).Assembly); + + var app = builder.Build(); + app.Urls.Clear(); + app.Urls.Add("http://[::1]:0"); + app.UseStaticFiles(); + app.UseRouting(); + app.MapRazorPages(); + + return app; + } + + private static WebApplication CreateExceptionHandlerApplication() + { + var builder = WebApplication.CreateBuilder(); + builder.Logging.ClearProviders(); + + var app = builder.Build(); + + app.UseExceptionHandler(exceptionHandlerApp => + { + exceptionHandlerApp.Run(async context => + { + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + var exceptionHandlerPathFeature = context.Features.Get(); + await context.Response.WriteAsync(exceptionHandlerPathFeature?.Error.Message ?? "An exception was thrown."); + }); + }); + + app.Urls.Clear(); + app.Urls.Add("http://[::1]:0"); + + // TODO: Remove this condition once ASP.NET Core 8.0.2. + // Currently, .NET 8 has a different behavior than .NET 6 and 7. + // This is because ASP.NET Core 8+ has native metric instrumentation. + // When ASP.NET Core 8.0.2 is released then its behavior will align with .NET 6/7. + // See: https://github.com/dotnet/aspnetcore/issues/52648#issuecomment-1853432776 +#if !NET8_0_OR_GREATER + app.MapGet("/Exception", (ctx) => throw new ApplicationException()); +#else + app.MapGet("/Exception", () => Results.Content(content: "Error", contentType: null, contentEncoding: null, statusCode: 500)); +#endif + + return app; + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/wwwroot/js/site.js b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/wwwroot/js/site.js new file mode 100644 index 00000000000..dcc7262061a --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/wwwroot/js/site.js @@ -0,0 +1,4 @@ +// Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification +// for details on configuring this project to bundle and minify static web assets. + +// Write your JavaScript code. diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/EventSourceTest.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/EventSourceTest.cs index 3139e55e105..66db1766771 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/EventSourceTest.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/EventSourceTest.cs @@ -1,31 +1,17 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Instrumentation.GrpcNetClient.Implementation; using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Instrumentation.Grpc.Tests +namespace OpenTelemetry.Instrumentation.Grpc.Tests; + +public class EventSourceTest { - public class EventSourceTest + [Fact] + public void EventSourceTest_GrpcInstrumentationEventSource() { - [Fact] - public void EventSourceTest_GrpcInstrumentationEventSource() - { - EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(GrpcInstrumentationEventSource.Log); - } + EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(GrpcInstrumentationEventSource.Log); } } diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcServer.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcServer.cs index 85e7e11967a..08fa9e8c7b9 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcServer.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcServer.cs @@ -1,104 +1,89 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #if !NETFRAMEWORK -using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace OpenTelemetry.Instrumentation.Grpc.Tests +namespace OpenTelemetry.Instrumentation.Grpc.Tests; + +public class GrpcServer : IDisposable + where TService : class { - public class GrpcServer : IDisposable - where TService : class - { - private static readonly Random GlobalRandom = new(); + private static readonly Random GlobalRandom = new(); - private readonly IHost host; + private readonly IHost host; - public GrpcServer() - { - // Allows gRPC client to call insecure gRPC services - // https://docs.microsoft.com/aspnet/core/grpc/troubleshoot?view=aspnetcore-3.1#call-insecure-grpc-services-with-net-core-client - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + public GrpcServer() + { + // Allows gRPC client to call insecure gRPC services + // https://docs.microsoft.com/aspnet/core/grpc/troubleshoot?view=aspnetcore-3.1#call-insecure-grpc-services-with-net-core-client + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - this.Port = 0; + this.Port = 0; - var retryCount = 5; - while (retryCount > 0) + var retryCount = 5; + while (retryCount > 0) + { + try + { + this.Port = GlobalRandom.Next(2000, 5000); + this.host = this.CreateServer(); + this.host.StartAsync().GetAwaiter().GetResult(); + break; + } + catch (IOException) { - try - { - this.Port = GlobalRandom.Next(2000, 5000); - this.host = this.CreateServer(); - this.host.StartAsync().GetAwaiter().GetResult(); - break; - } - catch (System.IO.IOException) - { - retryCount--; - this.host.Dispose(); - } + retryCount--; + this.host.Dispose(); } } + } - public int Port { get; } + public int Port { get; } - public void Dispose() - { - this.host.StopAsync(TimeSpan.FromSeconds(5)).GetAwaiter().GetResult(); - this.host.Dispose(); - GC.SuppressFinalize(this); - } + public void Dispose() + { + this.host.StopAsync(TimeSpan.FromSeconds(5)).GetAwaiter().GetResult(); + this.host.Dispose(); + GC.SuppressFinalize(this); + } - private IHost CreateServer() - { - var hostBuilder = Host.CreateDefaultBuilder() - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder - .ConfigureKestrel(options => - { - // Setup a HTTP/2 endpoint without TLS. - options.ListenLocalhost(this.Port, o => o.Protocols = HttpProtocols.Http2); - }) - .UseStartup(); - }); + private IHost CreateServer() + { + var hostBuilder = Host.CreateDefaultBuilder() + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder + .ConfigureKestrel(options => + { + // Setup a HTTP/2 endpoint without TLS. + options.ListenLocalhost(this.Port, o => o.Protocols = HttpProtocols.Http2); + }) + .UseStartup(); + }); - return hostBuilder.Build(); + return hostBuilder.Build(); + } + + private class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddGrpc(); } - private class Startup + public void Configure(IApplicationBuilder app) { - public void ConfigureServices(IServiceCollection services) - { - services.AddGrpc(); - } + app.UseRouting(); - public void Configure(IApplicationBuilder app) + app.UseEndpoints(endpoints => { - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapGrpcService(); - }); - } + endpoints.MapGrpcService(); + }); } } } diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTagHelperTests.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTagHelperTests.cs index f4d753044d0..fbb1928117d 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTagHelperTests.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTagHelperTests.cs @@ -1,79 +1,66 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + using System.Diagnostics; using OpenTelemetry.Instrumentation.GrpcNetClient; using OpenTelemetry.Trace; using Xunit; -namespace OpenTelemetry.Instrumentation.Grpc.Tests +namespace OpenTelemetry.Instrumentation.Grpc.Tests; + +public class GrpcTagHelperTests { - public class GrpcTagHelperTests + [Fact] + public void GrpcTagHelper_GetGrpcMethodFromActivity() { - [Fact] - public void GrpcTagHelper_GetGrpcMethodFromActivity() - { - var grpcMethod = "/some.service/somemethod"; - using var activity = new Activity("operationName"); - activity.SetTag(GrpcTagHelper.GrpcMethodTagName, grpcMethod); + var grpcMethod = "/some.service/somemethod"; + using var activity = new Activity("operationName"); + activity.SetTag(GrpcTagHelper.GrpcMethodTagName, grpcMethod); - var result = GrpcTagHelper.GetGrpcMethodFromActivity(activity); + var result = GrpcTagHelper.GetGrpcMethodFromActivity(activity); - Assert.Equal(grpcMethod, result); - } + Assert.Equal(grpcMethod, result); + } - [Theory] - [InlineData("Package.Service/Method", true, "Package.Service", "Method")] - [InlineData("/Package.Service/Method", true, "Package.Service", "Method")] - [InlineData("/ServiceWithNoPackage/Method", true, "ServiceWithNoPackage", "Method")] - [InlineData("/Some.Package.Service/Method", true, "Some.Package.Service", "Method")] - [InlineData("Invalid", false, "", "")] - public void GrpcTagHelper_TryParseRpcServiceAndRpcMethod(string grpcMethod, bool isSuccess, string expectedRpcService, string expectedRpcMethod) - { - var success = GrpcTagHelper.TryParseRpcServiceAndRpcMethod(grpcMethod, out var rpcService, out var rpcMethod); + [Theory] + [InlineData("Package.Service/Method", true, "Package.Service", "Method")] + [InlineData("/Package.Service/Method", true, "Package.Service", "Method")] + [InlineData("/ServiceWithNoPackage/Method", true, "ServiceWithNoPackage", "Method")] + [InlineData("/Some.Package.Service/Method", true, "Some.Package.Service", "Method")] + [InlineData("Invalid", false, "", "")] + public void GrpcTagHelper_TryParseRpcServiceAndRpcMethod(string grpcMethod, bool isSuccess, string expectedRpcService, string expectedRpcMethod) + { + var success = GrpcTagHelper.TryParseRpcServiceAndRpcMethod(grpcMethod, out var rpcService, out var rpcMethod); - Assert.Equal(isSuccess, success); - Assert.Equal(expectedRpcService, rpcService); - Assert.Equal(expectedRpcMethod, rpcMethod); - } + Assert.Equal(isSuccess, success); + Assert.Equal(expectedRpcService, rpcService); + Assert.Equal(expectedRpcMethod, rpcMethod); + } - [Fact] - public void GrpcTagHelper_GetGrpcStatusCodeFromActivity() - { - using var activity = new Activity("operationName"); - activity.SetTag(GrpcTagHelper.GrpcStatusCodeTagName, "0"); + [Fact] + public void GrpcTagHelper_GetGrpcStatusCodeFromActivity() + { + using var activity = new Activity("operationName"); + activity.SetTag(GrpcTagHelper.GrpcStatusCodeTagName, "0"); - bool validConversion = GrpcTagHelper.TryGetGrpcStatusCodeFromActivity(activity, out int status); - Assert.True(validConversion); + bool validConversion = GrpcTagHelper.TryGetGrpcStatusCodeFromActivity(activity, out int status); + Assert.True(validConversion); - var statusCode = GrpcTagHelper.ResolveSpanStatusForGrpcStatusCode(status); - activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, status); + var statusCode = GrpcTagHelper.ResolveSpanStatusForGrpcStatusCode(status); + activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, status); - Assert.Equal(ActivityStatusCode.Unset, statusCode); - Assert.Equal(status, activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); - } + Assert.Equal(ActivityStatusCode.Unset, statusCode); + Assert.Equal(status, activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); + } - [Fact] - public void GrpcTagHelper_GetGrpcStatusCodeFromEmptyActivity() - { - using var activity = new Activity("operationName"); + [Fact] + public void GrpcTagHelper_GetGrpcStatusCodeFromEmptyActivity() + { + using var activity = new Activity("operationName"); - bool validConversion = GrpcTagHelper.TryGetGrpcStatusCodeFromActivity(activity, out int status); - Assert.False(validConversion); - Assert.Equal(-1, status); - Assert.Null(activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); - } + bool validConversion = GrpcTagHelper.TryGetGrpcStatusCodeFromActivity(activity, out int status); + Assert.False(validConversion); + Assert.Equal(-1, status); + Assert.Null(activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); } } diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ClientTestHelpers.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ClientTestHelpers.cs index e0f3f76196b..27c8b5a374a 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ClientTestHelpers.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ClientTestHelpers.cs @@ -1,88 +1,76 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +#if NETFRAMEWORK using System.Net.Http; +#endif using System.Net.Http.Headers; using Google.Protobuf; using Grpc.Net.Compression; -namespace OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers +namespace OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers; + +internal static class ClientTestHelpers { - internal static class ClientTestHelpers + public static HttpClient CreateTestClient(Func> sendAsync, Uri baseAddress = null) { - public static HttpClient CreateTestClient(Func> sendAsync, Uri baseAddress = null) - { - var handler = TestHttpMessageHandler.Create(sendAsync); - var httpClient = new HttpClient(handler); - httpClient.BaseAddress = baseAddress ?? new Uri("https://localhost"); + var handler = TestHttpMessageHandler.Create(sendAsync); + var httpClient = new HttpClient(handler); + httpClient.BaseAddress = baseAddress ?? new Uri("https://localhost"); - return httpClient; - } + return httpClient; + } - public static Task CreateResponseContent(TResponse response, ICompressionProvider compressionProvider = null) - where TResponse : IMessage - { - return CreateResponseContentCore(new[] { response }, compressionProvider); - } + public static Task CreateResponseContent(TResponse response, ICompressionProvider compressionProvider = null) + where TResponse : IMessage + { + return CreateResponseContentCore(new[] { response }, compressionProvider); + } - public static async Task WriteResponseAsync(Stream ms, TResponse response, ICompressionProvider compressionProvider) - where TResponse : IMessage - { - var compress = false; + public static async Task WriteResponseAsync(Stream ms, TResponse response, ICompressionProvider compressionProvider) + where TResponse : IMessage + { + var compress = false; - byte[] data; - if (compressionProvider != null) - { - compress = true; + byte[] data; + if (compressionProvider != null) + { + compress = true; - var output = new MemoryStream(); - var compressionStream = compressionProvider.CreateCompressionStream(output, System.IO.Compression.CompressionLevel.Fastest); - var compressedData = response.ToByteArray(); + var output = new MemoryStream(); + var compressionStream = compressionProvider.CreateCompressionStream(output, System.IO.Compression.CompressionLevel.Fastest); + var compressedData = response.ToByteArray(); - compressionStream.Write(compressedData, 0, compressedData.Length); - compressionStream.Flush(); - compressionStream.Dispose(); - data = output.ToArray(); - } - else - { - data = response.ToByteArray(); - } + compressionStream.Write(compressedData, 0, compressedData.Length); + compressionStream.Flush(); + compressionStream.Dispose(); + data = output.ToArray(); + } + else + { + 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 - } + } - private static async Task CreateResponseContentCore(TResponse[] responses, ICompressionProvider compressionProvider) - where TResponse : IMessage + private static async Task CreateResponseContentCore(TResponse[] responses, ICompressionProvider compressionProvider) + where TResponse : IMessage + { + var ms = new MemoryStream(); + foreach (var response in responses) { - var ms = new MemoryStream(); - foreach (var response in responses) - { - await WriteResponseAsync(ms, response, compressionProvider).ConfigureAwait(false); - } - - ms.Seek(0, SeekOrigin.Begin); - var streamContent = new StreamContent(ms); - streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/grpc"); - return streamContent; + await WriteResponseAsync(ms, response, compressionProvider); } + + ms.Seek(0, SeekOrigin.Begin); + var streamContent = new StreamContent(ms); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/grpc"); + return streamContent; } } diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ResponseUtils.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ResponseUtils.cs index 244f5df0281..01180ebf54a 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ResponseUtils.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ResponseUtils.cs @@ -1,88 +1,76 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Buffers.Binary; using System.Diagnostics; using System.Net; +#if NETFRAMEWORK using System.Net.Http; +#endif using System.Net.Http.Headers; -namespace OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers +namespace OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers; + +internal static class ResponseUtils { - internal static class ResponseUtils + internal const string MessageEncodingHeader = "grpc-encoding"; + internal const string IdentityGrpcEncoding = "identity"; + internal const string StatusTrailer = "grpc-status"; + internal static readonly MediaTypeHeaderValue GrpcContentTypeHeaderValue = new MediaTypeHeaderValue("application/grpc"); + internal static readonly Version ProtocolVersion = new Version(2, 0); + private const int MessageDelimiterSize = 4; // how many bytes it takes to encode "Message-Length" + private const int HeaderSize = MessageDelimiterSize + 1; // message length + compression flag + + public static HttpResponseMessage CreateResponse( + HttpStatusCode statusCode, + HttpContent payload, + global::Grpc.Core.StatusCode? grpcStatusCode = global::Grpc.Core.StatusCode.OK) { - internal const string MessageEncodingHeader = "grpc-encoding"; - internal const string IdentityGrpcEncoding = "identity"; - internal const string StatusTrailer = "grpc-status"; - internal static readonly MediaTypeHeaderValue GrpcContentTypeHeaderValue = new MediaTypeHeaderValue("application/grpc"); - internal static readonly Version ProtocolVersion = new Version(2, 0); - private const int MessageDelimiterSize = 4; // how many bytes it takes to encode "Message-Length" - private const int HeaderSize = MessageDelimiterSize + 1; // message length + compression flag + payload.Headers.ContentType = GrpcContentTypeHeaderValue; - public static HttpResponseMessage CreateResponse( - HttpStatusCode statusCode, - HttpContent payload, - global::Grpc.Core.StatusCode? grpcStatusCode = global::Grpc.Core.StatusCode.OK) + var message = new HttpResponseMessage(statusCode) { - payload.Headers.ContentType = GrpcContentTypeHeaderValue; - - var message = new HttpResponseMessage(statusCode) - { - Content = payload, - Version = ProtocolVersion, - }; + Content = payload, + Version = ProtocolVersion, + }; - message.RequestMessage = new HttpRequestMessage(); + message.RequestMessage = new HttpRequestMessage(); #if NETFRAMEWORK - message.RequestMessage.Properties[TrailingHeadersHelpers.ResponseTrailersKey] = new ResponseTrailers(); + message.RequestMessage.Properties[TrailingHeadersHelpers.ResponseTrailersKey] = new ResponseTrailers(); #endif - message.Headers.Add(MessageEncodingHeader, IdentityGrpcEncoding); - - if (grpcStatusCode != null) - { - message.TrailingHeaders().Add(StatusTrailer, grpcStatusCode.Value.ToString("D")); - } + message.Headers.Add(MessageEncodingHeader, IdentityGrpcEncoding); - return message; + if (grpcStatusCode != null) + { + message.TrailingHeaders().Add(StatusTrailer, grpcStatusCode.Value.ToString("D")); } - public static Task WriteHeaderAsync(Stream stream, int length, bool compress, CancellationToken cancellationToken) - { - var headerData = new byte[HeaderSize]; + return message; + } - // Compression flag - headerData[0] = compress ? (byte)1 : (byte)0; + public static Task WriteHeaderAsync(Stream stream, int length, bool compress, CancellationToken cancellationToken) + { + var headerData = new byte[HeaderSize]; - // Message length - EncodeMessageLength(length, headerData.AsSpan(1)); + // Compression flag + headerData[0] = compress ? (byte)1 : (byte)0; - return stream.WriteAsync(headerData, 0, headerData.Length, cancellationToken); - } + // Message length + EncodeMessageLength(length, headerData.AsSpan(1)); - private static void EncodeMessageLength(int messageLength, Span destination) - { - Debug.Assert(destination.Length >= MessageDelimiterSize, "Buffer too small to encode message length."); + return stream.WriteAsync(headerData, 0, headerData.Length, cancellationToken); + } - BinaryPrimitives.WriteUInt32BigEndian(destination, (uint)messageLength); - } + private static void EncodeMessageLength(int messageLength, Span destination) + { + Debug.Assert(destination.Length >= MessageDelimiterSize, "Buffer too small to encode message length."); + + BinaryPrimitives.WriteUInt32BigEndian(destination, (uint)messageLength); + } #if NETFRAMEWORK - private class ResponseTrailers : HttpHeaders - { - } -#endif + private class ResponseTrailers : HttpHeaders + { } +#endif } diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TestHttpMessageHandler.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TestHttpMessageHandler.cs index 0681fe7f6c4..4f1837ce700 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TestHttpMessageHandler.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TestHttpMessageHandler.cs @@ -1,53 +1,41 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +#if NETFRAMEWORK using System.Net.Http; +#endif -namespace OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers +namespace OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers; + +public class TestHttpMessageHandler : HttpMessageHandler { - public class TestHttpMessageHandler : HttpMessageHandler + private readonly Func> sendAsync; + + public TestHttpMessageHandler(Func> sendAsync) { - private readonly Func> sendAsync; + this.sendAsync = sendAsync; + } - public TestHttpMessageHandler(Func> sendAsync) - { - this.sendAsync = sendAsync; - } + public static TestHttpMessageHandler Create(Func> sendAsync) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - public static TestHttpMessageHandler Create(Func> sendAsync) + return new TestHttpMessageHandler(async (request, cancellationToken) => { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - return new TestHttpMessageHandler(async (request, cancellationToken) => - { - using var registration = cancellationToken.Register(() => tcs.TrySetCanceled()); + using var registration = cancellationToken.Register(() => 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; + }); + } - public static TestHttpMessageHandler Create(Func> sendAsync) - { - return new TestHttpMessageHandler(sendAsync); - } + public static TestHttpMessageHandler Create(Func> sendAsync) + { + return new TestHttpMessageHandler(sendAsync); + } - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - return this.sendAsync(request, cancellationToken); - } + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return this.sendAsync(request, cancellationToken); } } diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TrailingHeadersHelpers.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TrailingHeadersHelpers.cs index 429f1d1b581..b54f0be65bc 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TrailingHeadersHelpers.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TrailingHeadersHelpers.cs @@ -1,59 +1,47 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +#if NETFRAMEWORK using System.Net.Http; +#endif using System.Net.Http.Headers; -namespace OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers +namespace OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers; + +internal static class TrailingHeadersHelpers { - internal static class TrailingHeadersHelpers - { - public static readonly string ResponseTrailersKey = "__ResponseTrailers"; + public static readonly string ResponseTrailersKey = "__ResponseTrailers"; - public static HttpHeaders TrailingHeaders(this HttpResponseMessage responseMessage) - { + public static HttpHeaders TrailingHeaders(this HttpResponseMessage responseMessage) + { #if !NETFRAMEWORK - return responseMessage.TrailingHeaders; + return responseMessage.TrailingHeaders; #else - if (responseMessage.RequestMessage.Properties.TryGetValue(ResponseTrailersKey, out var headers) && - headers is HttpHeaders httpHeaders) - { - return httpHeaders; - } + if (responseMessage.RequestMessage.Properties.TryGetValue(ResponseTrailersKey, out var headers) && + headers is HttpHeaders httpHeaders) + { + return httpHeaders; + } - // App targets .NET Standard 2.0 and the handler hasn't set trailers - // in RequestMessage.Properties with known key. Return empty collection. - // Client call will likely fail because it is unable to get a grpc-status. - return ResponseTrailers.Empty; + // App targets .NET Standard 2.0 and the handler hasn't set trailers + // in RequestMessage.Properties with known key. Return empty collection. + // Client call will likely fail because it is unable to get a grpc-status. + return ResponseTrailers.Empty; #endif - } + } #if NETFRAMEWORK - public static void EnsureTrailingHeaders(this HttpResponseMessage responseMessage) + public static void EnsureTrailingHeaders(this HttpResponseMessage responseMessage) + { + if (!responseMessage.RequestMessage.Properties.ContainsKey(ResponseTrailersKey)) { - if (!responseMessage.RequestMessage.Properties.ContainsKey(ResponseTrailersKey)) - { - responseMessage.RequestMessage.Properties[ResponseTrailersKey] = new ResponseTrailers(); - } + responseMessage.RequestMessage.Properties[ResponseTrailersKey] = new ResponseTrailers(); } + } - private class ResponseTrailers : HttpHeaders - { - public static readonly ResponseTrailers Empty = new ResponseTrailers(); - } -#endif + private class ResponseTrailers : HttpHeaders + { + public static readonly ResponseTrailers Empty = new ResponseTrailers(); } +#endif } diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.client.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.client.cs index 7e5e4f67daf..3799ef73a7f 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.client.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.client.cs @@ -1,27 +1,18 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Net; using Greet; +#if !NETFRAMEWORK using Grpc.Core; +#endif using Grpc.Net.Client; using Microsoft.Extensions.DependencyInjection; -using Moq; +#if !NETFRAMEWORK using OpenTelemetry.Context.Propagation; +using OpenTelemetry.Tests; +#endif using OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers; using OpenTelemetry.Instrumentation.GrpcNetClient; using OpenTelemetry.Instrumentation.GrpcNetClient.Implementation; @@ -29,459 +20,422 @@ using Xunit; using Status = OpenTelemetry.Trace.Status; -namespace OpenTelemetry.Instrumentation.Grpc.Tests +namespace OpenTelemetry.Instrumentation.Grpc.Tests; + +public partial class GrpcTests { - public partial class GrpcTests + [Theory] + [InlineData("http://localhost")] + [InlineData("http://localhost", false)] + [InlineData("http://127.0.0.1")] + [InlineData("http://127.0.0.1", false)] + [InlineData("http://[::1]")] + [InlineData("http://[::1]", false)] + public void GrpcClientCallsAreCollectedSuccessfully(string baseAddress, bool shouldEnrich = true) { - [Theory] - [InlineData("http://localhost")] - [InlineData("http://localhost", false)] - [InlineData("http://127.0.0.1")] - [InlineData("http://127.0.0.1", false)] - [InlineData("http://[::1]")] - [InlineData("http://[::1]", false)] - public void GrpcClientCallsAreCollectedSuccessfully(string baseAddress, bool shouldEnrich = true) - { - bool enrichWithHttpRequestMessageCalled = false; - bool enrichWithHttpResponseMessageCalled = false; + bool enrichWithHttpRequestMessageCalled = false; + bool enrichWithHttpResponseMessageCalled = false; - var uri = new Uri($"{baseAddress}:1234"); - var uriHostNameType = Uri.CheckHostName(uri.Host); + var uri = new Uri($"{baseAddress}:1234"); + var uriHostNameType = Uri.CheckHostName(uri.Host); - var httpClient = ClientTestHelpers.CreateTestClient(async request => - { - var streamContent = await ClientTestHelpers.CreateResponseContent(new HelloReply()).ConfigureAwait(false); - var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: global::Grpc.Core.StatusCode.OK); - response.TrailingHeaders().Add("grpc-message", "value"); - return response; - }); + using var httpClient = ClientTestHelpers.CreateTestClient(async request => + { + 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; + }); - var processor = new Mock>(); + var exportedItems = new List(); - using var parent = new Activity("parent") - .SetIdFormat(ActivityIdFormat.W3C) - .Start(); + using var parent = new Activity("parent") + .SetIdFormat(ActivityIdFormat.W3C) + .Start(); - using (Sdk.CreateTracerProviderBuilder() - .SetSampler(new AlwaysOnSampler()) - .AddGrpcClientInstrumentation(options => + using (Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddGrpcClientInstrumentation(options => + { + if (shouldEnrich) { - if (shouldEnrich) - { - options.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => { enrichWithHttpRequestMessageCalled = true; }; - options.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => { enrichWithHttpResponseMessageCalled = true; }; - } - }) - .AddProcessor(processor.Object) - .Build()) + options.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => { enrichWithHttpRequestMessageCalled = true; }; + options.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => { enrichWithHttpResponseMessageCalled = true; }; + } + }) + .AddInMemoryExporter(exportedItems) + .Build()) + { + var channel = GrpcChannel.ForAddress(uri, new GrpcChannelOptions { - var channel = GrpcChannel.ForAddress(uri, new GrpcChannelOptions - { - HttpClient = httpClient, - }); - var client = new Greeter.GreeterClient(channel); - var rs = client.SayHello(new HelloRequest()); - } + HttpClient = httpClient, + }); + var client = new Greeter.GreeterClient(channel); + var rs = client.SayHello(new HelloRequest()); + } - Assert.Equal(5, processor.Invocations.Count); // SetParentProvider/OnStart/OnEnd/OnShutdown/Dispose called. - var activity = (Activity)processor.Invocations[2].Arguments[0]; + Assert.Single(exportedItems); + var activity = exportedItems[0]; - ValidateGrpcActivity(activity); - Assert.Equal(parent.TraceId, activity.Context.TraceId); - Assert.Equal(parent.SpanId, activity.ParentSpanId); - Assert.NotEqual(parent.SpanId, activity.Context.SpanId); - Assert.NotEqual(default, activity.Context.SpanId); + ValidateGrpcActivity(activity); + Assert.Equal(parent.TraceId, activity.Context.TraceId); + Assert.Equal(parent.SpanId, activity.ParentSpanId); + Assert.NotEqual(parent.SpanId, activity.Context.SpanId); + Assert.NotEqual(default, activity.Context.SpanId); - Assert.Equal($"greet.Greeter/SayHello", activity.DisplayName); - Assert.Equal("grpc", activity.GetTagValue(SemanticConventions.AttributeRpcSystem)); - Assert.Equal("greet.Greeter", activity.GetTagValue(SemanticConventions.AttributeRpcService)); - Assert.Equal("SayHello", activity.GetTagValue(SemanticConventions.AttributeRpcMethod)); + Assert.Equal($"greet.Greeter/SayHello", activity.DisplayName); + Assert.Equal("grpc", activity.GetTagValue(SemanticConventions.AttributeRpcSystem)); + Assert.Equal("greet.Greeter", activity.GetTagValue(SemanticConventions.AttributeRpcService)); + Assert.Equal("SayHello", activity.GetTagValue(SemanticConventions.AttributeRpcMethod)); - if (uriHostNameType == UriHostNameType.IPv4 || uriHostNameType == UriHostNameType.IPv6) - { - Assert.Equal(uri.Host, activity.GetTagValue(SemanticConventions.AttributeNetPeerIp)); - Assert.Null(activity.GetTagValue(SemanticConventions.AttributeNetPeerName)); - } - else - { - Assert.Null(activity.GetTagValue(SemanticConventions.AttributeNetPeerIp)); - Assert.Equal(uri.Host, activity.GetTagValue(SemanticConventions.AttributeNetPeerName)); - } + if (uriHostNameType == UriHostNameType.IPv4 || uriHostNameType == UriHostNameType.IPv6) + { + Assert.Equal(uri.Host, activity.GetTagValue(SemanticConventions.AttributeServerSocketAddress)); + Assert.Null(activity.GetTagValue(SemanticConventions.AttributeServerAddress)); + } + else + { + Assert.Null(activity.GetTagValue(SemanticConventions.AttributeServerSocketAddress)); + Assert.Equal(uri.Host, activity.GetTagValue(SemanticConventions.AttributeServerAddress)); + } - Assert.Equal(uri.Port, activity.GetTagValue(SemanticConventions.AttributeNetPeerPort)); - Assert.Equal(Status.Unset, activity.GetStatus()); + Assert.Equal(uri.Port, activity.GetTagValue(SemanticConventions.AttributeServerPort)); + Assert.Equal(Status.Unset, activity.GetStatus()); - // Tags added by the library then removed from the instrumentation - Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcMethodTagName)); - Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcStatusCodeTagName)); - Assert.Equal(0, activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); + // Tags added by the library then removed from the instrumentation + Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcMethodTagName)); + Assert.Null(activity.GetTagValue(GrpcTagHelper.GrpcStatusCodeTagName)); + Assert.Equal(0, activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); - if (shouldEnrich) - { - Assert.True(enrichWithHttpRequestMessageCalled); - Assert.True(enrichWithHttpResponseMessageCalled); - } + if (shouldEnrich) + { + Assert.True(enrichWithHttpRequestMessageCalled); + Assert.True(enrichWithHttpResponseMessageCalled); } + } #if NET6_0_OR_GREATER - [Theory] - [InlineData(true)] - [InlineData(false)] - public void GrpcAndHttpClientInstrumentationIsInvoked(bool shouldEnrich) - { - var uri = new Uri($"http://localhost:{this.server.Port}"); - var processor = new Mock>(); - processor.Setup(x => x.OnStart(It.IsAny())).Callback(c => - { - c.SetTag("enrichedWithHttpRequestMessage", "no"); - c.SetTag("enrichedWithHttpResponseMessage", "no"); - }); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void GrpcAndHttpClientInstrumentationIsInvoked(bool shouldEnrich) + { + var uri = new Uri($"http://localhost:{this.server.Port}"); + var exportedItems = new List(); - using var parent = new Activity("parent") - .Start(); + using var parent = new Activity("parent") + .Start(); - using (Sdk.CreateTracerProviderBuilder() - .SetSampler(new AlwaysOnSampler()) - .AddGrpcClientInstrumentation(options => + using (Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddGrpcClientInstrumentation(options => + { + if (shouldEnrich) { - if (shouldEnrich) + options.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => + { + activity.SetTag("enrichedWithHttpRequestMessage", "yes"); + }; + + options.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => { - options.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => - { - activity.SetTag("enrichedWithHttpRequestMessage", "yes"); - }; - - options.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => - { - activity.SetTag("enrichedWithHttpResponseMessage", "yes"); - }; - } - }) - .AddHttpClientInstrumentation() - .AddProcessor(processor.Object) - .Build()) + activity.SetTag("enrichedWithHttpResponseMessage", "yes"); + }; + } + }) + .AddHttpClientInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build()) + { + // With net5, based on the grpc changes, the quantity of default activities changed. + // TODO: This is a workaround. https://github.com/open-telemetry/opentelemetry-dotnet/issues/1490 + using var channel = GrpcChannel.ForAddress(uri, new GrpcChannelOptions() { - // With net5, based on the grpc changes, the quantity of default activities changed. - // TODO: This is a workaround. https://github.com/open-telemetry/opentelemetry-dotnet/issues/1490 - using var channel = GrpcChannel.ForAddress(uri, new GrpcChannelOptions() - { - HttpClient = new HttpClient(), - }); + HttpClient = new HttpClient(), + }); - var client = new Greeter.GreeterClient(channel); - var rs = client.SayHello(new HelloRequest()); - } + var client = new Greeter.GreeterClient(channel); + var rs = client.SayHello(new HelloRequest()); + } - Assert.Equal(7, processor.Invocations.Count); // SetParentProvider + OnStart/OnEnd (gRPC) + OnStart/OnEnd (HTTP) + OnShutdown/Dispose called. - var httpSpan = (Activity)processor.Invocations[3].Arguments[0]; - var grpcSpan = (Activity)processor.Invocations[4].Arguments[0]; + Assert.Equal(2, exportedItems.Count); + var httpSpan = exportedItems.Single(activity => activity.OperationName == OperationNameHttpOut); + var grpcSpan = exportedItems.Single(activity => activity.OperationName == OperationNameGrpcOut); - ValidateGrpcActivity(grpcSpan); - Assert.Equal($"greet.Greeter/SayHello", grpcSpan.DisplayName); - Assert.Equal(0, grpcSpan.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); - Assert.Equal($"HTTP POST", httpSpan.DisplayName); - Assert.Equal(grpcSpan.SpanId, httpSpan.ParentSpanId); + ValidateGrpcActivity(grpcSpan); + Assert.Equal($"greet.Greeter/SayHello", grpcSpan.DisplayName); + Assert.Equal(0, grpcSpan.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); + Assert.Equal("POST", httpSpan.DisplayName); + Assert.Equal(grpcSpan.SpanId, httpSpan.ParentSpanId); - Assert.NotEmpty(grpcSpan.Tags.Where(tag => tag.Key == "enrichedWithHttpRequestMessage")); - Assert.NotEmpty(grpcSpan.Tags.Where(tag => tag.Key == "enrichedWithHttpResponseMessage")); - Assert.Equal(shouldEnrich ? "yes" : "no", grpcSpan.Tags.Where(tag => tag.Key == "enrichedWithHttpRequestMessage").FirstOrDefault().Value); - Assert.Equal(shouldEnrich ? "yes" : "no", grpcSpan.Tags.Where(tag => tag.Key == "enrichedWithHttpResponseMessage").FirstOrDefault().Value); + if (shouldEnrich) + { + Assert.Single(grpcSpan.Tags, tag => tag.Key == "enrichedWithHttpRequestMessage" && tag.Value == "yes"); + Assert.Single(grpcSpan.Tags, tag => tag.Key == "enrichedWithHttpResponseMessage" && tag.Value == "yes"); } - - [Fact] - public void GrpcAndHttpClientInstrumentationWithSuppressInstrumentation() + else { - var uri = new Uri($"http://localhost:{this.server.Port}"); - var processor = new Mock>(); - - using var parent = new Activity("parent") - .Start(); + Assert.Empty(grpcSpan.Tags.Where(tag => tag.Key == "enrichedWithHttpRequestMessage")); + Assert.Empty(grpcSpan.Tags.Where(tag => tag.Key == "enrichedWithHttpResponseMessage")); + } + } - using (Sdk.CreateTracerProviderBuilder() - .SetSampler(new AlwaysOnSampler()) - .AddGrpcClientInstrumentation(o => o.SuppressDownstreamInstrumentation = true) - .AddHttpClientInstrumentation() - .AddProcessor(processor.Object) - .Build()) + [Fact(Skip = "https://github.com/open-telemetry/opentelemetry-dotnet/issues/5092")] + public void GrpcAndHttpClientInstrumentationWithSuppressInstrumentation() + { + var uri = new Uri($"http://localhost:{this.server.Port}"); + var exportedItems = new List(); + + using var parent = new Activity("parent") + .Start(); + + using (Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddGrpcClientInstrumentation(o => o.SuppressDownstreamInstrumentation = true) + .AddHttpClientInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build()) + { + Parallel.ForEach( + new int[4], + new ParallelOptions { - Parallel.ForEach( - new int[4], - new ParallelOptions - { - MaxDegreeOfParallelism = 4, - }, - (value) => - { - var channel = GrpcChannel.ForAddress(uri); - var client = new Greeter.GreeterClient(channel); - var rs = client.SayHello(new HelloRequest()); - }); - } + MaxDegreeOfParallelism = 4, + }, + (value) => + { + var channel = GrpcChannel.ForAddress(uri); + var client = new Greeter.GreeterClient(channel); + var rs = client.SayHello(new HelloRequest()); + }); + } - Assert.Equal(11, processor.Invocations.Count); // SetParentProvider + OnStart/OnEnd (gRPC) * 4 + OnShutdown/Dispose called. - var grpcSpan1 = (Activity)processor.Invocations[2].Arguments[0]; - var grpcSpan2 = (Activity)processor.Invocations[4].Arguments[0]; - var grpcSpan3 = (Activity)processor.Invocations[6].Arguments[0]; - var grpcSpan4 = (Activity)processor.Invocations[8].Arguments[0]; + Assert.Equal(4, exportedItems.Count); + var grpcSpan1 = exportedItems[0]; + var grpcSpan2 = exportedItems[1]; + var grpcSpan3 = exportedItems[2]; + var grpcSpan4 = exportedItems[3]; - ValidateGrpcActivity(grpcSpan1); - Assert.Equal($"greet.Greeter/SayHello", grpcSpan1.DisplayName); - Assert.Equal(0, grpcSpan1.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); + ValidateGrpcActivity(grpcSpan1); + Assert.Equal($"greet.Greeter/SayHello", grpcSpan1.DisplayName); + Assert.Equal(0, grpcSpan1.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); - ValidateGrpcActivity(grpcSpan2); - Assert.Equal($"greet.Greeter/SayHello", grpcSpan2.DisplayName); - Assert.Equal(0, grpcSpan2.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); + ValidateGrpcActivity(grpcSpan2); + Assert.Equal($"greet.Greeter/SayHello", grpcSpan2.DisplayName); + Assert.Equal(0, grpcSpan2.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); - ValidateGrpcActivity(grpcSpan3); - Assert.Equal($"greet.Greeter/SayHello", grpcSpan3.DisplayName); - Assert.Equal(0, grpcSpan3.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); + ValidateGrpcActivity(grpcSpan3); + Assert.Equal($"greet.Greeter/SayHello", grpcSpan3.DisplayName); + Assert.Equal(0, grpcSpan3.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); - ValidateGrpcActivity(grpcSpan4); - Assert.Equal($"greet.Greeter/SayHello", grpcSpan4.DisplayName); - Assert.Equal(0, grpcSpan4.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); - } + ValidateGrpcActivity(grpcSpan4); + Assert.Equal($"greet.Greeter/SayHello", grpcSpan4.DisplayName); + Assert.Equal(0, grpcSpan4.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); + } - [Fact] - public void GrpcPropagatesContextWithSuppressInstrumentationOptionSetToTrue() + [Fact(Skip = "https://github.com/open-telemetry/opentelemetry-dotnet/issues/5092")] + public void GrpcPropagatesContextWithSuppressInstrumentationOptionSetToTrue() + { + try { - try - { - var uri = new Uri($"http://localhost:{this.server.Port}"); - var processor = new Mock>(); + var uri = new Uri($"http://localhost:{this.server.Port}"); + var exportedItems = new List(); - using var source = new ActivitySource("test-source"); + using var source = new ActivitySource("test-source"); - var propagator = new Mock(); - propagator.Setup(m => m.Inject(It.IsAny(), It.IsAny(), It.IsAny>())) - .Callback>((context, message, action) => - { - action(message, "customField", "customValue"); - }); + var propagator = new CustomTextMapPropagator(); + propagator.InjectValues.Add("customField", context => "customValue"); - Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] - { - new TraceContextPropagator(), - propagator.Object, - })); + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] + { + new TraceContextPropagator(), + propagator, + })); - using (Sdk.CreateTracerProviderBuilder() - .AddSource("test-source") - .AddGrpcClientInstrumentation(o => - { - o.SuppressDownstreamInstrumentation = true; - }) - .AddHttpClientInstrumentation() - .AddAspNetCoreInstrumentation(options => - { - options.EnrichWithHttpRequest = (activity, request) => - { - activity.SetCustomProperty("customField", request.Headers["customField"].ToString()); - }; - }) // Instrumenting the server side as well - .AddProcessor(processor.Object) - .Build()) + using (Sdk.CreateTracerProviderBuilder() + .AddSource("test-source") + .AddGrpcClientInstrumentation(o => + { + o.SuppressDownstreamInstrumentation = true; + }) + .AddHttpClientInstrumentation() + .AddAspNetCoreInstrumentation(options => { - using (var activity = source.StartActivity("parent")) + options.EnrichWithHttpRequest = (activity, request) => { - Assert.NotNull(activity); - var channel = GrpcChannel.ForAddress(uri); - var client = new Greeter.GreeterClient(channel); - var rs = client.SayHello(new HelloRequest()); - } - - WaitForProcessorInvocations(processor, 7); - } - - Assert.Equal(9, processor.Invocations.Count); // SetParentProvider + (OnStart + OnEnd) * 3 (parent, gRPC client, and server) + Shutdown + Dispose called. - - Assert.Single(processor.Invocations, invo => invo.Method.Name == "SetParentProvider"); - Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnStart), "parent")); - Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnStart), OperationNameGrpcOut)); - Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnStart), OperationNameHttpRequestIn)); - Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnEnd), OperationNameHttpRequestIn)); - Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnEnd), OperationNameGrpcOut)); - Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnEnd), "parent")); - Assert.Single(processor.Invocations, invo => invo.Method.Name == "OnShutdown"); - Assert.Single(processor.Invocations, invo => invo.Method.Name == nameof(processor.Object.Dispose)); - - var serverActivity = GetActivityFromProcessorInvocation(processor, nameof(processor.Object.OnEnd), OperationNameHttpRequestIn); - var clientActivity = GetActivityFromProcessorInvocation(processor, nameof(processor.Object.OnEnd), OperationNameGrpcOut); - - Assert.Equal($"greet.Greeter/SayHello", clientActivity.DisplayName); - Assert.Equal($"greet.Greeter/SayHello", serverActivity.DisplayName); - Assert.Equal(clientActivity.TraceId, serverActivity.TraceId); - Assert.Equal(clientActivity.SpanId, serverActivity.ParentSpanId); - Assert.Equal(0, clientActivity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); - Assert.Equal("customValue", serverActivity.GetCustomProperty("customField") as string); - } - finally + activity.SetCustomProperty("customField", request.Headers["customField"].ToString()); + }; + }) // Instrumenting the server side as well + .AddInMemoryExporter(exportedItems) + .Build()) { - Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] - { - new TraceContextPropagator(), - new BaggagePropagator(), - })); + using var activity = source.StartActivity("parent"); + Assert.NotNull(activity); + var channel = GrpcChannel.ForAddress(uri); + var client = new Greeter.GreeterClient(channel); + var rs = client.SayHello(new HelloRequest()); } - } - [Fact] - public void GrpcDoesNotPropagateContextWithSuppressInstrumentationOptionSetToFalse() + var serverActivity = exportedItems.Single(activity => activity.OperationName == OperationNameHttpRequestIn); + var clientActivity = exportedItems.Single(activity => activity.OperationName == OperationNameGrpcOut); + + Assert.Equal($"greet.Greeter/SayHello", clientActivity.DisplayName); + Assert.Equal($"POST /greet.Greeter/SayHello", serverActivity.DisplayName); + Assert.Equal(clientActivity.TraceId, serverActivity.TraceId); + Assert.Equal(clientActivity.SpanId, serverActivity.ParentSpanId); + Assert.Equal(0, clientActivity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); + Assert.Equal("customValue", serverActivity.GetCustomProperty("customField") as string); + } + finally { - try + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] { - var uri = new Uri($"http://localhost:{this.server.Port}"); - var processor = new Mock>(); + new TraceContextPropagator(), + new BaggagePropagator(), + })); + } + } - using var source = new ActivitySource("test-source"); + [Fact] + public void GrpcDoesNotPropagateContextWithSuppressInstrumentationOptionSetToFalse() + { + try + { + var uri = new Uri($"http://localhost:{this.server.Port}"); + var exportedItems = new List(); + using var source = new ActivitySource("test-source"); - bool isPropagatorCalled = false; - var propagator = new Mock(); - propagator.Setup(m => m.Inject(It.IsAny(), It.IsAny(), It.IsAny>())) - .Callback>((context, message, action) => - { - isPropagatorCalled = true; - }); + bool isPropagatorCalled = false; + var propagator = new CustomTextMapPropagator + { + Injected = (context) => isPropagatorCalled = true, + }; - Sdk.SetDefaultTextMapPropagator(propagator.Object); + Sdk.SetDefaultTextMapPropagator(propagator); - var headers = new Metadata(); + var headers = new Metadata(); - using (Sdk.CreateTracerProviderBuilder() - .AddSource("test-source") - .AddGrpcClientInstrumentation(o => - { - o.SuppressDownstreamInstrumentation = false; - }) - .AddProcessor(processor.Object) - .Build()) + using (Sdk.CreateTracerProviderBuilder() + .AddSource("test-source") + .AddGrpcClientInstrumentation(o => { - using var activity = source.StartActivity("parent"); - var channel = GrpcChannel.ForAddress(uri); - var client = new Greeter.GreeterClient(channel); - var rs = client.SayHello(new HelloRequest(), headers); - } + o.SuppressDownstreamInstrumentation = false; + }) + .AddInMemoryExporter(exportedItems) + .Build()) + { + using var activity = source.StartActivity("parent"); + var channel = GrpcChannel.ForAddress(uri); + var client = new Greeter.GreeterClient(channel); + var rs = client.SayHello(new HelloRequest(), headers); + } - Assert.Equal(7, processor.Invocations.Count); // SetParentProvider/OnShutdown/Dispose called. + Assert.Equal(2, exportedItems.Count); - Assert.Single(processor.Invocations, invo => invo.Method.Name == "SetParentProvider"); - Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnStart), "parent")); - Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnStart), OperationNameGrpcOut)); - Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnEnd), OperationNameGrpcOut)); - Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnEnd), "parent")); - Assert.Single(processor.Invocations, invo => invo.Method.Name == "OnShutdown"); - Assert.Single(processor.Invocations, invo => invo.Method.Name == nameof(processor.Object.Dispose)); + var parentActivity = exportedItems.Single(activity => activity.OperationName == "parent"); + var clientActivity = exportedItems.Single(activity => activity.OperationName == OperationNameGrpcOut); - // Propagator is not called - Assert.False(isPropagatorCalled); - } - finally + Assert.Equal(clientActivity.ParentSpanId, parentActivity.SpanId); + + // Propagator is not called + Assert.False(isPropagatorCalled); + } + finally + { + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] { - Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] - { - new TraceContextPropagator(), - new BaggagePropagator(), - })); - } + new TraceContextPropagator(), + new BaggagePropagator(), + })); } + } - [Fact] - public void GrpcClientInstrumentationRespectsSdkSuppressInstrumentation() + [Fact(Skip = "https://github.com/open-telemetry/opentelemetry-dotnet/issues/5092")] + public void GrpcClientInstrumentationRespectsSdkSuppressInstrumentation() + { + try { - try - { - var uri = new Uri($"http://localhost:{this.server.Port}"); - var processor = new Mock>(); + var uri = new Uri($"http://localhost:{this.server.Port}"); + var exportedItems = new List(); - using var source = new ActivitySource("test-source"); + using var source = new ActivitySource("test-source"); - bool isPropagatorCalled = false; - var propagator = new Mock(); - propagator.Setup(m => m.Inject(It.IsAny(), It.IsAny(), It.IsAny>())) - .Callback>((context, message, action) => - { - isPropagatorCalled = true; - }); + bool isPropagatorCalled = false; + var propagator = new CustomTextMapPropagator(); + propagator.Injected = (context) => isPropagatorCalled = true; - Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] - { - new TraceContextPropagator(), - propagator.Object, - })); + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] + { + new TraceContextPropagator(), + propagator, + })); - using (Sdk.CreateTracerProviderBuilder() - .AddSource("test-source") - .AddGrpcClientInstrumentation(o => - { - o.SuppressDownstreamInstrumentation = true; - }) - .AddProcessor(processor.Object) - .Build()) + using (Sdk.CreateTracerProviderBuilder() + .AddSource("test-source") + .AddGrpcClientInstrumentation(o => { - using var activity = source.StartActivity("parent"); - using (SuppressInstrumentationScope.Begin()) - { - var channel = GrpcChannel.ForAddress(uri); - var client = new Greeter.GreeterClient(channel); - var rs = client.SayHello(new HelloRequest()); - } - } - - // If suppressed, activity is not emitted and - // propagation is also not performed. - Assert.Equal(5, processor.Invocations.Count); // SetParentProvider + (OnStart + OnEnd) * 3 for parent + OnShutdown + Dispose called. - Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnStart), "parent")); - Assert.Single(processor.Invocations, GeneratePredicateForMoqProcessorActivity(nameof(processor.Object.OnEnd), "parent")); - Assert.False(isPropagatorCalled); - } - finally + o.SuppressDownstreamInstrumentation = true; + }) + .AddInMemoryExporter(exportedItems) + .Build()) { - Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] + using var activity = source.StartActivity("parent"); + using (SuppressInstrumentationScope.Begin()) { - new TraceContextPropagator(), - new BaggagePropagator(), - })); + var channel = GrpcChannel.ForAddress(uri); + var client = new Greeter.GreeterClient(channel); + var rs = client.SayHello(new HelloRequest()); + } } - } -#endif - [Fact] - public void AddGrpcClientInstrumentationNamedOptionsSupported() + // If suppressed, activity is not emitted and + // propagation is also not performed. + Assert.Single(exportedItems); + Assert.False(isPropagatorCalled); + } + finally { - int defaultExporterOptionsConfigureOptionsInvocations = 0; - int namedExporterOptionsConfigureOptionsInvocations = 0; + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] + { + new TraceContextPropagator(), + new BaggagePropagator(), + })); + } + } +#endif - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .ConfigureServices(services => - { - services.Configure(o => defaultExporterOptionsConfigureOptionsInvocations++); + [Fact] + public void AddGrpcClientInstrumentationNamedOptionsSupported() + { + int defaultExporterOptionsConfigureOptionsInvocations = 0; + int namedExporterOptionsConfigureOptionsInvocations = 0; - services.Configure("Instrumentation2", o => namedExporterOptionsConfigureOptionsInvocations++); - }) - .AddGrpcClientInstrumentation() - .AddGrpcClientInstrumentation("Instrumentation2", configure: null) - .Build(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => + { + services.Configure(o => defaultExporterOptionsConfigureOptionsInvocations++); - Assert.Equal(1, defaultExporterOptionsConfigureOptionsInvocations); - Assert.Equal(1, namedExporterOptionsConfigureOptionsInvocations); - } + services.Configure("Instrumentation2", o => namedExporterOptionsConfigureOptionsInvocations++); + }) + .AddGrpcClientInstrumentation() + .AddGrpcClientInstrumentation("Instrumentation2", configure: null) + .Build(); - [Fact] - public void Grpc_BadArgs() - { - TracerProviderBuilder builder = null; - Assert.Throws(() => builder.AddGrpcClientInstrumentation()); - } + Assert.Equal(1, defaultExporterOptionsConfigureOptionsInvocations); + Assert.Equal(1, namedExporterOptionsConfigureOptionsInvocations); + } - private static void ValidateGrpcActivity(Activity activityToValidate) - { - Assert.Equal(GrpcClientDiagnosticListener.ActivitySourceName, activityToValidate.Source.Name); - Assert.Equal(GrpcClientDiagnosticListener.Version.ToString(), activityToValidate.Source.Version); - Assert.Equal(ActivityKind.Client, activityToValidate.Kind); - } + [Fact] + public void Grpc_BadArgs() + { + TracerProviderBuilder builder = null; + Assert.Throws(() => builder.AddGrpcClientInstrumentation()); + } - private static Predicate GeneratePredicateForMoqProcessorActivity(string methodName, string activityOperationName) - { - return invo => invo.Method.Name == methodName && (invo.Arguments[0] as Activity)?.OperationName == activityOperationName; - } + private static void ValidateGrpcActivity(Activity activityToValidate) + { + Assert.Equal(GrpcClientDiagnosticListener.ActivitySourceName, activityToValidate.Source.Name); + Assert.Equal(GrpcClientDiagnosticListener.Version.ToString(), activityToValidate.Source.Version); + Assert.Equal(ActivityKind.Client, activityToValidate.Kind); } } diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.server.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.server.cs index 95751e75e7e..1c397a27abe 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.server.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.server.cs @@ -1,30 +1,14 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #if NET6_0_OR_GREATER -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Net; -using System.Threading; using Greet; using Grpc.Core; using Grpc.Net.Client; -using Moq; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Context.Propagation; using OpenTelemetry.Instrumentation.Grpc.Services.Tests; using OpenTelemetry.Instrumentation.GrpcNetClient; @@ -32,43 +16,111 @@ using Xunit; using Status = OpenTelemetry.Trace.Status; -namespace OpenTelemetry.Instrumentation.Grpc.Tests +namespace OpenTelemetry.Instrumentation.Grpc.Tests; + +public partial class GrpcTests : IDisposable { - public partial class GrpcTests : IDisposable + private const string OperationNameHttpRequestIn = "Microsoft.AspNetCore.Hosting.HttpRequestIn"; + private const string OperationNameGrpcOut = "Grpc.Net.Client.GrpcOut"; + private const string OperationNameHttpOut = "System.Net.Http.HttpRequestOut"; + + private readonly GrpcServer server; + + public GrpcTests() + { + this.server = new GrpcServer(); + } + + [Theory] + [InlineData(null)] + [InlineData("true")] + [InlineData("false")] + [InlineData("True")] + [InlineData("False")] + public void GrpcAspNetCoreInstrumentationAddsCorrectAttributes(string enableGrpcAspNetCoreSupport) { - private const string OperationNameHttpRequestIn = "Microsoft.AspNetCore.Hosting.HttpRequestIn"; - private const string OperationNameGrpcOut = "Grpc.Net.Client.GrpcOut"; - private const string OperationNameHttpOut = "System.Net.Http.HttpRequestOut"; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_ENABLE_GRPC_INSTRUMENTATION"] = enableGrpcAspNetCoreSupport, + }) + .Build(); + + var exportedItems = new List(); + using var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => services.AddSingleton(configuration)) + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build(); + + var clientLoopbackAddresses = new[] { IPAddress.Loopback.ToString(), IPAddress.IPv6Loopback.ToString() }; + var uri = new Uri($"http://localhost:{this.server.Port}"); - private readonly GrpcServer server; + using var channel = GrpcChannel.ForAddress(uri); + var client = new Greeter.GreeterClient(channel); + var returnMsg = client.SayHello(new HelloRequest()).Message; + Assert.False(string.IsNullOrEmpty(returnMsg)); - public GrpcTests() + WaitForExporterToReceiveItems(exportedItems, 1); + Assert.Single(exportedItems); + var activity = exportedItems[0]; + + Assert.Equal(ActivityKind.Server, activity.Kind); + + if (enableGrpcAspNetCoreSupport != null && enableGrpcAspNetCoreSupport.Equals("true", StringComparison.OrdinalIgnoreCase)) { - this.server = new GrpcServer(); + 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)); } - - [Theory] - [InlineData(null)] - [InlineData(true)] - [InlineData(false)] - public void GrpcAspNetCoreInstrumentationAddsCorrectAttributes(bool? enableGrpcAspNetCoreSupport) + else { - var exportedItems = new List(); - var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder(); + Assert.NotNull(activity.GetTagValue(GrpcTagHelper.GrpcMethodTagName)); + Assert.NotNull(activity.GetTagValue(GrpcTagHelper.GrpcStatusCodeTagName)); + } - if (enableGrpcAspNetCoreSupport.HasValue) - { - tracerProviderBuilder.AddAspNetCoreInstrumentation(options => - { - options.EnableGrpcAspNetCoreSupport = enableGrpcAspNetCoreSupport.Value; - }); - } - else - { - tracerProviderBuilder.AddAspNetCoreInstrumentation(); - } + 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); + } - using var tracerProvider = tracerProviderBuilder +#if NET6_0_OR_GREATER + [Theory(Skip = "Skipping for .NET 6 and higher due to bug #3023")] +#endif + [InlineData(null)] + [InlineData("true")] + [InlineData("false")] + [InlineData("True")] + [InlineData("False")] + public void GrpcAspNetCoreInstrumentationAddsCorrectAttributesWhenItCreatesNewActivity(string enableGrpcAspNetCoreSupport) + { + try + { + // B3Propagator along with the headers passed to the client.SayHello ensure that the instrumentation creates a sibling activity + Sdk.SetDefaultTextMapPropagator(new Extensions.Propagators.B3Propagator()); + var exportedItems = new List(); + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_ENABLE_GRPC_INSTRUMENTATION"] = enableGrpcAspNetCoreSupport, + }) + .Build(); + + using var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => services.AddSingleton(configuration)) + .AddAspNetCoreInstrumentation() .AddInMemoryExporter(exportedItems) .Build(); @@ -77,8 +129,14 @@ public void GrpcAspNetCoreInstrumentationAddsCorrectAttributes(bool? enableGrpcA using var channel = GrpcChannel.ForAddress(uri); var client = new Greeter.GreeterClient(channel); - var returnMsg = client.SayHello(new HelloRequest()).Message; - Assert.False(string.IsNullOrEmpty(returnMsg)); + var headers = new Metadata + { + { "traceparent", "00-120dc44db5b736468afb112197b0dbd3-5dfbdf27ec544544-01" }, + { "x-b3-traceid", "120dc44db5b736468afb112197b0dbd3" }, + { "x-b3-spanid", "b0966f651b9e0126" }, + { "x-b3-sampled", "1" }, + }; + client.SayHello(new HelloRequest(), headers); WaitForExporterToReceiveItems(exportedItems, 1); Assert.Single(exportedItems); @@ -86,7 +144,7 @@ public void GrpcAspNetCoreInstrumentationAddsCorrectAttributes(bool? enableGrpcA Assert.Equal(ActivityKind.Server, activity.Kind); - if (!enableGrpcAspNetCoreSupport.HasValue || enableGrpcAspNetCoreSupport.Value) + if (enableGrpcAspNetCoreSupport != null && enableGrpcAspNetCoreSupport.Equals("true", StringComparison.OrdinalIgnoreCase)) { Assert.Equal("grpc", activity.GetTagValue(SemanticConventions.AttributeRpcSystem)); Assert.Equal("greet.Greeter", activity.GetTagValue(SemanticConventions.AttributeRpcService)); @@ -113,139 +171,35 @@ public void GrpcAspNetCoreInstrumentationAddsCorrectAttributes(bool? enableGrpcA Assert.Equal($"http://localhost:{this.server.Port}/greet.Greeter/SayHello", activity.GetTagValue(SemanticConventions.AttributeHttpUrl)); Assert.StartsWith("grpc-dotnet", activity.GetTagValue(SemanticConventions.AttributeHttpUserAgent) as string); } - -#if NET6_0_OR_GREATER - [Theory(Skip = "Skipping for .NET 6 and higher due to bug #3023")] -#endif - [InlineData(null)] - [InlineData(true)] - [InlineData(false)] - public void GrpcAspNetCoreInstrumentationAddsCorrectAttributesWhenItCreatesNewActivity(bool? enableGrpcAspNetCoreSupport) + finally { - try + // Set the SDK to use the default propagator for other unit tests + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] { - // B3Propagator along with the headers passed to the client.SayHello ensure that the instrumentation creates a sibling activity - Sdk.SetDefaultTextMapPropagator(new Extensions.Propagators.B3Propagator()); - 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 headers = new Metadata - { - { "traceparent", "00-120dc44db5b736468afb112197b0dbd3-5dfbdf27ec544544-01" }, - { "x-b3-traceid", "120dc44db5b736468afb112197b0dbd3" }, - { "x-b3-spanid", "b0966f651b9e0126" }, - { "x-b3-sampled", "1" }, - }; - client.SayHello(new HelloRequest(), headers); - - 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); - } - finally - { - // Set the SDK to use the default propagator for other unit tests - Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] - { - new TraceContextPropagator(), - new BaggagePropagator(), - })); - } - } - - public void Dispose() - { - this.server.Dispose(); - GC.SuppressFinalize(this); - } - - private static void WaitForExporterToReceiveItems(List itemsReceived, int itemCount) - { - // 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 - Assert.True(SpinWait.SpinUntil( - () => - { - Thread.Sleep(10); - return itemsReceived.Count >= itemCount; - }, - TimeSpan.FromSeconds(1))); + new TraceContextPropagator(), + new BaggagePropagator(), + })); } + } - private static void WaitForProcessorInvocations(Mock> spanProcessor, int invocationCount) - { - // 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 - Assert.True(SpinWait.SpinUntil( - () => - { - Thread.Sleep(10); - return spanProcessor.Invocations.Count >= invocationCount; - }, - TimeSpan.FromSeconds(1))); - } + public void Dispose() + { + this.server.Dispose(); + GC.SuppressFinalize(this); + } - private static Activity GetActivityFromProcessorInvocation(Mock> processor, string methodName, string activityOperationName) - { - return processor.Invocations - .FirstOrDefault(invo => - { - return invo.Method.Name == methodName - && (invo.Arguments[0] as Activity)?.OperationName == activityOperationName; - })?.Arguments[0] as Activity; - } + private static void WaitForExporterToReceiveItems(List itemsReceived, int itemCount) + { + // 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 + Assert.True(SpinWait.SpinUntil( + () => + { + Thread.Sleep(10); + return itemsReceived.Count >= itemCount; + }, + TimeSpan.FromSeconds(1))); } } #endif diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/OpenTelemetry.Instrumentation.Grpc.Tests.csproj b/test/OpenTelemetry.Instrumentation.Grpc.Tests/OpenTelemetry.Instrumentation.Grpc.Tests.csproj index db4afec07b4..c56cc504187 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/OpenTelemetry.Instrumentation.Grpc.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/OpenTelemetry.Instrumentation.Grpc.Tests.csproj @@ -1,11 +1,8 @@ Unit test project for OpenTelemetry Grpc for .NET instrumentation - - net7.0;net6.0 - $(TargetFrameworks);net462 + $(TargetFrameworksForTests) $(NoWarn),CS8981 - disable @@ -16,33 +13,42 @@ - - + + - - - all + runtime; build; native; contentfiles; analyzers - + - - + + + + - + - - - - + + + + + + + + + + + + + diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/Services/GreeterService.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/Services/GreeterService.cs index 16bef79b11d..f2f7b4c1aac 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/Services/GreeterService.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/Services/GreeterService.cs @@ -1,52 +1,39 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + using Greet; using Grpc.Core; using Microsoft.Extensions.Logging; -namespace OpenTelemetry.Instrumentation.Grpc.Services.Tests +namespace OpenTelemetry.Instrumentation.Grpc.Services.Tests; + +public class GreeterService : Greeter.GreeterBase { - public class GreeterService : Greeter.GreeterBase - { - private readonly ILogger logger; + private readonly ILogger logger; - public GreeterService(ILoggerFactory loggerFactory) - { - this.logger = loggerFactory.CreateLogger(); - } + public GreeterService(ILoggerFactory loggerFactory) + { + this.logger = loggerFactory.CreateLogger(); + } - public override Task SayHello(HelloRequest request, ServerCallContext context) - { - this.logger.LogInformation("Sending hello to {Name}", request.Name); - return Task.FromResult(new HelloReply { Message = "Hello " + request.Name }); - } + public override Task SayHello(HelloRequest request, ServerCallContext context) + { + this.logger.LogInformation("Sending hello to {Name}", request.Name); + return Task.FromResult(new HelloReply { Message = "Hello " + request.Name }); + } - public override async Task SayHellos(HelloRequest request, IServerStreamWriter responseStream, ServerCallContext context) + public override async Task SayHellos(HelloRequest request, IServerStreamWriter responseStream, ServerCallContext context) + { + var i = 0; + while (!context.CancellationToken.IsCancellationRequested) { - var i = 0; - while (!context.CancellationToken.IsCancellationRequested) - { - var message = $"How are you {request.Name}? {++i}"; - this.logger.LogInformation("Sending greeting {Message}.", message); + var message = $"How are you {request.Name}? {++i}"; + this.logger.LogInformation("Sending greeting {Message}.", message); - await responseStream.WriteAsync(new HelloReply { Message = message }).ConfigureAwait(false); + await responseStream.WriteAsync(new HelloReply { Message = message }); - // Gotta look busy - await Task.Delay(1000).ConfigureAwait(false); - } + // Gotta look busy + await Task.Delay(1000); } } } diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/EventSourceTest.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/EventSourceTest.cs index 70fa093dd3e..0673de75553 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/EventSourceTest.cs +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/EventSourceTest.cs @@ -1,31 +1,17 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Instrumentation.Http.Implementation; using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Instrumentation.Http.Tests +namespace OpenTelemetry.Instrumentation.Http.Tests; + +public class EventSourceTest { - public class EventSourceTest + [Fact] + public void EventSourceTest_HttpInstrumentationEventSource() { - [Fact] - public void EventSourceTest_HttpInstrumentationEventSource() - { - EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(HttpInstrumentationEventSource.Log); - } + EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(HttpInstrumentationEventSource.Log); } } diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.Basic.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.Basic.cs index a993f025618..ba758f25103 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.Basic.cs +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.Basic.cs @@ -1,211 +1,283 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; #if NETFRAMEWORK -using System.Net; -#endif using System.Net.Http; +#endif using Microsoft.Extensions.DependencyInjection; -using Moq; using OpenTelemetry.Context.Propagation; using OpenTelemetry.Instrumentation.Http.Implementation; +using OpenTelemetry.Metrics; using OpenTelemetry.Tests; using OpenTelemetry.Trace; using Xunit; +using Xunit.Abstractions; + +namespace OpenTelemetry.Instrumentation.Http.Tests; -namespace OpenTelemetry.Instrumentation.Http.Tests +public partial class HttpClientTests : IDisposable { - public partial class HttpClientTests : IDisposable + private readonly ITestOutputHelper output; + private readonly IDisposable serverLifeTime; + private readonly string host; + private readonly int port; + private readonly string url; + + public HttpClientTests(ITestOutputHelper output) { - private readonly IDisposable serverLifeTime; - private readonly string url; + this.output = output; - public HttpClientTests() - { - this.serverLifeTime = TestHttpServer.RunServer( - (ctx) => + this.serverLifeTime = TestHttpServer.RunServer( + (ctx) => + { + string traceparent = ctx.Request.Headers["traceparent"]; + string custom_traceparent = ctx.Request.Headers["custom_traceparent"]; + if ((ctx.Request.Headers["contextRequired"] == null + || bool.Parse(ctx.Request.Headers["contextRequired"])) + && + (string.IsNullOrWhiteSpace(traceparent) + && string.IsNullOrWhiteSpace(custom_traceparent))) { - string traceparent = ctx.Request.Headers["traceparent"]; - string custom_traceparent = ctx.Request.Headers["custom_traceparent"]; - if (string.IsNullOrWhiteSpace(traceparent) - && string.IsNullOrWhiteSpace(custom_traceparent)) - { - ctx.Response.StatusCode = 500; - ctx.Response.StatusDescription = "Missing trace context"; - } - else if (ctx.Request.Url.PathAndQuery.Contains("500")) - { - ctx.Response.StatusCode = 500; - } - else if (ctx.Request.Url.PathAndQuery.Contains("redirect")) - { - ctx.Response.RedirectLocation = "/"; - ctx.Response.StatusCode = 302; - } - else - { - ctx.Response.StatusCode = 200; - } + ctx.Response.StatusCode = 500; + ctx.Response.StatusDescription = "Missing trace context"; + } + else if (ctx.Request.Url.PathAndQuery.Contains("500")) + { + ctx.Response.StatusCode = 500; + } + else if (ctx.Request.Url.PathAndQuery.Contains("redirect")) + { + ctx.Response.RedirectLocation = "/"; + ctx.Response.StatusCode = 302; + } + else if (ctx.Request.Headers["responseCode"] != null) + { + ctx.Response.StatusCode = int.Parse(ctx.Request.Headers["responseCode"]); + } + else + { + ctx.Response.StatusCode = 200; + } - ctx.Response.OutputStream.Close(); - }, - out var host, - out var port); + ctx.Response.OutputStream.Close(); + }, + out var host, + out var port); - this.url = $"http://{host}:{port}/"; - } + this.host = host; + this.port = port; + this.url = $"http://{host}:{port}/"; - [Fact] - public void AddHttpClientInstrumentation_NamedOptions() - { - int defaultExporterOptionsConfigureOptionsInvocations = 0; - int namedExporterOptionsConfigureOptionsInvocations = 0; + this.output.WriteLine($"HttpServer started: {this.url}"); + } - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .ConfigureServices(services => - { - services.Configure(o => defaultExporterOptionsConfigureOptionsInvocations++); + [Fact] + public void AddHttpClientInstrumentation_NamedOptions() + { + int defaultExporterOptionsConfigureOptionsInvocations = 0; + int namedExporterOptionsConfigureOptionsInvocations = 0; - services.Configure("Instrumentation2", o => namedExporterOptionsConfigureOptionsInvocations++); - }) - .AddHttpClientInstrumentation() - .AddHttpClientInstrumentation("Instrumentation2", configureHttpClientInstrumentationOptions: null) - .Build(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => + { + services.Configure(o => defaultExporterOptionsConfigureOptionsInvocations++); - Assert.Equal(1, defaultExporterOptionsConfigureOptionsInvocations); - Assert.Equal(1, namedExporterOptionsConfigureOptionsInvocations); - } + services.Configure("Instrumentation2", o => namedExporterOptionsConfigureOptionsInvocations++); + }) + .AddHttpClientInstrumentation() + .AddHttpClientInstrumentation("Instrumentation2", configureHttpClientTraceInstrumentationOptions: null) + .Build(); - [Fact] - public void AddHttpClientInstrumentation_BadArgs() - { - TracerProviderBuilder builder = null; - Assert.Throws(() => builder.AddHttpClientInstrumentation()); - } + Assert.Equal(1, defaultExporterOptionsConfigureOptionsInvocations); + Assert.Equal(1, namedExporterOptionsConfigureOptionsInvocations); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task InjectsHeadersAsync(bool shouldEnrich) + [Fact] + public void AddHttpClientInstrumentation_BadArgs() + { + TracerProviderBuilder builder = null; + Assert.Throws(() => builder.AddHttpClientInstrumentation()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task InjectsHeadersAsync(bool shouldEnrich) + { + var exportedItems = new List(); + + using var request = new HttpRequestMessage { - var processor = new Mock>(); - processor.Setup(x => x.OnStart(It.IsAny())).Callback(c => + RequestUri = new Uri(this.url), + Method = new HttpMethod("GET"), + }; + + using var parent = new Activity("parent") + .SetIdFormat(ActivityIdFormat.W3C) + .Start(); + parent.TraceStateString = "k1=v1,k2=v2"; + parent.ActivityTraceFlags = ActivityTraceFlags.Recorded; + + using (Sdk.CreateTracerProviderBuilder() + .AddHttpClientInstrumentation(o => { - c.SetTag("enrichedWithHttpWebRequest", "no"); - c.SetTag("enrichedWithHttpWebResponse", "no"); - c.SetTag("enrichedWithHttpRequestMessage", "no"); - c.SetTag("enrichedWithHttpResponseMessage", "no"); - }); + if (shouldEnrich) + { + o.EnrichWithHttpWebRequest = (activity, httpWebRequest) => + { + activity.SetTag("enrichedWithHttpWebRequest", "yes"); + }; - using var request = new HttpRequestMessage - { - RequestUri = new Uri(this.url), - Method = new HttpMethod("GET"), - }; + o.EnrichWithHttpWebResponse = (activity, httpWebResponse) => + { + activity.SetTag("enrichedWithHttpWebResponse", "yes"); + }; - using var parent = new Activity("parent") - .SetIdFormat(ActivityIdFormat.W3C) - .Start(); - parent.TraceStateString = "k1=v1,k2=v2"; - parent.ActivityTraceFlags = ActivityTraceFlags.Recorded; + o.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => + { + activity.SetTag("enrichedWithHttpRequestMessage", "yes"); + }; - using (Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation(o => - { - if (shouldEnrich) + o.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => { - o.EnrichWithHttpWebRequest = (activity, httpWebRequest) => - { - activity.SetTag("enrichedWithHttpWebRequest", "yes"); - }; - - o.EnrichWithHttpWebResponse = (activity, httpWebResponse) => - { - activity.SetTag("enrichedWithHttpWebResponse", "yes"); - }; - - o.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => - { - activity.SetTag("enrichedWithHttpRequestMessage", "yes"); - }; - - o.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => - { - activity.SetTag("enrichedWithHttpResponseMessage", "yes"); - }; - } - }) - .AddProcessor(processor.Object) - .Build()) - { - using var c = new HttpClient(); - await c.SendAsync(request).ConfigureAwait(false); - } + activity.SetTag("enrichedWithHttpResponseMessage", "yes"); + }; + } + }) + .AddInMemoryExporter(exportedItems) + .Build()) + { + using var c = new HttpClient(); + await c.SendAsync(request); + } - Assert.Equal(5, processor.Invocations.Count); // SetParentProvider/OnStart/OnEnd/OnShutdown/Dispose called. - var activity = (Activity)processor.Invocations[2].Arguments[0]; + Assert.Single(exportedItems); + var activity = exportedItems[0]; - Assert.Equal(ActivityKind.Client, activity.Kind); - Assert.Equal(parent.TraceId, activity.Context.TraceId); - Assert.Equal(parent.SpanId, activity.ParentSpanId); - Assert.NotEqual(parent.SpanId, activity.Context.SpanId); - Assert.NotEqual(default, activity.Context.SpanId); + Assert.Equal(ActivityKind.Client, activity.Kind); + Assert.Equal(parent.TraceId, activity.Context.TraceId); + Assert.Equal(parent.SpanId, activity.ParentSpanId); + Assert.NotEqual(parent.SpanId, activity.Context.SpanId); + Assert.NotEqual(default, activity.Context.SpanId); #if NETFRAMEWORK - // Note: On .NET Framework a HttpWebRequest is created and enriched - // not the HttpRequestMessage passed to HttpClient. - Assert.Empty(request.Headers); + // Note: On .NET Framework a HttpWebRequest is created and enriched + // not the HttpRequestMessage passed to HttpClient. + Assert.Empty(request.Headers); #else - Assert.True(request.Headers.TryGetValues("traceparent", out var traceparents)); - Assert.True(request.Headers.TryGetValues("tracestate", out var tracestates)); - Assert.Single(traceparents); - Assert.Single(tracestates); + Assert.True(request.Headers.TryGetValues("traceparent", out var traceparents)); + Assert.True(request.Headers.TryGetValues("tracestate", out var tracestates)); + Assert.Single(traceparents); + Assert.Single(tracestates); - Assert.Equal($"00-{activity.Context.TraceId}-{activity.Context.SpanId}-01", traceparents.Single()); - Assert.Equal("k1=v1,k2=v2", tracestates.Single()); + Assert.Equal($"00-{activity.Context.TraceId}-{activity.Context.SpanId}-01", traceparents.Single()); + Assert.Equal("k1=v1,k2=v2", tracestates.Single()); #endif #if NETFRAMEWORK - Assert.Equal(shouldEnrich ? "yes" : "no", activity.Tags.Where(tag => tag.Key == "enrichedWithHttpWebRequest").FirstOrDefault().Value); - Assert.Equal(shouldEnrich ? "yes" : "no", activity.Tags.Where(tag => tag.Key == "enrichedWithHttpWebResponse").FirstOrDefault().Value); + if (shouldEnrich) + { + Assert.Equal("yes", activity.Tags.Where(tag => tag.Key == "enrichedWithHttpWebRequest").FirstOrDefault().Value); + Assert.Equal("yes", activity.Tags.Where(tag => tag.Key == "enrichedWithHttpWebResponse").FirstOrDefault().Value); + } + else + { + Assert.DoesNotContain(activity.Tags, tag => tag.Key == "enrichedWithHttpWebRequest"); + Assert.DoesNotContain(activity.Tags, tag => tag.Key == "enrichedWithHttpWebResponse"); + } - Assert.Equal("no", activity.Tags.Where(tag => tag.Key == "enrichedWithHttpRequestMessage").FirstOrDefault().Value); - Assert.Equal("no", activity.Tags.Where(tag => tag.Key == "enrichedWithHttpResponseMessage").FirstOrDefault().Value); + Assert.DoesNotContain(activity.Tags, tag => tag.Key == "enrichedWithHttpRequestMessage"); + Assert.DoesNotContain(activity.Tags, tag => tag.Key == "enrichedWithHttpResponseMessage"); #else - Assert.Equal("no", activity.Tags.Where(tag => tag.Key == "enrichedWithHttpWebRequest").FirstOrDefault().Value); - Assert.Equal("no", activity.Tags.Where(tag => tag.Key == "enrichedWithHttpWebResponse").FirstOrDefault().Value); + Assert.DoesNotContain(activity.Tags, tag => tag.Key == "enrichedWithHttpWebRequest"); + Assert.DoesNotContain(activity.Tags, tag => tag.Key == "enrichedWithHttpWebResponse"); - Assert.Equal(shouldEnrich ? "yes" : "no", activity.Tags.Where(tag => tag.Key == "enrichedWithHttpRequestMessage").FirstOrDefault().Value); - Assert.Equal(shouldEnrich ? "yes" : "no", activity.Tags.Where(tag => tag.Key == "enrichedWithHttpResponseMessage").FirstOrDefault().Value); + if (shouldEnrich) + { + Assert.Equal("yes", activity.Tags.Where(tag => tag.Key == "enrichedWithHttpRequestMessage").FirstOrDefault().Value); + Assert.Equal("yes", activity.Tags.Where(tag => tag.Key == "enrichedWithHttpResponseMessage").FirstOrDefault().Value); + } + else + { + Assert.DoesNotContain(activity.Tags, tag => tag.Key == "enrichedWithHttpRequestMessage"); + Assert.DoesNotContain(activity.Tags, tag => tag.Key == "enrichedWithHttpResponseMessage"); + } #endif + } + + [Fact] + public async Task InjectsHeadersAsync_CustomFormat() + { + var propagator = new CustomTextMapPropagator(); + propagator.InjectValues.Add("custom_traceParent", context => $"00/{context.ActivityContext.TraceId}/{context.ActivityContext.SpanId}/01"); + propagator.InjectValues.Add("custom_traceState", context => Activity.Current.TraceStateString); + + var exportedItems = new List(); + + using var request = new HttpRequestMessage + { + RequestUri = new Uri(this.url), + Method = new HttpMethod("GET"), + }; + + using var parent = new Activity("parent") + .SetIdFormat(ActivityIdFormat.W3C) + .Start(); + parent.TraceStateString = "k1=v1,k2=v2"; + parent.ActivityTraceFlags = ActivityTraceFlags.Recorded; + + Sdk.SetDefaultTextMapPropagator(propagator); + + using (Sdk.CreateTracerProviderBuilder() + .AddHttpClientInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build()) + { + using var c = new HttpClient(); + await c.SendAsync(request); } - [Fact] - public async Task InjectsHeadersAsync_CustomFormat() + Assert.Single(exportedItems); + var activity = exportedItems[0]; + + Assert.Equal(ActivityKind.Client, activity.Kind); + Assert.Equal(parent.TraceId, activity.Context.TraceId); + Assert.Equal(parent.SpanId, activity.ParentSpanId); + Assert.NotEqual(parent.SpanId, activity.Context.SpanId); + Assert.NotEqual(default, activity.Context.SpanId); + +#if NETFRAMEWORK + // Note: On .NET Framework a HttpWebRequest is created and enriched + // not the HttpRequestMessage passed to HttpClient. + Assert.Empty(request.Headers); +#else + Assert.True(request.Headers.TryGetValues("custom_traceParent", out var traceParents)); + Assert.True(request.Headers.TryGetValues("custom_traceState", out var traceStates)); + Assert.Single(traceParents); + Assert.Single(traceStates); + + Assert.Equal($"00/{activity.Context.TraceId}/{activity.Context.SpanId}/01", traceParents.Single()); + Assert.Equal("k1=v1,k2=v2", traceStates.Single()); +#endif + + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] { - var propagator = new Mock(); - propagator.Setup(m => m.Inject(It.IsAny(), It.IsAny(), It.IsAny>())) - .Callback>((context, message, action) => - { - action(message, "custom_traceparent", $"00/{context.ActivityContext.TraceId}/{context.ActivityContext.SpanId}/01"); - action(message, "custom_tracestate", Activity.Current.TraceStateString); - }); + new TraceContextPropagator(), + new BaggagePropagator(), + })); + } - var processor = new Mock>(); + [Fact(Skip = "https://github.com/open-telemetry/opentelemetry-dotnet/issues/5092")] + public async Task RespectsSuppress() + { + try + { + var propagator = new CustomTextMapPropagator(); + propagator.InjectValues.Add("custom_traceParent", context => $"00/{context.ActivityContext.TraceId}/{context.ActivityContext.SpanId}/01"); + propagator.InjectValues.Add("custom_traceState", context => Activity.Current.TraceStateString); + + var exportedItems = new List(); using var request = new HttpRequestMessage { @@ -219,420 +291,452 @@ public async Task InjectsHeadersAsync_CustomFormat() parent.TraceStateString = "k1=v1,k2=v2"; parent.ActivityTraceFlags = ActivityTraceFlags.Recorded; - Sdk.SetDefaultTextMapPropagator(propagator.Object); + Sdk.SetDefaultTextMapPropagator(propagator); using (Sdk.CreateTracerProviderBuilder() .AddHttpClientInstrumentation() - .AddProcessor(processor.Object) + .AddInMemoryExporter(exportedItems) .Build()) { using var c = new HttpClient(); - await c.SendAsync(request).ConfigureAwait(false); + using (SuppressInstrumentationScope.Begin()) + { + await c.SendAsync(request); + } } - Assert.Equal(5, processor.Invocations.Count); // SetParentProvider/OnStart/OnEnd/OnShutdown/Dispose called. - var activity = (Activity)processor.Invocations[2].Arguments[0]; - - Assert.Equal(ActivityKind.Client, activity.Kind); - Assert.Equal(parent.TraceId, activity.Context.TraceId); - Assert.Equal(parent.SpanId, activity.ParentSpanId); - Assert.NotEqual(parent.SpanId, activity.Context.SpanId); - Assert.NotEqual(default, activity.Context.SpanId); - -#if NETFRAMEWORK - // Note: On .NET Framework a HttpWebRequest is created and enriched - // not the HttpRequestMessage passed to HttpClient. - Assert.Empty(request.Headers); -#else - Assert.True(request.Headers.TryGetValues("custom_traceparent", out var traceparents)); - Assert.True(request.Headers.TryGetValues("custom_tracestate", out var tracestates)); - Assert.Single(traceparents); - Assert.Single(tracestates); - - Assert.Equal($"00/{activity.Context.TraceId}/{activity.Context.SpanId}/01", traceparents.Single()); - Assert.Equal("k1=v1,k2=v2", tracestates.Single()); -#endif - + // If suppressed, activity is not emitted and + // propagation is also not performed. + Assert.Empty(exportedItems); + Assert.False(request.Headers.Contains("custom_traceParent")); + Assert.False(request.Headers.Contains("custom_traceState")); + } + finally + { Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] { new TraceContextPropagator(), new BaggagePropagator(), })); } + } - [Fact] - public async Task RespectsSuppress() + [Fact] + public async Task ExportsSpansCreatedForRetries() + { + var exportedItems = new List(); + using var request = new HttpRequestMessage { - try - { - var propagator = new Mock(); - propagator.Setup(m => m.Inject(It.IsAny(), It.IsAny(), It.IsAny>())) - .Callback>((context, message, action) => - { - action(message, "custom_traceparent", $"00/{context.ActivityContext.TraceId}/{context.ActivityContext.SpanId}/01"); - action(message, "custom_tracestate", Activity.Current.TraceStateString); - }); + RequestUri = new Uri(this.url), + Method = new HttpMethod("GET"), + }; + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddHttpClientInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build(); + + int maxRetries = 3; + using var clientHandler = new HttpClientHandler(); + using var retryHandler = new RetryHandler(clientHandler, maxRetries); + using var httpClient = new HttpClient(retryHandler); + await httpClient.SendAsync(request); + + // number of exported spans should be 3(maxRetries) + Assert.Equal(maxRetries, exportedItems.Count); + + var spanid1 = exportedItems[0].SpanId; + var spanid2 = exportedItems[1].SpanId; + var spanid3 = exportedItems[2].SpanId; + + // Validate span ids are different + Assert.NotEqual(spanid1, spanid2); + Assert.NotEqual(spanid3, spanid1); + Assert.NotEqual(spanid2, spanid3); + } - var processor = new Mock>(); + [Theory] + [InlineData("CONNECT", "CONNECT")] + [InlineData("DELETE", "DELETE")] + [InlineData("GET", "GET")] + [InlineData("PUT", "PUT")] + [InlineData("HEAD", "HEAD")] + [InlineData("OPTIONS", "OPTIONS")] + [InlineData("PATCH", "PATCH")] + [InlineData("Get", "GET")] + [InlineData("POST", "POST")] + [InlineData("TRACE", "TRACE")] + [InlineData("CUSTOM", "_OTHER")] + public async Task HttpRequestMethodIsSetOnActivityAsPerSpec(string originalMethod, string expectedMethod) + { + var exportedItems = new List(); + using var request = new HttpRequestMessage + { + RequestUri = new Uri(this.url), + Method = new HttpMethod(originalMethod), + }; - using var request = new HttpRequestMessage - { - RequestUri = new Uri(this.url), - Method = new HttpMethod("GET"), - }; + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddHttpClientInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build(); - using var parent = new Activity("parent") - .SetIdFormat(ActivityIdFormat.W3C) - .Start(); - parent.TraceStateString = "k1=v1,k2=v2"; - parent.ActivityTraceFlags = ActivityTraceFlags.Recorded; + using var httpClient = new HttpClient(); - Sdk.SetDefaultTextMapPropagator(propagator.Object); + try + { + await httpClient.SendAsync(request); + } + catch + { + // ignore error. + } - using (Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation() - .AddProcessor(processor.Object) - .Build()) - { - using var c = new HttpClient(); - using (SuppressInstrumentationScope.Begin()) - { - await c.SendAsync(request).ConfigureAwait(false); - } - } + Assert.Single(exportedItems); - // If suppressed, activity is not emitted and - // propagation is also not performed. - Assert.Equal(3, processor.Invocations.Count); // SetParentProvider/OnShutdown/Dispose called. - Assert.False(request.Headers.Contains("custom_traceparent")); - Assert.False(request.Headers.Contains("custom_tracestate")); - } - finally - { - Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] - { - new TraceContextPropagator(), - new BaggagePropagator(), - })); - } - } + var activity = exportedItems[0]; + + Assert.Contains(activity.TagObjects, t => t.Key == SemanticConventions.AttributeHttpRequestMethod); - [Fact] - public async Task ExportsSpansCreatedForRetries() + if (originalMethod.Equals(expectedMethod, StringComparison.OrdinalIgnoreCase)) { - var exportedItems = new List(); - using var request = new HttpRequestMessage - { - RequestUri = new Uri(this.url), - Method = new HttpMethod("GET"), - }; + Assert.Equal(expectedMethod, activity.DisplayName); + Assert.DoesNotContain(activity.TagObjects, t => t.Key == SemanticConventions.AttributeHttpRequestMethodOriginal); + } + else + { + Assert.Equal("HTTP", activity.DisplayName); + Assert.Equal(originalMethod, activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethodOriginal) as string); + } - using var traceprovider = Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation() - .AddInMemoryExporter(exportedItems) - .Build(); + Assert.Equal(expectedMethod, activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod) as string); + } - int maxRetries = 3; - using var clientHandler = new HttpClientHandler(); - using var retryHandler = new RetryHandler(clientHandler, maxRetries); - using var httpClient = new HttpClient(retryHandler); - await httpClient.SendAsync(request).ConfigureAwait(false); + [Theory] + [InlineData("CONNECT", "CONNECT")] + [InlineData("DELETE", "DELETE")] + [InlineData("GET", "GET")] + [InlineData("PUT", "PUT")] + [InlineData("HEAD", "HEAD")] + [InlineData("OPTIONS", "OPTIONS")] + [InlineData("PATCH", "PATCH")] + [InlineData("Get", "GET")] + [InlineData("POST", "POST")] + [InlineData("TRACE", "TRACE")] + [InlineData("CUSTOM", "_OTHER")] + public async Task HttpRequestMethodIsSetonRequestDurationMetricAsPerSpec(string originalMethod, string expectedMethod) + { + var metricItems = new List(); + using var request = new HttpRequestMessage + { + RequestUri = new Uri(this.url), + Method = new HttpMethod(originalMethod), + }; - // number of exported spans should be 3(maxRetries) - Assert.Equal(maxRetries, exportedItems.Count()); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddHttpClientInstrumentation() + .AddInMemoryExporter(metricItems) + .Build(); - var spanid1 = exportedItems[0].SpanId; - var spanid2 = exportedItems[1].SpanId; - var spanid3 = exportedItems[2].SpanId; + using var httpClient = new HttpClient(); - // Validate span ids are different - Assert.NotEqual(spanid1, spanid2); - Assert.NotEqual(spanid3, spanid1); - Assert.NotEqual(spanid2, spanid3); + try + { + await httpClient.SendAsync(request); } - - [Fact] - public async Task RedirectTest() + catch { - var processor = new Mock>(); - using (Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation() - .AddProcessor(processor.Object) - .Build()) - { - using var c = new HttpClient(); - await c.GetAsync($"{this.url}redirect").ConfigureAwait(false); - } - -#if NETFRAMEWORK - // Note: HttpWebRequest automatically handles redirects and reuses - // the same instance which is patched reflectively. There isn't a - // good way to produce two spans when redirecting that we have - // found. For now, this is not supported. + // ignore error. + } - Assert.Equal(5, processor.Invocations.Count); // SetParentProvider/OnStart/OnEnd/OnShutdown/Dispose called. + meterProvider.Dispose(); - var firstActivity = (Activity)processor.Invocations[2].Arguments[0]; // First OnEnd - Assert.Contains(firstActivity.TagObjects, t => t.Key == "http.status_code" && (int)t.Value == 200); -#else - Assert.Equal(7, processor.Invocations.Count); // SetParentProvider/OnStart/OnEnd/OnStart/OnEnd/OnShutdown/Dispose called. + var metric = metricItems.FirstOrDefault(m => m.Name == "http.client.request.duration"); - 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.NotNull(metric); - var secondActivity = (Activity)processor.Invocations[4].Arguments[0]; // Second OnEnd - Assert.Contains(secondActivity.TagObjects, t => t.Key == "http.status_code" && (int)t.Value == 200); -#endif + var metricPoints = new List(); + foreach (var p in metric.GetMetricPoints()) + { + metricPoints.Add(p); } - [Fact] - public async void RequestNotCollectedWhenInstrumentationFilterApplied() + Assert.Single(metricPoints); + var mp = metricPoints[0]; + + // Inspect Metric Attributes + var attributes = new Dictionary(); + foreach (var tag in mp.Tags) { - var exportedItems = new List(); + attributes[tag.Key] = tag.Value; + } - bool httpWebRequestFilterApplied = false; - bool httpRequestMessageFilterApplied = false; + Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpRequestMethod && kvp.Value.ToString() == expectedMethod); - using (Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation( - opt => - { - opt.FilterHttpWebRequest = (req) => - { - httpWebRequestFilterApplied = true; - return !req.RequestUri.OriginalString.Contains(this.url); - }; - opt.FilterHttpRequestMessage = (req) => - { - httpRequestMessageFilterApplied = true; - return !req.RequestUri.OriginalString.Contains(this.url); - }; - }) - .AddInMemoryExporter(exportedItems) - .Build()) - { - using var c = new HttpClient(); - await c.GetAsync(this.url).ConfigureAwait(false); - } + Assert.DoesNotContain(attributes, t => t.Key == SemanticConventions.AttributeHttpRequestMethodOriginal); + } + + [Fact] + public async Task RedirectTest() + { + var exportedItems = new List(); + using (Sdk.CreateTracerProviderBuilder() + .AddHttpClientInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build()) + { + using var c = new HttpClient(); + await c.GetAsync($"{this.url}redirect"); + } #if NETFRAMEWORK - Assert.True(httpWebRequestFilterApplied); - Assert.False(httpRequestMessageFilterApplied); + // Note: HttpWebRequest automatically handles redirects and reuses + // the same instance which is patched reflectively. There isn't a + // good way to produce two spans when redirecting that we have + // found. For now, this is not supported. + + Assert.Single(exportedItems); + Assert.Contains(exportedItems[0].TagObjects, t => t.Key == "http.response.status_code" && (int)t.Value == 200); #else - Assert.False(httpWebRequestFilterApplied); - Assert.True(httpRequestMessageFilterApplied); + Assert.Equal(2, exportedItems.Count); + Assert.Contains(exportedItems[0].TagObjects, t => t.Key == "http.response.status_code" && (int)t.Value == 302); + Assert.Contains(exportedItems[1].TagObjects, t => t.Key == "http.response.status_code" && (int)t.Value == 200); #endif + } - Assert.Empty(exportedItems); - } + [Fact] + public async void RequestNotCollectedWhenInstrumentationFilterApplied() + { + var exportedItems = new List(); - [Fact] - public async void RequestNotCollectedWhenInstrumentationFilterThrowsException() - { - var exportedItems = new List(); + bool httpWebRequestFilterApplied = false; + bool httpRequestMessageFilterApplied = false; - using (Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation( - (opt) => + using (Sdk.CreateTracerProviderBuilder() + .AddHttpClientInstrumentation( + opt => + { + opt.FilterHttpWebRequest = (req) => { - opt.FilterHttpWebRequest = (req) => throw new Exception("From InstrumentationFilter"); - opt.FilterHttpRequestMessage = (req) => throw new Exception("From InstrumentationFilter"); - }) - .AddInMemoryExporter(exportedItems) - .Build()) - { - using var c = new HttpClient(); - using var inMemoryEventListener = new InMemoryEventListener(HttpInstrumentationEventSource.Log); - await c.GetAsync(this.url).ConfigureAwait(false); - Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 4)); - } - - Assert.Empty(exportedItems); + httpWebRequestFilterApplied = true; + return !req.RequestUri.OriginalString.Contains(this.url); + }; + opt.FilterHttpRequestMessage = (req) => + { + httpRequestMessageFilterApplied = true; + return !req.RequestUri.OriginalString.Contains(this.url); + }; + }) + .AddInMemoryExporter(exportedItems) + .Build()) + { + using var c = new HttpClient(); + await c.GetAsync(this.url); } - [Fact] - public async Task ReportsExceptionEventForNetworkFailuresWithGetAsync() - { - var exportedItems = new List(); - bool exceptionThrown = false; +#if NETFRAMEWORK + Assert.True(httpWebRequestFilterApplied); + Assert.False(httpRequestMessageFilterApplied); +#else + Assert.False(httpWebRequestFilterApplied); + Assert.True(httpRequestMessageFilterApplied); +#endif - using var traceprovider = Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation(o => o.RecordException = true) - .AddInMemoryExporter(exportedItems) - .Build(); + Assert.Empty(exportedItems); + } - using var c = new HttpClient(); - try - { - await c.GetAsync("https://sdlfaldfjalkdfjlkajdflkajlsdjf.sdlkjafsdjfalfadslkf.com/").ConfigureAwait(false); - } - catch - { - exceptionThrown = true; - } + [Fact] + public async void RequestNotCollectedWhenInstrumentationFilterThrowsException() + { + var exportedItems = new List(); - // Exception is thrown and collected as event - Assert.True(exceptionThrown); - Assert.Single(exportedItems[0].Events.Where(evt => evt.Name.Equals("exception"))); + using (Sdk.CreateTracerProviderBuilder() + .AddHttpClientInstrumentation( + (opt) => + { + opt.FilterHttpWebRequest = (req) => throw new Exception("From InstrumentationFilter"); + opt.FilterHttpRequestMessage = (req) => throw new Exception("From InstrumentationFilter"); + }) + .AddInMemoryExporter(exportedItems) + .Build()) + { + using var c = new HttpClient(); + using var inMemoryEventListener = new InMemoryEventListener(HttpInstrumentationEventSource.Log); + await c.GetAsync(this.url); + Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 4)); } - [Fact] - public async Task DoesNotReportExceptionEventOnErrorResponseWithGetAsync() - { - var exportedItems = new List(); - bool exceptionThrown = false; + Assert.Empty(exportedItems); + } - using var traceprovider = Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation(o => o.RecordException = true) - .AddInMemoryExporter(exportedItems) - .Build(); + [Fact] + public async Task ReportsExceptionEventForNetworkFailuresWithGetAsync() + { + var exportedItems = new List(); + bool exceptionThrown = false; - using var c = new HttpClient(); - try - { - await c.GetAsync($"{this.url}500").ConfigureAwait(false); - } - catch - { - exceptionThrown = true; - } + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddHttpClientInstrumentation(o => o.RecordException = true) + .AddInMemoryExporter(exportedItems) + .Build(); - // Exception is not thrown and not collected as event - Assert.False(exceptionThrown); - Assert.Empty(exportedItems[0].Events); + using var c = new HttpClient(); + try + { + await c.GetAsync("https://sdlfaldfjalkdfjlkajdflkajlsdjf.sdlkjafsdjfalfadslkf.com/"); } - - [Fact] - public async Task DoesNotReportExceptionEventOnErrorResponseWithGetStringAsync() + catch { - var exportedItems = new List(); - bool exceptionThrown = false; - using var request = new HttpRequestMessage - { - RequestUri = new Uri($"{this.url}500"), - Method = new HttpMethod("GET"), - }; + exceptionThrown = true; + } - using var traceprovider = Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation(o => o.RecordException = true) - .AddInMemoryExporter(exportedItems) - .Build(); + // Exception is thrown and collected as event + Assert.True(exceptionThrown); + Assert.Single(exportedItems[0].Events.Where(evt => evt.Name.Equals("exception"))); + } - using var c = new HttpClient(); - try - { - await c.GetStringAsync($"{this.url}500").ConfigureAwait(false); - } - catch - { - exceptionThrown = true; - } + [Fact] + public async Task DoesNotReportExceptionEventOnErrorResponseWithGetAsync() + { + var exportedItems = new List(); + bool exceptionThrown = false; - // Exception is thrown and not collected as event - Assert.True(exceptionThrown); - Assert.Empty(exportedItems[0].Events); - } + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddHttpClientInstrumentation(o => o.RecordException = true) + .AddInMemoryExporter(exportedItems) + .Build(); - [Theory] - [InlineData(true, true)] - [InlineData(true, false)] - [InlineData(false, true)] - [InlineData(false, false)] - public async Task CustomPropagatorCalled(bool sample, bool createParentActivity) + using var c = new HttpClient(); + try { - ActivityContext parentContext = default; - ActivityContext contextFromPropagator = default; + await c.GetAsync($"{this.url}500"); + } + catch + { + exceptionThrown = true; + } - var propagator = new Mock(); + // Exception is not thrown and not collected as event + Assert.False(exceptionThrown); + Assert.Empty(exportedItems[0].Events); + } -#if NETFRAMEWORK - propagator.Setup(m => m.Inject(It.IsAny(), It.IsAny(), It.IsAny>())) - .Callback>((context, carrier, setter) => - { - contextFromPropagator = context.ActivityContext; + [Fact] + public async Task DoesNotReportExceptionEventOnErrorResponseWithGetStringAsync() + { + var exportedItems = new List(); + bool exceptionThrown = false; + using var request = new HttpRequestMessage + { + RequestUri = new Uri($"{this.url}500"), + Method = new HttpMethod("GET"), + }; - setter(carrier, "custom_traceparent", $"00/{contextFromPropagator.TraceId}/{contextFromPropagator.SpanId}/01"); - setter(carrier, "custom_tracestate", contextFromPropagator.TraceState); - }); -#else - propagator.Setup(m => m.Inject(It.IsAny(), It.IsAny(), It.IsAny>())) - .Callback>((context, carrier, setter) => - { - contextFromPropagator = context.ActivityContext; + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddHttpClientInstrumentation(o => o.RecordException = true) + .AddInMemoryExporter(exportedItems) + .Build(); - setter(carrier, "custom_traceparent", $"00/{contextFromPropagator.TraceId}/{contextFromPropagator.SpanId}/01"); - setter(carrier, "custom_tracestate", contextFromPropagator.TraceState); - }); -#endif + using var c = new HttpClient(); + try + { + await c.GetStringAsync($"{this.url}500"); + } + catch + { + exceptionThrown = true; + } - var exportedItems = new List(); + // Exception is thrown and not collected as event + Assert.True(exceptionThrown); + Assert.Empty(exportedItems[0].Events); + } - using (var traceprovider = Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation() - .AddInMemoryExporter(exportedItems) - .SetSampler(sample ? new ParentBasedSampler(new AlwaysOnSampler()) : new AlwaysOffSampler()) - .Build()) - { - var previousDefaultTextMapPropagator = Propagators.DefaultTextMapPropagator; - Sdk.SetDefaultTextMapPropagator(propagator.Object); + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public async Task CustomPropagatorCalled(bool sample, bool createParentActivity) + { + ActivityContext parentContext = default; + ActivityContext contextFromPropagator = default; - Activity parent = null; - if (createParentActivity) - { - parent = new Activity("parent") - .SetIdFormat(ActivityIdFormat.W3C) - .Start(); + var propagator = new CustomTextMapPropagator + { + Injected = (context) => contextFromPropagator = context.ActivityContext, + }; + propagator.InjectValues.Add("custom_traceParent", context => $"00/{context.ActivityContext.TraceId}/{context.ActivityContext.SpanId}/01"); + propagator.InjectValues.Add("custom_traceState", context => Activity.Current.TraceStateString); + + var exportedItems = new List(); + + using (var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddHttpClientInstrumentation() + .AddInMemoryExporter(exportedItems) + .SetSampler(sample ? new ParentBasedSampler(new AlwaysOnSampler()) : new AlwaysOffSampler()) + .Build()) + { + var previousDefaultTextMapPropagator = Propagators.DefaultTextMapPropagator; + Sdk.SetDefaultTextMapPropagator(propagator); - parent.TraceStateString = "k1=v1,k2=v2"; - parent.ActivityTraceFlags = ActivityTraceFlags.Recorded; + Activity parent = null; + if (createParentActivity) + { + parent = new Activity("parent") + .SetIdFormat(ActivityIdFormat.W3C) + .Start(); - parentContext = parent.Context; - } + parent.TraceStateString = "k1=v1,k2=v2"; + parent.ActivityTraceFlags = ActivityTraceFlags.Recorded; - using var request = new HttpRequestMessage - { - RequestUri = new Uri(this.url), - Method = new HttpMethod("GET"), - }; + parentContext = parent.Context; + } - using var c = new HttpClient(); - await c.SendAsync(request).ConfigureAwait(false); + using var request = new HttpRequestMessage + { + RequestUri = new Uri(this.url), + Method = new HttpMethod("GET"), + }; - parent?.Stop(); + using var c = new HttpClient(); + await c.SendAsync(request); - Sdk.SetDefaultTextMapPropagator(previousDefaultTextMapPropagator); - } + parent?.Stop(); - if (!sample) - { - Assert.Empty(exportedItems); - } - else - { - Assert.Single(exportedItems); - } + Sdk.SetDefaultTextMapPropagator(previousDefaultTextMapPropagator); + } - // Make sure custom propagator was called. - Assert.True(contextFromPropagator != default); - if (sample) - { - Assert.Equal(contextFromPropagator, exportedItems[0].Context); - } + if (!sample) + { + Assert.Empty(exportedItems); + } + else + { + Assert.Single(exportedItems); + } -#if NETFRAMEWORK - if (!sample && createParentActivity) - { - Assert.Equal(parentContext.TraceId, contextFromPropagator.TraceId); - Assert.Equal(parentContext.SpanId, contextFromPropagator.SpanId); - } -#endif + // Make sure custom propagator was called. + Assert.True(contextFromPropagator != default); + if (sample) + { + Assert.Equal(contextFromPropagator, exportedItems[0].Context); } - public void Dispose() +#if NETFRAMEWORK + if (!sample && createParentActivity) { - this.serverLifeTime?.Dispose(); - Activity.Current = null; - GC.SuppressFinalize(this); + Assert.Equal(parentContext.TraceId, contextFromPropagator.TraceId); + Assert.Equal(parentContext.SpanId, contextFromPropagator.SpanId); } +#endif + } + + public void Dispose() + { + this.serverLifeTime?.Dispose(); + this.output.WriteLine($"HttpServer stopped: {this.url}"); + Activity.Current = null; + GC.SuppressFinalize(this); } } diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs index e4e484e062b..8a65effef0c 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs @@ -1,65 +1,199 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +#if NETFRAMEWORK using System.Net.Http; +#endif +#if !NET8_0_OR_GREATER using System.Reflection; using System.Text.Json; -using Moq; +#endif using OpenTelemetry.Metrics; -using OpenTelemetry.Tests; using OpenTelemetry.Trace; using Xunit; -namespace OpenTelemetry.Instrumentation.Http.Tests +namespace OpenTelemetry.Instrumentation.Http.Tests; + +public partial class HttpClientTests { - public partial class HttpClientTests + public static readonly IEnumerable TestData = HttpTestData.ReadTestCases(); + + [Theory] + [MemberData(nameof(TestData))] + public async Task HttpOutCallsAreCollectedSuccessfullyTracesAndMetricsSemanticConventionsAsync(HttpTestData.HttpOutTestCase tc) + { + await HttpOutCallsAreCollectedSuccessfullyBodyAsync( + this.host, + this.port, + tc, + enableTracing: true, + enableMetrics: true); + } + + [Theory] + [MemberData(nameof(TestData))] + public async Task HttpOutCallsAreCollectedSuccessfullyMetricsOnlyAsync(HttpTestData.HttpOutTestCase tc) + { + await HttpOutCallsAreCollectedSuccessfullyBodyAsync( + this.host, + this.port, + tc, + enableTracing: false, + enableMetrics: true); + } + + [Theory] + [MemberData(nameof(TestData))] + public async Task HttpOutCallsAreCollectedSuccessfullyTracesOnlyAsync(HttpTestData.HttpOutTestCase tc) { - public static IEnumerable TestData => HttpTestData.ReadTestCases(); + await HttpOutCallsAreCollectedSuccessfullyBodyAsync( + this.host, + this.port, + tc, + enableTracing: true, + enableMetrics: false); + } - [Theory] - [MemberData(nameof(TestData))] - public async Task HttpOutCallsAreCollectedSuccessfullyAsync(HttpTestData.HttpOutTestCase tc) + [Theory] + [MemberData(nameof(TestData))] + public async Task HttpOutCallsAreCollectedSuccessfullyNoSignalsAsync(HttpTestData.HttpOutTestCase tc) + { + await HttpOutCallsAreCollectedSuccessfullyBodyAsync( + this.host, + this.port, + tc, + enableTracing: false, + enableMetrics: false); + } + +#if !NET8_0_OR_GREATER + [Fact] + public async Task DebugIndividualTestAsync() + { + var input = JsonSerializer.Deserialize( + @" + [ + { + ""name"": ""Response code: 399"", + ""method"": ""GET"", + ""url"": ""http://{host}:{port}/"", + ""responseCode"": 399, + ""responseExpected"": true, + ""spanName"": ""GET"", + ""spanStatus"": ""Unset"", + ""spanKind"": ""Client"", + ""spanAttributes"": { + ""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.HttpOutCallsAreCollectedSuccessfullyTracesAndMetricsSemanticConventionsAsync), BindingFlags.InvokeMethod, null, this, HttpTestData.GetArgumentsFromTestCaseObject(input).First()); + await t; + } +#endif + + [Fact] + public async Task CheckEnrichmentWhenSampling() + { + await CheckEnrichment(new AlwaysOffSampler(), false, this.url); + await CheckEnrichment(new AlwaysOnSampler(), true, this.url); + } + +#if NET8_0_OR_GREATER + [Theory] + [MemberData(nameof(TestData))] + public async Task ValidateNet8MetricsAsync(HttpTestData.HttpOutTestCase tc) + { + var metrics = new List(); + var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddHttpClientInstrumentation() + .AddInMemoryExporter(metrics) + .Build(); + + var testUrl = HttpTestData.NormalizeValues(tc.Url, this.host, this.port); + + try { - bool enrichWithHttpWebRequestCalled = false; - bool enrichWithHttpWebResponseCalled = false; - bool enrichWithHttpRequestMessageCalled = false; - bool enrichWithHttpResponseMessageCalled = false; - bool enrichWithExceptionCalled = false; - - using var serverLifeTime = TestHttpServer.RunServer( - (ctx) => - { - ctx.Response.StatusCode = tc.ResponseCode == 0 ? 200 : tc.ResponseCode; - ctx.Response.OutputStream.Close(); - }, - out var host, - out var port); + using var c = new HttpClient(); + using var request = new HttpRequestMessage + { + RequestUri = new Uri(testUrl), + Method = new HttpMethod(tc.Method), + }; - var processor = new Mock>(); - tc.Url = HttpTestData.NormalizeValues(tc.Url, host, port); + request.Headers.Add("contextRequired", "false"); + request.Headers.Add("responseCode", (tc.ResponseCode == 0 ? 200 : tc.ResponseCode).ToString()); + await c.SendAsync(request); + } + catch (Exception) + { + // test case can intentionally send request that will result in exception + } + finally + { + meterProvider.Dispose(); + } + + var requestMetrics = metrics + .Where(metric => + metric.Name == "http.client.request.duration" || + metric.Name == "http.client.active_requests" || + metric.Name == "http.client.request.time_in_queue" || + metric.Name == "http.client.connection.duration" || + metric.Name == "http.client.open_connections" || + metric.Name == "dns.lookup.duration") + .ToArray(); + + if (tc.ResponseExpected) + { + Assert.Equal(6, requestMetrics.Count()); + } + else + { + // http.client.connection.duration and http.client.open_connections will not be emitted. + Assert.Equal(4, requestMetrics.Count()); + } + } +#endif + + private static async Task HttpOutCallsAreCollectedSuccessfullyBodyAsync( + string host, + int port, + HttpTestData.HttpOutTestCase tc, + bool enableTracing, + bool enableMetrics) + { + bool enrichWithHttpWebRequestCalled = false; + bool enrichWithHttpWebResponseCalled = false; + bool enrichWithHttpRequestMessageCalled = false; + bool enrichWithHttpResponseMessageCalled = false; + bool enrichWithExceptionCalled = false; + + var testUrl = HttpTestData.NormalizeValues(tc.Url, host, port); + + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder(); - var metrics = new List(); + if (enableMetrics) + { + meterProviderBuilder + .AddHttpClientInstrumentation(); + } - var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddHttpClientInstrumentation() - .AddInMemoryExporter(metrics) - .Build(); + var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder(); - using (Sdk.CreateTracerProviderBuilder() + if (enableTracing) + { + tracerProviderBuilder .AddHttpClientInstrumentation((opt) => { opt.EnrichWithHttpWebRequest = (activity, httpRequestMessage) => { enrichWithHttpWebRequestCalled = true; }; @@ -68,48 +202,65 @@ public async Task HttpOutCallsAreCollectedSuccessfullyAsync(HttpTestData.HttpOut opt.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => { enrichWithHttpResponseMessageCalled = true; }; opt.EnrichWithException = (activity, exception) => { enrichWithExceptionCalled = true; }; opt.RecordException = tc.RecordException ?? false; - }) - .AddProcessor(processor.Object) - .Build()) + }); + } + + var metrics = new List(); + var activities = new List(); + + var meterProvider = meterProviderBuilder + .AddInMemoryExporter(metrics) + .Build(); + + var tracerProvider = tracerProviderBuilder + .AddInMemoryExporter(activities) + .Build(); + + try + { + using var c = new HttpClient(); + using var request = new HttpRequestMessage { - try - { - using var c = new HttpClient(); - using var request = new HttpRequestMessage - { - RequestUri = new Uri(tc.Url), - Method = new HttpMethod(tc.Method), -#if NETFRAMEWORK - Version = new Version(1, 1), -#else - Version = new Version(2, 0), -#endif - }; - - if (tc.Headers != null) - { - foreach (var header in tc.Headers) - { - request.Headers.Add(header.Key, header.Value); - } - } + RequestUri = new Uri(testUrl), + Method = new HttpMethod(tc.Method), + }; - await c.SendAsync(request).ConfigureAwait(false); - } - catch (Exception) + if (tc.Headers != null) + { + foreach (var header in tc.Headers) { - // test case can intentionally send request that will result in exception + request.Headers.Add(header.Key, header.Value); } } + request.Headers.Add("contextRequired", "false"); + request.Headers.Add("responseCode", (tc.ResponseCode == 0 ? 200 : tc.ResponseCode).ToString()); + + await c.SendAsync(request); + } + catch (Exception) + { + // test case can intentionally send request that will result in exception + } + finally + { + tracerProvider.Dispose(); meterProvider.Dispose(); + } - var requestMetrics = metrics - .Where(metric => metric.Name == "http.client.duration") - .ToArray(); + var requestMetrics = metrics + .Where(metric => metric.Name == "http.client.request.duration") + .ToArray(); - Assert.Equal(5, processor.Invocations.Count); // SetParentProvider/OnStart/OnEnd/OnShutdown/Dispose called. - var activity = (Activity)processor.Invocations[2].Arguments[0]; + var normalizedAttributesTestCase = tc.SpanAttributes.ToDictionary(x => x.Key, x => HttpTestData.NormalizeValues(x.Value, host, port)); + + if (!enableTracing) + { + Assert.Empty(activities); + } + else + { + var activity = Assert.Single(activities); Assert.Equal(ActivityKind.Client, activity.Kind); Assert.Equal(tc.SpanName, activity.DisplayName); @@ -134,21 +285,44 @@ public async Task HttpOutCallsAreCollectedSuccessfullyAsync(HttpTestData.HttpOut // Assert.Equal(tc.SpanStatus, d[span.Status.CanonicalCode]); Assert.Equal(tc.SpanStatus, activity.Status.ToString()); - - if (tc.SpanStatusHasDescription.HasValue) - { - var desc = activity.StatusDescription; - Assert.Equal(tc.SpanStatusHasDescription.Value, !string.IsNullOrEmpty(desc)); - } + Assert.Null(activity.StatusDescription); var normalizedAttributes = activity.TagObjects.Where(kv => !kv.Key.StartsWith("otel.")).ToDictionary(x => x.Key, x => x.Value.ToString()); - var normalizedAttributesTestCase = tc.SpanAttributes.ToDictionary(x => x.Key, x => HttpTestData.NormalizeValues(x.Value, host, port)); - Assert.Equal(normalizedAttributesTestCase.Count, normalizedAttributes.Count); + int numberOfTags = activity.Status == ActivityStatusCode.Error ? 5 : 4; + + var expectedAttributeCount = numberOfTags + (tc.ResponseExpected ? 2 : 0); - foreach (var kv in normalizedAttributesTestCase) + Assert.Equal(expectedAttributeCount, normalizedAttributes.Count); + + 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.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.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpResponseStatusCode]); + } + } + else { - Assert.Contains(activity.TagObjects, i => i.Key == kv.Key && i.Value.ToString().Equals(kv.Value, StringComparison.OrdinalIgnoreCase)); + 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"); +#elif NETFRAMEWORK + 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"); +#endif } if (tc.RecordException.HasValue && tc.RecordException.Value) @@ -156,14 +330,19 @@ public async Task HttpOutCallsAreCollectedSuccessfullyAsync(HttpTestData.HttpOut Assert.Single(activity.Events.Where(evt => evt.Name.Equals("exception"))); Assert.True(enrichWithExceptionCalled); } + } -#if NETFRAMEWORK + if (!enableMetrics) + { Assert.Empty(requestMetrics); -#else + } + else + { Assert.Single(requestMetrics); - var metric = requestMetrics[0]; + 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(); @@ -179,129 +358,140 @@ public async Task HttpOutCallsAreCollectedSuccessfullyAsync(HttpTestData.HttpOut var sum = metricPoint.GetHistogramSum(); Assert.Equal(1L, count); - Assert.Equal(activity.Duration.TotalMilliseconds, sum); - var attributes = new KeyValuePair[metricPoint.Tags.Count]; - int i = 0; + if (enableTracing) + { + var activity = Assert.Single(activities); +#if !NET8_0_OR_GREATER + Assert.Equal(activity.Duration.TotalSeconds, sum); +#endif + } + else + { + Assert.True(sum > 0); + } + + // Inspect Metric Attributes + var attributes = new Dictionary(); foreach (var tag in metricPoint.Tags) { - attributes[i++] = tag; + attributes[tag.Key] = tag.Value; } - var method = new KeyValuePair(SemanticConventions.AttributeHttpMethod, tc.Method); - var scheme = new KeyValuePair(SemanticConventions.AttributeHttpScheme, "http"); - var statusCode = new KeyValuePair(SemanticConventions.AttributeHttpStatusCode, tc.ResponseCode == 0 ? 200 : tc.ResponseCode); - var flavor = new KeyValuePair(SemanticConventions.AttributeHttpFlavor, "2.0"); - var hostName = new KeyValuePair(SemanticConventions.AttributeNetPeerName, tc.ResponseExpected ? host : "sdlfaldfjalkdfjlkajdflkajlsdjf"); - var portNumber = new KeyValuePair(SemanticConventions.AttributeNetPeerPort, port); - Assert.Contains(hostName, attributes); - Assert.Contains(portNumber, attributes); - Assert.Contains(method, attributes); - Assert.Contains(scheme, attributes); - Assert.Contains(flavor, attributes); + var numberOfTags = 4; if (tc.ResponseExpected) { - Assert.Contains(statusCode, attributes); - Assert.Equal(6, attributes.Length); + var expectedStatusCode = int.Parse(normalizedAttributesTestCase[SemanticConventions.AttributeHttpResponseStatusCode]); + numberOfTags = (expectedStatusCode >= 400) ? 5 : 4; // error.type extra tag } else { - Assert.DoesNotContain(statusCode, attributes); - Assert.Equal(5, attributes.Length); + numberOfTags = 5; // error.type would be extra } -#endif - } - - [Fact] - public async Task DebugIndividualTestAsync() - { - var input = JsonSerializer.Deserialize( - @" - [ - { - ""name"": ""Response code: 399"", - ""method"": ""GET"", - ""url"": ""http://{host}:{port}/"", - ""responseCode"": 399, - ""responseExpected"": true, - ""spanName"": ""HTTP GET"", - ""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}/"" - } - } - ] - ", - new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); - var t = (Task)this.GetType().InvokeMember(nameof(this.HttpOutCallsAreCollectedSuccessfullyAsync), BindingFlags.InvokeMethod, null, this, HttpTestData.GetArgumentsFromTestCaseObject(input).First()); - await t.ConfigureAwait(false); - } + var expectedAttributeCount = numberOfTags + (tc.ResponseExpected ? 2 : 0); // responsecode + protocolversion - [Fact] - public async Task CheckEnrichmentWhenSampling() - { - await CheckEnrichment(new AlwaysOffSampler(), false, this.url).ConfigureAwait(false); - await CheckEnrichment(new AlwaysOnSampler(), true, this.url).ConfigureAwait(false); - } + Assert.Equal(expectedAttributeCount, attributes.Count); - private static async Task CheckEnrichment(Sampler sampler, bool enrichExpected, string url) - { - bool enrichWithHttpWebRequestCalled = false; - bool enrichWithHttpWebResponseCalled = false; + 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]); - bool enrichWithHttpRequestMessageCalled = false; - bool enrichWithHttpResponseMessageCalled = false; + 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]); - var processor = new Mock>(); - using (Sdk.CreateTracerProviderBuilder() - .SetSampler(sampler) - .AddHttpClientInstrumentation(options => + if (tc.ResponseCode >= 400) { - options.EnrichWithHttpWebRequest = (activity, httpRequestMessage) => { enrichWithHttpWebRequestCalled = true; }; - options.EnrichWithHttpWebResponse = (activity, httpResponseMessage) => { enrichWithHttpWebResponseCalled = true; }; - - options.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => { enrichWithHttpRequestMessageCalled = true; }; - options.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => { enrichWithHttpResponseMessageCalled = true; }; - }) - .AddProcessor(processor.Object) - .Build()) + 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"); +#elif NETFRAMEWORK + 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"); +#endif + } + + // Inspect Histogram Bounds + var histogramBuckets = metricPoint.GetHistogramBuckets(); + var histogramBounds = new List(); + foreach (var t in histogramBuckets) { - using var c = new HttpClient(); - using var r = await c.GetAsync(url).ConfigureAwait(false); + histogramBounds.Add(t.ExplicitBound); } - if (enrichExpected) + // 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 histogramBoundsMatchCorrectly = Enumerable.SequenceEqual(expectedHistogramBoundsOld, histogramBounds) || + Enumerable.SequenceEqual(expectedHistogramBoundsNew, histogramBounds); + + Assert.True(histogramBoundsMatchCorrectly); + } + } + + private static async Task CheckEnrichment(Sampler sampler, bool enrichExpected, string url) + { + bool enrichWithHttpWebRequestCalled = false; + bool enrichWithHttpWebResponseCalled = false; + + bool enrichWithHttpRequestMessageCalled = false; + bool enrichWithHttpResponseMessageCalled = false; + + using (Sdk.CreateTracerProviderBuilder() + .SetSampler(sampler) + .AddHttpClientInstrumentation(options => { + options.EnrichWithHttpWebRequest = (activity, httpRequestMessage) => { enrichWithHttpWebRequestCalled = true; }; + options.EnrichWithHttpWebResponse = (activity, httpResponseMessage) => { enrichWithHttpWebResponseCalled = true; }; + + options.EnrichWithHttpRequestMessage = (activity, httpRequestMessage) => { enrichWithHttpRequestMessageCalled = true; }; + options.EnrichWithHttpResponseMessage = (activity, httpResponseMessage) => { enrichWithHttpResponseMessageCalled = true; }; + }) + .Build()) + { + using var c = new HttpClient(); + using var r = await c.GetAsync(url); + } + + if (enrichExpected) + { #if NETFRAMEWORK - Assert.True(enrichWithHttpWebRequestCalled); - Assert.True(enrichWithHttpWebResponseCalled); + Assert.True(enrichWithHttpWebRequestCalled); + Assert.True(enrichWithHttpWebResponseCalled); - Assert.False(enrichWithHttpRequestMessageCalled); - Assert.False(enrichWithHttpResponseMessageCalled); + Assert.False(enrichWithHttpRequestMessageCalled); + Assert.False(enrichWithHttpResponseMessageCalled); #else - Assert.False(enrichWithHttpWebRequestCalled); - Assert.False(enrichWithHttpWebResponseCalled); + Assert.False(enrichWithHttpWebRequestCalled); + Assert.False(enrichWithHttpWebResponseCalled); - Assert.True(enrichWithHttpRequestMessageCalled); - Assert.True(enrichWithHttpResponseMessageCalled); + Assert.True(enrichWithHttpRequestMessageCalled); + Assert.True(enrichWithHttpResponseMessageCalled); #endif - } - else - { - Assert.False(enrichWithHttpWebRequestCalled); - Assert.False(enrichWithHttpWebResponseCalled); + } + else + { + Assert.False(enrichWithHttpWebRequestCalled); + Assert.False(enrichWithHttpWebResponseCalled); - Assert.False(enrichWithHttpRequestMessageCalled); - Assert.False(enrichWithHttpResponseMessageCalled); - } + Assert.False(enrichWithHttpRequestMessageCalled); + Assert.False(enrichWithHttpResponseMessageCalled); } } } diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpTestData.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpTestData.cs index b6b0211c4a9..1bcfc557155 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpTestData.cs +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpTestData.cs @@ -1,84 +1,64 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Reflection; using System.Text.Json; -namespace OpenTelemetry.Instrumentation.Http.Tests +namespace OpenTelemetry.Instrumentation.Http.Tests; + +public static class HttpTestData { - public static class HttpTestData + public static IEnumerable ReadTestCases() { - public static IEnumerable ReadTestCases() - { - var assembly = Assembly.GetExecutingAssembly(); - var input = JsonSerializer.Deserialize( - assembly.GetManifestResourceStream("OpenTelemetry.Instrumentation.Http.Tests.http-out-test-cases.json"), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); - return GetArgumentsFromTestCaseObject(input); - } + var assembly = Assembly.GetExecutingAssembly(); + var input = JsonSerializer.Deserialize( + assembly.GetManifestResourceStream("OpenTelemetry.Instrumentation.Http.Tests.http-out-test-cases.json"), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + return GetArgumentsFromTestCaseObject(input); + } - public static IEnumerable GetArgumentsFromTestCaseObject(IEnumerable input) - { - var result = new List(); + public static IEnumerable GetArgumentsFromTestCaseObject(IEnumerable input) + { + var result = new List(); - foreach (var testCase in input) + foreach (var testCase in input) + { + result.Add(new object[] { - result.Add(new object[] - { - testCase, - }); - } - - return result; + testCase, + }); } - public static string NormalizeValues(string value, string host, int port) - { - return value - .Replace("{host}", host) - .Replace("{port}", port.ToString()) -#if NETFRAMEWORK - .Replace("{flavor}", "1.1"); -#else - .Replace("{flavor}", "2.0"); -#endif - } + return result; + } - public class HttpOutTestCase - { - public string Name { get; set; } + public static string NormalizeValues(string value, string host, int port) + { + return value + .Replace("{host}", host) + .Replace("{port}", port.ToString()) + .Replace("{flavor}", "1.1"); + } - public string Method { get; set; } + public class HttpOutTestCase + { + public string Name { get; set; } - public string Url { get; set; } + public string Method { get; set; } - public Dictionary Headers { get; set; } + public string Url { get; set; } - public int ResponseCode { get; set; } + public Dictionary Headers { get; set; } - public string SpanName { get; set; } + public int ResponseCode { get; set; } - public bool ResponseExpected { get; set; } + public string SpanName { get; set; } - public bool? RecordException { get; set; } + public bool ResponseExpected { get; set; } - public string SpanStatus { get; set; } + public bool? RecordException { get; set; } - public bool? SpanStatusHasDescription { get; set; } + public string SpanStatus { get; set; } - public Dictionary SpanAttributes { get; set; } - } + public Dictionary SpanAttributes { get; set; } } } diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestActivitySourceTests.netfx.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestActivitySourceTests.netfx.cs index 20e3efea1a6..33708c34d10 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestActivitySourceTests.netfx.cs +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestActivitySourceTests.netfx.cs @@ -1,339 +1,254 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #if NETFRAMEWORK -using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics; -using System.IO; -using System.Linq; using System.Net; using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; using OpenTelemetry.Instrumentation.Http.Implementation; using OpenTelemetry.Tests; using OpenTelemetry.Trace; using Xunit; -namespace OpenTelemetry.Instrumentation.Http.Tests +namespace OpenTelemetry.Instrumentation.Http.Tests; + +public class HttpWebRequestActivitySourceTests : IDisposable { - public class HttpWebRequestActivitySourceTests : IDisposable + private static bool validateBaggage; + private readonly IDisposable testServer; + private readonly string testServerHost; + private readonly int testServerPort; + private readonly string netPeerName; + private readonly int netPeerPort; + + static HttpWebRequestActivitySourceTests() { - private static bool validateBaggage; - private readonly IDisposable testServer; - private readonly string testServerHost; - private readonly int testServerPort; - private readonly string hostNameAndPort; - private readonly string netPeerName; - private readonly int netPeerPort; - - static HttpWebRequestActivitySourceTests() + HttpClientTraceInstrumentationOptions options = new() { - HttpClientInstrumentationOptions options = new() + EnrichWithHttpWebRequest = (activity, httpWebRequest) => { - EnrichWithHttpWebRequest = (activity, httpWebRequest) => - { - VerifyHeaders(httpWebRequest); - - if (validateBaggage) - { - ValidateBaggage(httpWebRequest); - } - }, - }; - - HttpWebRequestActivitySource.Options = options; - - // Need to touch something in HttpWebRequestActivitySource/Sdk to do the static injection. - GC.KeepAlive(HttpWebRequestActivitySource.Options); - _ = Sdk.SuppressInstrumentation; - } - - public HttpWebRequestActivitySourceTests() - { - Assert.Null(Activity.Current); - Activity.DefaultIdFormat = ActivityIdFormat.W3C; - Activity.ForceDefaultIdFormat = false; - - this.testServer = TestHttpServer.RunServer( - ctx => ProcessServerRequest(ctx), - out this.testServerHost, - out this.testServerPort); - - this.hostNameAndPort = $"{this.testServerHost}:{this.testServerPort}"; - this.netPeerName = this.testServerHost; - this.netPeerPort = this.testServerPort; + VerifyHeaders(httpWebRequest); - void ProcessServerRequest(HttpListenerContext context) - { - string redirects = context.Request.QueryString["redirects"]; - if (!string.IsNullOrWhiteSpace(redirects) && int.TryParse(redirects, out int parsedRedirects) && parsedRedirects > 0) + if (validateBaggage) { - context.Response.Redirect(this.BuildRequestUrl(queryString: $"redirects={--parsedRedirects}")); - context.Response.OutputStream.Close(); - return; + ValidateBaggage(httpWebRequest); } + }, + }; - string responseContent; - if (context.Request.QueryString["skipRequestContent"] == null) - { - using StreamReader readStream = new StreamReader(context.Request.InputStream); - - responseContent = readStream.ReadToEnd(); - } - else - { - responseContent = $"{{\"Id\":\"{Guid.NewGuid()}\"}}"; - } + HttpWebRequestActivitySource.TracingOptions = options; - string responseCode = context.Request.QueryString["responseCode"]; - if (!string.IsNullOrWhiteSpace(responseCode)) - { - context.Response.StatusCode = int.Parse(responseCode); - } - else - { - context.Response.StatusCode = 200; - } + _ = Sdk.SuppressInstrumentation; + } - if (context.Response.StatusCode != 204) - { - using StreamWriter writeStream = new StreamWriter(context.Response.OutputStream); + public HttpWebRequestActivitySourceTests() + { + Assert.Null(Activity.Current); + Activity.DefaultIdFormat = ActivityIdFormat.W3C; + Activity.ForceDefaultIdFormat = false; - writeStream.Write(responseContent); - } - else - { - context.Response.OutputStream.Close(); - } - } - } + this.testServer = TestHttpServer.RunServer( + ctx => ProcessServerRequest(ctx), + out this.testServerHost, + out this.testServerPort); - public void Dispose() - { - this.testServer.Dispose(); - } + this.netPeerName = this.testServerHost; + this.netPeerPort = this.testServerPort; - /// - /// A simple test to make sure the Http Diagnostic Source is added into the list of DiagnosticListeners. - /// - [Fact] - public void TestHttpDiagnosticListenerIsRegistered() + void ProcessServerRequest(HttpListenerContext context) { - bool listenerFound = false; - using ActivityListener activityListener = new ActivityListener + string redirects = context.Request.QueryString["redirects"]; + if (!string.IsNullOrWhiteSpace(redirects) && int.TryParse(redirects, out int parsedRedirects) && parsedRedirects > 0) { - ShouldListenTo = activitySource => - { - if (activitySource.Name == HttpWebRequestActivitySource.ActivitySourceName) - { - listenerFound = true; - return true; - } - - return false; - }, - }; - ActivitySource.AddActivityListener(activityListener); - Assert.True(listenerFound, "The Http Diagnostic Listener didn't get added to the AllListeners list."); - } + context.Response.Redirect(this.BuildRequestUrl(queryString: $"redirects={--parsedRedirects}")); + context.Response.OutputStream.Close(); + return; + } - /// - /// A simple test to make sure the Http Diagnostic Source is initialized properly after we subscribed to it, using - /// the subscribe overload with just the observer argument. - /// - [Fact] - public async Task TestReflectInitializationViaSubscription() - { - using var eventRecords = new ActivitySourceRecorder(); + string responseContent; + if (context.Request.QueryString["skipRequestContent"] == null) + { + using StreamReader readStream = new StreamReader(context.Request.InputStream); - // Send a random Http request to generate some events - using (var client = new HttpClient()) + responseContent = readStream.ReadToEnd(); + } + else { - (await client.GetAsync(this.BuildRequestUrl()).ConfigureAwait(false)).Dispose(); + responseContent = $"{{\"Id\":\"{Guid.NewGuid()}\"}}"; } - // Just make sure some events are written, to confirm we successfully subscribed to it. - // We should have exactly one Start and one Stop event - Assert.Equal(2, eventRecords.Records.Count); - } - - /// - /// Test to make sure we get both request and response events. - /// - [Theory] - [InlineData("GET")] - [InlineData("POST")] - [InlineData("POST", "skipRequestContent=1")] - public async Task TestBasicReceiveAndResponseEvents(string method, string queryString = null) - { - var url = this.BuildRequestUrl(queryString: queryString); + string responseCode = context.Request.QueryString["responseCode"]; + if (!string.IsNullOrWhiteSpace(responseCode)) + { + context.Response.StatusCode = int.Parse(responseCode); + } + else + { + context.Response.StatusCode = 200; + } - using var eventRecords = new ActivitySourceRecorder(); + if (context.Response.StatusCode != 204) + { + using StreamWriter writeStream = new StreamWriter(context.Response.OutputStream); - // Send a random Http request to generate some events - using (var client = new HttpClient()) + writeStream.Write(responseContent); + } + else { - (method == "GET" - ? await client.GetAsync(url).ConfigureAwait(false) - : await client.PostAsync(url, new StringContent("hello world")).ConfigureAwait(false)).Dispose(); + context.Response.OutputStream.Close(); } + } + } - // We should have exactly one Start and one Stop event - Assert.Equal(2, eventRecords.Records.Count); - Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start")); - Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop")); + public void Dispose() + { + this.testServer.Dispose(); + } - // Check to make sure: The first record must be a request, the next record must be a response. - Activity activity = AssertFirstEventWasStart(eventRecords); + /// + /// A simple test to make sure the Http Diagnostic Source is added into the list of DiagnosticListeners. + /// + [Fact] + public void TestHttpDiagnosticListenerIsRegistered() + { + bool listenerFound = false; + using ActivityListener activityListener = new ActivityListener + { + ShouldListenTo = activitySource => + { + if (activitySource.Name == HttpWebRequestActivitySource.ActivitySourceName) + { + listenerFound = true; + return true; + } - VerifyActivityStartTags(this.netPeerName, this.netPeerPort, method, url, activity); + return false; + }, + }; + ActivitySource.AddActivityListener(activityListener); + Assert.True(listenerFound, "The Http Diagnostic Listener didn't get added to the AllListeners list."); + } - Assert.True(eventRecords.Records.TryDequeue(out var stopEvent)); - Assert.Equal("Stop", stopEvent.Key); + /// + /// A simple test to make sure the Http Diagnostic Source is initialized properly after we subscribed to it, using + /// the subscribe overload with just the observer argument. + /// + [Fact] + public async Task TestReflectInitializationViaSubscription() + { + using var eventRecords = new ActivitySourceRecorder(); - VerifyActivityStopTags(200, activity); + // Send a random Http request to generate some events + using (var client = new HttpClient()) + { + (await client.GetAsync(this.BuildRequestUrl())).Dispose(); } - [Theory] - [InlineData("GET")] - [InlineData("POST")] - public async Task TestBasicReceiveAndResponseEventsWithoutSampling(string method) - { - using var eventRecords = new ActivitySourceRecorder(activitySamplingResult: ActivitySamplingResult.None); + // Just make sure some events are written, to confirm we successfully subscribed to it. + // We should have exactly one Start and one Stop event + Assert.Equal(2, eventRecords.Records.Count); + } - // Send a random Http request to generate some events - 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(); - } + /// + /// Test to make sure we get both request and response events. + /// + [Theory] + [InlineData("GET")] + [InlineData("POST")] + [InlineData("POST", "skipRequestContent=1")] + public async Task TestBasicReceiveAndResponseEvents(string method, string queryString = null) + { + var url = this.BuildRequestUrl(queryString: queryString); - // There should be no events because we turned off sampling. - Assert.Empty(eventRecords.Records); - } + using var eventRecords = new ActivitySourceRecorder(); - [Theory] - [InlineData("GET", 0)] - [InlineData("GET", 1)] - [InlineData("GET", 2)] - [InlineData("GET", 3)] - [InlineData("POST", 0)] - [InlineData("POST", 1)] - [InlineData("POST", 2)] - [InlineData("POST", 3)] - public async Task TestBasicReceiveAndResponseWebRequestEvents(string method, int mode) + // Send a random Http request to generate some events + using (var client = new HttpClient()) { - string url = this.BuildRequestUrl(); + (method == "GET" + ? await client.GetAsync(url) + : await client.PostAsync(url, new StringContent("hello world"))).Dispose(); + } - using var eventRecords = new ActivitySourceRecorder(); + // We should have exactly one Start and one Stop event + Assert.Equal(2, eventRecords.Records.Count); + Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start")); + Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop")); - // Send a random Http request to generate some events - var webRequest = (HttpWebRequest)WebRequest.Create(url); + // Check to make sure: The first record must be a request, the next record must be a response. + Activity activity = AssertFirstEventWasStart(eventRecords); - if (method == "POST") - { - webRequest.Method = method; + VerifyActivityStartTags(this.netPeerName, this.netPeerPort, method, url, activity); - Stream stream = null; - switch (mode) - { - case 0: - stream = webRequest.GetRequestStream(); - break; - case 1: - stream = await webRequest.GetRequestStreamAsync().ConfigureAwait(false); - break; - case 2: - { - object state = new object(); - using EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.ManualReset); - IAsyncResult asyncResult = webRequest.BeginGetRequestStream( - ar => - { - Assert.Equal(state, ar.AsyncState); - handle.Set(); - }, - state); - stream = webRequest.EndGetRequestStream(asyncResult); - if (!handle.WaitOne(TimeSpan.FromSeconds(30))) - { - throw new InvalidOperationException(); - } + Assert.True(eventRecords.Records.TryDequeue(out var stopEvent)); + Assert.Equal("Stop", stopEvent.Key); - handle.Dispose(); - } + VerifyActivityStopTags(200, activity); + } - break; - case 3: - { - using EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.ManualReset); - object state = new object(); - webRequest.BeginGetRequestStream( - ar => - { - stream = webRequest.EndGetRequestStream(ar); - Assert.Equal(state, ar.AsyncState); - handle.Set(); - }, - state); - if (!handle.WaitOne(TimeSpan.FromSeconds(30))) - { - throw new InvalidOperationException(); - } + [Theory] + [InlineData("GET")] + [InlineData("POST")] + public async Task TestBasicReceiveAndResponseEventsWithoutSampling(string method) + { + using var eventRecords = new ActivitySourceRecorder(activitySamplingResult: ActivitySamplingResult.None); - handle.Dispose(); - } + // Send a random Http request to generate some events + using (var client = new HttpClient()) + { + (method == "GET" + ? await client.GetAsync(this.BuildRequestUrl()) + : await client.PostAsync(this.BuildRequestUrl(), new StringContent("hello world"))).Dispose(); + } - break; - default: - throw new NotSupportedException(); - } + // There should be no events because we turned off sampling. + Assert.Empty(eventRecords.Records); + } - Assert.NotNull(stream); + [Theory] + [InlineData("GET", 0)] + [InlineData("GET", 1)] + [InlineData("GET", 2)] + [InlineData("GET", 3)] + [InlineData("POST", 0)] + [InlineData("POST", 1)] + [InlineData("POST", 2)] + [InlineData("POST", 3)] + public async Task TestBasicReceiveAndResponseWebRequestEvents(string method, int mode) + { + string url = this.BuildRequestUrl(); - using StreamWriter writer = new StreamWriter(stream); + using var eventRecords = new ActivitySourceRecorder(); - writer.WriteLine("hello world"); - } + // Send a random Http request to generate some events + var webRequest = (HttpWebRequest)WebRequest.Create(url); - WebResponse webResponse = null; + if (method == "POST") + { + webRequest.Method = method; + + Stream stream = null; switch (mode) { case 0: - webResponse = webRequest.GetResponse(); + stream = webRequest.GetRequestStream(); break; case 1: - webResponse = await webRequest.GetResponseAsync().ConfigureAwait(false); + stream = await webRequest.GetRequestStreamAsync(); break; case 2: { object state = new object(); using EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.ManualReset); - IAsyncResult asyncResult = webRequest.BeginGetResponse( + IAsyncResult asyncResult = webRequest.BeginGetRequestStream( ar => { Assert.Equal(state, ar.AsyncState); handle.Set(); }, state); - webResponse = webRequest.EndGetResponse(asyncResult); + stream = webRequest.EndGetRequestStream(asyncResult); if (!handle.WaitOne(TimeSpan.FromSeconds(30))) { throw new InvalidOperationException(); @@ -347,10 +262,10 @@ public async Task TestBasicReceiveAndResponseWebRequestEvents(string method, int { using EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.ManualReset); object state = new object(); - webRequest.BeginGetResponse( + webRequest.BeginGetRequestStream( ar => { - webResponse = webRequest.EndGetResponse(ar); + stream = webRequest.EndGetRequestStream(ar); Assert.Equal(state, ar.AsyncState); handle.Set(); }, @@ -368,532 +283,593 @@ public async Task TestBasicReceiveAndResponseWebRequestEvents(string method, int throw new NotSupportedException(); } - Assert.NotNull(webResponse); - - using StreamReader reader = new StreamReader(webResponse.GetResponseStream()); + Assert.NotNull(stream); - reader.ReadToEnd(); // Make sure response is not disposed. + using StreamWriter writer = new StreamWriter(stream); - // We should have exactly one Start and one Stop event - Assert.Equal(2, eventRecords.Records.Count); - Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start")); - Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop")); - - // Check to make sure: The first record must be a request, the next record must be a response. - Activity activity = AssertFirstEventWasStart(eventRecords); - - VerifyActivityStartTags(this.netPeerName, this.netPeerPort, method, url, activity); - - Assert.True(eventRecords.Records.TryDequeue(out var stopEvent)); - Assert.Equal("Stop", stopEvent.Key); - - VerifyActivityStopTags(200, activity); + writer.WriteLine("hello world"); } - [Fact] - public async Task TestTraceStateAndBaggage() + WebResponse webResponse = null; + switch (mode) { - try - { - using var eventRecords = new ActivitySourceRecorder(); - - using var parent = new Activity("w3c activity"); - parent.SetParentId(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom()); - parent.TraceStateString = "some=state"; - parent.Start(); - - Baggage.SetBaggage("k", "v"); - - // Send a random Http request to generate some events - using (var client = new HttpClient()) + case 0: + webResponse = webRequest.GetResponse(); + break; + case 1: + webResponse = await webRequest.GetResponseAsync(); + break; + case 2: { - (await client.GetAsync(this.BuildRequestUrl()).ConfigureAwait(false)).Dispose(); - } - - parent.Stop(); - - Assert.Equal(2, eventRecords.Records.Count()); - - // Check to make sure: The first record must be a request, the next record must be a response. - _ = AssertFirstEventWasStart(eventRecords); - } - finally - { - this.CleanUpActivity(); - } - } + object state = new object(); + using EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.ManualReset); + IAsyncResult asyncResult = webRequest.BeginGetResponse( + ar => + { + Assert.Equal(state, ar.AsyncState); + handle.Set(); + }, + state); + webResponse = webRequest.EndGetResponse(asyncResult); + if (!handle.WaitOne(TimeSpan.FromSeconds(30))) + { + throw new InvalidOperationException(); + } - [Theory] - [InlineData("GET")] - [InlineData("POST")] - public async Task DoNotInjectTraceParentWhenPresent(string method) - { - try - { - using var eventRecords = new ActivitySourceRecorder(); + handle.Dispose(); + } - // Send a random Http request to generate some events - using (var client = new HttpClient()) - using (var request = new HttpRequestMessage(HttpMethod.Get, this.BuildRequestUrl())) + break; + case 3: { - request.Headers.Add("traceparent", "00-abcdef0123456789abcdef0123456789-abcdef0123456789-01"); - - if (method == "GET") - { - request.Method = HttpMethod.Get; - } - else + using EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.ManualReset); + object state = new object(); + webRequest.BeginGetResponse( + ar => + { + webResponse = webRequest.EndGetResponse(ar); + Assert.Equal(state, ar.AsyncState); + handle.Set(); + }, + state); + if (!handle.WaitOne(TimeSpan.FromSeconds(30))) { - request.Method = HttpMethod.Post; - request.Content = new StringContent("hello world"); + throw new InvalidOperationException(); } - (await client.SendAsync(request).ConfigureAwait(false)).Dispose(); + handle.Dispose(); } - // No events are sent. - Assert.Empty(eventRecords.Records); - } - finally - { - this.CleanUpActivity(); - } + break; + default: + throw new NotSupportedException(); } - /// - /// Test to make sure we get both request and response events. - /// - [Theory] - [InlineData("GET")] - [InlineData("POST")] - public async Task TestResponseWithoutContentEvents(string method) - { - string url = this.BuildRequestUrl(queryString: "responseCode=204"); + Assert.NotNull(webResponse); - using var eventRecords = new ActivitySourceRecorder(); + using StreamReader reader = new StreamReader(webResponse.GetResponseStream()); - // Send a random Http request to generate some events - 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); - } + reader.ReadToEnd(); // Make sure response is not disposed. - // We should have exactly one Start and one Stop event - Assert.Equal(2, eventRecords.Records.Count); - Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start")); - Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop")); + // We should have exactly one Start and one Stop event + Assert.Equal(2, eventRecords.Records.Count); + Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start")); + Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop")); - // Check to make sure: The first record must be a request, the next record must be a response. - Activity activity = AssertFirstEventWasStart(eventRecords); + // Check to make sure: The first record must be a request, the next record must be a response. + Activity activity = AssertFirstEventWasStart(eventRecords); - VerifyActivityStartTags(this.netPeerName, this.netPeerPort, method, url, activity); + VerifyActivityStartTags(this.netPeerName, this.netPeerPort, method, url, activity); - Assert.True(eventRecords.Records.TryDequeue(out var stopEvent)); - Assert.Equal("Stop", stopEvent.Key); + Assert.True(eventRecords.Records.TryDequeue(out var stopEvent)); + Assert.Equal("Stop", stopEvent.Key); - VerifyActivityStopTags(204, activity); - } + VerifyActivityStopTags(200, activity); + } - /// - /// Test that if request is redirected, it gets only one Start and one Stop event. - /// - [Theory] - [InlineData("GET")] - [InlineData("POST")] - public async Task TestRedirectedRequest(string method) + [Fact] + public async Task TestTraceStateAndBaggage() + { + try { using var eventRecords = new ActivitySourceRecorder(); + using var parent = new Activity("w3c activity"); + parent.SetParentId(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom()); + parent.TraceStateString = "some=state"; + parent.Start(); + + Baggage.SetBaggage("k", "v"); + + // Send a random Http request to generate some events 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())).Dispose(); } - // We should have exactly one Start and one Stop event + parent.Stop(); + Assert.Equal(2, eventRecords.Records.Count()); - Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start")); - Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop")); - } - /// - /// Test exception in request processing: exception should have expected type/status and now be swallowed by reflection hook. - /// - [Theory] - [InlineData("GET")] - [InlineData("POST")] - public async Task TestRequestWithException(string method) + // Check to make sure: The first record must be a request, the next record must be a response. + _ = AssertFirstEventWasStart(eventRecords); + } + finally { - string host = Guid.NewGuid().ToString() + ".com"; - string url = method == "GET" - ? $"http://{host}" - : $"http://{host}"; + this.CleanUpActivity(); + } + } + [Theory] + [InlineData("GET")] + [InlineData("POST")] + public async Task DoNotInjectTraceParentWhenPresent(string method) + { + try + { using var eventRecords = new ActivitySourceRecorder(); - var ex = await Assert.ThrowsAsync(() => + // Send a random Http request to generate some events + using (var client = new HttpClient()) + using (var request = new HttpRequestMessage(HttpMethod.Get, this.BuildRequestUrl())) { - 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; - Assert.NotNull(webException); - Assert.True(webException.Status == WebExceptionStatus.NameResolutionFailure); - - // We should have one Start event and one Stop event with an exception. - Assert.Equal(2, eventRecords.Records.Count()); - Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start")); - Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop")); + request.Headers.Add("traceparent", "00-abcdef0123456789abcdef0123456789-abcdef0123456789-01"); - // Check to make sure: The first record must be a request, the next record must be an exception. - Activity activity = AssertFirstEventWasStart(eventRecords); - VerifyActivityStartTags(host, null, method, url, activity); + if (method == "GET") + { + request.Method = HttpMethod.Get; + } + else + { + request.Method = HttpMethod.Post; + request.Content = new StringContent("hello world"); + } - Assert.True(eventRecords.Records.TryDequeue(out KeyValuePair exceptionEvent)); - Assert.Equal("Stop", exceptionEvent.Key); + (await client.SendAsync(request)).Dispose(); + } - Assert.True(activity.Status != ActivityStatusCode.Unset); - Assert.NotNull(activity.StatusDescription); + // No events are sent. + Assert.Empty(eventRecords.Records); } - - /// - /// Test request cancellation: reflection hook does not throw. - /// - [Theory] - [InlineData("GET")] - [InlineData("POST")] - public async Task TestCanceledRequest(string method) + finally { - string url = this.BuildRequestUrl(); + this.CleanUpActivity(); + } + } - CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - using var eventRecords = new ActivitySourceRecorder(_ => { cts.Cancel(); }); + /// + /// Test to make sure we get both request and response events. + /// + [Theory] + [InlineData("GET")] + [InlineData("POST")] + public async Task TestResponseWithoutContentEvents(string method) + { + string url = this.BuildRequestUrl(queryString: "responseCode=204"); - using (var client = new HttpClient()) - { - var ex = await Assert.ThrowsAnyAsync(() => - { - 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); - } + using var eventRecords = new ActivitySourceRecorder(); - // We should have one Start event and one Stop event with an exception. - Assert.Equal(2, eventRecords.Records.Count()); - Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start")); - Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop")); + // Send a random Http request to generate some events + using (var client = new HttpClient()) + { + using HttpResponseMessage response = method == "GET" + ? await client.GetAsync(url) + : await client.PostAsync(url, new StringContent("hello world")); + } - Activity activity = AssertFirstEventWasStart(eventRecords); - VerifyActivityStartTags(this.netPeerName, this.netPeerPort, method, url, activity); + // We should have exactly one Start and one Stop event + Assert.Equal(2, eventRecords.Records.Count); + Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start")); + Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop")); - Assert.True(eventRecords.Records.TryDequeue(out KeyValuePair exceptionEvent)); - Assert.Equal("Stop", exceptionEvent.Key); + // Check to make sure: The first record must be a request, the next record must be a response. + Activity activity = AssertFirstEventWasStart(eventRecords); - Assert.True(exceptionEvent.Value.Status != ActivityStatusCode.Unset); - Assert.True(exceptionEvent.Value.StatusDescription == null); - } + VerifyActivityStartTags(this.netPeerName, this.netPeerPort, method, url, activity); - /// - /// Test request connection exception: reflection hook does not throw. - /// - [Theory] - [InlineData("GET")] - [InlineData("POST")] - public async Task TestSecureTransportFailureRequest(string method) - { - string url = "https://expired.badssl.com/"; + Assert.True(eventRecords.Records.TryDequeue(out var stopEvent)); + Assert.Equal("Stop", stopEvent.Key); - using var eventRecords = new ActivitySourceRecorder(); + VerifyActivityStopTags(204, activity); + } - using (var client = new HttpClient()) - { - var ex = await Assert.ThrowsAnyAsync(() => - { - // https://expired.badssl.com/ has an expired certificae. - return method == "GET" - ? client.GetAsync(url) - : client.PostAsync(url, new StringContent("hello world")); - }).ConfigureAwait(false); - Assert.True(ex is HttpRequestException); - } + /// + /// Test that if request is redirected, it gets only one Start and one Stop event. + /// + [Theory] + [InlineData("GET")] + [InlineData("POST")] + public async Task TestRedirectedRequest(string method) + { + using var eventRecords = new ActivitySourceRecorder(); - // We should have one Start event and one Stop event with an exception. - Assert.Equal(2, eventRecords.Records.Count()); - Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start")); - Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop")); + using (var client = new HttpClient()) + { + using HttpResponseMessage response = method == "GET" + ? await client.GetAsync(this.BuildRequestUrl(queryString: "redirects=10")) + : await client.PostAsync(this.BuildRequestUrl(queryString: "redirects=10"), new StringContent("hello world")); + } - Activity activity = AssertFirstEventWasStart(eventRecords); - VerifyActivityStartTags("expired.badssl.com", null, method, url, activity); + // We should have exactly one Start and one Stop event + Assert.Equal(2, eventRecords.Records.Count()); + Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start")); + Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop")); + } - Assert.True(eventRecords.Records.TryDequeue(out KeyValuePair exceptionEvent)); - Assert.Equal("Stop", exceptionEvent.Key); + /// + /// Test exception in request processing: exception should have expected type/status and now be swallowed by reflection hook. + /// + [Theory] + [InlineData("GET")] + [InlineData("POST")] + public async Task TestRequestWithException(string method) + { + string host = Guid.NewGuid().ToString() + ".com"; + string url = method == "GET" + ? $"http://{host}" + : $"http://{host}"; - Assert.True(exceptionEvent.Value.Status != ActivityStatusCode.Unset); - Assert.NotNull(exceptionEvent.Value.StatusDescription); - } + using var eventRecords = new ActivitySourceRecorder(); - /// - /// Test request connection retry: reflection hook does not throw. - /// - [Theory] - [InlineData("GET")] - [InlineData("POST")] - public async Task TestSecureTransportRetryFailureRequest(string method) + var ex = await Assert.ThrowsAsync(() => { - // This test sends an https request to an endpoint only set up for http. - // It should retry. What we want to test for is 1 start, 1 exception event even - // though multiple are actually sent. + return method == "GET" + ? new HttpClient().GetAsync(url) + : new HttpClient().PostAsync(url, new StringContent("hello world")); + }); + + // check that request failed because of the wrong domain name and not because of reflection + var webException = (WebException)ex.InnerException; + Assert.NotNull(webException); + Assert.True(webException.Status == WebExceptionStatus.NameResolutionFailure); + + // We should have one Start event and one Stop event with an exception. + Assert.Equal(2, eventRecords.Records.Count()); + Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start")); + Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop")); + + // Check to make sure: The first record must be a request, the next record must be an exception. + Activity activity = AssertFirstEventWasStart(eventRecords); + VerifyActivityStartTags(host, null, method, url, activity); + + Assert.True(eventRecords.Records.TryDequeue(out KeyValuePair exceptionEvent)); + Assert.Equal("Stop", exceptionEvent.Key); + + Assert.True(activity.Status != ActivityStatusCode.Unset); + Assert.Null(activity.StatusDescription); + } - string url = this.BuildRequestUrl(useHttps: true); + /// + /// Test request cancellation: reflection hook does not throw. + /// + [Theory] + [InlineData("GET")] + [InlineData("POST")] + public async Task TestCanceledRequest(string method) + { + string url = this.BuildRequestUrl(); - using var eventRecords = new ActivitySourceRecorder(); + CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + using var eventRecords = new ActivitySourceRecorder(_ => { cts.Cancel(); }); - using (var client = new HttpClient()) + using (var client = new HttpClient()) + { + var ex = await Assert.ThrowsAnyAsync(() => { - var ex = await Assert.ThrowsAnyAsync(() => - { - return method == "GET" - ? client.GetAsync(url) - : client.PostAsync(url, new StringContent("hello world")); - }).ConfigureAwait(false); - Assert.True(ex is HttpRequestException); - } + return method == "GET" + ? client.GetAsync(url, cts.Token) + : client.PostAsync(url, new StringContent("hello world"), cts.Token); + }); + Assert.True(ex is TaskCanceledException || ex is WebException); + } - // We should have one Start event and one Stop event with an exception. - Assert.Equal(2, eventRecords.Records.Count()); - Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start")); - Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop")); + // We should have one Start event and one Stop event with an exception. + Assert.Equal(2, eventRecords.Records.Count()); + Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start")); + Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop")); - Activity activity = AssertFirstEventWasStart(eventRecords); - VerifyActivityStartTags(this.netPeerName, this.netPeerPort, method, url, activity); + Activity activity = AssertFirstEventWasStart(eventRecords); + VerifyActivityStartTags(this.netPeerName, this.netPeerPort, method, url, activity); - Assert.True(eventRecords.Records.TryDequeue(out KeyValuePair exceptionEvent)); - Assert.Equal("Stop", exceptionEvent.Key); + Assert.True(eventRecords.Records.TryDequeue(out KeyValuePair exceptionEvent)); + Assert.Equal("Stop", exceptionEvent.Key); - Assert.True(exceptionEvent.Value.Status != ActivityStatusCode.Unset); - Assert.NotNull(exceptionEvent.Value.StatusDescription); - } + Assert.True(exceptionEvent.Value.Status != ActivityStatusCode.Unset); + Assert.True(exceptionEvent.Value.StatusDescription == null); + } - [Fact] - public async Task TestInvalidBaggage() - { - validateBaggage = true; - Baggage - .SetBaggage("key", "value") - .SetBaggage("bad/key", "value") - .SetBaggage("goodkey", "bad/value"); + /// + /// Test request connection exception: reflection hook does not throw. + /// + [Theory] + [InlineData("GET")] + [InlineData("POST")] + public async Task TestSecureTransportFailureRequest(string method) + { + string url = "https://expired.badssl.com/"; - using var eventRecords = new ActivitySourceRecorder(); + using var eventRecords = new ActivitySourceRecorder(); - using (var client = new HttpClient()) + using (var client = new HttpClient()) + { + var ex = await Assert.ThrowsAnyAsync(() => { - (await client.GetAsync(this.BuildRequestUrl()).ConfigureAwait(false)).Dispose(); - } + // https://expired.badssl.com/ has an expired certificae. + return method == "GET" + ? client.GetAsync(url) + : client.PostAsync(url, new StringContent("hello world")); + }); + Assert.True(ex is HttpRequestException); + } - Assert.Equal(2, eventRecords.Records.Count()); - Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start")); - Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop")); + // We should have one Start event and one Stop event with an exception. + Assert.Equal(2, eventRecords.Records.Count()); + Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start")); + Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop")); - validateBaggage = false; - } + Activity activity = AssertFirstEventWasStart(eventRecords); + VerifyActivityStartTags("expired.badssl.com", null, method, url, activity); - /// - /// Test to make sure every event record has the right dynamic properties. - /// - [Fact] - public void TestMultipleConcurrentRequests() - { - ServicePointManager.DefaultConnectionLimit = int.MaxValue; - using var parentActivity = new Activity("parent").Start(); - using var eventRecords = new ActivitySourceRecorder(); + Assert.True(eventRecords.Records.TryDequeue(out KeyValuePair exceptionEvent)); + Assert.Equal("Stop", exceptionEvent.Key); - Dictionary> requestData = new Dictionary>(); - for (int i = 0; i < 10; i++) - { - Uri uriWithRedirect = new Uri(this.BuildRequestUrl(queryString: $"q={i}&redirects=3")); + Assert.True(exceptionEvent.Value.Status != ActivityStatusCode.Unset); + Assert.Null(exceptionEvent.Value.StatusDescription); + } - requestData[uriWithRedirect] = null; - } + /// + /// Test request connection retry: reflection hook does not throw. + /// + [Theory] + [InlineData("GET")] + [InlineData("POST")] + public async Task TestSecureTransportRetryFailureRequest(string method) + { + // This test sends an https request to an endpoint only set up for http. + // It should retry. What we want to test for is 1 start, 1 exception event even + // though multiple are actually sent. - // Issue all requests simultaneously - using var httpClient = new HttpClient(); - Dictionary> tasks = new Dictionary>(); + string url = this.BuildRequestUrl(useHttps: true); - CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - foreach (var url in requestData.Keys) - { - tasks.Add(url, httpClient.GetAsync(url, cts.Token)); - } + using var eventRecords = new ActivitySourceRecorder(); - // wait up to 10 sec for all requests and suppress exceptions - Task.WhenAll(tasks.Select(t => t.Value).ToArray()).ContinueWith(tt => + using (var client = new HttpClient()) + { + var ex = await Assert.ThrowsAnyAsync(() => { - foreach (var task in tasks) - { - task.Value.Result?.Dispose(); - } - }).Wait(); + return method == "GET" + ? client.GetAsync(url) + : client.PostAsync(url, new StringContent("hello world")); + }); + Assert.True(ex is HttpRequestException); + } - // Examine the result. Make sure we got all successful requests. + // We should have one Start event and one Stop event with an exception. + Assert.Equal(2, eventRecords.Records.Count()); + Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start")); + Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop")); - // Just make sure some events are written, to confirm we successfully subscribed to it. We should have - // exactly 1 Start event per request and exactly 1 Stop event per response (if request succeeded) - var successfulTasks = tasks.Where(t => t.Value.Status == TaskStatus.RanToCompletion); + Activity activity = AssertFirstEventWasStart(eventRecords); + VerifyActivityStartTags(this.netPeerName, this.netPeerPort, method, url, activity); - Assert.Equal(tasks.Count, eventRecords.Records.Count(rec => rec.Key == "Start")); - Assert.Equal(successfulTasks.Count(), eventRecords.Records.Count(rec => rec.Key == "Stop")); + Assert.True(eventRecords.Records.TryDequeue(out KeyValuePair exceptionEvent)); + Assert.Equal("Stop", exceptionEvent.Key); - // Check to make sure: We have a WebRequest and a WebResponse for each successful request - foreach (var pair in eventRecords.Records) - { - Activity activity = pair.Value; + Assert.True(exceptionEvent.Value.Status != ActivityStatusCode.Unset); + Assert.Null(exceptionEvent.Value.StatusDescription); + } - Assert.True( - pair.Key == "Start" || - pair.Key == "Stop", - "An unexpected event of name " + pair.Key + "was received"); - } - } + [Fact] + public async Task TestInvalidBaggage() + { + validateBaggage = true; + Baggage + .SetBaggage("key", "value") + .SetBaggage("bad/key", "value") + .SetBaggage("goodkey", "bad/value"); + + using var eventRecords = new ActivitySourceRecorder(); - private static Activity AssertFirstEventWasStart(ActivitySourceRecorder eventRecords) + using (var client = new HttpClient()) { - Assert.True(eventRecords.Records.TryDequeue(out KeyValuePair startEvent)); - Assert.Equal("Start", startEvent.Key); - return startEvent.Value; + (await client.GetAsync(this.BuildRequestUrl())).Dispose(); } - private static void VerifyHeaders(HttpWebRequest startRequest) + Assert.Equal(2, eventRecords.Records.Count()); + Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start")); + Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop")); + + validateBaggage = false; + } + + /// + /// Test to make sure every event record has the right dynamic properties. + /// + [Fact] + public async Task TestMultipleConcurrentRequests() + { + ServicePointManager.DefaultConnectionLimit = int.MaxValue; + using var parentActivity = new Activity("parent").Start(); + using var eventRecords = new ActivitySourceRecorder(); + + Dictionary> requestData = new Dictionary>(); + for (int i = 0; i < 10; i++) { - var tracestate = startRequest.Headers["tracestate"]; - Assert.Equal("some=state", tracestate); + Uri uriWithRedirect = new Uri(this.BuildRequestUrl(queryString: $"q={i}&redirects=3")); + + requestData[uriWithRedirect] = null; + } - var baggage = startRequest.Headers["baggage"]; - Assert.Equal("k=v", baggage); + // Issue all requests simultaneously + using var httpClient = new HttpClient(); + Dictionary> tasks = new Dictionary>(); - var traceparent = startRequest.Headers["traceparent"]; - Assert.NotNull(traceparent); - Assert.Matches("^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$", traceparent); + CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + foreach (var url in requestData.Keys) + { + tasks.Add(url, httpClient.GetAsync(url, cts.Token)); } - private static void VerifyActivityStartTags(string netPeerName, int? netPeerPort, string method, string url, Activity activity) + // wait up to 10 sec for all requests and suppress exceptions + await Task.WhenAll(tasks.Select(t => t.Value).ToArray()).ContinueWith(async tt => { - Assert.NotNull(activity.TagObjects); - Assert.Equal(method, activity.GetTagValue(SemanticConventions.AttributeHttpMethod)); - if (netPeerPort != null) + foreach (var task in tasks) { - Assert.Equal(netPeerPort, activity.GetTagValue(SemanticConventions.AttributeNetPeerPort)); + (await task.Value)?.Dispose(); } + }); + + // Examine the result. Make sure we got all successful requests. + + // Just make sure some events are written, to confirm we successfully subscribed to it. We should have + // exactly 1 Start event per request and exactly 1 Stop event per response (if request succeeded) + var successfulTasks = tasks.Where(t => t.Value.Status == TaskStatus.RanToCompletion); + + Assert.Equal(tasks.Count, eventRecords.Records.Count(rec => rec.Key == "Start")); + Assert.Equal(successfulTasks.Count(), eventRecords.Records.Count(rec => rec.Key == "Stop")); - Assert.Equal(netPeerName, activity.GetTagValue(SemanticConventions.AttributeNetPeerName)); + // Check to make sure: We have a WebRequest and a WebResponse for each successful request + foreach (var pair in eventRecords.Records) + { + Activity activity = pair.Value; - Assert.Equal(url, activity.GetTagValue(SemanticConventions.AttributeHttpUrl)); + Assert.True( + pair.Key == "Start" || + pair.Key == "Stop", + "An unexpected event of name " + pair.Key + "was received"); } + } + + private static Activity AssertFirstEventWasStart(ActivitySourceRecorder eventRecords) + { + Assert.True(eventRecords.Records.TryDequeue(out KeyValuePair startEvent)); + Assert.Equal("Start", startEvent.Key); + return startEvent.Value; + } + + private static void VerifyHeaders(HttpWebRequest startRequest) + { + var tracestate = startRequest.Headers["tracestate"]; + Assert.Equal("some=state", tracestate); + + var baggage = startRequest.Headers["baggage"]; + Assert.Equal("k=v", baggage); + + var traceparent = startRequest.Headers["traceparent"]; + Assert.NotNull(traceparent); + Assert.Matches("^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$", traceparent); + } - private static void VerifyActivityStopTags(int statusCode, Activity activity) + private static void VerifyActivityStartTags(string netPeerName, int? netPeerPort, string method, string url, Activity activity) + { + Assert.NotNull(activity.TagObjects); + Assert.Equal(method, activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod)); + if (netPeerPort != null) { - Assert.Equal(statusCode, activity.GetTagValue(SemanticConventions.AttributeHttpStatusCode)); + Assert.Equal(netPeerPort, activity.GetTagValue(SemanticConventions.AttributeServerPort)); } - private static void ActivityEnrichment(Activity activity, string method, object obj) + Assert.Equal(netPeerName, activity.GetTagValue(SemanticConventions.AttributeServerAddress)); + + Assert.Equal(url, activity.GetTagValue(SemanticConventions.AttributeUrlFull)); + } + + private static void VerifyActivityStopTags(int statusCode, Activity activity) + { + Assert.Equal(statusCode, activity.GetTagValue(SemanticConventions.AttributeHttpResponseStatusCode)); + } + + private static void ActivityEnrichment(Activity activity, string method, object obj) + { + switch (method) { - switch (method) - { - case "OnStartActivity": - Assert.True(obj is HttpWebRequest); - VerifyHeaders(obj as HttpWebRequest); + case "OnStartActivity": + Assert.True(obj is HttpWebRequest); + VerifyHeaders(obj as HttpWebRequest); - if (validateBaggage) - { - ValidateBaggage(obj as HttpWebRequest); - } + if (validateBaggage) + { + ValidateBaggage(obj as HttpWebRequest); + } - break; + break; - case "OnStopActivity": - Assert.True(obj is HttpWebResponse); - break; + case "OnStopActivity": + Assert.True(obj is HttpWebResponse); + break; - case "OnException": - Assert.True(obj is Exception); - break; + case "OnException": + Assert.True(obj is Exception); + break; - default: - break; - } + default: + break; } + } - private static void ValidateBaggage(HttpWebRequest request) - { - string[] baggage = request.Headers["baggage"].Split(','); + private static void ValidateBaggage(HttpWebRequest request) + { + string[] baggage = request.Headers["baggage"].Split(','); - Assert.Equal(3, baggage.Length); - Assert.Contains("key=value", baggage); - Assert.Contains("bad%2Fkey=value", baggage); - Assert.Contains("goodkey=bad%2Fvalue", baggage); - } + Assert.Equal(3, baggage.Length); + Assert.Contains("key=value", baggage); + Assert.Contains("bad%2Fkey=value", baggage); + Assert.Contains("goodkey=bad%2Fvalue", baggage); + } - private string BuildRequestUrl(bool useHttps = false, string path = "echo", string queryString = null) - { - return $"{(useHttps ? "https" : "http")}://{this.testServerHost}:{this.testServerPort}/{path}{(string.IsNullOrWhiteSpace(queryString) ? string.Empty : $"?{queryString}")}"; - } + private string BuildRequestUrl(bool useHttps = false, string path = "echo", string queryString = null) + { + return $"{(useHttps ? "https" : "http")}://{this.testServerHost}:{this.testServerPort}/{path}{(string.IsNullOrWhiteSpace(queryString) ? string.Empty : $"?{queryString}")}"; + } - private void CleanUpActivity() + private void CleanUpActivity() + { + while (Activity.Current != null) { - while (Activity.Current != null) - { - Activity.Current.Stop(); - } + Activity.Current.Stop(); } + } - /// - /// is a helper class for recording events. - /// - private class ActivitySourceRecorder : IDisposable - { - private readonly Action> onEvent; - private readonly ActivityListener activityListener; + /// + /// is a helper class for recording events. + /// + private class ActivitySourceRecorder : IDisposable + { + private readonly Action> onEvent; + private readonly ActivityListener activityListener; - public ActivitySourceRecorder(Action> onEvent = null, ActivitySamplingResult activitySamplingResult = ActivitySamplingResult.AllDataAndRecorded) + public ActivitySourceRecorder(Action> onEvent = null, ActivitySamplingResult activitySamplingResult = ActivitySamplingResult.AllDataAndRecorded) + { + this.activityListener = new ActivityListener { - this.activityListener = new ActivityListener - { - ShouldListenTo = (activitySource) => activitySource.Name == HttpWebRequestActivitySource.ActivitySourceName, - ActivityStarted = this.ActivityStarted, - ActivityStopped = this.ActivityStopped, - Sample = (ref ActivityCreationOptions options) => activitySamplingResult, - }; + ShouldListenTo = (activitySource) => activitySource.Name == HttpWebRequestActivitySource.ActivitySourceName, + ActivityStarted = this.ActivityStarted, + ActivityStopped = this.ActivityStopped, + Sample = (ref ActivityCreationOptions options) => activitySamplingResult, + }; - ActivitySource.AddActivityListener(this.activityListener); + ActivitySource.AddActivityListener(this.activityListener); - this.onEvent = onEvent; - } + this.onEvent = onEvent; + } - public ConcurrentQueue> Records { get; } = new ConcurrentQueue>(); + public ConcurrentQueue> Records { get; } = new ConcurrentQueue>(); - public void Dispose() - { - this.activityListener.Dispose(); - } + public void Dispose() + { + this.activityListener.Dispose(); + } - public void ActivityStarted(Activity activity) => this.Record("Start", activity); + public void ActivityStarted(Activity activity) => this.Record("Start", activity); - public void ActivityStopped(Activity activity) => this.Record("Stop", activity); + public void ActivityStopped(Activity activity) => this.Record("Stop", activity); - private void Record(string eventName, Activity activity) - { - var record = new KeyValuePair(eventName, activity); + private void Record(string eventName, Activity activity) + { + var record = new KeyValuePair(eventName, activity); - this.Records.Enqueue(record); - this.onEvent?.Invoke(record); - } + this.Records.Enqueue(record); + this.onEvent?.Invoke(record); } } } diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.Basic.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.Basic.cs index f7f3da60106..b7f70add10b 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.Basic.cs +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.Basic.cs @@ -1,27 +1,10 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Net; -#if !NETFRAMEWORK -using System.Net.Http; -#endif using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Moq; using OpenTelemetry.Context.Propagation; using OpenTelemetry.Instrumentation.Http.Implementation; using OpenTelemetry.Tests; @@ -30,371 +13,351 @@ #pragma warning disable SYSLIB0014 // Type or member is obsolete -namespace OpenTelemetry.Instrumentation.Http.Tests +namespace OpenTelemetry.Instrumentation.Http.Tests; + +public partial class HttpWebRequestTests : IDisposable { - public partial class HttpWebRequestTests : IDisposable - { - private readonly IDisposable serverLifeTime; - private readonly string url; + private readonly IDisposable serverLifeTime; + private readonly string url; - public HttpWebRequestTests() - { - Assert.Null(Activity.Current); - Activity.DefaultIdFormat = ActivityIdFormat.W3C; - Activity.ForceDefaultIdFormat = false; + public HttpWebRequestTests() + { + Assert.Null(Activity.Current); + Activity.DefaultIdFormat = ActivityIdFormat.W3C; + Activity.ForceDefaultIdFormat = false; - this.serverLifeTime = TestHttpServer.RunServer( - (ctx) => + this.serverLifeTime = TestHttpServer.RunServer( + (ctx) => + { + if (string.IsNullOrWhiteSpace(ctx.Request.Headers["traceparent"]) + && string.IsNullOrWhiteSpace(ctx.Request.Headers["custom_traceparent"]) + && ctx.Request.QueryString["bypassHeaderCheck"] != "true") { - if (string.IsNullOrWhiteSpace(ctx.Request.Headers["traceparent"]) - && string.IsNullOrWhiteSpace(ctx.Request.Headers["custom_traceparent"]) - && ctx.Request.QueryString["bypassHeaderCheck"] != "true") - { - ctx.Response.StatusCode = 500; - ctx.Response.StatusDescription = "Missing trace context"; - } - else if (ctx.Request.Url.PathAndQuery.Contains("500")) - { - ctx.Response.StatusCode = 500; - } - else - { - ctx.Response.StatusCode = 200; - } + ctx.Response.StatusCode = 500; + ctx.Response.StatusDescription = "Missing trace context"; + } + else if (ctx.Request.Url.PathAndQuery.Contains("500")) + { + ctx.Response.StatusCode = 500; + } + else + { + ctx.Response.StatusCode = 200; + } - ctx.Response.OutputStream.Close(); - }, - out var host, - out var port); + ctx.Response.OutputStream.Close(); + }, + out var host, + out var port); - this.url = $"http://{host}:{port}/"; - } + this.url = $"http://{host}:{port}/"; + } - public void Dispose() - { - this.serverLifeTime?.Dispose(); - } + public void Dispose() + { + this.serverLifeTime?.Dispose(); + } - [Fact] - public async Task BacksOffIfAlreadyInstrumented() - { - var activityProcessor = new Mock>(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddProcessor(activityProcessor.Object) - .AddHttpClientInstrumentation() - .Build(); + [Fact] + public async Task BacksOffIfAlreadyInstrumented() + { + var exportedItems = new List(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddInMemoryExporter(exportedItems) + .AddHttpClientInstrumentation() + .Build(); - var request = (HttpWebRequest)WebRequest.Create(this.url); + var request = (HttpWebRequest)WebRequest.Create(this.url); - request.Method = "GET"; + request.Method = "GET"; - request.Headers.Add("traceparent", "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01"); + 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 - // is needed to prevent issues when the same request is re-used for - // things like redirects or SSL negotiation. - Assert.Equal(1, activityProcessor.Invocations.Count); // SetParentProvider called + Assert.Empty(exportedItems); #else - Assert.Equal(3, activityProcessor.Invocations.Count); // SetParentProvider/Begin/End called + Assert.Single(exportedItems); #endif - } + } - [Fact] - public async Task RequestNotCollectedWhenInstrumentationFilterApplied() - { - bool httpWebRequestFilterApplied = false; - bool httpRequestMessageFilterApplied = false; + [Fact] + public async Task RequestNotCollectedWhenInstrumentationFilterApplied() + { + bool httpWebRequestFilterApplied = false; + bool httpRequestMessageFilterApplied = false; - List exportedItems = new(); + var exportedItems = new List(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddInMemoryExporter(exportedItems) - .AddHttpClientInstrumentation( - options => + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddInMemoryExporter(exportedItems) + .AddHttpClientInstrumentation( + options => + { + options.FilterHttpWebRequest = (req) => + { + httpWebRequestFilterApplied = true; + return !req.RequestUri.OriginalString.Contains(this.url); + }; + options.FilterHttpRequestMessage = (req) => { - options.FilterHttpWebRequest = (req) => - { - httpWebRequestFilterApplied = true; - return !req.RequestUri.OriginalString.Contains(this.url); - }; - options.FilterHttpRequestMessage = (req) => - { - httpRequestMessageFilterApplied = true; - return !req.RequestUri.OriginalString.Contains(this.url); - }; - }) - .Build(); + httpRequestMessageFilterApplied = true; + return !req.RequestUri.OriginalString.Contains(this.url); + }; + }) + .Build(); - var request = (HttpWebRequest)WebRequest.Create($"{this.url}?bypassHeaderCheck=true"); + var request = (HttpWebRequest)WebRequest.Create($"{this.url}?bypassHeaderCheck=true"); - request.Method = "GET"; + request.Method = "GET"; - using var response = await request.GetResponseAsync().ConfigureAwait(false); + using var response = await request.GetResponseAsync(); #if NETFRAMEWORK - Assert.True(httpWebRequestFilterApplied); - Assert.False(httpRequestMessageFilterApplied); + Assert.True(httpWebRequestFilterApplied); + Assert.False(httpRequestMessageFilterApplied); #else - Assert.False(httpWebRequestFilterApplied); - Assert.True(httpRequestMessageFilterApplied); + Assert.False(httpWebRequestFilterApplied); + Assert.True(httpRequestMessageFilterApplied); #endif - Assert.Empty(exportedItems); - } - - [Fact] - public async Task RequestNotCollectedWhenInstrumentationFilterThrowsException() - { - List exportedItems = new(); + Assert.Empty(exportedItems); + } - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddInMemoryExporter(exportedItems) - .AddHttpClientInstrumentation( - c => - { - c.FilterHttpWebRequest = (req) => throw new Exception("From Instrumentation filter"); - c.FilterHttpRequestMessage = (req) => throw new Exception("From Instrumentation filter"); - }) - .Build(); + [Fact] + public async Task RequestNotCollectedWhenInstrumentationFilterThrowsException() + { + var exportedItems = new List(); - using (var inMemoryEventListener = new InMemoryEventListener(HttpInstrumentationEventSource.Log)) - { - var request = (HttpWebRequest)WebRequest.Create($"{this.url}?bypassHeaderCheck=true"); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddInMemoryExporter(exportedItems) + .AddHttpClientInstrumentation( + c => + { + c.FilterHttpWebRequest = (req) => throw new Exception("From Instrumentation filter"); + c.FilterHttpRequestMessage = (req) => throw new Exception("From Instrumentation filter"); + }) + .Build(); - request.Method = "GET"; + using (var inMemoryEventListener = new InMemoryEventListener(HttpInstrumentationEventSource.Log)) + { + var request = (HttpWebRequest)WebRequest.Create($"{this.url}?bypassHeaderCheck=true"); - using var response = await request.GetResponseAsync().ConfigureAwait(false); + request.Method = "GET"; - Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 4)); - } + using var response = await request.GetResponseAsync(); - Assert.Empty(exportedItems); + Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 4)); } - [Fact] - public async Task InjectsHeadersAsync() - { - var activityProcessor = new Mock>(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddProcessor(activityProcessor.Object) - .AddHttpClientInstrumentation() - .Build(); + Assert.Empty(exportedItems); + } - var request = (HttpWebRequest)WebRequest.Create(this.url); + [Fact] + public async Task InjectsHeadersAsync() + { + var exportedItems = new List(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddInMemoryExporter(exportedItems) + .AddHttpClientInstrumentation() + .Build(); - request.Method = "GET"; + var request = (HttpWebRequest)WebRequest.Create(this.url); + + request.Method = "GET"; - using var parent = new Activity("parent") - .SetIdFormat(ActivityIdFormat.W3C) - .Start(); - parent.TraceStateString = "k1=v1,k2=v2"; - parent.ActivityTraceFlags = ActivityTraceFlags.Recorded; + using var parent = new Activity("parent") + .SetIdFormat(ActivityIdFormat.W3C) + .Start(); + 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]; + Assert.Single(exportedItems); + var activity = exportedItems[0]; - Assert.Equal(parent.TraceId, activity.Context.TraceId); - Assert.Equal(parent.SpanId, activity.ParentSpanId); - Assert.NotEqual(parent.SpanId, activity.Context.SpanId); - Assert.NotEqual(default, activity.Context.SpanId); + Assert.Equal(parent.TraceId, activity.Context.TraceId); + Assert.Equal(parent.SpanId, activity.ParentSpanId); + Assert.NotEqual(parent.SpanId, activity.Context.SpanId); + Assert.NotEqual(default, activity.Context.SpanId); #if NETFRAMEWORK - string traceparent = request.Headers.Get("traceparent"); - string tracestate = request.Headers.Get("tracestate"); + string traceparent = request.Headers.Get("traceparent"); + string tracestate = request.Headers.Get("tracestate"); - Assert.Equal($"00-{activity.Context.TraceId}-{activity.Context.SpanId}-01", traceparent); - Assert.Equal("k1=v1,k2=v2", tracestate); + Assert.Equal($"00-{activity.Context.TraceId}-{activity.Context.SpanId}-01", traceparent); + Assert.Equal("k1=v1,k2=v2", tracestate); #else - // Note: On .NET HttpRequestMessage is created and enriched - // not the HttpWebRequest that was executed. - Assert.Empty(request.Headers); + // Note: On .NET HttpRequestMessage is created and enriched + // not the HttpWebRequest that was executed. + Assert.Empty(request.Headers); #endif - } + } - [Theory] - [InlineData(true, true)] - [InlineData(true, false)] - [InlineData(false, true)] - [InlineData(false, false)] - public async Task CustomPropagatorCalled(bool sample, bool createParentActivity) + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public async Task CustomPropagatorCalled(bool sample, bool createParentActivity) + { + ActivityContext parentContext = default; + ActivityContext contextFromPropagator = default; + + var propagator = new CustomTextMapPropagator { - ActivityContext parentContext = default; - ActivityContext contextFromPropagator = default; + Injected = (PropagationContext context) => contextFromPropagator = context.ActivityContext, + }; + propagator.InjectValues.Add("custom_traceParent", context => $"00/{context.ActivityContext.TraceId}/{context.ActivityContext.SpanId}/01"); + propagator.InjectValues.Add("custom_traceState", context => Activity.Current.TraceStateString); + + var exportedItems = new List(); + + using (var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddHttpClientInstrumentation() + .AddInMemoryExporter(exportedItems) + .SetSampler(sample ? new ParentBasedSampler(new AlwaysOnSampler()) : new AlwaysOffSampler()) + .Build()) + { + var previousDefaultTextMapPropagator = Propagators.DefaultTextMapPropagator; + Sdk.SetDefaultTextMapPropagator(propagator); - var propagator = new Mock(); -#if NETFRAMEWORK - propagator.Setup(m => m.Inject(It.IsAny(), It.IsAny(), It.IsAny>())) - .Callback>((context, carrier, setter) => - { - contextFromPropagator = context.ActivityContext; + Activity parent = null; + if (createParentActivity) + { + parent = new Activity("parent") + .SetIdFormat(ActivityIdFormat.W3C) + .Start(); - setter(carrier, "traceparent", $"00/{contextFromPropagator.TraceId}/{contextFromPropagator.SpanId}/01"); - setter(carrier, "tracestate", contextFromPropagator.TraceState); - }); -#else - propagator.Setup(m => m.Inject(It.IsAny(), It.IsAny(), It.IsAny>())) - .Callback>((context, carrier, setter) => - { - contextFromPropagator = context.ActivityContext; + parent.TraceStateString = "k1=v1,k2=v2"; + parent.ActivityTraceFlags = ActivityTraceFlags.Recorded; - setter(carrier, "traceparent", $"00/{contextFromPropagator.TraceId}/{contextFromPropagator.SpanId}/01"); - setter(carrier, "tracestate", contextFromPropagator.TraceState); - }); -#endif + parentContext = parent.Context; + } - var exportedItems = new List(); + var request = (HttpWebRequest)WebRequest.Create(this.url); - using (var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation() - .AddInMemoryExporter(exportedItems) - .SetSampler(sample ? new ParentBasedSampler(new AlwaysOnSampler()) : new AlwaysOffSampler()) - .Build()) - { - var previousDefaultTextMapPropagator = Propagators.DefaultTextMapPropagator; - Sdk.SetDefaultTextMapPropagator(propagator.Object); + request.Method = "GET"; - Activity parent = null; - if (createParentActivity) - { - parent = new Activity("parent") - .SetIdFormat(ActivityIdFormat.W3C) - .Start(); + using var response = await request.GetResponseAsync(); - parent.TraceStateString = "k1=v1,k2=v2"; - parent.ActivityTraceFlags = ActivityTraceFlags.Recorded; + parent?.Stop(); - parentContext = parent.Context; - } + Sdk.SetDefaultTextMapPropagator(previousDefaultTextMapPropagator); + } - var request = (HttpWebRequest)WebRequest.Create(this.url); + if (!sample) + { + Assert.Empty(exportedItems); + } + else + { + Assert.Single(exportedItems); + } - request.Method = "GET"; + Assert.True(contextFromPropagator != default); + if (sample) + { + Assert.Equal(contextFromPropagator, exportedItems[0].Context); + } - using var response = await request.GetResponseAsync().ConfigureAwait(false); +#if NETFRAMEWORK + if (!sample && createParentActivity) + { + Assert.Equal(parentContext.TraceId, contextFromPropagator.TraceId); + Assert.Equal(parentContext.SpanId, contextFromPropagator.SpanId); + } +#endif + } - parent?.Stop(); + [Theory] + [InlineData(null)] + [InlineData("CustomName")] + public void AddHttpClientInstrumentationUsesOptionsApi(string name) + { + name ??= Options.DefaultName; - Sdk.SetDefaultTextMapPropagator(previousDefaultTextMapPropagator); - } + int configurationDelegateInvocations = 0; - if (!sample) + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => { - Assert.Empty(exportedItems); - } - else + services.Configure(name, o => configurationDelegateInvocations++); + }) + .AddHttpClientInstrumentation(name, options => { - Assert.Single(exportedItems); - } + Assert.IsType(options); + }) + .Build(); - Assert.True(contextFromPropagator != default); - if (sample) - { - Assert.Equal(contextFromPropagator, exportedItems[0].Context); - } + Assert.Equal(1, configurationDelegateInvocations); + } -#if NETFRAMEWORK - if (!sample && createParentActivity) - { - Assert.Equal(parentContext.TraceId, contextFromPropagator.TraceId); - Assert.Equal(parentContext.SpanId, contextFromPropagator.SpanId); - } -#endif - } + [Fact] + public async Task ReportsExceptionEventForNetworkFailures() + { + var exportedItems = new List(); + bool exceptionThrown = false; - [Theory] - [InlineData(null)] - [InlineData("CustomName")] - public void AddHttpClientInstrumentationUsesOptionsApi(string name) - { - name ??= Options.DefaultName; + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddHttpClientInstrumentation(o => o.RecordException = true) + .AddInMemoryExporter(exportedItems) + .Build(); - int configurationDelegateInvocations = 0; + try + { + var request = (HttpWebRequest)WebRequest.Create("https://sdlfaldfjalkdfjlkajdflkajlsdjf.sdlkjafsdjfalfadslkf.com/"); - var activityProcessor = new Mock>(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .ConfigureServices(services => - { - services.Configure(name, o => configurationDelegateInvocations++); - }) - .AddProcessor(activityProcessor.Object) - .AddHttpClientInstrumentation(name, options => - { - Assert.IsType(options); - }) - .Build(); + request.Method = "GET"; - Assert.Equal(1, configurationDelegateInvocations); + using var response = await request.GetResponseAsync(); } - - [Fact] - public async Task ReportsExceptionEventForNetworkFailures() + catch { - var exportedItems = new List(); - bool exceptionThrown = false; - - using var traceprovider = Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation(o => o.RecordException = true) - .AddInMemoryExporter(exportedItems) - .Build(); - - try - { - var request = (HttpWebRequest)WebRequest.Create("https://sdlfaldfjalkdfjlkajdflkajlsdjf.sdlkjafsdjfalfadslkf.com/"); + exceptionThrown = true; + } - request.Method = "GET"; + // Exception is thrown and collected as event + Assert.True(exceptionThrown); + Assert.Single(exportedItems[0].Events.Where(evt => evt.Name.Equals("exception"))); + } - using var response = await request.GetResponseAsync().ConfigureAwait(false); - } - catch - { - exceptionThrown = true; - } + [Fact] + public async Task ReportsExceptionEventOnErrorResponse() + { + var exportedItems = new List(); + bool exceptionThrown = false; - // Exception is thrown and collected as event - Assert.True(exceptionThrown); - Assert.Single(exportedItems[0].Events.Where(evt => evt.Name.Equals("exception"))); - } + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddHttpClientInstrumentation(o => o.RecordException = true) + .AddInMemoryExporter(exportedItems) + .Build(); - [Fact] - public async Task ReportsExceptionEventOnErrorResponse() + try { - var exportedItems = new List(); - bool exceptionThrown = false; + var request = (HttpWebRequest)WebRequest.Create($"{this.url}500"); - using var traceprovider = Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation(o => o.RecordException = true) - .AddInMemoryExporter(exportedItems) - .Build(); - - try - { - var request = (HttpWebRequest)WebRequest.Create($"{this.url}500"); - - request.Method = "GET"; + request.Method = "GET"; - using var response = await request.GetResponseAsync().ConfigureAwait(false); - } - catch - { - exceptionThrown = true; - } + using var response = await request.GetResponseAsync(); + } + catch + { + exceptionThrown = true; + } #if NETFRAMEWORK - // Exception is thrown and collected as event - Assert.True(exceptionThrown); - Assert.Single(exportedItems[0].Events.Where(evt => evt.Name.Equals("exception"))); + // Exception is thrown and collected as event + Assert.True(exceptionThrown); + Assert.Single(exportedItems[0].Events.Where(evt => evt.Name.Equals("exception"))); #else - // Note: On .NET Core exceptions through HttpWebRequest do not - // trigger exception events they just throw: - // https://github.com/dotnet/runtime/blob/cc5ba0994d6e8a6f5e4a63d1c921a68eda4350e8/src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs#L1371 - Assert.True(exceptionThrown); - Assert.DoesNotContain(exportedItems[0].Events, evt => evt.Name.Equals("exception")); + // Note: On .NET Core exceptions through HttpWebRequest do not + // trigger exception events they just throw: + // https://github.com/dotnet/runtime/blob/cc5ba0994d6e8a6f5e4a63d1c921a68eda4350e8/src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs#L1371 + Assert.True(exceptionThrown); + Assert.DoesNotContain(exportedItems[0].Events, evt => evt.Name.Equals("exception")); #endif - } } } diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.cs index 55d3cacaf1d..19de685bc41 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.cs +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.cs @@ -1,199 +1,186 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Net; using System.Text.Json; -using Moq; using OpenTelemetry.Tests; using OpenTelemetry.Trace; using Xunit; #pragma warning disable SYSLIB0014 // Type or member is obsolete -namespace OpenTelemetry.Instrumentation.Http.Tests +namespace OpenTelemetry.Instrumentation.Http.Tests; + +public partial class HttpWebRequestTests { - public partial class HttpWebRequestTests - { - public static IEnumerable TestData => HttpTestData.ReadTestCases(); + public static IEnumerable TestData => HttpTestData.ReadTestCases(); - [Theory] - [MemberData(nameof(TestData))] - public void HttpOutCallsAreCollectedSuccessfully(HttpTestData.HttpOutTestCase tc) - { - using var serverLifeTime = TestHttpServer.RunServer( - (ctx) => - { - ctx.Response.StatusCode = tc.ResponseCode == 0 ? 200 : tc.ResponseCode; - ctx.Response.OutputStream.Close(); - }, - out var host, - out var port); - - bool enrichWithHttpWebRequestCalled = false; - bool enrichWithHttpWebResponseCalled = false; - bool enrichWithHttpRequestMessageCalled = false; - bool enrichWithHttpResponseMessageCalled = false; - bool enrichWithExceptionCalled = false; - - var activityProcessor = new Mock>(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddProcessor(activityProcessor.Object) - .AddHttpClientInstrumentation(options => - { - options.EnrichWithHttpWebRequest = (activity, httpWebRequest) => { enrichWithHttpWebRequestCalled = true; }; - options.EnrichWithHttpWebResponse = (activity, httpWebResponse) => { enrichWithHttpWebResponseCalled = true; }; - options.EnrichWithHttpRequestMessage = (activity, request) => { enrichWithHttpRequestMessageCalled = true; }; - options.EnrichWithHttpResponseMessage = (activity, response) => { enrichWithHttpResponseMessageCalled = true; }; - options.EnrichWithException = (activity, exception) => { enrichWithExceptionCalled = true; }; - options.RecordException = tc.RecordException ?? false; - }) - .Build(); - - tc.Url = HttpTestData.NormalizeValues(tc.Url, host, port); - - try + [Theory] + [MemberData(nameof(TestData))] + public void HttpOutCallsAreCollectedSuccessfully(HttpTestData.HttpOutTestCase tc) + { + using var serverLifeTime = TestHttpServer.RunServer( + (ctx) => { - var request = (HttpWebRequest)WebRequest.Create(tc.Url); + ctx.Response.StatusCode = tc.ResponseCode == 0 ? 200 : tc.ResponseCode; + ctx.Response.OutputStream.Close(); + }, + out var host, + out var port); + + bool enrichWithHttpWebRequestCalled = false; + bool enrichWithHttpWebResponseCalled = false; + bool enrichWithHttpRequestMessageCalled = false; + bool enrichWithHttpResponseMessageCalled = false; + bool enrichWithExceptionCalled = false; + + var exportedItems = new List(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddInMemoryExporter(exportedItems) + .AddHttpClientInstrumentation(options => + { + options.EnrichWithHttpWebRequest = (activity, httpWebRequest) => { enrichWithHttpWebRequestCalled = true; }; + options.EnrichWithHttpWebResponse = (activity, httpWebResponse) => { enrichWithHttpWebResponseCalled = true; }; + options.EnrichWithHttpRequestMessage = (activity, request) => { enrichWithHttpRequestMessageCalled = true; }; + options.EnrichWithHttpResponseMessage = (activity, response) => { enrichWithHttpResponseMessageCalled = true; }; + options.EnrichWithException = (activity, exception) => { enrichWithExceptionCalled = true; }; + options.RecordException = tc.RecordException ?? false; + }) + .Build(); + + tc.Url = HttpTestData.NormalizeValues(tc.Url, host, port); + + try + { + var request = (HttpWebRequest)WebRequest.Create(tc.Url); - request.Method = tc.Method; + request.Method = tc.Method; - if (tc.Headers != null) + if (tc.Headers != null) + { + foreach (var header in tc.Headers) { - foreach (var header in tc.Headers) - { - request.Headers.Add(header.Key, header.Value); - } + request.Headers.Add(header.Key, header.Value); } + } - request.ContentLength = 0; + request.ContentLength = 0; - using var response = (HttpWebResponse)request.GetResponse(); + using var response = (HttpWebResponse)request.GetResponse(); - using var streamReader = new StreamReader(response.GetResponseStream()); - streamReader.ReadToEnd(); - } - catch (Exception) - { - // test case can intentionally send request that will result in exception - tc.ResponseExpected = false; - } + using var streamReader = new StreamReader(response.GetResponseStream()); + streamReader.ReadToEnd(); + } + catch (Exception) + { + // test case can intentionally send request that will result in exception + tc.ResponseExpected = false; + } - Assert.Equal(3, activityProcessor.Invocations.Count); // SetParentProvider/Begin/End called - var activity = (Activity)activityProcessor.Invocations[2].Arguments[0]; - ValidateHttpWebRequestActivity(activity); - Assert.Equal(tc.SpanName, activity.DisplayName); + Assert.Single(exportedItems); + var activity = exportedItems[0]; + ValidateHttpWebRequestActivity(activity); + Assert.Equal(tc.SpanName, activity.DisplayName); - tc.SpanAttributes = tc.SpanAttributes.ToDictionary( - x => x.Key, - x => + tc.SpanAttributes = tc.SpanAttributes.ToDictionary( + x => x.Key, + x => + { + if (x.Key == "network.protocol.version") { - if (x.Key == "http.flavor") - { - return "1.1"; - } + return "1.1"; + } - return HttpTestData.NormalizeValues(x.Value, host, port); - }); + return HttpTestData.NormalizeValues(x.Value, host, port); + }); - foreach (KeyValuePair tag in activity.TagObjects) + foreach (KeyValuePair tag in activity.TagObjects) + { + var tagValue = tag.Value.ToString(); + + if (!tc.SpanAttributes.TryGetValue(tag.Key, out string value)) { - var tagValue = tag.Value.ToString(); + if (tag.Key == SpanAttributeConstants.StatusCodeKey) + { + Assert.Equal(tc.SpanStatus, tagValue); + continue; + } + + if (tag.Key == SpanAttributeConstants.StatusDescriptionKey) + { + Assert.Null(tagValue); + continue; + } - if (!tc.SpanAttributes.TryGetValue(tag.Key, out string value)) + if (tag.Key == SemanticConventions.AttributeErrorType) { - if (tag.Key == SpanAttributeConstants.StatusCodeKey) - { - Assert.Equal(tc.SpanStatus, tagValue); - continue; - } - - if (tag.Key == SpanAttributeConstants.StatusDescriptionKey) - { - if (tc.SpanStatusHasDescription.HasValue) - { - Assert.Equal(tc.SpanStatusHasDescription.Value, !string.IsNullOrEmpty(tagValue)); - } - - continue; - } - - Assert.True(false, $"Tag {tag.Key} was not found in test data."); + // TODO: Add validation for error.type in test cases. + continue; } - Assert.Equal(value, tagValue); + Assert.Fail($"Tag {tag.Key} was not found in test data."); } + Assert.Equal(value, tagValue); + } + #if NETFRAMEWORK - Assert.True(enrichWithHttpWebRequestCalled); - Assert.False(enrichWithHttpRequestMessageCalled); - if (tc.ResponseExpected) - { - Assert.True(enrichWithHttpWebResponseCalled); - Assert.False(enrichWithHttpResponseMessageCalled); - } + Assert.True(enrichWithHttpWebRequestCalled); + Assert.False(enrichWithHttpRequestMessageCalled); + if (tc.ResponseExpected) + { + Assert.True(enrichWithHttpWebResponseCalled); + Assert.False(enrichWithHttpResponseMessageCalled); + } #else - Assert.False(enrichWithHttpWebRequestCalled); - Assert.True(enrichWithHttpRequestMessageCalled); - if (tc.ResponseExpected) - { - Assert.False(enrichWithHttpWebResponseCalled); - Assert.True(enrichWithHttpResponseMessageCalled); - } + Assert.False(enrichWithHttpWebRequestCalled); + Assert.True(enrichWithHttpRequestMessageCalled); + if (tc.ResponseExpected) + { + Assert.False(enrichWithHttpWebResponseCalled); + Assert.True(enrichWithHttpResponseMessageCalled); + } #endif - if (tc.RecordException.HasValue && tc.RecordException.Value) - { - Assert.Single(activity.Events.Where(evt => evt.Name.Equals("exception"))); - Assert.True(enrichWithExceptionCalled); - } + if (tc.RecordException.HasValue && tc.RecordException.Value) + { + Assert.Single(activity.Events.Where(evt => evt.Name.Equals("exception"))); + Assert.True(enrichWithExceptionCalled); } + } - [Fact] - public void DebugIndividualTest() - { - var input = JsonSerializer.Deserialize( - @" + [Fact] + public void DebugIndividualTest() + { + var input = JsonSerializer.Deserialize( + @" { ""name"": ""Http version attribute populated"", ""method"": ""GET"", ""url"": ""http://{host}:{port}/"", ""responseCode"": 200, - ""spanName"": ""HTTP GET"", + ""spanName"": ""GET"", ""spanStatus"": ""UNSET"", ""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}/"" } } ", - new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); - this.HttpOutCallsAreCollectedSuccessfully(input); - } + new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + this.HttpOutCallsAreCollectedSuccessfully(input); + } - private static void ValidateHttpWebRequestActivity(Activity activityToValidate) - { - Assert.Equal(ActivityKind.Client, activityToValidate.Kind); - } + private static void ValidateHttpWebRequestActivity(Activity activityToValidate) + { + Assert.Equal(ActivityKind.Client, activityToValidate.Kind); } } diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/OpenTelemetry.Instrumentation.Http.Tests.csproj b/test/OpenTelemetry.Instrumentation.Http.Tests/OpenTelemetry.Instrumentation.Http.Tests.csproj index 37d64e3c0b7..9bb4c1710c7 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/OpenTelemetry.Instrumentation.Http.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/OpenTelemetry.Instrumentation.Http.Tests.csproj @@ -1,34 +1,33 @@ Unit test project for OpenTelemetry HTTP instrumentations - - net7.0;net6.0 - $(TargetFrameworks);net462 - + $(TargetFrameworksForTests) disable - - + - + + + - - - - all + runtime; build; native; contentfiles; analyzers + + + + PreserveNewest @@ -37,7 +36,14 @@ + + + + + + + diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/RetryHandler.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/RetryHandler.cs index 4556e7e5359..d1a977b7e84 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/RetryHandler.cs +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/RetryHandler.cs @@ -1,52 +1,40 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +#if NETFRAMEWORK using System.Net.Http; +#endif -namespace OpenTelemetry.Tests +namespace OpenTelemetry.Tests; + +public class RetryHandler : DelegatingHandler { - public class RetryHandler : DelegatingHandler + private readonly int maxRetries; + + public RetryHandler(HttpMessageHandler innerHandler, int maxRetries) + : base(innerHandler) { - private readonly int maxRetries; + this.maxRetries = maxRetries; + } - public RetryHandler(HttpMessageHandler innerHandler, int maxRetries) - : base(innerHandler) + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + HttpResponseMessage response = null; + for (int i = 0; i < this.maxRetries; i++) { - this.maxRetries = maxRetries; - } + response?.Dispose(); - protected override async Task SendAsync( - HttpRequestMessage request, - CancellationToken cancellationToken) - { - HttpResponseMessage response = null; - for (int i = 0; i < this.maxRetries; i++) + try + { + response = await base.SendAsync(request, cancellationToken); + } + catch { - response?.Dispose(); - - try - { - response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); - } - catch - { - } } - - return response; } + + return response; } } 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 833e26098bb..489b96ee4a0 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/http-out-test-cases.json +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/http-out-test-cases.json @@ -3,34 +3,34 @@ "name": "Successful GET call to localhost", "method": "GET", "url": "http://{host}:{port}/", - "spanName": "HTTP GET", + "spanName": "GET", "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}/" } }, { "name": "Successfully POST call to localhost", "method": "POST", "url": "http://{host}:{port}/", - "spanName": "HTTP POST", + "spanName": "POST", "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}/" } }, { @@ -38,17 +38,17 @@ "method": "GET", "url": "http://{host}:{port}/path/to/resource/", "responseCode": 200, - "spanName": "HTTP GET", + "spanName": "GET", "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/" } }, { @@ -56,71 +56,69 @@ "method": "GET", "url": "http://{host}:{port}/path/to/resource#fragment", "responseCode": 200, - "spanName": "HTTP GET", + "spanName": "GET", "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, - "spanName": "HTTP GET", + "spanName": "GET", "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": "Call that cannot resolve DNS will be reported as error span", "method": "GET", "url": "http://sdlfaldfjalkdfjlkajdflkajlsdjf:{port}/", - "spanName": "HTTP GET", + "spanName": "GET", "spanStatus": "Error", - "spanStatusHasDescription": true, "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}/" } }, { "name": "Call that cannot resolve DNS will be reported as error span. And Records exception", "method": "GET", "url": "http://sdlfaldfjalkdfjlkajdflkajlsdjf:{port}/", - "spanName": "HTTP GET", + "spanName": "GET", "spanStatus": "Error", - "spanStatusHasDescription": true, "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}/" } }, { @@ -128,17 +126,17 @@ "method": "GET", "url": "http://{host}:{port}/", "responseCode": 200, - "spanName": "HTTP GET", + "spanName": "GET", "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}/" } }, { @@ -146,17 +144,17 @@ "method": "GET", "url": "http://{host}:{port}/", "responseCode": 200, - "spanName": "HTTP GET", + "spanName": "GET", "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}/" } }, { @@ -164,17 +162,17 @@ "method": "GET", "url": "http://{host}:{port}/", "responseCode": 399, - "spanName": "HTTP GET", + "spanName": "GET", "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}/" } }, { @@ -182,17 +180,17 @@ "method": "GET", "url": "http://{host}:{port}/", "responseCode": 400, - "spanName": "HTTP GET", + "spanName": "GET", "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}/" } }, { @@ -200,17 +198,17 @@ "method": "GET", "url": "http://{host}:{port}/", "responseCode": 401, - "spanName": "HTTP GET", + "spanName": "GET", "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}/" } }, { @@ -218,17 +216,17 @@ "method": "GET", "url": "http://{host}:{port}/", "responseCode": 403, - "spanName": "HTTP GET", + "spanName": "GET", "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}/" } }, { @@ -236,17 +234,17 @@ "method": "GET", "url": "http://{host}:{port}/", "responseCode": 404, - "spanName": "HTTP GET", + "spanName": "GET", "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}/" } }, { @@ -254,17 +252,17 @@ "method": "GET", "url": "http://{host}:{port}/", "responseCode": 429, - "spanName": "HTTP GET", + "spanName": "GET", "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}/" } }, { @@ -272,17 +270,17 @@ "method": "GET", "url": "http://{host}:{port}/", "responseCode": 501, - "spanName": "HTTP GET", + "spanName": "GET", "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}/" } }, { @@ -290,17 +288,17 @@ "method": "GET", "url": "http://{host}:{port}/", "responseCode": 503, - "spanName": "HTTP GET", + "spanName": "GET", "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}/" } }, { @@ -308,35 +306,17 @@ "method": "GET", "url": "http://{host}:{port}/", "responseCode": 504, - "spanName": "HTTP GET", + "spanName": "GET", "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}/" - } - }, - { - "name": "Response code: 600", - "method": "GET", - "url": "http://{host}:{port}/", - "responseCode": 600, - "spanName": "HTTP GET", - "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": "600", - "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}/" } }, { @@ -344,17 +324,17 @@ "method": "GET", "url": "http://{host}:{port}/", "responseCode": 200, - "spanName": "HTTP GET", + "spanName": "GET", "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/EventSourceTest.cs b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/EventSourceTest.cs index 67d80719eac..27a48869679 100644 --- a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/EventSourceTest.cs +++ b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/EventSourceTest.cs @@ -1,31 +1,17 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Instrumentation.SqlClient.Implementation; using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Instrumentation.SqlClient.Tests +namespace OpenTelemetry.Instrumentation.SqlClient.Tests; + +public class EventSourceTest { - public class EventSourceTest + [Fact] + public void EventSourceTest_SqlClientInstrumentationEventSource() { - [Fact] - public void EventSourceTest_SqlClientInstrumentationEventSource() - { - EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(SqlClientInstrumentationEventSource.Log); - } + EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(SqlClientInstrumentationEventSource.Log); } } diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/OpenTelemetry.Instrumentation.SqlClient.Tests.csproj b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/OpenTelemetry.Instrumentation.SqlClient.Tests.csproj index 59b52279c45..2075d0eba89 100644 --- a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/OpenTelemetry.Instrumentation.SqlClient.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/OpenTelemetry.Instrumentation.SqlClient.Tests.csproj @@ -1,32 +1,26 @@ Unit test project for OpenTelemetry SqlClient instrumentations - - net7.0;net6.0 - $(TargetFrameworks);net462 - $(TARGET_FRAMEWORK) - + $(TargetFrameworksForTests) disable - - - - - + + + + + - - - all + runtime; build; native; contentfiles; analyzers diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientInstrumentationOptionsTests.cs b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientInstrumentationOptionsTests.cs deleted file mode 100644 index 42becbe969d..00000000000 --- a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientInstrumentationOptionsTests.cs +++ /dev/null @@ -1,106 +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 OpenTelemetry.Trace; -using Xunit; - -namespace OpenTelemetry.Instrumentation.SqlClient.Tests -{ - public class SqlClientInstrumentationOptionsTests - { - static SqlClientInstrumentationOptionsTests() - { - Activity.DefaultIdFormat = ActivityIdFormat.W3C; - Activity.ForceDefaultIdFormat = true; - - var listener = new ActivityListener - { - ShouldListenTo = _ => true, - Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, - }; - - ActivitySource.AddActivityListener(listener); - } - - [Theory] - [InlineData("localhost", "localhost", null, null, null)] - [InlineData("127.0.0.1", null, "127.0.0.1", null, null)] - [InlineData("127.0.0.1,1433", null, "127.0.0.1", null, null)] - [InlineData("127.0.0.1, 1818", null, "127.0.0.1", null, "1818")] - [InlineData("127.0.0.1 \\ instanceName", null, "127.0.0.1", "instanceName", null)] - [InlineData("127.0.0.1\\instanceName, 1818", null, "127.0.0.1", "instanceName", "1818")] - [InlineData("tcp:127.0.0.1\\instanceName, 1818", null, "127.0.0.1", "instanceName", "1818")] - [InlineData("tcp:localhost", "localhost", null, null, null)] - [InlineData("tcp : localhost", "localhost", null, null, null)] - [InlineData("np : localhost", "localhost", null, null, null)] - [InlineData("lpc:localhost", "localhost", null, null, null)] - [InlineData("np:\\\\localhost\\pipe\\sql\\query", "localhost", null, null, null)] - [InlineData("np : \\\\localhost\\pipe\\sql\\query", "localhost", null, null, null)] - [InlineData("np:\\\\localhost\\pipe\\MSSQL$instanceName\\sql\\query", "localhost", null, "instanceName", null)] - public void ParseDataSourceTests( - string dataSource, - string expectedServerHostName, - string expectedServerIpAddress, - string expectedInstanceName, - string expectedPort) - { - var sqlConnectionDetails = SqlClientInstrumentationOptions.ParseDataSource(dataSource); - - Assert.NotNull(sqlConnectionDetails); - Assert.Equal(expectedServerHostName, sqlConnectionDetails.ServerHostName); - Assert.Equal(expectedServerIpAddress, sqlConnectionDetails.ServerIpAddress); - Assert.Equal(expectedInstanceName, sqlConnectionDetails.InstanceName); - Assert.Equal(expectedPort, sqlConnectionDetails.Port); - } - - [Theory] - [InlineData(true, "localhost", "localhost", null, null, null)] - [InlineData(true, "127.0.0.1,1433", null, "127.0.0.1", null, null)] - [InlineData(true, "127.0.0.1,1434", null, "127.0.0.1", null, "1434")] - [InlineData(true, "127.0.0.1\\instanceName, 1818", null, "127.0.0.1", "instanceName", "1818")] - [InlineData(false, "localhost", "localhost", null, null, null)] - public void SqlClientInstrumentationOptions_EnableConnectionLevelAttributes( - bool enableConnectionLevelAttributes, - string dataSource, - string expectedServerHostName, - string expectedServerIpAddress, - string expectedInstanceName, - string expectedPort) - { - var source = new ActivitySource("sql-client-instrumentation"); - var activity = source.StartActivity("Test Sql Activity"); - var options = new SqlClientInstrumentationOptions - { - EnableConnectionLevelAttributes = enableConnectionLevelAttributes, - }; - options.AddConnectionLevelDetailsToActivity(dataSource, activity); - - if (!enableConnectionLevelAttributes) - { - Assert.Equal(expectedServerHostName, activity.GetTagValue(SemanticConventions.AttributePeerService)); - } - else - { - Assert.Equal(expectedServerHostName, activity.GetTagValue(SemanticConventions.AttributeNetPeerName)); - } - - Assert.Equal(expectedServerIpAddress, activity.GetTagValue(SemanticConventions.AttributeNetPeerIp)); - Assert.Equal(expectedInstanceName, activity.GetTagValue(SemanticConventions.AttributeDbMsSqlInstanceName)); - Assert.Equal(expectedPort, activity.GetTagValue(SemanticConventions.AttributeNetPeerPort)); - } - } -} diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientIntegrationTests.cs b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientIntegrationTests.cs index 45f4408d61f..93e04926434 100644 --- a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientIntegrationTests.cs +++ b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientIntegrationTests.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Data; using System.Diagnostics; @@ -25,107 +12,106 @@ using Testcontainers.SqlEdge; using Xunit; -namespace OpenTelemetry.Instrumentation.SqlClient.Tests +namespace OpenTelemetry.Instrumentation.SqlClient.Tests; + +public sealed class SqlClientIntegrationTests : IAsyncLifetime { - public sealed class SqlClientIntegrationTests : IAsyncLifetime - { - // The Microsoft SQL Server Docker image is not compatible with ARM devices, such as Macs with Apple Silicon. - private readonly IContainer databaseContainer = Architecture.Arm64.Equals(RuntimeInformation.ProcessArchitecture) ? new SqlEdgeBuilder().Build() : new MsSqlBuilder().Build(); + // The Microsoft SQL Server Docker image is not compatible with ARM devices, such as Macs with Apple Silicon. + private readonly IContainer databaseContainer = Architecture.Arm64.Equals(RuntimeInformation.ProcessArchitecture) ? new SqlEdgeBuilder().Build() : new MsSqlBuilder().Build(); - public Task InitializeAsync() - { - return this.databaseContainer.StartAsync(); - } + public Task InitializeAsync() + { + return this.databaseContainer.StartAsync(); + } - public Task DisposeAsync() - { - return this.databaseContainer.DisposeAsync().AsTask(); - } + public Task DisposeAsync() + { + return this.databaseContainer.DisposeAsync().AsTask(); + } - [Trait("CategoryName", "SqlIntegrationTests")] - [EnabledOnDockerPlatformTheory(EnabledOnDockerPlatformTheoryAttribute.DockerPlatform.Linux)] - [InlineData(CommandType.Text, "select 1/1", false)] - [InlineData(CommandType.Text, "select 1/1", false, true)] - [InlineData(CommandType.Text, "select 1/0", false, false, true)] - [InlineData(CommandType.Text, "select 1/0", false, false, true, false, false)] - [InlineData(CommandType.Text, "select 1/0", false, false, true, true, false)] - [InlineData(CommandType.StoredProcedure, "sp_who", false)] - [InlineData(CommandType.StoredProcedure, "sp_who", true)] - public void SuccessfulCommandTest( - CommandType commandType, - string commandText, - bool captureStoredProcedureCommandName, - bool captureTextCommandContent = false, - bool isFailure = false, - bool recordException = false, - bool shouldEnrich = true) - { + [Trait("CategoryName", "SqlIntegrationTests")] + [EnabledOnDockerPlatformTheory(EnabledOnDockerPlatformTheoryAttribute.DockerPlatform.Linux)] + [InlineData(CommandType.Text, "select 1/1", false)] + [InlineData(CommandType.Text, "select 1/1", false, true)] + [InlineData(CommandType.Text, "select 1/0", false, false, true)] + [InlineData(CommandType.Text, "select 1/0", false, false, true, false, false)] + [InlineData(CommandType.Text, "select 1/0", false, false, true, true, false)] + [InlineData(CommandType.StoredProcedure, "sp_who", false)] + [InlineData(CommandType.StoredProcedure, "sp_who", true)] + public void SuccessfulCommandTest( + CommandType commandType, + string commandText, + bool captureStoredProcedureCommandName, + bool captureTextCommandContent = false, + bool isFailure = false, + bool recordException = false, + bool shouldEnrich = true) + { #if NETFRAMEWORK - // Disable things not available on netfx - recordException = false; - shouldEnrich = false; + // Disable things not available on netfx + recordException = false; + shouldEnrich = false; #endif - var sampler = new TestSampler(); - var activities = new List(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .SetSampler(sampler) - .AddInMemoryExporter(activities) - .AddSqlClientInstrumentation(options => - { + var sampler = new TestSampler(); + var activities = new List(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(sampler) + .AddInMemoryExporter(activities) + .AddSqlClientInstrumentation(options => + { #if !NETFRAMEWORK - options.SetDbStatementForStoredProcedure = captureStoredProcedureCommandName; - options.SetDbStatementForText = captureTextCommandContent; + options.SetDbStatementForStoredProcedure = captureStoredProcedureCommandName; + options.SetDbStatementForText = captureTextCommandContent; #else - options.SetDbStatementForText = captureStoredProcedureCommandName || captureTextCommandContent; + options.SetDbStatementForText = captureStoredProcedureCommandName || captureTextCommandContent; #endif - options.RecordException = recordException; - if (shouldEnrich) - { - options.Enrich = SqlClientTests.ActivityEnrichment; - } - }) - .Build(); + options.RecordException = recordException; + if (shouldEnrich) + { + options.Enrich = SqlClientTests.ActivityEnrichment; + } + }) + .Build(); - using SqlConnection sqlConnection = new SqlConnection(this.GetConnectionString()); + using SqlConnection sqlConnection = new SqlConnection(this.GetConnectionString()); - sqlConnection.Open(); + sqlConnection.Open(); - string dataSource = sqlConnection.DataSource; + string dataSource = sqlConnection.DataSource; - sqlConnection.ChangeDatabase("master"); + sqlConnection.ChangeDatabase("master"); - using SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection) - { - CommandType = commandType, - }; + using SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection) + { + CommandType = commandType, + }; - try - { - sqlCommand.ExecuteNonQuery(); - } - catch - { - } + try + { + sqlCommand.ExecuteNonQuery(); + } + catch + { + } - Assert.Single(activities); - var activity = activities[0]; + Assert.Single(activities); + var activity = activities[0]; - SqlClientTests.VerifyActivityData(commandType, commandText, captureStoredProcedureCommandName, captureTextCommandContent, isFailure, recordException, shouldEnrich, dataSource, activity); - SqlClientTests.VerifySamplingParameters(sampler.LatestSamplingParameters); - } + SqlClientTests.VerifyActivityData(commandType, commandText, captureStoredProcedureCommandName, captureTextCommandContent, isFailure, recordException, shouldEnrich, dataSource, activity); + SqlClientTests.VerifySamplingParameters(sampler.LatestSamplingParameters); + } - private string GetConnectionString() + private string GetConnectionString() + { + switch (this.databaseContainer) { - switch (this.databaseContainer) - { - case SqlEdgeContainer container: - return container.GetConnectionString(); - case MsSqlContainer container: - return container.GetConnectionString(); - default: - throw new InvalidOperationException($"Container type ${this.databaseContainer.GetType().Name} not supported."); - } + case SqlEdgeContainer container: + return container.GetConnectionString(); + case MsSqlContainer container: + return container.GetConnectionString(); + default: + throw new InvalidOperationException($"Container type ${this.databaseContainer.GetType().Name} not supported."); } } } diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTests.cs b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTests.cs index b7124668b22..ca05b959ee9 100644 --- a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTests.cs +++ b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTests.cs @@ -1,478 +1,466 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Data; using System.Diagnostics; using Microsoft.Data.SqlClient; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Instrumentation.SqlClient.Implementation; +#if !NETFRAMEWORK using OpenTelemetry.Tests; +#endif using OpenTelemetry.Trace; using Xunit; -namespace OpenTelemetry.Instrumentation.SqlClient.Tests +namespace OpenTelemetry.Instrumentation.SqlClient.Tests; + +public class SqlClientTests : IDisposable { - public class SqlClientTests : IDisposable - { - private const string TestConnectionString = "Data Source=(localdb)\\MSSQLLocalDB;Database=master"; + private const string TestConnectionString = "Data Source=(localdb)\\MSSQLLocalDB;Database=master"; - private readonly FakeSqlClientDiagnosticSource fakeSqlClientDiagnosticSource; + private readonly FakeSqlClientDiagnosticSource fakeSqlClientDiagnosticSource; - public SqlClientTests() - { - this.fakeSqlClientDiagnosticSource = new FakeSqlClientDiagnosticSource(); - } + public SqlClientTests() + { + this.fakeSqlClientDiagnosticSource = new FakeSqlClientDiagnosticSource(); + } - public void Dispose() - { - this.fakeSqlClientDiagnosticSource.Dispose(); - GC.SuppressFinalize(this); - } + public void Dispose() + { + this.fakeSqlClientDiagnosticSource.Dispose(); + GC.SuppressFinalize(this); + } - [Fact] - public void SqlClient_BadArgs() - { - TracerProviderBuilder builder = null; - Assert.Throws(() => builder.AddSqlClientInstrumentation()); - } + [Fact] + public void SqlClient_BadArgs() + { + TracerProviderBuilder builder = null; + Assert.Throws(() => builder.AddSqlClientInstrumentation()); + } - [Fact] - public void SqlClient_NamedOptions() - { - int defaultExporterOptionsConfigureOptionsInvocations = 0; - int namedExporterOptionsConfigureOptionsInvocations = 0; + [Fact] + public void SqlClient_NamedOptions() + { + int defaultExporterOptionsConfigureOptionsInvocations = 0; + int namedExporterOptionsConfigureOptionsInvocations = 0; - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .ConfigureServices(services => - { - services.Configure(o => defaultExporterOptionsConfigureOptionsInvocations++); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => + { + services.Configure(o => defaultExporterOptionsConfigureOptionsInvocations++); - services.Configure("Instrumentation2", o => namedExporterOptionsConfigureOptionsInvocations++); - }) - .AddSqlClientInstrumentation() - .AddSqlClientInstrumentation("Instrumentation2", configureSqlClientInstrumentationOptions: null) - .Build(); + services.Configure("Instrumentation2", o => namedExporterOptionsConfigureOptionsInvocations++); + }) + .AddSqlClientInstrumentation() + .AddSqlClientInstrumentation("Instrumentation2", configureSqlClientTraceInstrumentationOptions: null) + .Build(); - Assert.Equal(1, defaultExporterOptionsConfigureOptionsInvocations); - Assert.Equal(1, namedExporterOptionsConfigureOptionsInvocations); - } + Assert.Equal(1, defaultExporterOptionsConfigureOptionsInvocations); + Assert.Equal(1, namedExporterOptionsConfigureOptionsInvocations); + } - // DiagnosticListener-based instrumentation is only available on .NET Core + // DiagnosticListener-based instrumentation is only available on .NET Core #if !NETFRAMEWORK - [Theory] - [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataAfterExecuteCommand, CommandType.StoredProcedure, "SP_GetOrders", true, false)] - [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataAfterExecuteCommand, CommandType.StoredProcedure, "SP_GetOrders", true, false, false)] - [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataAfterExecuteCommand, CommandType.Text, "select * from sys.databases", true, false)] - [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataAfterExecuteCommand, CommandType.Text, "select * from sys.databases", true, false, false)] - [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftAfterExecuteCommand, CommandType.StoredProcedure, "SP_GetOrders", false, true)] - [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftAfterExecuteCommand, CommandType.StoredProcedure, "SP_GetOrders", false, true, false)] - [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftAfterExecuteCommand, CommandType.Text, "select * from sys.databases", false, true)] - [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftAfterExecuteCommand, CommandType.Text, "select * from sys.databases", false, true, false)] - public void SqlClientCallsAreCollectedSuccessfully( - string beforeCommand, - string afterCommand, - CommandType commandType, - string commandText, - bool captureStoredProcedureCommandName, - bool captureTextCommandContent, - bool shouldEnrich = true) - { - using var sqlConnection = new SqlConnection(TestConnectionString); - using var sqlCommand = sqlConnection.CreateCommand(); + [Theory] + [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataAfterExecuteCommand, CommandType.StoredProcedure, "SP_GetOrders", true, false)] + [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataAfterExecuteCommand, CommandType.StoredProcedure, "SP_GetOrders", true, false, false)] + [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataAfterExecuteCommand, CommandType.Text, "select * from sys.databases", true, false)] + [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataAfterExecuteCommand, CommandType.Text, "select * from sys.databases", true, false, false)] + [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftAfterExecuteCommand, CommandType.StoredProcedure, "SP_GetOrders", false, true)] + [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftAfterExecuteCommand, CommandType.StoredProcedure, "SP_GetOrders", false, true, false)] + [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftAfterExecuteCommand, CommandType.Text, "select * from sys.databases", false, true)] + [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftAfterExecuteCommand, CommandType.Text, "select * from sys.databases", false, true, false)] + public void SqlClientCallsAreCollectedSuccessfully( + string beforeCommand, + string afterCommand, + CommandType commandType, + string commandText, + bool captureStoredProcedureCommandName, + bool captureTextCommandContent, + bool shouldEnrich = true) + { + using var sqlConnection = new SqlConnection(TestConnectionString); + using var sqlCommand = sqlConnection.CreateCommand(); - var activities = new List(); - using (Sdk.CreateTracerProviderBuilder() - .AddSqlClientInstrumentation( - (opt) => + var activities = new List(); + using (Sdk.CreateTracerProviderBuilder() + .AddSqlClientInstrumentation( + (opt) => + { + opt.SetDbStatementForText = captureTextCommandContent; + opt.SetDbStatementForStoredProcedure = captureStoredProcedureCommandName; + if (shouldEnrich) { - opt.SetDbStatementForText = captureTextCommandContent; - opt.SetDbStatementForStoredProcedure = captureStoredProcedureCommandName; - if (shouldEnrich) - { - opt.Enrich = ActivityEnrichment; - } - }) - .AddInMemoryExporter(activities) - .Build()) - { - var operationId = Guid.NewGuid(); - sqlCommand.CommandType = commandType; - sqlCommand.CommandText = commandText; + opt.Enrich = ActivityEnrichment; + } + }) + .AddInMemoryExporter(activities) + .Build()) + { + var operationId = Guid.NewGuid(); + sqlCommand.CommandType = commandType; + sqlCommand.CommandText = commandText; - var beforeExecuteEventData = new - { - OperationId = operationId, - Command = sqlCommand, - Timestamp = (long?)1000000L, - }; + var beforeExecuteEventData = new + { + OperationId = operationId, + Command = sqlCommand, + Timestamp = (long?)1000000L, + }; - this.fakeSqlClientDiagnosticSource.Write( - beforeCommand, - beforeExecuteEventData); + this.fakeSqlClientDiagnosticSource.Write( + beforeCommand, + beforeExecuteEventData); - var afterExecuteEventData = new - { - OperationId = operationId, - Command = sqlCommand, - Timestamp = 2000000L, - }; - - this.fakeSqlClientDiagnosticSource.Write( - afterCommand, - afterExecuteEventData); - } + var afterExecuteEventData = new + { + OperationId = operationId, + Command = sqlCommand, + Timestamp = 2000000L, + }; - Assert.Single(activities); - var activity = activities[0]; - - VerifyActivityData( - sqlCommand.CommandType, - sqlCommand.CommandText, - captureStoredProcedureCommandName, - captureTextCommandContent, - false, - false, - shouldEnrich, - sqlConnection.DataSource, - activity); + this.fakeSqlClientDiagnosticSource.Write( + afterCommand, + afterExecuteEventData); } - [Theory] - [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataWriteCommandError)] - [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataWriteCommandError, false)] - [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataWriteCommandError, false, true)] - [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftWriteCommandError)] - [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftWriteCommandError, false)] - [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftWriteCommandError, false, true)] - public void SqlClientErrorsAreCollectedSuccessfully(string beforeCommand, string errorCommand, bool shouldEnrich = true, bool recordException = false) - { - using var sqlConnection = new SqlConnection(TestConnectionString); - using var sqlCommand = sqlConnection.CreateCommand(); - - var activities = new List(); - using (Sdk.CreateTracerProviderBuilder() - .AddSqlClientInstrumentation(options => - { - options.RecordException = recordException; - if (shouldEnrich) - { - options.Enrich = ActivityEnrichment; - } - }) - .AddInMemoryExporter(activities) - .Build()) - { - var operationId = Guid.NewGuid(); - sqlCommand.CommandText = "SP_GetOrders"; - sqlCommand.CommandType = CommandType.StoredProcedure; - - var beforeExecuteEventData = new - { - OperationId = operationId, - Command = sqlCommand, - Timestamp = (long?)1000000L, - }; + Assert.Single(activities); + var activity = activities[0]; + + VerifyActivityData( + sqlCommand.CommandType, + sqlCommand.CommandText, + captureStoredProcedureCommandName, + captureTextCommandContent, + false, + false, + shouldEnrich, + sqlConnection.DataSource, + activity); + } - this.fakeSqlClientDiagnosticSource.Write( - beforeCommand, - beforeExecuteEventData); + [Theory] + [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataWriteCommandError)] + [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataWriteCommandError, false)] + [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand, SqlClientDiagnosticListener.SqlDataWriteCommandError, false, true)] + [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftWriteCommandError)] + [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftWriteCommandError, false)] + [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, SqlClientDiagnosticListener.SqlMicrosoftWriteCommandError, false, true)] + public void SqlClientErrorsAreCollectedSuccessfully(string beforeCommand, string errorCommand, bool shouldEnrich = true, bool recordException = false) + { + using var sqlConnection = new SqlConnection(TestConnectionString); + using var sqlCommand = sqlConnection.CreateCommand(); - var commandErrorEventData = new + var activities = new List(); + using (Sdk.CreateTracerProviderBuilder() + .AddSqlClientInstrumentation(options => + { + options.RecordException = recordException; + if (shouldEnrich) { - OperationId = operationId, - Command = sqlCommand, - Exception = new Exception("Boom!"), - Timestamp = 2000000L, - }; - - this.fakeSqlClientDiagnosticSource.Write( - errorCommand, - commandErrorEventData); - } - - Assert.Single(activities); - var activity = activities[0]; - - VerifyActivityData( - sqlCommand.CommandType, - sqlCommand.CommandText, - true, - false, - true, - recordException, - shouldEnrich, - sqlConnection.DataSource, - activity); - } - - [Theory] - [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand)] - [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand)] - public void SqlClientCreatesActivityWithDbSystem( - string beforeCommand) + options.Enrich = ActivityEnrichment; + } + }) + .AddInMemoryExporter(activities) + .Build()) { - using var sqlConnection = new SqlConnection(TestConnectionString); - using var sqlCommand = sqlConnection.CreateCommand(); + var operationId = Guid.NewGuid(); + sqlCommand.CommandText = "SP_GetOrders"; + sqlCommand.CommandType = CommandType.StoredProcedure; - var sampler = new TestSampler + var beforeExecuteEventData = new { - SamplingAction = _ => new SamplingResult(SamplingDecision.Drop), + OperationId = operationId, + Command = sqlCommand, + Timestamp = (long?)1000000L, }; - using (Sdk.CreateTracerProviderBuilder() - .AddSqlClientInstrumentation() - .SetSampler(sampler) - .Build()) + + this.fakeSqlClientDiagnosticSource.Write( + beforeCommand, + beforeExecuteEventData); + + var commandErrorEventData = new { - this.fakeSqlClientDiagnosticSource.Write(beforeCommand, new { }); - } + OperationId = operationId, + Command = sqlCommand, + Exception = new Exception("Boom!"), + Timestamp = 2000000L, + }; - VerifySamplingParameters(sampler.LatestSamplingParameters); + this.fakeSqlClientDiagnosticSource.Write( + errorCommand, + commandErrorEventData); } - [Fact] - public void ShouldCollectTelemetryWhenFilterEvaluatesToTrue() - { - var activities = this.RunCommandWithFilter( - cmd => - { - cmd.CommandText = "select 2"; - }, - cmd => - { - if (cmd is SqlCommand command) - { - return command.CommandText == "select 2"; - } + Assert.Single(activities); + var activity = activities[0]; + + VerifyActivityData( + sqlCommand.CommandType, + sqlCommand.CommandText, + true, + false, + true, + recordException, + shouldEnrich, + sqlConnection.DataSource, + activity); + } - return true; - }); + [Theory] + [InlineData(SqlClientDiagnosticListener.SqlDataBeforeExecuteCommand)] + [InlineData(SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand)] + public void SqlClientCreatesActivityWithDbSystem( + string beforeCommand) + { + using var sqlConnection = new SqlConnection(TestConnectionString); + using var sqlCommand = sqlConnection.CreateCommand(); - Assert.Single(activities); - Assert.True(activities[0].IsAllDataRequested); - Assert.True(activities[0].ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); + var sampler = new TestSampler + { + SamplingAction = _ => new SamplingResult(SamplingDecision.Drop), + }; + using (Sdk.CreateTracerProviderBuilder() + .AddSqlClientInstrumentation() + .SetSampler(sampler) + .Build()) + { + this.fakeSqlClientDiagnosticSource.Write(beforeCommand, new { }); } - [Fact] - public void ShouldNotCollectTelemetryWhenFilterEvaluatesToFalse() - { - var activities = this.RunCommandWithFilter( - cmd => - { - cmd.CommandText = "select 1"; - }, - cmd => + VerifySamplingParameters(sampler.LatestSamplingParameters); + } + + [Fact] + public void ShouldCollectTelemetryWhenFilterEvaluatesToTrue() + { + var activities = this.RunCommandWithFilter( + cmd => + { + cmd.CommandText = "select 2"; + }, + cmd => + { + if (cmd is SqlCommand command) { - if (cmd is SqlCommand command) - { - return command.CommandText == "select 2"; - } + return command.CommandText == "select 2"; + } - return true; - }); + return true; + }); - Assert.Empty(activities); - } + Assert.Single(activities); + Assert.True(activities[0].IsAllDataRequested); + Assert.True(activities[0].ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); + } - [Fact] - public void ShouldNotCollectTelemetryAndShouldNotPropagateExceptionWhenFilterThrowsException() - { - var activities = this.RunCommandWithFilter( - cmd => + [Fact] + public void ShouldNotCollectTelemetryWhenFilterEvaluatesToFalse() + { + var activities = this.RunCommandWithFilter( + cmd => + { + cmd.CommandText = "select 1"; + }, + cmd => + { + if (cmd is SqlCommand command) { - cmd.CommandText = "select 1"; - }, - cmd => throw new InvalidOperationException("foobar")); + return command.CommandText == "select 2"; + } - Assert.Empty(activities); - } + return true; + }); + + Assert.Empty(activities); + } + + [Fact] + public void ShouldNotCollectTelemetryAndShouldNotPropagateExceptionWhenFilterThrowsException() + { + var activities = this.RunCommandWithFilter( + cmd => + { + cmd.CommandText = "select 1"; + }, + cmd => throw new InvalidOperationException("foobar")); + + Assert.Empty(activities); + } #endif - internal static void VerifyActivityData( - CommandType commandType, - string commandText, - bool captureStoredProcedureCommandName, - bool captureTextCommandContent, - bool isFailure, - bool recordException, - bool shouldEnrich, - string dataSource, - Activity activity) + internal static void VerifyActivityData( + CommandType commandType, + string commandText, + bool captureStoredProcedureCommandName, + bool captureTextCommandContent, + bool isFailure, + bool recordException, + bool shouldEnrich, + string dataSource, + Activity activity) + { + Assert.Equal("master", activity.DisplayName); + Assert.Equal(ActivityKind.Client, activity.Kind); + + if (!isFailure) + { + Assert.Equal(ActivityStatusCode.Unset, activity.Status); + } + else { - Assert.Equal("master", activity.DisplayName); - Assert.Equal(ActivityKind.Client, activity.Kind); + var status = activity.GetStatus(); + Assert.Equal(ActivityStatusCode.Error, activity.Status); + Assert.NotNull(activity.StatusDescription); - if (!isFailure) + if (recordException) { - Assert.Equal(ActivityStatusCode.Unset, activity.Status); + var events = activity.Events.ToList(); + Assert.Single(events); + + Assert.Equal(SemanticConventions.AttributeExceptionEventName, events[0].Name); } else { - var status = activity.GetStatus(); - Assert.Equal(ActivityStatusCode.Error, activity.Status); - Assert.NotNull(activity.StatusDescription); + Assert.Empty(activity.Events); + } + } - if (recordException) - { - var events = activity.Events.ToList(); - Assert.Single(events); + if (shouldEnrich) + { + Assert.NotEmpty(activity.Tags.Where(tag => tag.Key == "enriched")); + Assert.Equal("yes", activity.Tags.Where(tag => tag.Key == "enriched").FirstOrDefault().Value); + } + else + { + Assert.Empty(activity.Tags.Where(tag => tag.Key == "enriched")); + } - Assert.Equal(SemanticConventions.AttributeExceptionEventName, events[0].Name); + Assert.Equal(SqlActivitySourceHelper.MicrosoftSqlServerDatabaseSystemName, activity.GetTagValue(SemanticConventions.AttributeDbSystem)); + Assert.Equal("master", activity.GetTagValue(SemanticConventions.AttributeDbName)); + + switch (commandType) + { + case CommandType.StoredProcedure: + if (captureStoredProcedureCommandName) + { + Assert.Equal(commandText, activity.GetTagValue(SemanticConventions.AttributeDbStatement)); } else { - Assert.Empty(activity.Events); + Assert.Null(activity.GetTagValue(SemanticConventions.AttributeDbStatement)); } - } - - if (shouldEnrich) - { - Assert.NotEmpty(activity.Tags.Where(tag => tag.Key == "enriched")); - Assert.Equal("yes", activity.Tags.Where(tag => tag.Key == "enriched").FirstOrDefault().Value); - } - else - { - Assert.Empty(activity.Tags.Where(tag => tag.Key == "enriched")); - } - Assert.Equal(SqlActivitySourceHelper.MicrosoftSqlServerDatabaseSystemName, activity.GetTagValue(SemanticConventions.AttributeDbSystem)); - Assert.Equal("master", activity.GetTagValue(SemanticConventions.AttributeDbName)); + break; - switch (commandType) - { - case CommandType.StoredProcedure: - if (captureStoredProcedureCommandName) - { - Assert.Equal(commandText, activity.GetTagValue(SemanticConventions.AttributeDbStatement)); - } - else - { - Assert.Null(activity.GetTagValue(SemanticConventions.AttributeDbStatement)); - } + case CommandType.Text: + if (captureTextCommandContent) + { + Assert.Equal(commandText, activity.GetTagValue(SemanticConventions.AttributeDbStatement)); + } + else + { + Assert.Null(activity.GetTagValue(SemanticConventions.AttributeDbStatement)); + } - break; + break; + } - case CommandType.Text: - if (captureTextCommandContent) - { - Assert.Equal(commandText, activity.GetTagValue(SemanticConventions.AttributeDbStatement)); - } - else - { - Assert.Null(activity.GetTagValue(SemanticConventions.AttributeDbStatement)); - } + Assert.Equal(dataSource, activity.GetTagValue(SemanticConventions.AttributePeerService)); + } - break; - } + internal static void VerifySamplingParameters(SamplingParameters samplingParameters) + { + Assert.NotNull(samplingParameters.Tags); + Assert.Contains( + samplingParameters.Tags, + kvp => kvp.Key == SemanticConventions.AttributeDbSystem + && (string)kvp.Value == SqlActivitySourceHelper.MicrosoftSqlServerDatabaseSystemName); + } - Assert.Equal(dataSource, activity.GetTagValue(SemanticConventions.AttributePeerService)); - } + internal static void ActivityEnrichment(Activity activity, string method, object obj) + { + activity.SetTag("enriched", "yes"); - internal static void VerifySamplingParameters(SamplingParameters samplingParameters) + switch (method) { - Assert.NotNull(samplingParameters.Tags); - Assert.Contains( - samplingParameters.Tags, - kvp => kvp.Key == SemanticConventions.AttributeDbSystem - && (string)kvp.Value == SqlActivitySourceHelper.MicrosoftSqlServerDatabaseSystemName); + case "OnCustom": + Assert.True(obj is SqlCommand); + break; + + default: + break; } + } - internal static void ActivityEnrichment(Activity activity, string method, object obj) +#if !NETFRAMEWORK + private Activity[] RunCommandWithFilter( + Action sqlCommandSetup, + Func filter) + { + using var sqlConnection = new SqlConnection(TestConnectionString); + using var sqlCommand = sqlConnection.CreateCommand(); + + var activities = new List(); + using (Sdk.CreateTracerProviderBuilder() + .AddSqlClientInstrumentation( + options => + { + options.Filter = filter; + }) + .AddInMemoryExporter(activities) + .Build()) { - activity.SetTag("enriched", "yes"); + var operationId = Guid.NewGuid(); + sqlCommandSetup(sqlCommand); - switch (method) + var beforeExecuteEventData = new { - case "OnCustom": - Assert.True(obj is SqlCommand); - break; + OperationId = operationId, + Command = sqlCommand, + Timestamp = (long?)1000000L, + }; - default: - break; - } - } + this.fakeSqlClientDiagnosticSource.Write( + SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, + beforeExecuteEventData); -#if !NETFRAMEWORK - private Activity[] RunCommandWithFilter( - Action sqlCommandSetup, - Func filter) - { - using var sqlConnection = new SqlConnection(TestConnectionString); - using var sqlCommand = sqlConnection.CreateCommand(); - - var activities = new List(); - using (Sdk.CreateTracerProviderBuilder() - .AddSqlClientInstrumentation( - options => - { - options.Filter = filter; - }) - .AddInMemoryExporter(activities) - .Build()) + var afterExecuteEventData = new { - var operationId = Guid.NewGuid(); - sqlCommandSetup(sqlCommand); + OperationId = operationId, + Command = sqlCommand, + Timestamp = 2000000L, + }; - var beforeExecuteEventData = new - { - OperationId = operationId, - Command = sqlCommand, - Timestamp = (long?)1000000L, - }; + this.fakeSqlClientDiagnosticSource.Write( + SqlClientDiagnosticListener.SqlMicrosoftAfterExecuteCommand, + afterExecuteEventData); + } - this.fakeSqlClientDiagnosticSource.Write( - SqlClientDiagnosticListener.SqlMicrosoftBeforeExecuteCommand, - beforeExecuteEventData); + return activities.ToArray(); + } +#endif - var afterExecuteEventData = new - { - OperationId = operationId, - Command = sqlCommand, - Timestamp = 2000000L, - }; - - this.fakeSqlClientDiagnosticSource.Write( - SqlClientDiagnosticListener.SqlMicrosoftAfterExecuteCommand, - afterExecuteEventData); - } + private class FakeSqlClientDiagnosticSource : IDisposable + { + private readonly DiagnosticListener listener; - return activities.ToArray(); + public FakeSqlClientDiagnosticSource() + { + this.listener = new DiagnosticListener(SqlClientInstrumentation.SqlClientDiagnosticListenerName); } -#endif - private class FakeSqlClientDiagnosticSource : IDisposable + public void Write(string name, object value) { - private readonly DiagnosticListener listener; - - public FakeSqlClientDiagnosticSource() + if (this.listener.IsEnabled(name)) { - this.listener = new DiagnosticListener(SqlClientInstrumentation.SqlClientDiagnosticListenerName); - } - - public void Write(string name, object value) - { - if (this.listener.IsEnabled(name)) - { - this.listener.Write(name, value); - } + this.listener.Write(name, value); } + } - public void Dispose() - { - this.listener.Dispose(); - } + public void Dispose() + { + this.listener.Dispose(); } } } diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTraceInstrumentationOptionsTests.cs b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTraceInstrumentationOptionsTests.cs new file mode 100644 index 00000000000..47ad7d4fe00 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTraceInstrumentationOptionsTests.cs @@ -0,0 +1,92 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Instrumentation.SqlClient.Tests; + +public class SqlClientTraceInstrumentationOptionsTests +{ + static SqlClientTraceInstrumentationOptionsTests() + { + Activity.DefaultIdFormat = ActivityIdFormat.W3C; + Activity.ForceDefaultIdFormat = true; + + var listener = new ActivityListener + { + ShouldListenTo = _ => true, + Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, + }; + + ActivitySource.AddActivityListener(listener); + } + + [Theory] + [InlineData("localhost", "localhost", null, null, null)] + [InlineData("127.0.0.1", null, "127.0.0.1", null, null)] + [InlineData("127.0.0.1,1433", null, "127.0.0.1", null, null)] + [InlineData("127.0.0.1, 1818", null, "127.0.0.1", null, "1818")] + [InlineData("127.0.0.1 \\ instanceName", null, "127.0.0.1", "instanceName", null)] + [InlineData("127.0.0.1\\instanceName, 1818", null, "127.0.0.1", "instanceName", "1818")] + [InlineData("tcp:127.0.0.1\\instanceName, 1818", null, "127.0.0.1", "instanceName", "1818")] + [InlineData("tcp:localhost", "localhost", null, null, null)] + [InlineData("tcp : localhost", "localhost", null, null, null)] + [InlineData("np : localhost", "localhost", null, null, null)] + [InlineData("lpc:localhost", "localhost", null, null, null)] + [InlineData("np:\\\\localhost\\pipe\\sql\\query", "localhost", null, null, null)] + [InlineData("np : \\\\localhost\\pipe\\sql\\query", "localhost", null, null, null)] + [InlineData("np:\\\\localhost\\pipe\\MSSQL$instanceName\\sql\\query", "localhost", null, "instanceName", null)] + public void ParseDataSourceTests( + string dataSource, + string expectedServerHostName, + string expectedServerIpAddress, + string expectedInstanceName, + string expectedPort) + { + var sqlConnectionDetails = SqlClientTraceInstrumentationOptions.ParseDataSource(dataSource); + + Assert.NotNull(sqlConnectionDetails); + Assert.Equal(expectedServerHostName, sqlConnectionDetails.ServerHostName); + Assert.Equal(expectedServerIpAddress, sqlConnectionDetails.ServerIpAddress); + Assert.Equal(expectedInstanceName, sqlConnectionDetails.InstanceName); + Assert.Equal(expectedPort, sqlConnectionDetails.Port); + } + + [Theory] + [InlineData(true, "localhost", "localhost", null, null, null)] + [InlineData(true, "127.0.0.1,1433", null, "127.0.0.1", null, null)] + [InlineData(true, "127.0.0.1,1434", null, "127.0.0.1", null, "1434")] + [InlineData(true, "127.0.0.1\\instanceName, 1818", null, "127.0.0.1", "instanceName", "1818")] + [InlineData(false, "localhost", "localhost", null, null, null)] + public void SqlClientTraceInstrumentationOptions_EnableConnectionLevelAttributes( + bool enableConnectionLevelAttributes, + string dataSource, + string expectedServerHostName, + string expectedServerIpAddress, + string expectedInstanceName, + string expectedPort) + { + var source = new ActivitySource("sql-client-instrumentation"); + var activity = source.StartActivity("Test Sql Activity"); + var options = new SqlClientTraceInstrumentationOptions() + { + EnableConnectionLevelAttributes = enableConnectionLevelAttributes, + }; + options.AddConnectionLevelDetailsToActivity(dataSource, activity); + + if (!enableConnectionLevelAttributes) + { + Assert.Equal(expectedServerHostName, activity.GetTagValue(SemanticConventions.AttributePeerService)); + } + else + { + Assert.Equal(expectedServerHostName, activity.GetTagValue(SemanticConventions.AttributeServerAddress)); + } + + Assert.Equal(expectedServerIpAddress, activity.GetTagValue(SemanticConventions.AttributeServerSocketAddress)); + Assert.Equal(expectedInstanceName, activity.GetTagValue(SemanticConventions.AttributeDbMsSqlInstanceName)); + Assert.Equal(expectedPort, activity.GetTagValue(SemanticConventions.AttributeServerPort)); + } +} diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlEventSourceTests.netfx.cs b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlEventSourceTests.netfx.cs index 007b42c2340..5394c75a0fb 100644 --- a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlEventSourceTests.netfx.cs +++ b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlEventSourceTests.netfx.cs @@ -1,388 +1,368 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #if NETFRAMEWORK -using System; using System.Data; using System.Data.SqlClient; using System.Diagnostics; using System.Diagnostics.Tracing; -using System.Threading.Tasks; -using Moq; using OpenTelemetry.Instrumentation.SqlClient.Implementation; using OpenTelemetry.Tests; using OpenTelemetry.Trace; using Xunit; -namespace OpenTelemetry.Instrumentation.SqlClient.Tests +namespace OpenTelemetry.Instrumentation.SqlClient.Tests; + +public class SqlEventSourceTests { - public class SqlEventSourceTests + /* + To run the integration tests, set the OTEL_SQLCONNECTIONSTRING machine-level environment variable to a valid Sql Server connection string. + + To use Docker... + 1) Run: docker run -d --name sql2019 -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=Pass@word" -p 5433:1433 mcr.microsoft.com/mssql/server:2019-latest + 2) Set OTEL_SQLCONNECTIONSTRING as: Data Source=127.0.0.1,5433; User ID=sa; Password=Pass@word + */ + + private const string SqlConnectionStringEnvVarName = "OTEL_SQLCONNECTIONSTRING"; + private static readonly string SqlConnectionString = SkipUnlessEnvVarFoundTheoryAttribute.GetEnvironmentVariable(SqlConnectionStringEnvVarName); + + [Trait("CategoryName", "SqlIntegrationTests")] + [SkipUnlessEnvVarFoundTheory(SqlConnectionStringEnvVarName)] + [InlineData(CommandType.Text, "select 1/1", false)] + [InlineData(CommandType.Text, "select 1/0", false, true)] + [InlineData(CommandType.StoredProcedure, "sp_who", false)] + [InlineData(CommandType.StoredProcedure, "sp_who", true)] + public async Task SuccessfulCommandTest(CommandType commandType, string commandText, bool captureText, bool isFailure = false) { - /* - To run the integration tests, set the OTEL_SQLCONNECTIONSTRING machine-level environment variable to a valid Sql Server connection string. - - To use Docker... - 1) Run: docker run -d --name sql2019 -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=Pass@word" -p 5433:1433 mcr.microsoft.com/mssql/server:2019-latest - 2) Set OTEL_SQLCONNECTIONSTRING as: Data Source=127.0.0.1,5433; User ID=sa; Password=Pass@word - */ - - private const string SqlConnectionStringEnvVarName = "OTEL_SQLCONNECTIONSTRING"; - private static readonly string SqlConnectionString = SkipUnlessEnvVarFoundTheoryAttribute.GetEnvironmentVariable(SqlConnectionStringEnvVarName); - - [Trait("CategoryName", "SqlIntegrationTests")] - [SkipUnlessEnvVarFoundTheory(SqlConnectionStringEnvVarName)] - [InlineData(CommandType.Text, "select 1/1", false)] - [InlineData(CommandType.Text, "select 1/0", false, true)] - [InlineData(CommandType.StoredProcedure, "sp_who", false)] - [InlineData(CommandType.StoredProcedure, "sp_who", true)] - public async Task SuccessfulCommandTest(CommandType commandType, string commandText, bool captureText, bool isFailure = false) - { - var activityProcessor = new Mock>(); - using var shutdownSignal = Sdk.CreateTracerProviderBuilder() - .AddProcessor(activityProcessor.Object) - .AddSqlClientInstrumentation(options => - { - options.SetDbStatementForText = captureText; - }) - .Build(); + var exportedItems = new List(); + using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + .AddInMemoryExporter(exportedItems) + .AddSqlClientInstrumentation(options => + { + options.SetDbStatementForText = captureText; + }) + .Build(); - using SqlConnection sqlConnection = new SqlConnection(SqlConnectionString); + using SqlConnection sqlConnection = new SqlConnection(SqlConnectionString); - await sqlConnection.OpenAsync().ConfigureAwait(false); + await sqlConnection.OpenAsync(); - string dataSource = sqlConnection.DataSource; + string dataSource = sqlConnection.DataSource; - sqlConnection.ChangeDatabase("master"); + sqlConnection.ChangeDatabase("master"); - using SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection) - { - CommandType = commandType, - }; + using SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection) + { + CommandType = commandType, + }; - try - { - await sqlCommand.ExecuteNonQueryAsync().ConfigureAwait(false); - } - catch - { - } + try + { + await sqlCommand.ExecuteNonQueryAsync(); + } + catch + { + } - Assert.Equal(3, activityProcessor.Invocations.Count); + Assert.Single(exportedItems); + var activity = exportedItems[0]; - var activity = (Activity)activityProcessor.Invocations[1].Arguments[0]; + VerifyActivityData(commandText, captureText, isFailure, dataSource, activity); + } - VerifyActivityData(commandText, captureText, isFailure, dataSource, activity); - } + [Theory] + [InlineData(typeof(FakeBehavingAdoNetSqlEventSource), CommandType.Text, "select 1/1", false)] + [InlineData(typeof(FakeBehavingAdoNetSqlEventSource), CommandType.Text, "select 1/0", false, true)] + [InlineData(typeof(FakeBehavingAdoNetSqlEventSource), CommandType.StoredProcedure, "sp_who", false)] + [InlineData(typeof(FakeBehavingAdoNetSqlEventSource), CommandType.StoredProcedure, "sp_who", true, false, 0, true)] + [InlineData(typeof(FakeBehavingMdsSqlEventSource), CommandType.Text, "select 1/1", false)] + [InlineData(typeof(FakeBehavingMdsSqlEventSource), CommandType.Text, "select 1/0", false, true)] + [InlineData(typeof(FakeBehavingMdsSqlEventSource), CommandType.StoredProcedure, "sp_who", false)] + [InlineData(typeof(FakeBehavingMdsSqlEventSource), CommandType.StoredProcedure, "sp_who", true, false, 0, true)] + public void EventSourceFakeTests( + Type eventSourceType, + CommandType commandType, + string commandText, + bool captureText, + bool isFailure = false, + int sqlExceptionNumber = 0, + bool enableConnectionLevelAttributes = false) + { + using IFakeBehavingSqlEventSource fakeSqlEventSource = (IFakeBehavingSqlEventSource)Activator.CreateInstance(eventSourceType); - [Theory] - [InlineData(typeof(FakeBehavingAdoNetSqlEventSource), CommandType.Text, "select 1/1", false)] - [InlineData(typeof(FakeBehavingAdoNetSqlEventSource), CommandType.Text, "select 1/0", false, true)] - [InlineData(typeof(FakeBehavingAdoNetSqlEventSource), CommandType.StoredProcedure, "sp_who", false)] - [InlineData(typeof(FakeBehavingAdoNetSqlEventSource), CommandType.StoredProcedure, "sp_who", true, false, 0, true)] - [InlineData(typeof(FakeBehavingMdsSqlEventSource), CommandType.Text, "select 1/1", false)] - [InlineData(typeof(FakeBehavingMdsSqlEventSource), CommandType.Text, "select 1/0", false, true)] - [InlineData(typeof(FakeBehavingMdsSqlEventSource), CommandType.StoredProcedure, "sp_who", false)] - [InlineData(typeof(FakeBehavingMdsSqlEventSource), CommandType.StoredProcedure, "sp_who", true, false, 0, true)] - public void EventSourceFakeTests( - Type eventSourceType, - CommandType commandType, - string commandText, - bool captureText, - bool isFailure = false, - int sqlExceptionNumber = 0, - bool enableConnectionLevelAttributes = false) - { - using IFakeBehavingSqlEventSource fakeSqlEventSource = (IFakeBehavingSqlEventSource)Activator.CreateInstance(eventSourceType); + var exportedItems = new List(); + using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + .AddInMemoryExporter(exportedItems) + .AddSqlClientInstrumentation(options => + { + options.SetDbStatementForText = captureText; + options.EnableConnectionLevelAttributes = enableConnectionLevelAttributes; + }) + .Build(); - var activityProcessor = new Mock>(); - using var shutdownSignal = Sdk.CreateTracerProviderBuilder() - .AddProcessor(activityProcessor.Object) - .AddSqlClientInstrumentation(options => - { - options.SetDbStatementForText = captureText; - options.EnableConnectionLevelAttributes = enableConnectionLevelAttributes; - }) - .Build(); + int objectId = Guid.NewGuid().GetHashCode(); - int objectId = Guid.NewGuid().GetHashCode(); + fakeSqlEventSource.WriteBeginExecuteEvent(objectId, "127.0.0.1", "master", commandType == CommandType.StoredProcedure ? commandText : string.Empty); - fakeSqlEventSource.WriteBeginExecuteEvent(objectId, "127.0.0.1", "master", commandType == CommandType.StoredProcedure ? commandText : string.Empty); + // success is stored in the first bit in compositeState 0b001 + int successFlag = !isFailure ? 1 : 0; - // success is stored in the first bit in compositeState 0b001 - int successFlag = !isFailure ? 1 : 0; + // isSqlException is stored in the second bit in compositeState 0b010 + int isSqlExceptionFlag = sqlExceptionNumber > 0 ? 2 : 0; - // isSqlException is stored in the second bit in compositeState 0b010 - int isSqlExceptionFlag = sqlExceptionNumber > 0 ? 2 : 0; + // synchronous state is stored in the third bit in compositeState 0b100 + int synchronousFlag = false ? 4 : 0; - // synchronous state is stored in the third bit in compositeState 0b100 - int synchronousFlag = false ? 4 : 0; + int compositeState = successFlag | isSqlExceptionFlag | synchronousFlag; - int compositeState = successFlag | isSqlExceptionFlag | synchronousFlag; + fakeSqlEventSource.WriteEndExecuteEvent(objectId, compositeState, sqlExceptionNumber); + shutdownSignal.Dispose(); + Assert.Single(exportedItems); - fakeSqlEventSource.WriteEndExecuteEvent(objectId, compositeState, sqlExceptionNumber); - shutdownSignal.Dispose(); - Assert.Equal(5, activityProcessor.Invocations.Count); // SetTracerProvider/OnStart/OnEnd/OnShutdown/Dispose called. + var activity = exportedItems[0]; - var activity = (Activity)activityProcessor.Invocations[2].Arguments[0]; + VerifyActivityData(commandText, captureText, isFailure, "127.0.0.1", activity, enableConnectionLevelAttributes); + } - VerifyActivityData(commandText, captureText, isFailure, "127.0.0.1", activity, enableConnectionLevelAttributes); - } + [Theory] + [InlineData(typeof(FakeMisbehavingAdoNetSqlEventSource))] + [InlineData(typeof(FakeMisbehavingMdsSqlEventSource))] + public void EventSourceFakeUnknownEventWithNullPayloadTest(Type eventSourceType) + { + using IFakeMisbehavingSqlEventSource fakeSqlEventSource = (IFakeMisbehavingSqlEventSource)Activator.CreateInstance(eventSourceType); - [Theory] - [InlineData(typeof(FakeMisbehavingAdoNetSqlEventSource))] - [InlineData(typeof(FakeMisbehavingMdsSqlEventSource))] - public void EventSourceFakeUnknownEventWithNullPayloadTest(Type eventSourceType) - { - using IFakeMisbehavingSqlEventSource fakeSqlEventSource = (IFakeMisbehavingSqlEventSource)Activator.CreateInstance(eventSourceType); + var exportedItems = new List(); + using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + .AddInMemoryExporter(exportedItems) + .AddSqlClientInstrumentation() + .Build(); - var activityProcessor = new Mock>(); - using var shutdownSignal = Sdk.CreateTracerProviderBuilder() - .AddProcessor(activityProcessor.Object) - .AddSqlClientInstrumentation() - .Build(); + fakeSqlEventSource.WriteUnknownEventWithNullPayload(); - fakeSqlEventSource.WriteUnknownEventWithNullPayload(); + shutdownSignal.Dispose(); - shutdownSignal.Dispose(); + Assert.Empty(exportedItems); + } - Assert.Equal(3, activityProcessor.Invocations.Count); // SetTracerProvider/OnShutdown/Dispose called. - } + [Theory] + [InlineData(typeof(FakeMisbehavingAdoNetSqlEventSource))] + [InlineData(typeof(FakeMisbehavingMdsSqlEventSource))] + public void EventSourceFakeInvalidPayloadTest(Type eventSourceType) + { + using IFakeMisbehavingSqlEventSource fakeSqlEventSource = (IFakeMisbehavingSqlEventSource)Activator.CreateInstance(eventSourceType); - [Theory] - [InlineData(typeof(FakeMisbehavingAdoNetSqlEventSource))] - [InlineData(typeof(FakeMisbehavingMdsSqlEventSource))] - public void EventSourceFakeInvalidPayloadTest(Type eventSourceType) - { - using IFakeMisbehavingSqlEventSource fakeSqlEventSource = (IFakeMisbehavingSqlEventSource)Activator.CreateInstance(eventSourceType); + var exportedItems = new List(); + using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + .AddInMemoryExporter(exportedItems) + .AddSqlClientInstrumentation() + .Build(); - var activityProcessor = new Mock>(); - using var shutdownSignal = Sdk.CreateTracerProviderBuilder() - .AddProcessor(activityProcessor.Object) - .AddSqlClientInstrumentation() - .Build(); + fakeSqlEventSource.WriteBeginExecuteEvent("arg1"); - fakeSqlEventSource.WriteBeginExecuteEvent("arg1"); + fakeSqlEventSource.WriteEndExecuteEvent("arg1", "arg2", "arg3", "arg4"); + shutdownSignal.Dispose(); - fakeSqlEventSource.WriteEndExecuteEvent("arg1", "arg2", "arg3", "arg4"); - shutdownSignal.Dispose(); + Assert.Empty(exportedItems); + } - Assert.Equal(3, activityProcessor.Invocations.Count); // SetTracerProvider/OnShutdown/Dispose called. - } + [Theory] + [InlineData(typeof(FakeBehavingAdoNetSqlEventSource))] + [InlineData(typeof(FakeBehavingMdsSqlEventSource))] + public void DefaultCaptureTextFalse(Type eventSourceType) + { + using IFakeBehavingSqlEventSource fakeSqlEventSource = (IFakeBehavingSqlEventSource)Activator.CreateInstance(eventSourceType); - [Theory] - [InlineData(typeof(FakeBehavingAdoNetSqlEventSource))] - [InlineData(typeof(FakeBehavingMdsSqlEventSource))] - public void DefaultCaptureTextFalse(Type eventSourceType) - { - using IFakeBehavingSqlEventSource fakeSqlEventSource = (IFakeBehavingSqlEventSource)Activator.CreateInstance(eventSourceType); + var exportedItems = new List(); + using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + .AddInMemoryExporter(exportedItems) + .AddSqlClientInstrumentation() + .Build(); - var activityProcessor = new Mock>(); - using var shutdownSignal = Sdk.CreateTracerProviderBuilder() - .AddProcessor(activityProcessor.Object) - .AddSqlClientInstrumentation() - .Build(); + int objectId = Guid.NewGuid().GetHashCode(); - int objectId = Guid.NewGuid().GetHashCode(); + const string commandText = "TestCommandTest"; + fakeSqlEventSource.WriteBeginExecuteEvent(objectId, "127.0.0.1", "master", commandText); - const string commandText = "TestCommandTest"; - fakeSqlEventSource.WriteBeginExecuteEvent(objectId, "127.0.0.1", "master", commandText); + // success is stored in the first bit in compositeState 0b001 + int successFlag = 1; - // success is stored in the first bit in compositeState 0b001 - int successFlag = 1; + // isSqlException is stored in the second bit in compositeState 0b010 + int isSqlExceptionFlag = 2; - // isSqlException is stored in the second bit in compositeState 0b010 - int isSqlExceptionFlag = 2; + // synchronous state is stored in the third bit in compositeState 0b100 + int synchronousFlag = 4; - // synchronous state is stored in the third bit in compositeState 0b100 - int synchronousFlag = 4; + int compositeState = successFlag | isSqlExceptionFlag | synchronousFlag; - int compositeState = successFlag | isSqlExceptionFlag | synchronousFlag; + fakeSqlEventSource.WriteEndExecuteEvent(objectId, compositeState, 0); + shutdownSignal.Dispose(); + Assert.Single(exportedItems); - fakeSqlEventSource.WriteEndExecuteEvent(objectId, compositeState, 0); - shutdownSignal.Dispose(); - Assert.Equal(5, activityProcessor.Invocations.Count); // SetTracerProvider/OnStart/OnEnd/OnShutdown/Dispose called. + var activity = exportedItems[0]; - var activity = (Activity)activityProcessor.Invocations[2].Arguments[0]; + const bool captureText = false; + VerifyActivityData(commandText, captureText, false, "127.0.0.1", activity, false); + } - const bool captureText = false; - VerifyActivityData(commandText, captureText, false, "127.0.0.1", activity, false); - } + private static void VerifyActivityData( + string commandText, + bool captureText, + bool isFailure, + string dataSource, + Activity activity, + bool enableConnectionLevelAttributes = false) + { + Assert.Equal("master", activity.DisplayName); + Assert.Equal(ActivityKind.Client, activity.Kind); + Assert.Equal(SqlActivitySourceHelper.MicrosoftSqlServerDatabaseSystemName, activity.GetTagValue(SemanticConventions.AttributeDbSystem)); - private static void VerifyActivityData( - string commandText, - bool captureText, - bool isFailure, - string dataSource, - Activity activity, - bool enableConnectionLevelAttributes = false) + if (!enableConnectionLevelAttributes) { - Assert.Equal("master", activity.DisplayName); - Assert.Equal(ActivityKind.Client, activity.Kind); - Assert.Equal(SqlActivitySourceHelper.MicrosoftSqlServerDatabaseSystemName, activity.GetTagValue(SemanticConventions.AttributeDbSystem)); + Assert.Equal(dataSource, activity.GetTagValue(SemanticConventions.AttributePeerService)); + } + else + { + var connectionDetails = SqlClientTraceInstrumentationOptions.ParseDataSource(dataSource); - if (!enableConnectionLevelAttributes) + if (!string.IsNullOrEmpty(connectionDetails.ServerHostName)) { - Assert.Equal(dataSource, activity.GetTagValue(SemanticConventions.AttributePeerService)); + Assert.Equal(connectionDetails.ServerHostName, activity.GetTagValue(SemanticConventions.AttributeNetPeerName)); } else { - var connectionDetails = SqlClientInstrumentationOptions.ParseDataSource(dataSource); - - if (!string.IsNullOrEmpty(connectionDetails.ServerHostName)) - { - Assert.Equal(connectionDetails.ServerHostName, activity.GetTagValue(SemanticConventions.AttributeNetPeerName)); - } - else - { - Assert.Equal(connectionDetails.ServerIpAddress, activity.GetTagValue(SemanticConventions.AttributeNetPeerIp)); - } - - if (!string.IsNullOrEmpty(connectionDetails.InstanceName)) - { - Assert.Equal(connectionDetails.InstanceName, activity.GetTagValue(SemanticConventions.AttributeDbMsSqlInstanceName)); - } - - if (!string.IsNullOrEmpty(connectionDetails.Port)) - { - Assert.Equal(connectionDetails.Port, activity.GetTagValue(SemanticConventions.AttributeNetPeerPort)); - } + Assert.Equal(connectionDetails.ServerIpAddress, activity.GetTagValue(SemanticConventions.AttributeServerSocketAddress)); } - Assert.Equal("master", activity.GetTagValue(SemanticConventions.AttributeDbName)); - - // "db.statement_type" is never set by the SqlEventSource instrumentation - Assert.Null(activity.GetTagValue(SpanAttributeConstants.DatabaseStatementTypeKey)); - if (captureText) - { - Assert.Equal(commandText, activity.GetTagValue(SemanticConventions.AttributeDbStatement)); - } - else + if (!string.IsNullOrEmpty(connectionDetails.InstanceName)) { - Assert.Null(activity.GetTagValue(SemanticConventions.AttributeDbStatement)); + Assert.Equal(connectionDetails.InstanceName, activity.GetTagValue(SemanticConventions.AttributeDbMsSqlInstanceName)); } - if (!isFailure) + if (!string.IsNullOrEmpty(connectionDetails.Port)) { - Assert.Equal(ActivityStatusCode.Unset, activity.Status); - } - else - { - Assert.Equal(ActivityStatusCode.Error, activity.Status); - Assert.NotNull(activity.StatusDescription); + Assert.Equal(connectionDetails.Port, activity.GetTagValue(SemanticConventions.AttributeNetPeerPort)); } } -#pragma warning disable SA1201 // Elements should appear in the correct order + Assert.Equal("master", activity.GetTagValue(SemanticConventions.AttributeDbName)); - // Helper interface to be able to have single test method for multiple EventSources, want to keep it close to the event sources themselves. - private interface IFakeBehavingSqlEventSource : IDisposable -#pragma warning restore SA1201 // Elements should appear in the correct order + if (captureText) { - void WriteBeginExecuteEvent(int objectId, string dataSource, string databaseName, string commandText); - - void WriteEndExecuteEvent(int objectId, int compositeState, int sqlExceptionNumber); + Assert.Equal(commandText, activity.GetTagValue(SemanticConventions.AttributeDbStatement)); + } + else + { + Assert.Null(activity.GetTagValue(SemanticConventions.AttributeDbStatement)); } - private interface IFakeMisbehavingSqlEventSource : IDisposable + if (!isFailure) + { + Assert.Equal(ActivityStatusCode.Unset, activity.Status); + } + else { - void WriteBeginExecuteEvent(string arg1); + Assert.Equal(ActivityStatusCode.Error, activity.Status); + Assert.NotNull(activity.StatusDescription); + } + } - void WriteEndExecuteEvent(string arg1, string arg2, string arg3, string arg4); +#pragma warning disable SA1201 // Elements should appear in the correct order - void WriteUnknownEventWithNullPayload(); - } + // Helper interface to be able to have single test method for multiple EventSources, want to keep it close to the event sources themselves. + private interface IFakeBehavingSqlEventSource : IDisposable +#pragma warning restore SA1201 // Elements should appear in the correct order + { + void WriteBeginExecuteEvent(int objectId, string dataSource, string databaseName, string commandText); - [EventSource(Name = SqlEventSourceListener.AdoNetEventSourceName + "-FakeFriendly")] - private class FakeBehavingAdoNetSqlEventSource : EventSource, IFakeBehavingSqlEventSource + void WriteEndExecuteEvent(int objectId, int compositeState, int sqlExceptionNumber); + } + + private interface IFakeMisbehavingSqlEventSource : IDisposable + { + void WriteBeginExecuteEvent(string arg1); + + void WriteEndExecuteEvent(string arg1, string arg2, string arg3, string arg4); + + void WriteUnknownEventWithNullPayload(); + } + + [EventSource(Name = SqlEventSourceListener.AdoNetEventSourceName + "-FakeFriendly")] + private class FakeBehavingAdoNetSqlEventSource : EventSource, IFakeBehavingSqlEventSource + { + [Event(SqlEventSourceListener.BeginExecuteEventId)] + public void WriteBeginExecuteEvent(int objectId, string dataSource, string databaseName, string commandText) { - [Event(SqlEventSourceListener.BeginExecuteEventId)] - public void WriteBeginExecuteEvent(int objectId, string dataSource, string databaseName, string commandText) - { - this.WriteEvent(SqlEventSourceListener.BeginExecuteEventId, objectId, dataSource, databaseName, commandText); - } + this.WriteEvent(SqlEventSourceListener.BeginExecuteEventId, objectId, dataSource, databaseName, commandText); + } - [Event(SqlEventSourceListener.EndExecuteEventId)] - public void WriteEndExecuteEvent(int objectId, int compositeState, int sqlExceptionNumber) - { - this.WriteEvent(SqlEventSourceListener.EndExecuteEventId, objectId, compositeState, sqlExceptionNumber); - } + [Event(SqlEventSourceListener.EndExecuteEventId)] + public void WriteEndExecuteEvent(int objectId, int compositeState, int sqlExceptionNumber) + { + this.WriteEvent(SqlEventSourceListener.EndExecuteEventId, objectId, compositeState, sqlExceptionNumber); } + } - [EventSource(Name = SqlEventSourceListener.MdsEventSourceName + "-FakeFriendly")] - private class FakeBehavingMdsSqlEventSource : EventSource, IFakeBehavingSqlEventSource + [EventSource(Name = SqlEventSourceListener.MdsEventSourceName + "-FakeFriendly")] + private class FakeBehavingMdsSqlEventSource : EventSource, IFakeBehavingSqlEventSource + { + [Event(SqlEventSourceListener.BeginExecuteEventId)] + public void WriteBeginExecuteEvent(int objectId, string dataSource, string databaseName, string commandText) { - [Event(SqlEventSourceListener.BeginExecuteEventId)] - public void WriteBeginExecuteEvent(int objectId, string dataSource, string databaseName, string commandText) - { - this.WriteEvent(SqlEventSourceListener.BeginExecuteEventId, objectId, dataSource, databaseName, commandText); - } + this.WriteEvent(SqlEventSourceListener.BeginExecuteEventId, objectId, dataSource, databaseName, commandText); + } - [Event(SqlEventSourceListener.EndExecuteEventId)] - public void WriteEndExecuteEvent(int objectId, int compositeState, int sqlExceptionNumber) - { - this.WriteEvent(SqlEventSourceListener.EndExecuteEventId, objectId, compositeState, sqlExceptionNumber); - } + [Event(SqlEventSourceListener.EndExecuteEventId)] + public void WriteEndExecuteEvent(int objectId, int compositeState, int sqlExceptionNumber) + { + this.WriteEvent(SqlEventSourceListener.EndExecuteEventId, objectId, compositeState, sqlExceptionNumber); } + } - [EventSource(Name = SqlEventSourceListener.AdoNetEventSourceName + "-FakeEvil")] - private class FakeMisbehavingAdoNetSqlEventSource : EventSource, IFakeMisbehavingSqlEventSource + [EventSource(Name = SqlEventSourceListener.AdoNetEventSourceName + "-FakeEvil")] + private class FakeMisbehavingAdoNetSqlEventSource : EventSource, IFakeMisbehavingSqlEventSource + { + [Event(SqlEventSourceListener.BeginExecuteEventId)] + public void WriteBeginExecuteEvent(string arg1) { - [Event(SqlEventSourceListener.BeginExecuteEventId)] - public void WriteBeginExecuteEvent(string arg1) - { - this.WriteEvent(SqlEventSourceListener.BeginExecuteEventId, arg1); - } + this.WriteEvent(SqlEventSourceListener.BeginExecuteEventId, arg1); + } - [Event(SqlEventSourceListener.EndExecuteEventId)] - public void WriteEndExecuteEvent(string arg1, string arg2, string arg3, string arg4) - { - this.WriteEvent(SqlEventSourceListener.EndExecuteEventId, arg1, arg2, arg3, arg4); - } + [Event(SqlEventSourceListener.EndExecuteEventId)] + public void WriteEndExecuteEvent(string arg1, string arg2, string arg3, string arg4) + { + this.WriteEvent(SqlEventSourceListener.EndExecuteEventId, arg1, arg2, arg3, arg4); + } - [Event(3)] - public void WriteUnknownEventWithNullPayload() - { - object[] args = null; + [Event(3)] + public void WriteUnknownEventWithNullPayload() + { + object[] args = null; - this.WriteEvent(3, args); - } + this.WriteEvent(3, args); } + } - [EventSource(Name = SqlEventSourceListener.MdsEventSourceName + "-FakeEvil")] - private class FakeMisbehavingMdsSqlEventSource : EventSource, IFakeMisbehavingSqlEventSource + [EventSource(Name = SqlEventSourceListener.MdsEventSourceName + "-FakeEvil")] + private class FakeMisbehavingMdsSqlEventSource : EventSource, IFakeMisbehavingSqlEventSource + { + [Event(SqlEventSourceListener.BeginExecuteEventId)] + public void WriteBeginExecuteEvent(string arg1) { - [Event(SqlEventSourceListener.BeginExecuteEventId)] - public void WriteBeginExecuteEvent(string arg1) - { - this.WriteEvent(SqlEventSourceListener.BeginExecuteEventId, arg1); - } + this.WriteEvent(SqlEventSourceListener.BeginExecuteEventId, arg1); + } - [Event(SqlEventSourceListener.EndExecuteEventId)] - public void WriteEndExecuteEvent(string arg1, string arg2, string arg3, string arg4) - { - this.WriteEvent(SqlEventSourceListener.EndExecuteEventId, arg1, arg2, arg3, arg4); - } + [Event(SqlEventSourceListener.EndExecuteEventId)] + public void WriteEndExecuteEvent(string arg1, string arg2, string arg3, string arg4) + { + this.WriteEvent(SqlEventSourceListener.EndExecuteEventId, arg1, arg2, arg3, arg4); + } - [Event(3)] - public void WriteUnknownEventWithNullPayload() - { - object[] args = null; + [Event(3)] + public void WriteUnknownEventWithNullPayload() + { + object[] args = null; - this.WriteEvent(3, args); - } + this.WriteEvent(3, args); } } } diff --git a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/Dockerfile b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/Dockerfile index cadf139e98b..9723867f4f3 100644 --- a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/Dockerfile +++ b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/Dockerfile @@ -2,22 +2,22 @@ # 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 WORKDIR /w3c RUN apt-get update && apt-get install -y git -RUN git clone https://github.com/w3c/trace-context.git +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" -RUN dotnet publish "OpenTelemetry.Instrumentation.W3cTraceContext.Tests.csproj" -c "${PUBLISH_CONFIGURATION}" -f "${PUBLISH_FRAMEWORK}" -o /drop -p:IntegrationBuild=true -p:TARGET_FRAMEWORK=${PUBLISH_FRAMEWORK} +RUN dotnet publish "OpenTelemetry.Instrumentation.W3cTraceContext.Tests.csproj" -c "${PUBLISH_CONFIGURATION}" -f "${PUBLISH_FRAMEWORK}" -o /drop -p:IntegrationBuild=true FROM mcr.microsoft.com/dotnet/sdk:${TEST_SDK_VERSION} AS final WORKDIR /test diff --git a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/OpenTelemetry.Instrumentation.W3cTraceContext.Tests.csproj b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/OpenTelemetry.Instrumentation.W3cTraceContext.Tests.csproj index 037a4bb53d2..81fa3eb1932 100644 --- a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/OpenTelemetry.Instrumentation.W3cTraceContext.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/OpenTelemetry.Instrumentation.W3cTraceContext.Tests.csproj @@ -2,10 +2,7 @@ Unit test project for OpenTelemetry ASP.NET Core instrumentation for W3C Trace Context Trace - - net7.0;net6.0 - $(TARGET_FRAMEWORK) - + $(TargetFrameworksForAspNetCoreTests) disable @@ -13,25 +10,22 @@ - - all + runtime; build; native; contentfiles; analyzers - - - - - - + + + + diff --git a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/W3CTraceContextTests.cs b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/W3CTraceContextTests.cs index adecf19a0f5..3ab1d5d178c 100644 --- a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/W3CTraceContextTests.cs +++ b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/W3CTraceContextTests.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Text; @@ -25,139 +12,134 @@ using Xunit; using Xunit.Abstractions; -namespace OpenTelemetry.Instrumentation.W3cTraceContext.Tests +namespace OpenTelemetry.Instrumentation.W3cTraceContext.Tests; + +public class W3CTraceContextTests : IDisposable { - public class W3CTraceContextTests : IDisposable + /* + To run the tests, invoke docker-compose.yml from the root of the repo: + opentelemetry>docker-compose --file=test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/docker-compose.yml --project-directory=. up --exit-code-from=tests --build + */ + private const string W3cTraceContextEnvVarName = "OTEL_W3CTRACECONTEXT"; + private static readonly Version AspNetCoreHostingVersion = typeof(Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory).Assembly.GetName().Version; + private readonly HttpClient httpClient = new(); + private readonly ITestOutputHelper output; + + public W3CTraceContextTests(ITestOutputHelper output) { - /* - To run the tests, invoke docker-compose.yml from the root of the repo: - opentelemetry>docker-compose --file=test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/docker-compose.yml --project-directory=. up --exit-code-from=tests --build - */ - private const string W3cTraceContextEnvVarName = "OTEL_W3CTRACECONTEXT"; - private static readonly Version AspNetCoreHostingVersion = typeof(Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory).Assembly.GetName().Version; - private readonly HttpClient httpClient = new HttpClient(); - private readonly ITestOutputHelper output; - - public W3CTraceContextTests(ITestOutputHelper output) - { - this.output = output; - } + this.output = output; + } + + [Trait("CategoryName", "W3CTraceContextTests")] + [SkipUnlessEnvVarFoundTheory(W3cTraceContextEnvVarName)] + [InlineData("placeholder")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1026:Theory methods should use all of their parameters", Justification = "Need to use SkipUnlessEnvVarFoundTheory")] + public void W3CTraceContextTestSuiteAsync(string value) + { + // configure SDK + using var tracerprovider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .Build(); + + var builder = WebApplication.CreateBuilder(); + using var app = builder.Build(); - [Trait("CategoryName", "W3CTraceContextTests")] - [SkipUnlessEnvVarFoundTheory(W3cTraceContextEnvVarName)] - [InlineData("placeholder")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1026:Theory methods should use all of their parameters", Justification = "Need to use SkipUnlessEnvVarFoundTheory")] - public void W3CTraceContextTestSuiteAsync(string value) + app.MapPost("/", async ([FromBody] Data[] data) => { - // configure SDK - using var tracerprovider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation() - .Build(); - - var builder = WebApplication.CreateBuilder(); - using var app = builder.Build(); - - // disabling due to failing dotnet-format - // TODO: investigate why dotnet-format fails. -#pragma warning disable SA1008 // Opening parenthesis should be spaced correctly - app.MapPost("/", async([FromBody] Data[] data) => + var result = string.Empty; + if (data != null) { - var result = string.Empty; - if (data != null) + foreach (var argument in data) { - foreach (var argument in data) + using var request = new HttpRequestMessage(HttpMethod.Post, argument.Url) { - using var request = new HttpRequestMessage(HttpMethod.Post, argument.Url) - { - Content = new StringContent( - JsonSerializer.Serialize(argument.Arguments), - Encoding.UTF8, - "application/json"), - }; - await this.httpClient.SendAsync(request).ConfigureAwait(false); - } - } - else - { - result = "done"; + Content = new StringContent( + JsonSerializer.Serialize(argument.Arguments), + Encoding.UTF8, + "application/json"), + }; + await this.httpClient.SendAsync(request); } + } + else + { + result = "done"; + } - return result; - }); -#pragma warning restore SA1008 // Opening parenthesis should be spaced correctly + return result; + }); - app.RunAsync(); + app.RunAsync(); - string result = RunCommand("python", "trace-context/test/test.py http://localhost:5000/"); + string result = RunCommand("python", "trace-context/test/test.py http://localhost:5000/"); - // Assert - string lastLine = ParseLastLine(result); + // Assert + string lastLine = ParseLastLine(result); - this.output.WriteLine("result:" + result); + this.output.WriteLine("result:" + result); - // Assert on the last line + // Assert on the last line - // TODO: Investigate failures on .NET6 vs .NET7. To see the details - // run the tests with console logger (done automatically by the CI - // jobs). + // TODO: Investigate failures on .NET6 vs .NET7. To see the details + // run the tests with console logger (done automatically by the CI + // jobs). - if (AspNetCoreHostingVersion.Major <= 6) - { - Assert.StartsWith("FAILED (failures=5)", lastLine); - } - else - { - Assert.StartsWith("FAILED (failures=3)", lastLine); - } - } - - public void Dispose() + if (AspNetCoreHostingVersion.Major <= 6) { - this.httpClient.Dispose(); + Assert.StartsWith("FAILED (failures=3)", lastLine); } - - private static string RunCommand(string command, string args) + else { - using var proc = new Process - { - StartInfo = new ProcessStartInfo - { - FileName = command, - Arguments = $" {args}", - UseShellExecute = false, - RedirectStandardError = true, - RedirectStandardOutput = true, - CreateNoWindow = true, - WorkingDirectory = ".", - }, - }; - proc.Start(); - - // TODO: after W3C Trace Context test suite passes, it might go in standard output - var results = proc.StandardError.ReadToEnd(); - proc.WaitForExit(); - return results; + Assert.StartsWith("OK", lastLine); } + } - private static string ParseLastLine(string output) + public void Dispose() + { + this.httpClient.Dispose(); + } + + private static string RunCommand(string command, string args) + { + using var proc = new Process { - if (output.Length <= 1) + StartInfo = new ProcessStartInfo { - return output; - } + FileName = command, + Arguments = $" {args}", + UseShellExecute = false, + RedirectStandardError = true, + RedirectStandardOutput = true, + CreateNoWindow = true, + WorkingDirectory = ".", + }, + }; + proc.Start(); + + // TODO: after W3C Trace Context test suite passes, it might go in standard output + var results = proc.StandardError.ReadToEnd(); + proc.WaitForExit(); + return results; + } - // The output ends with '\n', which should be ignored. - var lastNewLineCharacterPos = output.LastIndexOf('\n', output.Length - 2); - return output.Substring(lastNewLineCharacterPos + 1); + private static string ParseLastLine(string output) + { + if (output.Length <= 1) + { + return output; } - public class Data - { - [JsonPropertyName("url")] - public string Url { get; set; } + // The output ends with '\n', which should be ignored. + var lastNewLineCharacterPos = output.LastIndexOf('\n', output.Length - 2); + return output.Substring(lastNewLineCharacterPos + 1); + } - [JsonPropertyName("arguments")] - public Data[] Arguments { get; set; } - } + public class Data + { + [JsonPropertyName("url")] + public string Url { get; set; } + + [JsonPropertyName("arguments")] + public Data[] Arguments { get; set; } } } diff --git a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/docker-compose.yml b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/docker-compose.yml index ae3253078ce..7c421786f5c 100644 --- a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/docker-compose.yml +++ b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/docker-compose.yml @@ -11,4 +11,3 @@ services: command: --TestCaseFilter:CategoryName=W3CTraceContextTests environment: - OTEL_W3CTRACECONTEXT=enabled - - SPEC_LEVEL=2 diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/IntegrationTests.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/IntegrationTests.cs new file mode 100644 index 00000000000..d8f93b2df0c --- /dev/null +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/IntegrationTests.cs @@ -0,0 +1,134 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using OpenTelemetry.Context.Propagation; +using OpenTelemetry.Trace; +using OpenTracing; +using Xunit; + +namespace OpenTelemetry.Shims.OpenTracing.Tests; + +public class IntegrationTests +{ + private const string ChildActivitySource = "ChildActivitySource"; + private const string ParentActivitySource = "ParentActivitySource"; + + [Theory] + [InlineData(SamplingDecision.Drop, SamplingDecision.Drop, SamplingDecision.Drop)] + [InlineData(SamplingDecision.Drop, SamplingDecision.RecordAndSample, SamplingDecision.Drop)] + [InlineData(SamplingDecision.Drop, SamplingDecision.RecordOnly, SamplingDecision.Drop)] + [InlineData(SamplingDecision.RecordOnly, SamplingDecision.RecordAndSample, SamplingDecision.RecordOnly)] + [InlineData(SamplingDecision.RecordAndSample, SamplingDecision.RecordOnly, SamplingDecision.RecordAndSample)] + [InlineData(SamplingDecision.RecordAndSample, SamplingDecision.Drop, SamplingDecision.RecordAndSample)] + public void WithActivities( + SamplingDecision parentActivitySamplingDecision, + SamplingDecision shimSamplingDecision, + SamplingDecision childActivitySamplingDecision) + { + var exportedSpans = new List(); + + const string ParentActivityName = "ParentActivity"; + const string ShimActivityName = "ShimActivity"; + const string ChildActivityName = "ChildActivity"; + + var testSampler = new TestSampler((samplingParameters) => + samplingParameters.Name switch + { + ParentActivityName => parentActivitySamplingDecision, + ShimActivityName => shimSamplingDecision, + ChildActivityName => childActivitySamplingDecision, + _ => SamplingDecision.Drop, + }); + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddInMemoryExporter(exportedSpans) + .SetSampler(testSampler) + .When( + parentActivitySamplingDecision == SamplingDecision.RecordAndSample, + b => b.AddSource(ParentActivitySource)) + .When( + shimSamplingDecision == SamplingDecision.RecordAndSample, + b => b.AddSource("opentracing-shim")) + .When( + childActivitySamplingDecision == SamplingDecision.RecordAndSample, + b => b.AddSource(ChildActivitySource)) + .Build(); + + ITracer otTracer = new TracerShim( + tracerProvider, + Propagators.DefaultTextMapPropagator); + + // Real usage requires a call OpenTracing.Util.GlobalTracer.Register(otTracer), + // however, that can only happen once per process, we don't do it here so we + // can run multiple tests in the same process. + + using var parentActivitySource = new ActivitySource(ParentActivitySource); + using var childActivitySource = new ActivitySource(ChildActivitySource); + + using (var parentActivity = parentActivitySource.StartActivity(ParentActivityName)) + { + using (IScope parentScope = otTracer.BuildSpan(ShimActivityName).StartActive()) + { + parentScope.Span.SetTag("parent", true); + + using var childActivity = childActivitySource.StartActivity(ChildActivityName); + } + } + + var expectedExportedSpans = new string[] + { + childActivitySamplingDecision == SamplingDecision.RecordAndSample ? ChildActivityName : null, + shimSamplingDecision == SamplingDecision.RecordAndSample ? ShimActivityName : null, + parentActivitySamplingDecision == SamplingDecision.RecordAndSample ? ParentActivityName : null, + } + .Where(s => s is not null) + .ToList(); + + for (int i = 0; i < expectedExportedSpans.Count; i++) + { + Assert.Equal(expectedExportedSpans[i], exportedSpans[i].DisplayName); + } + + if (childActivitySamplingDecision == SamplingDecision.RecordAndSample) + { + if (shimSamplingDecision == SamplingDecision.RecordAndSample) + { + Assert.Same(exportedSpans[1], exportedSpans[0].Parent); + } + } + } + + private class TestSampler : Sampler + { + private readonly Func shouldSampleDelegate; + + public TestSampler(Func shouldSampleDelegate) + { + this.shouldSampleDelegate = shouldSampleDelegate; + } + + public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) + { + return new SamplingResult(this.shouldSampleDelegate(samplingParameters)); + } + } +} + +[System.Diagnostics.CodeAnalysis.SuppressMessage( + "StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Local use only")] +internal static class ConditionalTracerProviderBuilderExtension +{ + public static TracerProviderBuilder When( + this TracerProviderBuilder builder, + bool condition, + Func conditionalDelegate) + { + if (condition) + { + builder = conditionalDelegate(builder); + } + + return builder; + } +} diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/ListenAndSampleAllActivitySources.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/ListenAndSampleAllActivitySources.cs new file mode 100644 index 00000000000..eccc55b95eb --- /dev/null +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/ListenAndSampleAllActivitySources.cs @@ -0,0 +1,35 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using Xunit; + +namespace OpenTelemetry.Shims.OpenTracing.Tests; + +[CollectionDefinition(nameof(ListenAndSampleAllActivitySources))] +public sealed class ListenAndSampleAllActivitySources : ICollectionFixture +{ + public sealed class Fixture : IDisposable + { + private readonly ActivityListener listener; + + public Fixture() + { + Activity.DefaultIdFormat = ActivityIdFormat.W3C; + Activity.ForceDefaultIdFormat = true; + + this.listener = new ActivityListener + { + ShouldListenTo = _ => true, + Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, + }; + + ActivitySource.AddActivityListener(this.listener); + } + + public void Dispose() + { + this.listener.Dispose(); + } + } +} diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/OpenTelemetry.Shims.OpenTracing.Tests.csproj b/test/OpenTelemetry.Shims.OpenTracing.Tests/OpenTelemetry.Shims.OpenTracing.Tests.csproj index d66b4b183b7..7e08df554f3 100644 --- a/test/OpenTelemetry.Shims.OpenTracing.Tests/OpenTelemetry.Shims.OpenTracing.Tests.csproj +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/OpenTelemetry.Shims.OpenTracing.Tests.csproj @@ -1,24 +1,22 @@ Unit test project for OpenTelemetry.Shims.OpenTracing - - net7.0;net6.0 - + $(TargetFrameworksForTests) disable - - - all + runtime; build; native; contentfiles; analyzers + + diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/ScopeManagerShimTests.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/ScopeManagerShimTests.cs index ddf86fe77d9..6f4ca876c55 100644 --- a/test/OpenTelemetry.Shims.OpenTracing.Tests/ScopeManagerShimTests.cs +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/ScopeManagerShimTests.cs @@ -1,105 +1,69 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -using Moq; using OpenTelemetry.Trace; using Xunit; -namespace OpenTelemetry.Shims.OpenTracing.Tests -{ - public class ScopeManagerShimTests - { - private const string SpanName = "MySpanName/1"; - private const string TracerName = "defaultactivitysource"; - - static ScopeManagerShimTests() - { - Activity.DefaultIdFormat = ActivityIdFormat.W3C; - Activity.ForceDefaultIdFormat = true; +namespace OpenTelemetry.Shims.OpenTracing.Tests; - var listener = new ActivityListener - { - ShouldListenTo = _ => true, - Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, - }; - - ActivitySource.AddActivityListener(listener); - } +[Collection(nameof(ListenAndSampleAllActivitySources))] +public class ScopeManagerShimTests +{ + private const string SpanName = "MySpanName/1"; + private const string TracerName = "defaultactivitysource"; - [Fact] - public void CtorArgumentValidation() - { - Assert.Throws(() => new ScopeManagerShim(null)); - } + [Fact] + public void Active_IsNull() + { + var shim = new ScopeManagerShim(); - [Fact] - public void Active_IsNull() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new ScopeManagerShim(tracer); + Assert.Null(Activity.Current); + Assert.Null(shim.Active); + } - Assert.Null(Activity.Current); - Assert.Null(shim.Active); - } + [Fact] + public void Active_IsNotNull() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new ScopeManagerShim(); + var openTracingSpan = new SpanShim(tracer.StartSpan(SpanName)); - [Fact] - public void Active_IsNotNull() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new ScopeManagerShim(tracer); - var openTracingSpan = new SpanShim(tracer.StartSpan(SpanName)); + var scope = shim.Activate(openTracingSpan, true); + Assert.NotNull(scope); - var scope = shim.Activate(openTracingSpan, true); - Assert.NotNull(scope); + var activeScope = shim.Active; + Assert.Equal(scope.Span.Context.SpanId, activeScope.Span.Context.SpanId); + openTracingSpan.Finish(); + } - var activeScope = shim.Active; - Assert.Equal(scope.Span.Context.SpanId, activeScope.Span.Context.SpanId); - openTracingSpan.Finish(); - } + [Fact] + public void Activate_SpanMustBeShim() + { + var shim = new ScopeManagerShim(); - [Fact] - public void Activate_SpanMustBeShim() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new ScopeManagerShim(tracer); + Assert.Throws(() => shim.Activate(new TestSpan(), true)); + } - Assert.Throws(() => shim.Activate(new Mock().Object, true)); - } + [Fact] + public void Activate() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new ScopeManagerShim(); + var spanShim = new SpanShim(tracer.StartSpan(SpanName)); - [Fact] - public void Activate() + using (shim.Activate(spanShim, true)) { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new ScopeManagerShim(tracer); - var spanShim = new SpanShim(tracer.StartSpan(SpanName)); - - using (shim.Activate(spanShim, true)) - { #if DEBUG - Assert.Equal(1, shim.SpanScopeTableCount); + Assert.Equal(1, shim.SpanScopeTableCount); #endif - } + } #if DEBUG - Assert.Equal(0, shim.SpanScopeTableCount); + Assert.Equal(0, shim.SpanScopeTableCount); #endif - spanShim.Finish(); - Assert.NotEqual(default, spanShim.Span.Activity.Duration); - } + spanShim.Finish(); + Assert.NotEqual(default, spanShim.Span.Activity.Duration); } } diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanBuilderShimTests.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanBuilderShimTests.cs index 16e35fb6fd4..ce546c868bf 100644 --- a/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanBuilderShimTests.cs +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanBuilderShimTests.cs @@ -1,355 +1,328 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Trace; using Xunit; -namespace OpenTelemetry.Shims.OpenTracing.Tests +namespace OpenTelemetry.Shims.OpenTracing.Tests; + +[Collection(nameof(ListenAndSampleAllActivitySources))] +public class SpanBuilderShimTests { - public class SpanBuilderShimTests + private const string SpanName1 = "MySpanName/1"; + private const string SpanName2 = "MySpanName/2"; + private const string TracerName = "defaultactivitysource"; + + [Fact] + public void CtorArgumentValidation() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + Assert.Throws(() => new SpanBuilderShim(null, "foo")); + Assert.Throws(() => new SpanBuilderShim(tracer, null)); + } + + [Fact] + public void IgnoreActiveSpan() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanBuilderShim(tracer, "foo"); + + // Add a parent. The shim requires that the ISpan implementation be a SpanShim + shim.AsChildOf(new SpanShim(tracer.StartSpan(SpanName1))); + + // Set to Ignore + shim.IgnoreActiveSpan(); + + // build + var spanShim = (SpanShim)shim.Start(); + + Assert.Equal("foo", spanShim.Span.Activity.OperationName); + } + + [Fact] + public void StartWithExplicitTimestamp() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanBuilderShim(tracer, "foo"); + + var startTimestamp = DateTimeOffset.Now; + shim.WithStartTimestamp(startTimestamp); + + // build + var spanShim = (SpanShim)shim.Start(); + + Assert.Equal(startTimestamp, spanShim.Span.Activity.StartTimeUtc); + } + + [Fact] + public void AsChildOf_WithNullSpan() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanBuilderShim(tracer, "foo"); + + // Add a null parent + shim.AsChildOf((global::OpenTracing.ISpan)null); + + // build + var spanShim = (SpanShim)shim.Start(); + + Assert.Equal("foo", spanShim.Span.Activity.OperationName); + Assert.Null(spanShim.Span.Activity.Parent); + } + + [Fact] + public void AsChildOf_WithSpan() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanBuilderShim(tracer, "foo"); + + // Add a parent. + var span = new SpanShim(tracer.StartSpan(SpanName1)); + shim.AsChildOf(span); + + // build + var spanShim = (SpanShim)shim.Start(); + + Assert.Equal("foo", spanShim.Span.Activity.OperationName); + Assert.NotNull(spanShim.Span.Activity.ParentId); + } + + [Fact] + public void Start_ActivityOperationRootSpanChecks() + { + // Create an activity + using var activity = new Activity("foo") + .SetIdFormat(ActivityIdFormat.W3C) + .Start(); + + // matching root operation name + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanBuilderShim(tracer, "foo"); + var spanShim1 = (SpanShim)shim.Start(); + + Assert.Equal("foo", spanShim1.Span.Activity.OperationName); + + // mis-matched root operation name + shim = new SpanBuilderShim(tracer, "foo"); + var spanShim2 = (SpanShim)shim.Start(); + + Assert.Equal("foo", spanShim2.Span.Activity.OperationName); + Assert.Equal(spanShim1.Context.TraceId, spanShim2.Context.TraceId); + } + + [Fact] + public void AsChildOf_MultipleCallsWithSpan() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanBuilderShim(tracer, "foo"); + + // Multiple calls + var span1 = new SpanShim(tracer.StartSpan(SpanName1)); + var span2 = new SpanShim(tracer.StartSpan(SpanName2)); + shim.AsChildOf(span1); + shim.AsChildOf(span2); + + // build + var spanShim = (SpanShim)shim.Start(); + + Assert.Equal("foo", spanShim.Span.Activity.OperationName); + Assert.Contains(spanShim.Context.TraceId, spanShim.Span.Activity.TraceId.ToHexString()); + + // TODO: Check for multi level parenting + } + + [Fact] + public void AsChildOf_WithNullSpanContext() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanBuilderShim(tracer, "foo"); + + // Add a null parent + shim.AsChildOf((global::OpenTracing.ISpanContext)null); + + // build + var spanShim = (SpanShim)shim.Start(); + + // should be no parent. + Assert.Null(spanShim.Span.Activity.Parent); + } + + [Fact] + public void AsChildOfWithSpanContext() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanBuilderShim(tracer, "foo"); + + // Add a parent + var spanContext = SpanContextShimTests.GetSpanContextShim(); + _ = shim.AsChildOf(spanContext); + + // build + var spanShim = (SpanShim)shim.Start(); + + Assert.NotNull(spanShim.Span.Activity.ParentId); + } + + [Fact] + public void AsChildOf_MultipleCallsWithSpanContext() { - private const string SpanName1 = "MySpanName/1"; - private const string SpanName2 = "MySpanName/2"; - private const string TracerName = "defaultactivitysource"; - - static SpanBuilderShimTests() - { - Activity.DefaultIdFormat = ActivityIdFormat.W3C; - Activity.ForceDefaultIdFormat = true; - - var listener = new ActivityListener - { - ShouldListenTo = _ => true, - Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, - }; - - ActivitySource.AddActivityListener(listener); - } - - [Fact] - public void CtorArgumentValidation() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - Assert.Throws(() => new SpanBuilderShim(null, "foo")); - Assert.Throws(() => new SpanBuilderShim(tracer, null)); - } - - [Fact] - public void IgnoreActiveSpan() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanBuilderShim(tracer, "foo"); - - // Add a parent. The shim requires that the ISpan implementation be a SpanShim - shim.AsChildOf(new SpanShim(tracer.StartSpan(SpanName1))); - - // Set to Ignore - shim.IgnoreActiveSpan(); - - // build - var spanShim = (SpanShim)shim.Start(); - - Assert.Equal("foo", spanShim.Span.Activity.OperationName); - } - - [Fact] - public void StartWithExplicitTimestamp() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanBuilderShim(tracer, "foo"); - - var startTimestamp = DateTimeOffset.Now; - shim.WithStartTimestamp(startTimestamp); - - // build - var spanShim = (SpanShim)shim.Start(); - - Assert.Equal(startTimestamp, spanShim.Span.Activity.StartTimeUtc); - } - - [Fact] - public void AsChildOf_WithNullSpan() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanBuilderShim(tracer, "foo"); - - // Add a null parent - shim.AsChildOf((global::OpenTracing.ISpan)null); - - // build - var spanShim = (SpanShim)shim.Start(); - - Assert.Equal("foo", spanShim.Span.Activity.OperationName); - Assert.Null(spanShim.Span.Activity.Parent); - } - - [Fact] - public void AsChildOf_WithSpan() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanBuilderShim(tracer, "foo"); - - // Add a parent. - var span = new SpanShim(tracer.StartSpan(SpanName1)); - shim.AsChildOf(span); - - // build - var spanShim = (SpanShim)shim.Start(); - - Assert.Equal("foo", spanShim.Span.Activity.OperationName); - Assert.NotNull(spanShim.Span.Activity.ParentId); - } - - [Fact] - public void Start_ActivityOperationRootSpanChecks() - { - // Create an activity - using var activity = new Activity("foo") - .SetIdFormat(ActivityIdFormat.W3C) - .Start(); - - // matching root operation name - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanBuilderShim(tracer, "foo"); - var spanShim1 = (SpanShim)shim.Start(); - - Assert.Equal("foo", spanShim1.Span.Activity.OperationName); - - // mis-matched root operation name - shim = new SpanBuilderShim(tracer, "foo"); - var spanShim2 = (SpanShim)shim.Start(); - - Assert.Equal("foo", spanShim2.Span.Activity.OperationName); - Assert.Equal(spanShim1.Context.TraceId, spanShim2.Context.TraceId); - } - - [Fact] - public void AsChildOf_MultipleCallsWithSpan() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanBuilderShim(tracer, "foo"); - - // Multiple calls - var span1 = new SpanShim(tracer.StartSpan(SpanName1)); - var span2 = new SpanShim(tracer.StartSpan(SpanName2)); - shim.AsChildOf(span1); - shim.AsChildOf(span2); - - // build - var spanShim = (SpanShim)shim.Start(); + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanBuilderShim(tracer, "foo"); - Assert.Equal("foo", spanShim.Span.Activity.OperationName); - Assert.Contains(spanShim.Context.TraceId, spanShim.Span.Activity.TraceId.ToHexString()); + // Multiple calls + var spanContext1 = SpanContextShimTests.GetSpanContextShim(); + var spanContext2 = SpanContextShimTests.GetSpanContextShim(); - // TODO: Check for multi level parenting - } + // Add parent context + shim.AsChildOf(spanContext1); + + // Adds as link as parent context already exists + shim.AsChildOf(spanContext2); + + // build + var spanShim = (SpanShim)shim.Start(); + + Assert.Equal("foo", spanShim.Span.Activity.OperationName); + Assert.Contains(spanContext1.TraceId, spanShim.Span.Activity.ParentId); + Assert.Equal(spanContext2.SpanId, spanShim.Span.Activity.Links.First().Context.SpanId.ToHexString()); + } + + [Fact] + public void WithTag_KeyIsSpanKindStringValue() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanBuilderShim(tracer, "foo"); + + shim.WithTag(global::OpenTracing.Tag.Tags.SpanKind.Key, global::OpenTracing.Tag.Tags.SpanKindClient); + + // build + var spanShim = (SpanShim)shim.Start(); + + // Not an attribute + Assert.Empty(spanShim.Span.Activity.Tags); + Assert.Equal("foo", spanShim.Span.Activity.OperationName); + Assert.Equal(ActivityKind.Client, spanShim.Span.Activity.Kind); + } + + [Fact] + public void WithTag_KeyIsErrorStringValue() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanBuilderShim(tracer, "foo"); + + shim.WithTag(global::OpenTracing.Tag.Tags.Error.Key, "true"); + + // build + var spanShim = (SpanShim)shim.Start(); + + // Span status should be set + Assert.Equal(Status.Error, spanShim.Span.Activity.GetStatus()); + } + + [Fact] + public void WithTag_KeyIsNullStringValue() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanBuilderShim(tracer, "foo"); - [Fact] - public void AsChildOf_WithNullSpanContext() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanBuilderShim(tracer, "foo"); - - // Add a null parent - shim.AsChildOf((global::OpenTracing.ISpanContext)null); - - // build - var spanShim = (SpanShim)shim.Start(); + shim.WithTag((string)null, "unused"); - // should be no parent. - Assert.Null(spanShim.Span.Activity.Parent); - } + // build + var spanShim = (SpanShim)shim.Start(); + + // Null key was ignored + Assert.Empty(spanShim.Span.Activity.Tags); + } + + [Fact] + public void WithTag_ValueIsNullStringValue() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanBuilderShim(tracer, "foo"); + + shim.WithTag("foo", null); + + // build + var spanShim = (SpanShim)shim.Start(); + + // Null value was turned into string.empty + Assert.Equal("foo", spanShim.Span.Activity.Tags.First().Key); + Assert.Equal(string.Empty, spanShim.Span.Activity.Tags.First().Value); + } + + [Fact] + public void WithTag_KeyIsErrorBoolValue() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanBuilderShim(tracer, "foo"); + + shim.WithTag(global::OpenTracing.Tag.Tags.Error.Key, true); + + // build + var spanShim = (SpanShim)shim.Start(); + + // Span status should be set + Assert.Equal(Status.Error, spanShim.Span.Activity.GetStatus()); + } + + [Fact] + public void WithTag_VariousValueTypes() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanBuilderShim(tracer, "foo"); + + shim.WithTag("foo", "unused"); + shim.WithTag("bar", false); + shim.WithTag("baz", 1); + shim.WithTag("bizzle", 1D); + shim.WithTag(new global::OpenTracing.Tag.BooleanTag("shnizzle"), true); + shim.WithTag(new global::OpenTracing.Tag.IntOrStringTag("febrizzle"), "unused"); + shim.WithTag(new global::OpenTracing.Tag.StringTag("mobizzle"), "unused"); + + // build + var spanShim = (SpanShim)shim.Start(); + + // Just verify the count + Assert.Equal(7, spanShim.Span.Activity.Tags.Count()); + } + + [Fact] + public void Start() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanBuilderShim(tracer, "foo"); + + // build + var span = shim.Start() as SpanShim; + + // Just check the return value is a SpanShim and that the underlying OpenTelemetry Span. + // There is nothing left to verify because the rest of the tests were already calling .Start() prior to verification. + Assert.NotNull(span); + Assert.Equal("foo", span.Span.Activity.OperationName); + } + + [Fact] + public void Start_UnderAspNetCoreInstrumentation() + { + // Simulate a span from AspNetCore instrumentation as parent. + using var source = new ActivitySource("Microsoft.AspNetCore.Hosting.HttpRequestIn"); + using var parentSpan = source.StartActivity("OTelParent"); + Assert.NotNull(parentSpan); - [Fact] - public void AsChildOfWithSpanContext() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanBuilderShim(tracer, "foo"); - - // Add a parent - var spanContext = SpanContextShimTests.GetSpanContextShim(); - _ = shim.AsChildOf(spanContext); - - // build - var spanShim = (SpanShim)shim.Start(); + // Start the OpenTracing span. + var tracer = TracerProvider.Default.GetTracer(TracerName); + var builderShim = new SpanBuilderShim(tracer, "foo"); + var spanShim = builderShim.StartActive().Span as SpanShim; + Assert.NotNull(spanShim); - Assert.NotNull(spanShim.Span.Activity.ParentId); - } + var telemetrySpan = spanShim.Span; + Assert.Same(telemetrySpan.Activity, Activity.Current); + Assert.Same(parentSpan, telemetrySpan.Activity.Parent); - [Fact] - public void AsChildOf_MultipleCallsWithSpanContext() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanBuilderShim(tracer, "foo"); - - // Multiple calls - var spanContext1 = SpanContextShimTests.GetSpanContextShim(); - var spanContext2 = SpanContextShimTests.GetSpanContextShim(); - - // Add parent context - shim.AsChildOf(spanContext1); - - // Adds as link as parent context already exists - shim.AsChildOf(spanContext2); - - // build - var spanShim = (SpanShim)shim.Start(); - - Assert.Equal("foo", spanShim.Span.Activity.OperationName); - Assert.Contains(spanContext1.TraceId, spanShim.Span.Activity.ParentId); - Assert.Equal(spanContext2.SpanId, spanShim.Span.Activity.Links.First().Context.SpanId.ToHexString()); - } - - [Fact] - public void WithTag_KeyIsSpanKindStringValue() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanBuilderShim(tracer, "foo"); - - shim.WithTag(global::OpenTracing.Tag.Tags.SpanKind.Key, global::OpenTracing.Tag.Tags.SpanKindClient); + // Dispose the spanShim.Span and ensure correct state for Activity.Current + spanShim.Span.Dispose(); - // build - var spanShim = (SpanShim)shim.Start(); - - // Not an attribute - Assert.Empty(spanShim.Span.Activity.Tags); - Assert.Equal("foo", spanShim.Span.Activity.OperationName); - Assert.Equal(ActivityKind.Client, spanShim.Span.Activity.Kind); - } - - [Fact] - public void WithTag_KeyIsErrorStringValue() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanBuilderShim(tracer, "foo"); - - shim.WithTag(global::OpenTracing.Tag.Tags.Error.Key, "true"); - - // build - var spanShim = (SpanShim)shim.Start(); - - // Span status should be set - Assert.Equal(Status.Error, spanShim.Span.Activity.GetStatus()); - } - - [Fact] - public void WithTag_KeyIsNullStringValue() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanBuilderShim(tracer, "foo"); - - shim.WithTag((string)null, "unused"); - - // build - var spanShim = (SpanShim)shim.Start(); - - // Null key was ignored - Assert.Empty(spanShim.Span.Activity.Tags); - } - - [Fact] - public void WithTag_ValueIsNullStringValue() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanBuilderShim(tracer, "foo"); - - shim.WithTag("foo", null); - - // build - var spanShim = (SpanShim)shim.Start(); - - // Null value was turned into string.empty - Assert.Equal("foo", spanShim.Span.Activity.Tags.First().Key); - Assert.Equal(string.Empty, spanShim.Span.Activity.Tags.First().Value); - } - - [Fact] - public void WithTag_KeyIsErrorBoolValue() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanBuilderShim(tracer, "foo"); - - shim.WithTag(global::OpenTracing.Tag.Tags.Error.Key, true); - - // build - var spanShim = (SpanShim)shim.Start(); - - // Span status should be set - Assert.Equal(Status.Error, spanShim.Span.Activity.GetStatus()); - } - - [Fact] - public void WithTag_VariousValueTypes() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanBuilderShim(tracer, "foo"); - - shim.WithTag("foo", "unused"); - shim.WithTag("bar", false); - shim.WithTag("baz", 1); - shim.WithTag("bizzle", 1D); - shim.WithTag(new global::OpenTracing.Tag.BooleanTag("shnizzle"), true); - shim.WithTag(new global::OpenTracing.Tag.IntOrStringTag("febrizzle"), "unused"); - shim.WithTag(new global::OpenTracing.Tag.StringTag("mobizzle"), "unused"); - - // build - var spanShim = (SpanShim)shim.Start(); - - // Just verify the count - Assert.Equal(7, spanShim.Span.Activity.Tags.Count()); - } - - [Fact] - public void Start() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanBuilderShim(tracer, "foo"); - - // build - var span = shim.Start() as SpanShim; - - // Just check the return value is a SpanShim and that the underlying OpenTelemetry Span. - // There is nothing left to verify because the rest of the tests were already calling .Start() prior to verification. - Assert.NotNull(span); - Assert.Equal("foo", span.Span.Activity.OperationName); - } - - [Fact] - public void Start_UnderAspNetCoreInstrumentation() - { - // Simulate a span from AspNetCore instrumentation as parent. - using var source = new ActivitySource("Microsoft.AspNetCore.Hosting.HttpRequestIn"); - using var parentSpan = source.StartActivity("OTelParent"); - Assert.NotNull(parentSpan); - - // Start the OpenTracing span. - var tracer = TracerProvider.Default.GetTracer(TracerName); - var builderShim = new SpanBuilderShim(tracer, "foo"); - var spanShim = builderShim.StartActive().Span as SpanShim; - Assert.NotNull(spanShim); - - var telemetrySpan = spanShim.Span; - Assert.Same(telemetrySpan.Activity, Activity.Current); - Assert.Same(parentSpan, telemetrySpan.Activity.Parent); - - // Dispose the spanShim.Span and ensure correct state for Activity.Current - spanShim.Span.Dispose(); - - Assert.Same(parentSpan, Activity.Current); - } + Assert.Same(parentSpan, Activity.Current); } } diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanContextShimTests.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanContextShimTests.cs index 8b881938e00..4bd5896a69f 100644 --- a/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanContextShimTests.cs +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanContextShimTests.cs @@ -1,61 +1,40 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Trace; using Xunit; -namespace OpenTelemetry.Shims.OpenTracing.Tests +namespace OpenTelemetry.Shims.OpenTracing.Tests; + +public class SpanContextShimTests { - public class SpanContextShimTests + [Fact] + public void GetTraceId() + { + var shim = GetSpanContextShim(); + + Assert.Equal(shim.TraceId.ToString(), shim.TraceId); + } + + [Fact] + public void GetSpanId() + { + var shim = GetSpanContextShim(); + + Assert.Equal(shim.SpanId.ToString(), shim.SpanId); + } + + [Fact] + public void GetBaggage() + { + var shim = GetSpanContextShim(); + var baggage = shim.GetBaggageItems(); + Assert.Empty(baggage); + } + + internal static SpanContextShim GetSpanContextShim() { - [Fact] - public void CtorArgumentValidation() - { - Assert.Throws(() => new SpanContextShim(default)); - Assert.Throws(() => new SpanContextShim(new SpanContext(default, default, ActivityTraceFlags.None))); - } - - [Fact] - public void GetTraceId() - { - var shim = GetSpanContextShim(); - - Assert.Equal(shim.TraceId.ToString(), shim.TraceId); - } - - [Fact] - public void GetSpanId() - { - var shim = GetSpanContextShim(); - - Assert.Equal(shim.SpanId.ToString(), shim.SpanId); - } - - [Fact] - public void GetBaggage() - { - var shim = GetSpanContextShim(); - var baggage = shim.GetBaggageItems(); - Assert.Empty(baggage); - } - - internal static SpanContextShim GetSpanContextShim() - { - return new SpanContextShim(new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None)); - } + return new SpanContextShim(new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None)); } } diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanShimTests.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanShimTests.cs index 1f4241bcad9..80178f405be 100644 --- a/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanShimTests.cs +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanShimTests.cs @@ -1,338 +1,318 @@ -// // 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; +// SPDX-License-Identifier: Apache-2.0 + using OpenTelemetry.Trace; using OpenTracing.Tag; using Xunit; -namespace OpenTelemetry.Shims.OpenTracing.Tests +namespace OpenTelemetry.Shims.OpenTracing.Tests; + +[Collection(nameof(ListenAndSampleAllActivitySources))] +public class SpanShimTests { - public class SpanShimTests - { - private const string SpanName = "MySpanName/1"; - private const string TracerName = "defaultactivitysource"; + private const string SpanName = "MySpanName/1"; + private const string TracerName = "defaultactivitysource"; - static SpanShimTests() - { - Activity.DefaultIdFormat = ActivityIdFormat.W3C; - Activity.ForceDefaultIdFormat = true; + [Fact] + public void CtorArgumentValidation() + { + Assert.Throws(() => new SpanShim(null)); + } - var listener = new ActivityListener - { - ShouldListenTo = _ => true, - Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, - }; + [Fact] + public void SpanContextIsNotNull() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanShim(tracer.StartSpan(SpanName)); - ActivitySource.AddActivityListener(listener); - } + // ISpanContext validation handled in a separate test class + Assert.NotNull(shim.Context); + } - [Fact] - public void CtorArgumentValidation() - { - Assert.Throws(() => new SpanShim(null)); - } + [Fact] + public void FinishSpan() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanShim(tracer.StartSpan(SpanName)); - [Fact] - public void SpanContextIsNotNull() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanShim(tracer.StartSpan(SpanName)); + shim.Finish(); - // ISpanContext validation handled in a separate test class - Assert.NotNull(shim.Context); - } + Assert.NotEqual(default, shim.Span.Activity.Duration); + } - [Fact] - public void FinishSpan() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanShim(tracer.StartSpan(SpanName)); + [Fact] + public void FinishSpanUsingSpecificTimestamp() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanShim(tracer.StartSpan(SpanName)); - shim.Finish(); +#if NETFRAMEWORK + // Under the hood the Activity start time uses DateTime.UtcNow, which + // doesn't have the same precision as DateTimeOffset.UtcNow on the .NET Framework. + // Add a sleep big enough to ensure that the test doesn't break due to the + // low resolution of DateTime.UtcNow on the .NET Framework. + Thread.Sleep(TimeSpan.FromMilliseconds(20)); +#endif - Assert.NotEqual(default, shim.Span.Activity.Duration); - } + var endTime = DateTimeOffset.UtcNow; + shim.Finish(endTime); - [Fact] - public void FinishSpanUsingSpecificTimestamp() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanShim(tracer.StartSpan(SpanName)); + Assert.Equal(endTime - shim.Span.Activity.StartTimeUtc, shim.Span.Activity.Duration); + } - var endTime = DateTimeOffset.UtcNow; - shim.Finish(endTime); + [Fact] + public void SetOperationName() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanShim(tracer.StartSpan(SpanName)); - Assert.Equal(endTime - shim.Span.Activity.StartTimeUtc, shim.Span.Activity.Duration); - } + // parameter validation + Assert.Throws(() => shim.SetOperationName(null)); - [Fact] - public void SetOperationName() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanShim(tracer.StartSpan(SpanName)); + shim.SetOperationName("bar"); + Assert.Equal("bar", shim.Span.Activity.DisplayName); + } - // parameter validation - Assert.Throws(() => shim.SetOperationName(null)); + [Fact] + public void GetBaggageItem() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanShim(tracer.StartSpan(SpanName)); - shim.SetOperationName("bar"); - Assert.Equal("bar", shim.Span.Activity.DisplayName); - } + // parameter validation + Assert.Throws(() => shim.GetBaggageItem(null)); - [Fact] - public void GetBaggageItem() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanShim(tracer.StartSpan(SpanName)); + // TODO - Method not implemented + } - // parameter validation - Assert.Throws(() => shim.GetBaggageItem(null)); + [Fact] + public void Log() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanShim(tracer.StartSpan(SpanName)); - // TODO - Method not implemented - } + shim.Log("foo"); - [Fact] - public void Log() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanShim(tracer.StartSpan(SpanName)); + Assert.Single(shim.Span.Activity.Events); + var first = shim.Span.Activity.Events.First(); + Assert.Equal("foo", first.Name); + Assert.False(first.Tags.Any()); + } - shim.Log("foo"); + [Fact] + public void LogWithExplicitTimestamp() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanShim(tracer.StartSpan(SpanName)); - Assert.Single(shim.Span.Activity.Events); - var first = shim.Span.Activity.Events.First(); - Assert.Equal("foo", first.Name); - Assert.False(first.Tags.Any()); - } + var now = DateTimeOffset.UtcNow; + shim.Log(now, "foo"); - [Fact] - public void LogWithExplicitTimestamp() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanShim(tracer.StartSpan(SpanName)); + Assert.Single(shim.Span.Activity.Events); + var first = shim.Span.Activity.Events.First(); + Assert.Equal("foo", first.Name); + Assert.Equal(now, first.Timestamp); + Assert.False(first.Tags.Any()); + } - var now = DateTimeOffset.UtcNow; - shim.Log(now, "foo"); + [Fact] + public void LogUsingFields() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanShim(tracer.StartSpan(SpanName)); - Assert.Single(shim.Span.Activity.Events); - var first = shim.Span.Activity.Events.First(); - Assert.Equal("foo", first.Name); - Assert.Equal(now, first.Timestamp); - Assert.False(first.Tags.Any()); - } + Assert.Throws(() => shim.Log((IEnumerable>)null)); - [Fact] - public void LogUsingFields() + shim.Log(new List> { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanShim(tracer.StartSpan(SpanName)); + new KeyValuePair("foo", "bar"), + }); - Assert.Throws(() => shim.Log((IEnumerable>)null)); + // "event" is a special event name + shim.Log(new List> + { + new KeyValuePair("event", "foo"), + }); - shim.Log(new List> - { - new KeyValuePair("foo", "bar"), - }); + var first = shim.Span.Activity.Events.FirstOrDefault(); + var last = shim.Span.Activity.Events.LastOrDefault(); - // "event" is a special event name - shim.Log(new List> - { - new KeyValuePair("event", "foo"), - }); + Assert.Equal(2, shim.Span.Activity.Events.Count()); - var first = shim.Span.Activity.Events.FirstOrDefault(); - var last = shim.Span.Activity.Events.LastOrDefault(); + Assert.Equal(SpanShim.DefaultEventName, first.Name); + Assert.True(first.Tags.Any()); - Assert.Equal(2, shim.Span.Activity.Events.Count()); + Assert.Equal("foo", last.Name); + Assert.False(last.Tags.Any()); + } - Assert.Equal(SpanShim.DefaultEventName, first.Name); - Assert.True(first.Tags.Any()); + [Fact] + public void LogUsingFieldsWithExplicitTimestamp() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanShim(tracer.StartSpan(SpanName)); - Assert.Equal("foo", last.Name); - Assert.False(last.Tags.Any()); - } + Assert.Throws(() => shim.Log((IEnumerable>)null)); + var now = DateTimeOffset.UtcNow; - [Fact] - public void LogUsingFieldsWithExplicitTimestamp() + shim.Log(now, new List> { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanShim(tracer.StartSpan(SpanName)); - - Assert.Throws(() => shim.Log((IEnumerable>)null)); - var now = DateTimeOffset.UtcNow; - - shim.Log(now, new List> - { - new KeyValuePair("foo", "bar"), - }); - - // "event" is a special event name - shim.Log(now, new List> - { - new KeyValuePair("event", "foo"), - }); - - Assert.Equal(2, shim.Span.Activity.Events.Count()); - var first = shim.Span.Activity.Events.First(); - var last = shim.Span.Activity.Events.Last(); - - Assert.Equal(SpanShim.DefaultEventName, first.Name); - Assert.True(first.Tags.Any()); - Assert.Equal(now, first.Timestamp); - - Assert.Equal("foo", last.Name); - Assert.False(last.Tags.Any()); - Assert.Equal(now, last.Timestamp); - } - - [Fact] - public void SetTagStringValue() + new KeyValuePair("foo", "bar"), + }); + + // "event" is a special event name + shim.Log(now, new List> { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanShim(tracer.StartSpan(SpanName)); + new KeyValuePair("event", "foo"), + }); - Assert.Throws(() => shim.SetTag((string)null, "foo")); + Assert.Equal(2, shim.Span.Activity.Events.Count()); + var first = shim.Span.Activity.Events.First(); + var last = shim.Span.Activity.Events.Last(); - shim.SetTag("foo", "bar"); + Assert.Equal(SpanShim.DefaultEventName, first.Name); + Assert.True(first.Tags.Any()); + Assert.Equal(now, first.Timestamp); - Assert.Single(shim.Span.Activity.Tags); - Assert.Equal("foo", shim.Span.Activity.Tags.First().Key); - Assert.Equal("bar", shim.Span.Activity.Tags.First().Value); - } + Assert.Equal("foo", last.Name); + Assert.False(last.Tags.Any()); + Assert.Equal(now, last.Timestamp); + } - [Fact] - public void SetTagBoolValue() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanShim(tracer.StartSpan(SpanName)); + [Fact] + public void SetTagStringValue() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanShim(tracer.StartSpan(SpanName)); - Assert.Throws(() => shim.SetTag((string)null, true)); + Assert.Throws(() => shim.SetTag((string)null, "foo")); - shim.SetTag("foo", true); - shim.SetTag(Tags.Error.Key, true); + shim.SetTag("foo", "bar"); - Assert.Equal("foo", shim.Span.Activity.TagObjects.First().Key); - Assert.True((bool)shim.Span.Activity.TagObjects.First().Value); + Assert.Single(shim.Span.Activity.Tags); + Assert.Equal("foo", shim.Span.Activity.Tags.First().Key); + Assert.Equal("bar", shim.Span.Activity.Tags.First().Value); + } - // A boolean tag named "error" is a special case that must be checked - Assert.Equal(Status.Error, shim.Span.Activity.GetStatus()); + [Fact] + public void SetTagBoolValue() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanShim(tracer.StartSpan(SpanName)); - shim.SetTag(Tags.Error.Key, false); - Assert.Equal(Status.Ok, shim.Span.Activity.GetStatus()); - } + Assert.Throws(() => shim.SetTag((string)null, true)); - [Fact] - public void SetTagIntValue() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanShim(tracer.StartSpan(SpanName)); + shim.SetTag("foo", true); + shim.SetTag(Tags.Error.Key, true); - Assert.Throws(() => shim.SetTag((string)null, 1)); + Assert.Equal("foo", shim.Span.Activity.TagObjects.First().Key); + Assert.True((bool)shim.Span.Activity.TagObjects.First().Value); - shim.SetTag("foo", 1); + // A boolean tag named "error" is a special case that must be checked + Assert.Equal(Status.Error, shim.Span.Activity.GetStatus()); - Assert.Single(shim.Span.Activity.TagObjects); - Assert.Equal("foo", shim.Span.Activity.TagObjects.First().Key); - Assert.Equal(1L, (int)shim.Span.Activity.TagObjects.First().Value); - } + shim.SetTag(Tags.Error.Key, false); + Assert.Equal(Status.Ok, shim.Span.Activity.GetStatus()); + } - [Fact] - public void SetTagDoubleValue() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanShim(tracer.StartSpan(SpanName)); + [Fact] + public void SetTagIntValue() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanShim(tracer.StartSpan(SpanName)); - Assert.Throws(() => shim.SetTag(null, 1D)); + Assert.Throws(() => shim.SetTag((string)null, 1)); - shim.SetTag("foo", 1D); + shim.SetTag("foo", 1); - Assert.Single(shim.Span.Activity.TagObjects); - Assert.Equal("foo", shim.Span.Activity.TagObjects.First().Key); - Assert.Equal(1, (double)shim.Span.Activity.TagObjects.First().Value); - } + Assert.Single(shim.Span.Activity.TagObjects); + Assert.Equal("foo", shim.Span.Activity.TagObjects.First().Key); + Assert.Equal(1L, (int)shim.Span.Activity.TagObjects.First().Value); + } - [Fact] - public void SetTagBooleanTagValue() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanShim(tracer.StartSpan(SpanName)); + [Fact] + public void SetTagDoubleValue() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanShim(tracer.StartSpan(SpanName)); - Assert.Throws(() => shim.SetTag((BooleanTag)null, true)); + Assert.Throws(() => shim.SetTag(null, 1D)); - shim.SetTag(new BooleanTag("foo"), true); - shim.SetTag(new BooleanTag(Tags.Error.Key), true); + shim.SetTag("foo", 1D); - Assert.Equal("foo", shim.Span.Activity.TagObjects.First().Key); - Assert.True((bool)shim.Span.Activity.TagObjects.First().Value); + Assert.Single(shim.Span.Activity.TagObjects); + Assert.Equal("foo", shim.Span.Activity.TagObjects.First().Key); + Assert.Equal(1, (double)shim.Span.Activity.TagObjects.First().Value); + } - // A boolean tag named "error" is a special case that must be checked - Assert.Equal(Status.Error, shim.Span.Activity.GetStatus()); + [Fact] + public void SetTagBooleanTagValue() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanShim(tracer.StartSpan(SpanName)); - shim.SetTag(Tags.Error.Key, false); - Assert.Equal(Status.Ok, shim.Span.Activity.GetStatus()); - } + Assert.Throws(() => shim.SetTag((BooleanTag)null, true)); - [Fact] - public void SetTagStringTagValue() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanShim(tracer.StartSpan(SpanName)); + shim.SetTag(new BooleanTag("foo"), true); + shim.SetTag(new BooleanTag(Tags.Error.Key), true); - Assert.Throws(() => shim.SetTag((StringTag)null, "foo")); + Assert.Equal("foo", shim.Span.Activity.TagObjects.First().Key); + Assert.True((bool)shim.Span.Activity.TagObjects.First().Value); - shim.SetTag(new StringTag("foo"), "bar"); + // A boolean tag named "error" is a special case that must be checked + Assert.Equal(Status.Error, shim.Span.Activity.GetStatus()); - Assert.Single(shim.Span.Activity.Tags); - Assert.Equal("foo", shim.Span.Activity.Tags.First().Key); - Assert.Equal("bar", shim.Span.Activity.Tags.First().Value); - } + shim.SetTag(Tags.Error.Key, false); + Assert.Equal(Status.Ok, shim.Span.Activity.GetStatus()); + } - [Fact] - public void SetTagIntTagValue() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanShim(tracer.StartSpan(SpanName)); + [Fact] + public void SetTagStringTagValue() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanShim(tracer.StartSpan(SpanName)); - Assert.Throws(() => shim.SetTag((IntTag)null, 1)); + Assert.Throws(() => shim.SetTag((StringTag)null, "foo")); - shim.SetTag(new IntTag("foo"), 1); + shim.SetTag(new StringTag("foo"), "bar"); - Assert.Single(shim.Span.Activity.TagObjects); - Assert.Equal("foo", shim.Span.Activity.TagObjects.First().Key); - Assert.Equal(1L, (int)shim.Span.Activity.TagObjects.First().Value); - } + Assert.Single(shim.Span.Activity.Tags); + Assert.Equal("foo", shim.Span.Activity.Tags.First().Key); + Assert.Equal("bar", shim.Span.Activity.Tags.First().Value); + } - [Fact] - public void SetTagIntOrStringTagValue() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanShim(tracer.StartSpan(SpanName)); + [Fact] + public void SetTagIntTagValue() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanShim(tracer.StartSpan(SpanName)); + + Assert.Throws(() => shim.SetTag((IntTag)null, 1)); + + shim.SetTag(new IntTag("foo"), 1); + + Assert.Single(shim.Span.Activity.TagObjects); + Assert.Equal("foo", shim.Span.Activity.TagObjects.First().Key); + Assert.Equal(1L, (int)shim.Span.Activity.TagObjects.First().Value); + } + + [Fact] + public void SetTagIntOrStringTagValue() + { + var tracer = TracerProvider.Default.GetTracer(TracerName); + var shim = new SpanShim(tracer.StartSpan(SpanName)); - Assert.Throws(() => shim.SetTag((IntOrStringTag)null, "foo")); + Assert.Throws(() => shim.SetTag((IntOrStringTag)null, "foo")); - shim.SetTag(new IntOrStringTag("foo"), 1); - shim.SetTag(new IntOrStringTag("bar"), "baz"); + shim.SetTag(new IntOrStringTag("foo"), 1); + shim.SetTag(new IntOrStringTag("bar"), "baz"); - Assert.Equal(2, shim.Span.Activity.TagObjects.Count()); + Assert.Equal(2, shim.Span.Activity.TagObjects.Count()); - Assert.Equal("foo", shim.Span.Activity.TagObjects.First().Key); - Assert.Equal(1L, (int)shim.Span.Activity.TagObjects.First().Value); + Assert.Equal("foo", shim.Span.Activity.TagObjects.First().Key); + Assert.Equal(1L, (int)shim.Span.Activity.TagObjects.First().Value); - Assert.Equal("bar", shim.Span.Activity.TagObjects.Last().Key); - Assert.Equal("baz", shim.Span.Activity.TagObjects.Last().Value); - } + Assert.Equal("bar", shim.Span.Activity.TagObjects.Last().Key); + Assert.Equal("baz", shim.Span.Activity.TagObjects.Last().Value); } } diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/TestFormatTextMap.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/TestFormatTextMap.cs new file mode 100644 index 00000000000..83d04095313 --- /dev/null +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/TestFormatTextMap.cs @@ -0,0 +1,10 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTracing.Propagation; + +namespace OpenTelemetry.Shims.OpenTracing.Tests; + +internal class TestFormatTextMap : IFormat +{ +} diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/TestSpan.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/TestSpan.cs new file mode 100644 index 00000000000..f1e836f76b7 --- /dev/null +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/TestSpan.cs @@ -0,0 +1,97 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTracing; +using OpenTracing.Tag; + +namespace OpenTelemetry.Shims.OpenTracing.Tests; + +internal class TestSpan : ISpan +{ + public ISpanContext Context => throw new NotImplementedException(); + + public void Finish() + { + throw new NotImplementedException(); + } + + public void Finish(DateTimeOffset finishTimestamp) + { + throw new NotImplementedException(); + } + + public string GetBaggageItem(string key) + { + throw new NotImplementedException(); + } + + public ISpan Log(IEnumerable> fields) + { + throw new NotImplementedException(); + } + + public ISpan Log(DateTimeOffset timestamp, IEnumerable> fields) + { + throw new NotImplementedException(); + } + + public ISpan Log(string @event) + { + throw new NotImplementedException(); + } + + public ISpan Log(DateTimeOffset timestamp, string @event) + { + throw new NotImplementedException(); + } + + public ISpan SetBaggageItem(string key, string value) + { + throw new NotImplementedException(); + } + + public ISpan SetOperationName(string operationName) + { + throw new NotImplementedException(); + } + + public ISpan SetTag(string key, string value) + { + throw new NotImplementedException(); + } + + public ISpan SetTag(string key, bool value) + { + throw new NotImplementedException(); + } + + public ISpan SetTag(string key, int value) + { + throw new NotImplementedException(); + } + + public ISpan SetTag(string key, double value) + { + throw new NotImplementedException(); + } + + public ISpan SetTag(BooleanTag tag, bool value) + { + throw new NotImplementedException(); + } + + public ISpan SetTag(IntOrStringTag tag, string value) + { + throw new NotImplementedException(); + } + + public ISpan SetTag(IntTag tag, int value) + { + throw new NotImplementedException(); + } + + public ISpan SetTag(StringTag tag, string value) + { + throw new NotImplementedException(); + } +} diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/TestSpanContext.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/TestSpanContext.cs new file mode 100644 index 00000000000..d0b5af69991 --- /dev/null +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/TestSpanContext.cs @@ -0,0 +1,18 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTracing; + +namespace OpenTelemetry.Shims.OpenTracing.Tests; + +internal class TestSpanContext : ISpanContext +{ + public string TraceId => throw new NotImplementedException(); + + public string SpanId => throw new NotImplementedException(); + + public IEnumerable> GetBaggageItems() + { + throw new NotImplementedException(); + } +} diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/TestTextMap.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/TestTextMap.cs new file mode 100644 index 00000000000..7396b5543e6 --- /dev/null +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/TestTextMap.cs @@ -0,0 +1,34 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections; +using OpenTracing.Propagation; + +namespace OpenTelemetry.Shims.OpenTracing.Tests; + +internal class TestTextMap : ITextMap +{ + public bool GetEnumeratorCalled { get; private set; } + + public bool SetCalled { get; private set; } + +#pragma warning disable IDE0028 // Simplify collection initialization + public Dictionary Items { get; } = new(); +#pragma warning restore IDE0028 // Simplify collection initialization + + public IEnumerator> GetEnumerator() + { + this.GetEnumeratorCalled = true; + return this.Items.GetEnumerator(); + } + + public void Set(string key, string value) + { + this.SetCalled = true; + } + + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } +} diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/TracerShimTests.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/TracerShimTests.cs index 70593032727..578ed8ccbf3 100644 --- a/test/OpenTelemetry.Shims.OpenTracing.Tests/TracerShimTests.cs +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/TracerShimTests.cs @@ -1,215 +1,180 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Collections; using System.Diagnostics; -using Moq; using OpenTelemetry.Context.Propagation; using OpenTelemetry.Trace; using OpenTracing; using OpenTracing.Propagation; using Xunit; -namespace OpenTelemetry.Shims.OpenTracing.Tests +namespace OpenTelemetry.Shims.OpenTracing.Tests; + +public class TracerShimTests { - public class TracerShimTests + [Fact] + public void CtorArgumentValidation() { - private const string TracerName = "defaultactivitysource"; + // null tracer provider and text format + Assert.Throws(() => new TracerShim(null, null)); - [Fact] - public void CtorArgumentValidation() - { - // null tracer and text format - Assert.Throws(() => new TracerShim(null, null)); + // null tracer provider + Assert.Throws(() => new TracerShim(null, new TraceContextPropagator())); + } - // null tracer - Assert.Throws(() => new TracerShim(null, new TraceContextPropagator())); + [Fact] + public void ScopeManager_NotNull() + { + var shim = new TracerShim(TracerProvider.Default, new TraceContextPropagator()); - // null context format - var tracerMock = new Mock(); - Assert.Throws(() => new TracerShim(TracerProvider.Default.GetTracer("test"), null)); - } + // Internals of the ScopeManagerShim tested elsewhere + Assert.NotNull(shim.ScopeManager as ScopeManagerShim); + } - [Fact] - public void ScopeManager_NotNull() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new TracerShim(tracer, new TraceContextPropagator()); + [Fact] + public void BuildSpan_NotNull() + { + var shim = new TracerShim(TracerProvider.Default, new TraceContextPropagator()); - // Internals of the ScopeManagerShim tested elsewhere - Assert.NotNull(shim.ScopeManager as ScopeManagerShim); - } + // Internals of the SpanBuilderShim tested elsewhere + Assert.NotNull(shim.BuildSpan("foo") as SpanBuilderShim); + } - [Fact] - public void BuildSpan_NotNull() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new TracerShim(tracer, new TraceContextPropagator()); + [Fact] + public void Inject_ArgumentValidation() + { + var shim = new TracerShim(TracerProvider.Default, new TraceContextPropagator()); - // Internals of the SpanBuilderShim tested elsewhere - Assert.NotNull(shim.BuildSpan("foo") as SpanBuilderShim); - } + var spanContextShim = new SpanContextShim(new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None)); + var testFormat = new TestFormatTextMap(); + var testCarrier = new TestTextMap(); - [Fact] - public void Inject_ArgumentValidation() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new TracerShim(tracer, new TraceContextPropagator()); + Assert.Throws(() => shim.Inject(null, testFormat, testCarrier)); + Assert.Throws(() => shim.Inject(new TestSpanContext(), testFormat, testCarrier)); + Assert.Throws(() => shim.Inject(spanContextShim, null, testCarrier)); + Assert.Throws(() => shim.Inject(spanContextShim, testFormat, null)); + } - var spanContextShim = new SpanContextShim(new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None)); - var mockFormat = new Mock>(); - var mockCarrier = new Mock(); + [Fact] + public void Inject_UnknownFormatIgnored() + { + var shim = new TracerShim(TracerProvider.Default, new TraceContextPropagator()); - Assert.Throws(() => shim.Inject(null, mockFormat.Object, mockCarrier.Object)); - Assert.Throws(() => shim.Inject(new Mock().Object, mockFormat.Object, mockCarrier.Object)); - Assert.Throws(() => shim.Inject(spanContextShim, null, mockCarrier.Object)); - Assert.Throws(() => shim.Inject(spanContextShim, mockFormat.Object, null)); - } + var spanContextShim = new SpanContextShim(new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded)); - [Fact] - public void Inject_UnknownFormatIgnored() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new TracerShim(tracer, new TraceContextPropagator()); + // Only two specific types of ITextMap are supported, and neither is a Mock. + var testCarrier = new TestTextMap(); + shim.Inject(spanContextShim, new TestFormatTextMap(), testCarrier); - var spanContextShim = new SpanContextShim(new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded)); + // Verify that the test carrier was never called. + Assert.False(testCarrier.SetCalled); + } - // Only two specific types of ITextMap are supported, and neither is a Mock. - var mockCarrier = new Mock(); - shim.Inject(spanContextShim, new Mock>().Object, mockCarrier.Object); + [Fact] + public void Extract_ArgumentValidation() + { + var shim = new TracerShim(TracerProvider.Default, new TraceContextPropagator()); - // Verify that the carrier mock was never called. - mockCarrier.Verify(x => x.Set(It.IsAny(), It.IsAny()), Times.Never); - } + Assert.Throws(() => shim.Extract(null, new TestTextMap())); + Assert.Throws(() => shim.Extract(new TestFormatTextMap(), null)); + } - [Fact] - public void Extract_ArgumentValidation() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new TracerShim(tracer, new TraceContextPropagator()); + [Fact] + public void Extract_UnknownFormatIgnored() + { + var shim = new TracerShim(TracerProvider.Default, new TraceContextPropagator()); + _ = new SpanContextShim(new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None)); - Assert.Throws(() => shim.Extract(null, new Mock().Object)); - Assert.Throws(() => shim.Extract(new Mock>().Object, null)); - } + // Only two specific types of ITextMap are supported, and neither is a Mock. + var testCarrier = new TestTextMap(); + _ = shim.Extract(new TestFormatTextMap(), testCarrier); - [Fact] - public void Extract_UnknownFormatIgnored() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new TracerShim(tracer, new TraceContextPropagator()); + // Verify that the test carrier was never called. + Assert.False(testCarrier.SetCalled); + } - var spanContextShim = new SpanContextShim(new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None)); + [Fact] + public void Extract_InvalidTraceParent() + { + var shim = new TracerShim(TracerProvider.Default, new TraceContextPropagator()); - // Only two specific types of ITextMap are supported, and neither is a Mock. - var mockCarrier = new Mock(); - var context = shim.Extract(new Mock>().Object, mockCarrier.Object); + var testCarrier = new TestTextMap(); - // Verify that the carrier mock was never called. - mockCarrier.Verify(x => x.GetEnumerator(), Times.Never); - } + // The ProxyTracer uses OpenTelemetry.Context.Propagation.TraceContextPropagator, so we need to satisfy the traceparent key at the least + testCarrier.Items["traceparent"] = "unused"; - [Fact] - public void Extract_InvalidTraceParent() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new TracerShim(tracer, new TraceContextPropagator()); + var spanContextShim = shim.Extract(BuiltinFormats.TextMap, testCarrier) as SpanContextShim; - var mockCarrier = new Mock(); + // Verify that the carrier was called + Assert.True(testCarrier.GetEnumeratorCalled); - // The ProxyTracer uses OpenTelemetry.Context.Propagation.TraceContextPropagator, so we need to satisfy the traceparent key at the least - var carrierMap = new Dictionary - { - // This is an invalid traceparent value - { "traceparent", "unused" }, - }; + Assert.Null(spanContextShim); + } - mockCarrier.Setup(x => x.GetEnumerator()).Returns(carrierMap.GetEnumerator()); - var spanContextShim = shim.Extract(BuiltinFormats.TextMap, mockCarrier.Object) as SpanContextShim; + [Fact] + public void InjectExtract_TextMap_Ok() + { + var carrier = new TextMapCarrier(); - // Verify that the carrier was called - mockCarrier.Verify(x => x.GetEnumerator(), Times.Once); + var spanContextShim = new SpanContextShim(new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None)); - Assert.Null(spanContextShim); - } + var format = new TraceContextPropagator(); - [Fact] - public void InjectExtract_TextMap_Ok() - { - var carrier = new TextMapCarrier(); + var shim = new TracerShim(TracerProvider.Default, format); - var spanContextShim = new SpanContextShim(new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None)); + // first inject + shim.Inject(spanContextShim, BuiltinFormats.TextMap, carrier); - var format = new TraceContextPropagator(); + // then extract + var extractedSpanContext = shim.Extract(BuiltinFormats.TextMap, carrier); - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new TracerShim(tracer, format); + AssertOpenTracerSpanContextEqual(spanContextShim, extractedSpanContext); + } - // first inject - shim.Inject(spanContextShim, BuiltinFormats.TextMap, carrier); + private static void AssertOpenTracerSpanContextEqual(ISpanContext source, ISpanContext target) + { + Assert.Equal(source.TraceId, target.TraceId); + Assert.Equal(source.SpanId, target.SpanId); - // then extract - var extractedSpanContext = shim.Extract(BuiltinFormats.TextMap, carrier); + // TODO BaggageItems are not implemented yet. + } - AssertOpenTracerSpanContextEqual(spanContextShim, extractedSpanContext); - } + /// + /// Simple ITextMap implementation used for the inject/extract tests. + /// + /// + private class TextMapCarrier : ITextMap + { + private readonly Dictionary map = new(); - private static void AssertOpenTracerSpanContextEqual(ISpanContext source, ISpanContext target) - { - Assert.Equal(source.TraceId, target.TraceId); - Assert.Equal(source.SpanId, target.SpanId); + public IDictionary Map => this.map; - // TODO BaggageItems are not implemented yet. - } + public IEnumerator> GetEnumerator() => this.map.GetEnumerator(); - /// - /// Simple ITextMap implementation used for the inject/extract tests. - /// - /// - private class TextMapCarrier : ITextMap + public void Set(string key, string value) { - private readonly Dictionary map = new(); - - public IDictionary Map => this.map; + this.map[key] = value; + } - public IEnumerator> GetEnumerator() => this.map.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => this.map.GetEnumerator(); + } - public void Set(string key, string value) - { - this.map[key] = value; - } + /// + /// Simple IBinary implementation used for the inject/extract tests. + /// + /// + private class BinaryCarrier : IBinary + { + private readonly MemoryStream carrierStream = new(); - IEnumerator IEnumerable.GetEnumerator() => this.map.GetEnumerator(); - } + public MemoryStream Get() => this.carrierStream; - /// - /// Simple IBinary implementation used for the inject/extract tests. - /// - /// - private class BinaryCarrier : IBinary + public void Set(MemoryStream stream) { - private readonly MemoryStream carrierStream = new(); - - public MemoryStream Get() => this.carrierStream; - - public void Set(MemoryStream stream) - { - this.carrierStream.SetLength(stream.Length); - this.carrierStream.Seek(0, SeekOrigin.Begin); - stream.CopyTo(this.carrierStream, (int)this.carrierStream.Length); - } + this.carrierStream.SetLength(stream.Length); + this.carrierStream.Seek(0, SeekOrigin.Begin); + stream.CopyTo(this.carrierStream, (int)this.carrierStream.Length); } } } diff --git a/test/OpenTelemetry.Tests.Stress.Logs/DummyProcessor.cs b/test/OpenTelemetry.Tests.Stress.Logs/DummyProcessor.cs index 616252da3e5..aeb1963ec74 100644 --- a/test/OpenTelemetry.Tests.Stress.Logs/DummyProcessor.cs +++ b/test/OpenTelemetry.Tests.Stress.Logs/DummyProcessor.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Logs; diff --git a/test/OpenTelemetry.Tests.Stress.Logs/OpenTelemetry.Tests.Stress.Logs.csproj b/test/OpenTelemetry.Tests.Stress.Logs/OpenTelemetry.Tests.Stress.Logs.csproj index 9ca31242391..e75a64bbcbf 100644 --- a/test/OpenTelemetry.Tests.Stress.Logs/OpenTelemetry.Tests.Stress.Logs.csproj +++ b/test/OpenTelemetry.Tests.Stress.Logs/OpenTelemetry.Tests.Stress.Logs.csproj @@ -2,18 +2,11 @@ Exe - - net7.0;net6.0;net462 - - - disable + $(TargetFrameworksForTests) - - - - + diff --git a/test/OpenTelemetry.Tests.Stress.Logs/Payload.cs b/test/OpenTelemetry.Tests.Stress.Logs/Payload.cs index 37fdcfcc243..fa8041eeb56 100644 --- a/test/OpenTelemetry.Tests.Stress.Logs/Payload.cs +++ b/test/OpenTelemetry.Tests.Stress.Logs/Payload.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 namespace OpenTelemetry.Tests.Stress; diff --git a/test/OpenTelemetry.Tests.Stress.Logs/Program.cs b/test/OpenTelemetry.Tests.Stress.Logs/Program.cs index 8efc9695a88..dececdacb51 100644 --- a/test/OpenTelemetry.Tests.Stress.Logs/Program.cs +++ b/test/OpenTelemetry.Tests.Stress.Logs/Program.cs @@ -1,52 +1,55 @@ -// // 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.Runtime.CompilerServices; +// SPDX-License-Identifier: Apache-2.0 + using Microsoft.Extensions.Logging; namespace OpenTelemetry.Tests.Stress; -public partial class Program +public static class Program { - private static ILogger logger; - private static Payload payload = new Payload(); + public static int Main(string[] args) + { + return StressTestFactory.RunSynchronously(args); + } - public static void Main() + private sealed class LogsStressTest : StressTest { - using var loggerFactory = LoggerFactory.Create(builder => + private static readonly Payload Payload = new(); + private readonly ILoggerFactory loggerFactory; + private readonly ILogger logger; + + public LogsStressTest(StressTestOptions options) + : base(options) { - builder.AddOpenTelemetry(options => + this.loggerFactory = LoggerFactory.Create(builder => { - options.AddProcessor(new DummyProcessor()); + builder.AddOpenTelemetry(options => + { + options.AddProcessor(new DummyProcessor()); + }); }); - }); - logger = loggerFactory.CreateLogger(); + this.logger = this.loggerFactory.CreateLogger(); + } - Stress(prometheusPort: 9464); - } + protected override void RunWorkItemInParallel() + { + this.logger.Log( + logLevel: LogLevel.Information, + eventId: 2, + state: Payload, + exception: null, + formatter: (state, ex) => string.Empty); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static void Run() - { - logger.Log( - logLevel: LogLevel.Information, - eventId: 2, - state: payload, - exception: null, - formatter: (state, ex) => string.Empty); + protected override void Dispose(bool isDisposing) + { + if (isDisposing) + { + this.loggerFactory.Dispose(); + } + + base.Dispose(isDisposing); + } } } diff --git a/test/OpenTelemetry.Tests.Stress.Logs/README.md b/test/OpenTelemetry.Tests.Stress.Logs/README.md index 20c4b87db6e..b2e50ece933 100644 --- a/test/OpenTelemetry.Tests.Stress.Logs/README.md +++ b/test/OpenTelemetry.Tests.Stress.Logs/README.md @@ -10,5 +10,23 @@ based on the [OpenTelemetry.Tests.Stress](../OpenTelemetry.Tests.Stress/README.m Open a console, run the following command from the current folder: ```sh -dotnet run --framework net6.0 --configuration Release +dotnet run --framework net8.0 --configuration Release +``` + +To see command line options available, run the following command from the +current folder: + +```sh +dotnet run --framework net8.0 --configuration Release -- --help +``` + +The help output includes settings and their explanations: + +```text + -c, --concurrency The concurrency (maximum degree of parallelism) for the stress test. Default value: Environment.ProcessorCount. + + -p, --internal_port The Prometheus http listener port where Prometheus will be exposed for retrieving internal metrics while the stress test is running. Set to '0' to + disable. Default value: 9464. + + -d, --duration The duration for the stress test to run in seconds. If set to '0' or a negative value the stress test will run until canceled. Default value: 0. ``` diff --git a/test/OpenTelemetry.Tests.Stress.Metrics/OpenTelemetry.Tests.Stress.Metrics.csproj b/test/OpenTelemetry.Tests.Stress.Metrics/OpenTelemetry.Tests.Stress.Metrics.csproj index 683f9e8c9a8..d162e31f732 100644 --- a/test/OpenTelemetry.Tests.Stress.Metrics/OpenTelemetry.Tests.Stress.Metrics.csproj +++ b/test/OpenTelemetry.Tests.Stress.Metrics/OpenTelemetry.Tests.Stress.Metrics.csproj @@ -2,22 +2,16 @@ Exe - - net7.0;net6.0;net462 - - - disable + $(TargetFrameworksForTests) - + + - - - diff --git a/test/OpenTelemetry.Tests.Stress.Metrics/Program.cs b/test/OpenTelemetry.Tests.Stress.Metrics/Program.cs index 6147c2fb247..17360444c8e 100644 --- a/test/OpenTelemetry.Tests.Stress.Metrics/Program.cs +++ b/test/OpenTelemetry.Tests.Stress.Metrics/Program.cs @@ -1,79 +1,141 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Metrics; -using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; +using CommandLine; using OpenTelemetry.Metrics; namespace OpenTelemetry.Tests.Stress; -public partial class Program +public static class Program { - private const int ArraySize = 10; - - // Note: Uncomment the below line if you want to run Histogram stress test - private const int MaxHistogramMeasurement = 1000; + private enum MetricsStressTestType + { + /// Histogram. + Histogram, - private static readonly Meter TestMeter = new(Utils.GetCurrentMethodName()); - private static readonly Counter TestCounter = TestMeter.CreateCounter("TestCounter"); - private static readonly string[] DimensionValues = new string[ArraySize]; - private static readonly ThreadLocal ThreadLocalRandom = new(() => new Random()); + /// Counter. + Counter, + } - // Note: Uncomment the below line if you want to run Histogram stress test - private static readonly Histogram TestHistogram = TestMeter.CreateHistogram("TestHistogram"); + public static int Main(string[] args) + { + return StressTestFactory.RunSynchronously(args); + } - public static void Main() + private sealed class MetricsStressTest : StressTest { - for (int i = 0; i < ArraySize; i++) + private const int ArraySize = 10; + private const int MaxHistogramMeasurement = 1000; + + private static readonly Meter TestMeter = new(Utils.GetCurrentMethodName()); + private static readonly Histogram TestHistogram = TestMeter.CreateHistogram("TestHistogram"); + private static readonly Counter TestCounter = TestMeter.CreateCounter("TestCounter"); + private static readonly string[] DimensionValues = new string[ArraySize]; + private static readonly ThreadLocal ThreadLocalRandom = new(() => new Random()); + private readonly MeterProvider meterProvider; + + static MetricsStressTest() { - DimensionValues[i] = $"DimValue{i}"; + for (int i = 0; i < ArraySize; i++) + { + DimensionValues[i] = $"DimValue{i}"; + } } - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(TestMeter.Name) + public MetricsStressTest(MetricsStressTestOptions options) + : base(options) + { + var builder = Sdk.CreateMeterProviderBuilder().AddMeter(TestMeter.Name); + + if (options.PrometheusTestMetricsPort != 0) + { + builder.AddPrometheusHttpListener(o => o.UriPrefixes = new string[] { $"http://localhost:{options.PrometheusTestMetricsPort}/" }); + } + + if (options.EnableExemplars) + { + builder.SetExemplarFilter(ExemplarFilterType.AlwaysOn); + } + + if (options.AddViewToFilterTags) + { + builder + .AddView("TestCounter", new MetricStreamConfiguration { TagKeys = new string[] { "DimName1" } }) + .AddView("TestHistogram", new MetricStreamConfiguration { TagKeys = new string[] { "DimName1" } }); + } - // .SetExemplarFilter(new AlwaysOnExemplarFilter()) - .AddPrometheusHttpListener( - options => options.UriPrefixes = new string[] { $"http://localhost:9185/" }) - .Build(); + if (options.AddOtlpExporter) + { + builder.AddOtlpExporter((exporterOptions, readerOptions) => + { + readerOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = options.OtlpExporterExportIntervalMilliseconds; + }); + } - Stress(prometheusPort: 9464); + this.meterProvider = builder.Build(); + } + + protected override void WriteRunInformationToConsole() + { + if (this.Options.PrometheusTestMetricsPort != 0) + { + Console.Write($", testPrometheusEndpoint = http://localhost:{this.Options.PrometheusTestMetricsPort}/metrics/"); + } + } + + protected override void RunWorkItemInParallel() + { + var random = ThreadLocalRandom.Value!; + if (this.Options.TestType == MetricsStressTestType.Histogram) + { + TestHistogram.Record( + random.Next(MaxHistogramMeasurement), + new("DimName1", DimensionValues[random.Next(0, ArraySize)]), + new("DimName2", DimensionValues[random.Next(0, ArraySize)]), + new("DimName3", DimensionValues[random.Next(0, ArraySize)])); + } + else if (this.Options.TestType == MetricsStressTestType.Counter) + { + TestCounter.Add( + 100, + new("DimName1", DimensionValues[random.Next(0, ArraySize)]), + new("DimName2", DimensionValues[random.Next(0, ArraySize)]), + new("DimName3", DimensionValues[random.Next(0, ArraySize)])); + } + } + + protected override void Dispose(bool isDisposing) + { + if (isDisposing) + { + this.meterProvider.Dispose(); + } + + base.Dispose(isDisposing); + } } - // Note: Uncomment the below lines if you want to run Counter stress test - // [MethodImpl(MethodImplOptions.AggressiveInlining)] - // protected static void Run() - // { - // var random = ThreadLocalRandom.Value; - // TestCounter.Add( - // 100, - // new("DimName1", DimensionValues[random.Next(0, ArraySize)]), - // new("DimName2", DimensionValues[random.Next(0, ArraySize)]), - // new("DimName3", DimensionValues[random.Next(0, ArraySize)])); - // } - - // Note: Uncomment the below lines if you want to run Histogram stress test - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static void Run() + private sealed class MetricsStressTestOptions : StressTestOptions { - var random = ThreadLocalRandom.Value; - TestHistogram.Record( - random.Next(MaxHistogramMeasurement), - new("DimName1", DimensionValues[random.Next(0, ArraySize)]), - new("DimName2", DimensionValues[random.Next(0, ArraySize)]), - new("DimName3", DimensionValues[random.Next(0, ArraySize)])); + [JsonConverter(typeof(JsonStringEnumConverter))] + [Option('t', "type", HelpText = "The metrics stress test type to run. Valid values: [Histogram, Counter]. Default value: Histogram.", Required = false)] + public MetricsStressTestType TestType { get; set; } = MetricsStressTestType.Histogram; + + [Option('m', "metrics_port", HelpText = "The Prometheus http listener port where Prometheus will be exposed for retrieving test metrics while the stress test is running. Set to '0' to disable. Default value: 9185.", Required = false)] + public int PrometheusTestMetricsPort { get; set; } = 9185; + + [Option('v', "view", HelpText = "Whether or not a view should be configured to filter tags for the stress test. Default value: False.", Required = false)] + public bool AddViewToFilterTags { get; set; } + + [Option('o', "otlp", HelpText = "Whether or not an OTLP exporter should be added for the stress test. Default value: False.", Required = false)] + public bool AddOtlpExporter { get; set; } + + [Option('i', "interval", HelpText = "The OTLP exporter export interval in milliseconds. Default value: 5000.", Required = false)] + public int OtlpExporterExportIntervalMilliseconds { get; set; } = 5000; + + [Option('e', "exemplars", HelpText = "Whether or not to enable exemplars for the stress test. Default value: False.", Required = false)] + public bool EnableExemplars { get; set; } } } diff --git a/test/OpenTelemetry.Tests.Stress.Metrics/README.md b/test/OpenTelemetry.Tests.Stress.Metrics/README.md index c05f1c6758e..3201c7c5900 100644 --- a/test/OpenTelemetry.Tests.Stress.Metrics/README.md +++ b/test/OpenTelemetry.Tests.Stress.Metrics/README.md @@ -5,15 +5,41 @@ This stress test is specifically for Metrics SDK, and is based on the * [Running the stress test](#running-the-stress-test) -> **Note** -> To run the stress tests for Histogram, comment out the `Run` method -for `Counter` and uncomment everything related to `Histogram` in the -[Program.cs](../OpenTelemetry.Tests.Stress.Metrics/Program.cs). - ## Running the stress test 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 +``` + +To see command line options available, run the following command from the +current folder: + +```sh +dotnet run --framework net8.0 --configuration Release -- --help +``` + +The help output includes settings and their explanations: + +```text + -t, --type The metrics stress test type to run. Valid values: [Histogram, Counter]. Default value: Histogram. + + -m, --metrics_port The Prometheus http listener port where Prometheus will be exposed for retrieving test metrics while the stress test is running. Set to '0' to disable. + Default value: 9185. + + -v, --view Whether or not a view should be configured to filter tags for the stress test. Default value: False. + + -o, --otlp Whether or not an OTLP exporter should be added for the stress test. Default value: False. + + -i, --interval The OTLP exporter export interval in milliseconds. Default value: 5000. + + -e, --exemplars Whether or not to enable exemplars for the stress test. Default value: False. + + -c, --concurrency The concurrency (maximum degree of parallelism) for the stress test. Default value: Environment.ProcessorCount. + + -p, --internal_port The Prometheus http listener port where Prometheus will be exposed for retrieving internal metrics while the stress test is running. Set to '0' to + disable. Default value: 9464. + + -d, --duration The duration for the stress test to run in seconds. If set to '0' or a negative value the stress test will run until canceled. Default value: 0. ``` diff --git a/test/OpenTelemetry.Tests.Stress.Traces/OpenTelemetry.Tests.Stress.Traces.csproj b/test/OpenTelemetry.Tests.Stress.Traces/OpenTelemetry.Tests.Stress.Traces.csproj index d90f9e2f9e3..7a32563d8b5 100644 --- a/test/OpenTelemetry.Tests.Stress.Traces/OpenTelemetry.Tests.Stress.Traces.csproj +++ b/test/OpenTelemetry.Tests.Stress.Traces/OpenTelemetry.Tests.Stress.Traces.csproj @@ -2,18 +2,11 @@ Exe - - net7.0;net6.0;net462 + $(TargetFrameworksForTests) - - - - - - - + diff --git a/test/OpenTelemetry.Tests.Stress.Traces/Program.cs b/test/OpenTelemetry.Tests.Stress.Traces/Program.cs index 2d4087b74cb..422a44a99ef 100644 --- a/test/OpenTelemetry.Tests.Stress.Traces/Program.cs +++ b/test/OpenTelemetry.Tests.Stress.Traces/Program.cs @@ -1,45 +1,46 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -using System.Runtime.CompilerServices; -using OpenTelemetry.Metrics; using OpenTelemetry.Trace; namespace OpenTelemetry.Tests.Stress; -public partial class Program +public static class Program { - private static readonly ActivitySource ActivitySource = new ActivitySource("OpenTelemetry.Tests.Stress"); - - public static void Main() + public static int Main(string[] args) { - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(ActivitySource.Name) - .Build(); - - Stress(prometheusPort: 9464); + return StressTestFactory.RunSynchronously(args); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static void Run() + private sealed class TracesStressTest : StressTest { - using (var activity = ActivitySource.StartActivity("test")) + private static readonly ActivitySource ActivitySource = new("OpenTelemetry.Tests.Stress"); + private readonly TracerProvider tracerProvider; + + public TracesStressTest(StressTestOptions options) + : base(options) + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(ActivitySource.Name) + .Build(); + } + + protected override void RunWorkItemInParallel() { + using var activity = ActivitySource.StartActivity("test"); + activity?.SetTag("foo", "value"); } + + protected override void Dispose(bool isDisposing) + { + if (isDisposing) + { + this.tracerProvider.Dispose(); + } + + base.Dispose(isDisposing); + } } } diff --git a/test/OpenTelemetry.Tests.Stress.Traces/README.md b/test/OpenTelemetry.Tests.Stress.Traces/README.md index 54998de1e76..005614d3361 100644 --- a/test/OpenTelemetry.Tests.Stress.Traces/README.md +++ b/test/OpenTelemetry.Tests.Stress.Traces/README.md @@ -10,5 +10,23 @@ based on the [OpenTelemetry.Tests.Stress](../OpenTelemetry.Tests.Stress/README.m Open a console, run the following command from the current folder: ```sh -dotnet run --framework net6.0 --configuration Release +dotnet run --framework net8.0 --configuration Release +``` + +To see command line options available, run the following command from the +current folder: + +```sh +dotnet run --framework net8.0 --configuration Release -- --help +``` + +The help output includes settings and their explanations: + +```text + -c, --concurrency The concurrency (maximum degree of parallelism) for the stress test. Default value: Environment.ProcessorCount. + + -p, --internal_port The Prometheus http listener port where Prometheus will be exposed for retrieving internal metrics while the stress test is running. Set to '0' to + disable. Default value: 9464. + + -d, --duration The duration for the stress test to run in seconds. If set to '0' or a negative value the stress test will run until canceled. Default value: 0. ``` diff --git a/test/OpenTelemetry.Tests.Stress/Meat.cs b/test/OpenTelemetry.Tests.Stress/Meat.cs deleted file mode 100644 index 287090db0b3..00000000000 --- a/test/OpenTelemetry.Tests.Stress/Meat.cs +++ /dev/null @@ -1,32 +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.Runtime.CompilerServices; - -namespace OpenTelemetry.Tests.Stress; - -public partial class Program -{ - public static void Main() - { - Stress(concurrency: 1, prometheusPort: 9464); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static void Run() - { - } -} diff --git a/test/OpenTelemetry.Tests.Stress/OpenTelemetry.Tests.Stress.csproj b/test/OpenTelemetry.Tests.Stress/OpenTelemetry.Tests.Stress.csproj index 0aa5cd799e4..01af1c993ae 100644 --- a/test/OpenTelemetry.Tests.Stress/OpenTelemetry.Tests.Stress.csproj +++ b/test/OpenTelemetry.Tests.Stress/OpenTelemetry.Tests.Stress.csproj @@ -1,12 +1,14 @@ Exe - - net7.0;net6.0;net462 + $(TargetFrameworksForTests) + + + diff --git a/test/OpenTelemetry.Tests.Stress/Program.cs b/test/OpenTelemetry.Tests.Stress/Program.cs new file mode 100644 index 00000000000..a5f6fb8975e --- /dev/null +++ b/test/OpenTelemetry.Tests.Stress/Program.cs @@ -0,0 +1,24 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Tests.Stress; + +public static class Program +{ + public static int Main(string[] args) + { + return StressTestFactory.RunSynchronously(args); + } + + private sealed class DemoStressTest : StressTest + { + public DemoStressTest(StressTestOptions options) + : base(options) + { + } + + protected override void RunWorkItemInParallel() + { + } + } +} diff --git a/test/OpenTelemetry.Tests.Stress/README.md b/test/OpenTelemetry.Tests.Stress/README.md index 890b1d0cc9b..0aa032669fb 100644 --- a/test/OpenTelemetry.Tests.Stress/README.md +++ b/test/OpenTelemetry.Tests.Stress/README.md @@ -18,26 +18,45 @@ Open a console, run the following command from the current folder: ```sh -dotnet run --framework net6.0 --configuration Release +dotnet run --framework net8.0 --configuration Release +``` + +To see command line options available, run the following command from the +current folder: + +```sh +dotnet run --framework net8.0 --configuration Release -- --help +``` + +The help output includes settings and their explanations: + +```text + -c, --concurrency The concurrency (maximum degree of parallelism) for the stress test. Default value: Environment.ProcessorCount. + + -p, --internal_port The Prometheus http listener port where Prometheus will be exposed for retrieving internal metrics while the stress test is running. Set to '0' to + disable. Default value: 9464. + + -d, --duration The duration for the stress test to run in seconds. If set to '0' or a negative value the stress test will run until canceled. Default value: 0. ``` Once the application started, you will see the performance number updates from -the console window title. +the console window title and the console window itself. -Use the `SPACE` key to toggle the console output, which is off by default. +While a test is running... -Use the `ENTER` key to print the latest performance statistics. +* Use the `SPACE` key to toggle the console output, which is on by default. -Use the `ESC` key to exit the stress test. +* Use the `ENTER` key to print the latest performance statistics. + +* Use the `ESC` key to exit the stress test. + +Example output while a test is running: ```text -Running (concurrency = 1), press to stop... -2021-09-28T18:47:17.6807622Z Loops: 17,549,732,467, Loops/Second: 738,682,519, CPU Cycles/Loop: 3 -2021-09-28T18:47:17.8846348Z Loops: 17,699,532,304, Loops/Second: 731,866,438, CPU Cycles/Loop: 3 -2021-09-28T18:47:18.0914577Z Loops: 17,850,498,225, Loops/Second: 730,931,752, CPU Cycles/Loop: 3 -2021-09-28T18:47:18.2992864Z Loops: 18,000,133,808, Loops/Second: 724,029,883, CPU Cycles/Loop: 3 -2021-09-28T18:47:18.5052989Z Loops: 18,150,598,194, Loops/Second: 733,026,161, CPU Cycles/Loop: 3 -2021-09-28T18:47:18.7116733Z Loops: 18,299,461,007, Loops/Second: 724,950,210, CPU Cycles/Loop: 3 +Options: {"Concurrency":20,"PrometheusInternalMetricsPort":9464,"DurationSeconds":0} +Run OpenTelemetry.Tests.Stress.exe --help to see available options. +Running (concurrency = 20, internalPrometheusEndpoint = http://localhost:9464/metrics/), press to stop, press to toggle statistics in the console... +Loops: 17,384,826,748, Loops/Second: 2,375,222,037, CPU Cycles/Loop: 24, RunningTime (Seconds): 7 ``` The stress test metrics are exposed via @@ -76,52 +95,88 @@ process_runtime_dotnet_gc_allocations_size_bytes 5485192 1658950184752 Create a simple console application with the following code: ```csharp -using System.Runtime.CompilerServices; +using OpenTelemetry.Tests.Stress; -public partial class Program +public static class Program { - public static void Main() + public static int Main(string[] args) { - Stress(concurrency: 10, prometheusPort: 9464); + return StressTestFactory.RunSynchronously(args); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static void Run() + private sealed class MyStressTest : StressTest { - // add your logic here + public MyStressTest(StressTestOptions options) + : base(options) + { + } + + protected override void RunWorkItemInParallel() + { + } } } ``` -Add the [`Skeleton.cs`](./Skeleton.cs) file to your `*.csproj` file: +Add the following project reference to the project: ```xml - - - + ``` -Add the following packages to the project: +Now you are ready to run your own stress test. Add test logic in the +`RunWorkItemInParallel` method to measure performance. -```shell -dotnet add package OpenTelemetry.Exporter.Prometheus --prerelease -dotnet add package OpenTelemetry.Instrumentation.Runtime --prerelease -``` +To define custom options create an options class which derives from +`StressTestOptions`: -Now you are ready to run your own stress test. +```csharp +using CommandLine; +using OpenTelemetry.Tests.Stress; + +public static class Program +{ + public static int Main(string[] args) + { + return StressTestFactory.RunSynchronously(args); + } + + private sealed class MyStressTest : StressTest + { + public MyStressTest(MyStressTestOptions options) + : base(options) + { + } + + protected override void RunWorkItemInParallel() + { + // Use this.Options here to access options supplied + // on the command line. + } + } + + private sealed class MyStressTestOptions : StressTestOptions + { + [Option('r', "rate", HelpText = "Add help text here for the rate option. Default value: 0.", Required = false)] + public int Rate { get; set; } = 0; + } +} +``` Some useful notes: -* You can specify the concurrency using `Stress(concurrency: {concurrency - number})`, the default value is the number of CPU cores. Keep in mind that - concurrency level does not equal to the number of threads. -* You can specify a local PrometheusExporter listening port using - `Stress(prometheusPort: {port number})`, the default value is `0`, which will - turn off the PrometheusExporter. -* You want to put `[MethodImpl(MethodImplOptions.AggressiveInlining)]` on - `Run()`, this helps to reduce extra flushes on the CPU instruction cache. -* You might want to run the stress test under `Release` mode rather than `Debug` - mode. +* It is generally best practice to run the stress test for code compiled in + `Release` configuration rather than `Debug` configuration. `Debug` builds + typically are not optimized and contain extra code which will change the + performance characteristics of the logic under test. The stress test will + write a warning message to the console when starting if compiled with `Debug` + configuration. +* You can specify the concurrency using `-c` or `--concurrency` command line + argument, the default value if not specified is the number of CPU cores. Keep + in mind that concurrency level does not equal to the number of threads. +* You can use the duration `-d` or `--duration` command line argument to run the + stress test for a specific time period. This is useful when comparing changes + across multiple runs. ## Understanding the results @@ -130,4 +185,6 @@ Some useful notes: sliding window of few hundreds of milliseconds. * `CPU Cycles/Loop` represents the average CPU cycles for each `Run()` invocation, based on a small sliding window of few hundreds of milliseconds. -* `Runaway Time` represents the runaway time (seconds) since the test started. +* `Total Running Time` represents the running time (seconds) since the test started. +* `GC Total Allocated Bytes` (not available on .NET Framework) shows the total + amount of memory allocated while the test was running. diff --git a/test/OpenTelemetry.Tests.Stress/Skeleton.cs b/test/OpenTelemetry.Tests.Stress/Skeleton.cs deleted file mode 100644 index 6c80bd0fe36..00000000000 --- a/test/OpenTelemetry.Tests.Stress/Skeleton.cs +++ /dev/null @@ -1,185 +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 System.Diagnostics.Metrics; -using System.Runtime.InteropServices; -using OpenTelemetry.Metrics; - -namespace OpenTelemetry.Tests.Stress; - -public partial class Program -{ - private static volatile bool bContinue = true; - private static volatile string output = "Test results not available yet."; - - static Program() - { - } - - public static void Stress(int concurrency = 0, int prometheusPort = 0) - { -#if DEBUG - Console.WriteLine("***WARNING*** The current build is DEBUG which may affect timing!"); - Console.WriteLine(); -#endif - - if (concurrency < 0) - { - throw new ArgumentOutOfRangeException(nameof(concurrency), "concurrency level should be a non-negative number."); - } - - if (concurrency == 0) - { - concurrency = Environment.ProcessorCount; - } - - using var meter = new Meter("OpenTelemetry.Tests.Stress." + Guid.NewGuid().ToString("D")); - var cntLoopsTotal = 0UL; - meter.CreateObservableCounter( - "OpenTelemetry.Tests.Stress.Loops", - () => unchecked((long)cntLoopsTotal), - description: "The total number of `Run()` invocations that are completed."); - var dLoopsPerSecond = 0D; - meter.CreateObservableGauge( - "OpenTelemetry.Tests.Stress.LoopsPerSecond", - () => dLoopsPerSecond, - description: "The rate of `Run()` invocations based on a small sliding window of few hundreds of milliseconds."); - var dCpuCyclesPerLoop = 0D; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - meter.CreateObservableGauge( - "OpenTelemetry.Tests.Stress.CpuCyclesPerLoop", - () => dCpuCyclesPerLoop, - description: "The average CPU cycles for each `Run()` invocation, based on a small sliding window of few hundreds of milliseconds."); - } - - using var meterProvider = prometheusPort != 0 ? Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddRuntimeInstrumentation() - .AddPrometheusHttpListener( - options => options.UriPrefixes = new string[] { $"http://localhost:{prometheusPort}/" }) - .Build() : null; - - var statistics = new long[concurrency]; - var watchForTotal = Stopwatch.StartNew(); - - Parallel.Invoke( - () => - { - Console.Write($"Running (concurrency = {concurrency}"); - - if (prometheusPort != 0) - { - Console.Write($", prometheusEndpoint = http://localhost:{prometheusPort}/metrics/"); - } - - Console.WriteLine("), press to stop..."); - - var bOutput = false; - var watch = new Stopwatch(); - while (true) - { - if (Console.KeyAvailable) - { - var key = Console.ReadKey(true).Key; - - switch (key) - { - case ConsoleKey.Enter: - Console.WriteLine(string.Format("{0} {1}", DateTime.UtcNow.ToString("O"), output)); - break; - case ConsoleKey.Escape: - bContinue = false; - return; - case ConsoleKey.Spacebar: - bOutput = !bOutput; - break; - } - - continue; - } - - if (bOutput) - { - Console.WriteLine(string.Format("{0} {1}", DateTime.UtcNow.ToString("O"), output)); - } - - var cntLoopsOld = (ulong)statistics.Sum(); - var cntCpuCyclesOld = GetCpuCycles(); - - watch.Restart(); - Thread.Sleep(200); - watch.Stop(); - - cntLoopsTotal = (ulong)statistics.Sum(); - var cntCpuCyclesNew = GetCpuCycles(); - - var nLoops = cntLoopsTotal - cntLoopsOld; - var nCpuCycles = cntCpuCyclesNew - cntCpuCyclesOld; - - dLoopsPerSecond = (double)nLoops / ((double)watch.ElapsedMilliseconds / 1000.0); - dCpuCyclesPerLoop = nLoops == 0 ? 0 : nCpuCycles / nLoops; - - output = $"Loops: {cntLoopsTotal:n0}, Loops/Second: {dLoopsPerSecond:n0}, CPU Cycles/Loop: {dCpuCyclesPerLoop:n0}, RunwayTime (Seconds): {watchForTotal.Elapsed.TotalSeconds:n0} "; - Console.Title = output; - } - }, - () => - { - Parallel.For(0, concurrency, (i) => - { - statistics[i] = 0; - while (bContinue) - { - Run(); - statistics[i]++; - } - }); - }); - - watchForTotal.Stop(); - cntLoopsTotal = (ulong)statistics.Sum(); - var totalLoopsPerSecond = (double)cntLoopsTotal / ((double)watchForTotal.ElapsedMilliseconds / 1000.0); - var cntCpuCyclesTotal = GetCpuCycles(); - var cpuCyclesPerLoopTotal = cntLoopsTotal == 0 ? 0 : cntCpuCyclesTotal / cntLoopsTotal; - Console.WriteLine("Stopping the stress test..."); - Console.WriteLine($"* Total Runaway Time (seconds) {watchForTotal.Elapsed.TotalSeconds:n0}"); - Console.WriteLine($"* Total Loops: {cntLoopsTotal:n0}"); - Console.WriteLine($"* Average Loops/Second: {totalLoopsPerSecond:n0}"); - Console.WriteLine($"* Average CPU Cycles/Loop: {cpuCyclesPerLoopTotal:n0}"); - } - - [DllImport("kernel32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool QueryProcessCycleTime(IntPtr hProcess, out ulong cycles); - - private static ulong GetCpuCycles() - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return 0; - } - - if (!QueryProcessCycleTime((IntPtr)(-1), out var cycles)) - { - return 0; - } - - return cycles; - } -} diff --git a/test/OpenTelemetry.Tests.Stress/StressTest.cs b/test/OpenTelemetry.Tests.Stress/StressTest.cs new file mode 100644 index 00000000000..ae19c7f8ece --- /dev/null +++ b/test/OpenTelemetry.Tests.Stress/StressTest.cs @@ -0,0 +1,209 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Runtime.InteropServices; +using System.Text.Json; +using OpenTelemetry.Metrics; + +namespace OpenTelemetry.Tests.Stress; + +public abstract class StressTest : IDisposable + where T : StressTestOptions +{ + private volatile bool bContinue = true; + private volatile string output = "Test results not available yet."; + + protected StressTest(T options) + { + this.Options = options ?? throw new ArgumentNullException(nameof(options)); + } + + public T Options { get; } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + public void RunSynchronously() + { +#if DEBUG + Console.WriteLine("***WARNING*** The current build is DEBUG which may affect timing!"); + Console.WriteLine(); +#endif + + var options = this.Options; + + if (options.Concurrency < 0) + { + throw new ArgumentOutOfRangeException(nameof(options.Concurrency), "Concurrency level should be a non-negative number."); + } + + if (options.Concurrency == 0) + { + options.Concurrency = Environment.ProcessorCount; + } + + using var meter = new Meter("OpenTelemetry.Tests.Stress." + Guid.NewGuid().ToString("D")); + var cntLoopsTotal = 0UL; + meter.CreateObservableCounter( + "OpenTelemetry.Tests.Stress.Loops", + () => unchecked((long)cntLoopsTotal), + description: "The total number of `Run()` invocations that are completed."); + var dLoopsPerSecond = 0D; + meter.CreateObservableGauge( + "OpenTelemetry.Tests.Stress.LoopsPerSecond", + () => dLoopsPerSecond, + description: "The rate of `Run()` invocations based on a small sliding window of few hundreds of milliseconds."); + var dCpuCyclesPerLoop = 0D; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + meter.CreateObservableGauge( + "OpenTelemetry.Tests.Stress.CpuCyclesPerLoop", + () => dCpuCyclesPerLoop, + description: "The average CPU cycles for each `Run()` invocation, based on a small sliding window of few hundreds of milliseconds."); + } + + using var meterProvider = options.PrometheusInternalMetricsPort != 0 ? Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddRuntimeInstrumentation() + .AddPrometheusHttpListener(o => o.UriPrefixes = new string[] { $"http://localhost:{options.PrometheusInternalMetricsPort}/" }) + .Build() : null; + + var statistics = new long[options.Concurrency]; + var watchForTotal = Stopwatch.StartNew(); + + TimeSpan? duration = options.DurationSeconds > 0 + ? TimeSpan.FromSeconds(options.DurationSeconds) + : null; + + Parallel.Invoke( + () => + { + Console.WriteLine($"Options: {JsonSerializer.Serialize(options)}"); + Console.WriteLine($"Run {Process.GetCurrentProcess().ProcessName}.exe --help to see available options."); + Console.Write($"Running (concurrency = {options.Concurrency}"); + + if (options.PrometheusInternalMetricsPort != 0) + { + Console.Write($", internalPrometheusEndpoint = http://localhost:{options.PrometheusInternalMetricsPort}/metrics/"); + } + + this.WriteRunInformationToConsole(); + + Console.WriteLine("), press to stop, press to toggle statistics in the console..."); + Console.WriteLine(this.output); + + var outputCursorTop = Console.CursorTop - 1; + + var bOutput = true; + var watch = new Stopwatch(); + while (true) + { + if (Console.KeyAvailable) + { + var key = Console.ReadKey(true).Key; + + switch (key) + { + case ConsoleKey.Enter: + Console.WriteLine(string.Format("{0} {1}", DateTime.UtcNow.ToString("O"), this.output)); + break; + case ConsoleKey.Escape: + this.bContinue = false; + return; + case ConsoleKey.Spacebar: + bOutput = !bOutput; + break; + } + + continue; + } + + if (bOutput) + { + var tempCursorLeft = Console.CursorLeft; + var tempCursorTop = Console.CursorTop; + Console.SetCursorPosition(0, outputCursorTop); + Console.WriteLine(this.output.PadRight(Console.BufferWidth)); + Console.SetCursorPosition(tempCursorLeft, tempCursorTop); + } + + var cntLoopsOld = (ulong)statistics.Sum(); + var cntCpuCyclesOld = StressTestNativeMethods.GetCpuCycles(); + + watch.Restart(); + Thread.Sleep(200); + watch.Stop(); + + cntLoopsTotal = (ulong)statistics.Sum(); + var cntCpuCyclesNew = StressTestNativeMethods.GetCpuCycles(); + + var nLoops = cntLoopsTotal - cntLoopsOld; + var nCpuCycles = cntCpuCyclesNew - cntCpuCyclesOld; + + dLoopsPerSecond = (double)nLoops / ((double)watch.ElapsedMilliseconds / 1000.0); + dCpuCyclesPerLoop = nLoops == 0 ? 0 : nCpuCycles / nLoops; + + var totalElapsedTime = watchForTotal.Elapsed; + + if (duration.HasValue) + { + this.output = $"Loops: {cntLoopsTotal:n0}, Loops/Second: {dLoopsPerSecond:n0}, CPU Cycles/Loop: {dCpuCyclesPerLoop:n0}, RemainingTime (Seconds): {(duration.Value - totalElapsedTime).TotalSeconds:n0}"; + if (totalElapsedTime > duration) + { + this.bContinue = false; + return; + } + } + else + { + this.output = $"Loops: {cntLoopsTotal:n0}, Loops/Second: {dLoopsPerSecond:n0}, CPU Cycles/Loop: {dCpuCyclesPerLoop:n0}, RunningTime (Seconds): {totalElapsedTime.TotalSeconds:n0}"; + } + + Console.Title = this.output; + } + }, + () => + { + Parallel.For(0, options.Concurrency, (i) => + { + ref var count = ref statistics[i]; + + while (this.bContinue) + { + this.RunWorkItemInParallel(); + count++; + } + }); + }); + + watchForTotal.Stop(); + cntLoopsTotal = (ulong)statistics.Sum(); + var totalLoopsPerSecond = (double)cntLoopsTotal / ((double)watchForTotal.ElapsedMilliseconds / 1000.0); + var cntCpuCyclesTotal = StressTestNativeMethods.GetCpuCycles(); + var cpuCyclesPerLoopTotal = cntLoopsTotal == 0 ? 0 : cntCpuCyclesTotal / cntLoopsTotal; + Console.WriteLine("Stopping the stress test..."); + Console.WriteLine($"* Total Running Time (Seconds) {watchForTotal.Elapsed.TotalSeconds:n0}"); + Console.WriteLine($"* Total Loops: {cntLoopsTotal:n0}"); + Console.WriteLine($"* Average Loops/Second: {totalLoopsPerSecond:n0}"); + Console.WriteLine($"* Average CPU Cycles/Loop: {cpuCyclesPerLoopTotal:n0}"); +#if !NETFRAMEWORK + Console.WriteLine($"* GC Total Allocated Bytes: {GC.GetTotalAllocatedBytes()}"); +#endif + } + + protected virtual void WriteRunInformationToConsole() + { + } + + protected abstract void RunWorkItemInParallel(); + + protected virtual void Dispose(bool isDisposing) + { + } +} diff --git a/test/OpenTelemetry.Tests.Stress/StressTestFactory.cs b/test/OpenTelemetry.Tests.Stress/StressTestFactory.cs new file mode 100644 index 00000000000..6f3e7ff9ea7 --- /dev/null +++ b/test/OpenTelemetry.Tests.Stress/StressTestFactory.cs @@ -0,0 +1,34 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using CommandLine; + +namespace OpenTelemetry.Tests.Stress; + +public static class StressTestFactory +{ + public static int RunSynchronously(string[] commandLineArguments) + where TStressTest : StressTest + { + return RunSynchronously(commandLineArguments); + } + + public static int RunSynchronously(string[] commandLineArguments) + where TStressTest : StressTest + where TStressTestOptions : StressTestOptions + { + return Parser.Default.ParseArguments(commandLineArguments) + .MapResult( + CreateStressTestAndRunSynchronously, + _ => 1); + + static int CreateStressTestAndRunSynchronously(TStressTestOptions options) + { + using var stressTest = (TStressTest)Activator.CreateInstance(typeof(TStressTest), options)!; + + stressTest.RunSynchronously(); + + return 0; + } + } +} diff --git a/test/OpenTelemetry.Tests.Stress/StressTestNativeMethods.cs b/test/OpenTelemetry.Tests.Stress/StressTestNativeMethods.cs new file mode 100644 index 00000000000..da3df1c2864 --- /dev/null +++ b/test/OpenTelemetry.Tests.Stress/StressTestNativeMethods.cs @@ -0,0 +1,28 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Runtime.InteropServices; + +namespace OpenTelemetry.Tests.Stress; + +internal static class StressTestNativeMethods +{ + public static ulong GetCpuCycles() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return 0; + } + + if (!QueryProcessCycleTime((IntPtr)(-1), out var cycles)) + { + return 0; + } + + return cycles; + } + + [DllImport("kernel32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool QueryProcessCycleTime(IntPtr hProcess, out ulong cycles); +} diff --git a/test/OpenTelemetry.Tests.Stress/StressTestOptions.cs b/test/OpenTelemetry.Tests.Stress/StressTestOptions.cs new file mode 100644 index 00000000000..2dcb2b2e47c --- /dev/null +++ b/test/OpenTelemetry.Tests.Stress/StressTestOptions.cs @@ -0,0 +1,18 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using CommandLine; + +namespace OpenTelemetry.Tests.Stress; + +public class StressTestOptions +{ + [Option('c', "concurrency", HelpText = "The concurrency (maximum degree of parallelism) for the stress test. Default value: Environment.ProcessorCount.", Required = false)] + public int Concurrency { get; set; } + + [Option('p', "internal_port", HelpText = "The Prometheus http listener port where Prometheus will be exposed for retrieving internal metrics while the stress test is running. Set to '0' to disable. Default value: 9464.", Required = false)] + public int PrometheusInternalMetricsPort { get; set; } = 9464; + + [Option('d', "duration", HelpText = "The duration for the stress test to run in seconds. If set to '0' or a negative value the stress test will run until canceled. Default value: 0.", Required = false)] + public int DurationSeconds { get; set; } +} diff --git a/test/OpenTelemetry.Tests/BaseExporterTest.cs b/test/OpenTelemetry.Tests/BaseExporterTest.cs index d749bd21838..39095636996 100644 --- a/test/OpenTelemetry.Tests/BaseExporterTest.cs +++ b/test/OpenTelemetry.Tests/BaseExporterTest.cs @@ -1,60 +1,46 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Xunit; -namespace OpenTelemetry.Tests +namespace OpenTelemetry.Tests; + +public class BaseExporterTest { - public class BaseExporterTest + [Fact] + public void Verify_ForceFlush_HandlesException() { - [Fact] - public void Verify_ForceFlush_HandlesException() - { - // By default, ForceFlush should return true. - var testExporter = new DelegatingExporter(); - Assert.True(testExporter.ForceFlush()); + // By default, ForceFlush should return true. + var testExporter = new DelegatingExporter(); + Assert.True(testExporter.ForceFlush()); - // BaseExporter should catch any exceptions and return false. - var exceptionTestExporter = new DelegatingExporter - { - OnForceFlushFunc = (timeout) => throw new Exception("test exception"), - }; - Assert.False(exceptionTestExporter.ForceFlush()); - } - - [Fact] - public void Verify_Shutdown_HandlesSecond() + // BaseExporter should catch any exceptions and return false. + var exceptionTestExporter = new DelegatingExporter { - // By default, ForceFlush should return true. - var testExporter = new DelegatingExporter(); - Assert.True(testExporter.Shutdown()); + OnForceFlushFunc = (timeout) => throw new Exception("test exception"), + }; + Assert.False(exceptionTestExporter.ForceFlush()); + } - // A second Shutdown should return false. - Assert.False(testExporter.Shutdown()); - } + [Fact] + public void Verify_Shutdown_HandlesSecond() + { + // By default, ForceFlush should return true. + var testExporter = new DelegatingExporter(); + Assert.True(testExporter.Shutdown()); + + // A second Shutdown should return false. + Assert.False(testExporter.Shutdown()); + } - [Fact] - public void Verify_Shutdown_HandlesException() + [Fact] + public void Verify_Shutdown_HandlesException() + { + // BaseExporter should catch any exceptions and return false. + var exceptionTestExporter = new DelegatingExporter { - // BaseExporter should catch any exceptions and return false. - var exceptionTestExporter = new DelegatingExporter - { - OnShutdownFunc = (timeout) => throw new Exception("test exception"), - }; - Assert.False(exceptionTestExporter.Shutdown()); - } + OnShutdownFunc = (timeout) => throw new Exception("test exception"), + }; + Assert.False(exceptionTestExporter.Shutdown()); } } diff --git a/test/OpenTelemetry.Tests/BaseProcessorTest.cs b/test/OpenTelemetry.Tests/BaseProcessorTest.cs index e453292c540..61516290135 100644 --- a/test/OpenTelemetry.Tests/BaseProcessorTest.cs +++ b/test/OpenTelemetry.Tests/BaseProcessorTest.cs @@ -1,67 +1,53 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Xunit; -namespace OpenTelemetry.Tests +namespace OpenTelemetry.Tests; + +public class BaseProcessorTest { - public class BaseProcessorTest + [Fact] + public void Verify_ForceFlush_HandlesException() { - [Fact] - public void Verify_ForceFlush_HandlesException() - { - // By default, ForceFlush should return true. - var testProcessor = new DelegatingProcessor(); - Assert.True(testProcessor.ForceFlush()); + // By default, ForceFlush should return true. + var testProcessor = new DelegatingProcessor(); + Assert.True(testProcessor.ForceFlush()); - // BaseExporter should catch any exceptions and return false. - testProcessor.OnForceFlushFunc = (timeout) => throw new Exception("test exception"); - Assert.False(testProcessor.ForceFlush()); - } + // BaseExporter should catch any exceptions and return false. + testProcessor.OnForceFlushFunc = (timeout) => throw new Exception("test exception"); + Assert.False(testProcessor.ForceFlush()); + } - [Fact] - public void Verify_Shutdown_HandlesSecond() - { - // By default, Shutdown should return true. - var testProcessor = new DelegatingProcessor(); - Assert.True(testProcessor.Shutdown()); + [Fact] + public void Verify_Shutdown_HandlesSecond() + { + // By default, Shutdown should return true. + var testProcessor = new DelegatingProcessor(); + Assert.True(testProcessor.Shutdown()); - // A second Shutdown should return false. - Assert.False(testProcessor.Shutdown()); - } + // A second Shutdown should return false. + Assert.False(testProcessor.Shutdown()); + } - [Fact] - public void Verify_Shutdown_HandlesException() + [Fact] + public void Verify_Shutdown_HandlesException() + { + // BaseExporter should catch any exceptions and return false. + var exceptionTestProcessor = new DelegatingProcessor { - // BaseExporter should catch any exceptions and return false. - var exceptionTestProcessor = new DelegatingProcessor - { - OnShutdownFunc = (timeout) => throw new Exception("test exception"), - }; - Assert.False(exceptionTestProcessor.Shutdown()); - } + OnShutdownFunc = (timeout) => throw new Exception("test exception"), + }; + Assert.False(exceptionTestProcessor.Shutdown()); + } - [Fact] - public void NoOp() - { - var testProcessor = new DelegatingProcessor(); + [Fact] + public void NoOp() + { + var testProcessor = new DelegatingProcessor(); - // These two methods are no-op, but account for 7% of the test coverage. - testProcessor.OnStart(new object()); - testProcessor.OnEnd(new object()); - } + // These two methods are no-op, but account for 7% of the test coverage. + testProcessor.OnStart(new object()); + testProcessor.OnEnd(new object()); } } diff --git a/test/OpenTelemetry.Tests/Concurrency/MetricsConcurrencyTests.cs b/test/OpenTelemetry.Tests/Concurrency/MetricsConcurrencyTests.cs new file mode 100644 index 00000000000..e20be44ee87 --- /dev/null +++ b/test/OpenTelemetry.Tests/Concurrency/MetricsConcurrencyTests.cs @@ -0,0 +1,57 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +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/EventSourceTest.cs b/test/OpenTelemetry.Tests/EventSourceTest.cs index 2d98b6fcfb9..5e7d3e2f258 100644 --- a/test/OpenTelemetry.Tests/EventSourceTest.cs +++ b/test/OpenTelemetry.Tests/EventSourceTest.cs @@ -1,37 +1,16 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -using OpenTelemetry.Instrumentation; using OpenTelemetry.Internal; using Xunit; -namespace OpenTelemetry.Tests +namespace OpenTelemetry.Tests; + +public class EventSourceTest { - public class EventSourceTest + [Fact] + public void EventSourceTest_OpenTelemetrySdkEventSource() { - [Fact] - public void EventSourceTest_InstrumentationEventSource() - { - EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(InstrumentationEventSource.Log); - } - - [Fact] - public void EventSourceTest_OpenTelemetrySdkEventSource() - { - EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(OpenTelemetrySdkEventSource.Log); - } + EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(OpenTelemetrySdkEventSource.Log); } } diff --git a/test/OpenTelemetry.Tests/Instrumentation/ActivityInstrumentationHelperTest.cs b/test/OpenTelemetry.Tests/Instrumentation/ActivityInstrumentationHelperTest.cs index 15c31e62ea3..e5841f53e64 100644 --- a/test/OpenTelemetry.Tests/Instrumentation/ActivityInstrumentationHelperTest.cs +++ b/test/OpenTelemetry.Tests/Instrumentation/ActivityInstrumentationHelperTest.cs @@ -1,53 +1,39 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using Xunit; -namespace OpenTelemetry.Instrumentation.Tests +namespace OpenTelemetry.Instrumentation.Tests; + +public class ActivityInstrumentationHelperTest { - public class ActivityInstrumentationHelperTest + [Theory] + [InlineData("TestActivitySource", null)] + [InlineData("TestActivitySource", "1.0.0")] + public void SetActivitySource(string name, string version) { - [Theory] - [InlineData("TestActivitySource", null)] - [InlineData("TestActivitySource", "1.0.0")] - public void SetActivitySource(string name, string version) - { - using var activity = new Activity("Test"); - using var activitySource = new ActivitySource(name, version); + using var activity = new Activity("Test"); + using var activitySource = new ActivitySource(name, version); - activity.Start(); - ActivityInstrumentationHelper.SetActivitySourceProperty(activity, activitySource); - Assert.Equal(activitySource.Name, activity.Source.Name); - Assert.Equal(activitySource.Version, activity.Source.Version); - activity.Stop(); - } + activity.Start(); + ActivityInstrumentationHelper.SetActivitySourceProperty(activity, activitySource); + Assert.Equal(activitySource.Name, activity.Source.Name); + Assert.Equal(activitySource.Version, activity.Source.Version); + activity.Stop(); + } - [Theory] - [InlineData(ActivityKind.Client)] - [InlineData(ActivityKind.Consumer)] - [InlineData(ActivityKind.Internal)] - [InlineData(ActivityKind.Producer)] - [InlineData(ActivityKind.Server)] - public void SetActivityKind(ActivityKind activityKind) - { - using var activity = new Activity("Test"); - activity.Start(); - ActivityInstrumentationHelper.SetKindProperty(activity, activityKind); - Assert.Equal(activityKind, activity.Kind); - } + [Theory] + [InlineData(ActivityKind.Client)] + [InlineData(ActivityKind.Consumer)] + [InlineData(ActivityKind.Internal)] + [InlineData(ActivityKind.Producer)] + [InlineData(ActivityKind.Server)] + public void SetActivityKind(ActivityKind activityKind) + { + using var activity = new Activity("Test"); + activity.Start(); + ActivityInstrumentationHelper.SetKindProperty(activity, activityKind); + Assert.Equal(activityKind, activity.Kind); } } diff --git a/test/OpenTelemetry.Tests/Instrumentation/PropertyFetcherTest.cs b/test/OpenTelemetry.Tests/Instrumentation/PropertyFetcherTest.cs index 36c47bfe965..9acf58b3128 100644 --- a/test/OpenTelemetry.Tests/Instrumentation/PropertyFetcherTest.cs +++ b/test/OpenTelemetry.Tests/Instrumentation/PropertyFetcherTest.cs @@ -1,95 +1,155 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using Xunit; -namespace OpenTelemetry.Instrumentation.Tests +namespace OpenTelemetry.Instrumentation.Tests; + +public class PropertyFetcherTest { - public class PropertyFetcherTest + [Fact] + public void FetchValidProperty() { - [Fact] - public void FetchValidProperty() - { - using var activity = new Activity("test"); - var fetch = new PropertyFetcher("DisplayName"); - Assert.True(fetch.TryFetch(activity, out string result)); - Assert.Equal(activity.DisplayName, result); - } + using var activity = new Activity("test"); - [Fact] - public void FetchInvalidProperty() - { - using var activity = new Activity("test"); - var fetch = new PropertyFetcher("DisplayName2"); - Assert.False(fetch.TryFetch(activity, out string result)); + var fetch = new PropertyFetcher("DisplayName"); - var fetchInt = new PropertyFetcher("DisplayName2"); - Assert.False(fetchInt.TryFetch(activity, out int resultInt)); + Assert.Equal(0, fetch.NumberOfInnerFetchers); - Assert.Equal(default, result); - Assert.Equal(default, resultInt); - } + Assert.True(fetch.TryFetch(activity, out string result)); + Assert.Equal(activity.DisplayName, result); - [Fact] - public void FetchNullProperty() - { - var fetch = new PropertyFetcher("null"); - Assert.False(fetch.TryFetch(null, out _)); - } + Assert.Equal(1, fetch.NumberOfInnerFetchers); - [Fact] - public void FetchPropertyMultiplePayloadTypes() - { - var fetch = new PropertyFetcher("Property"); + Assert.True(fetch.TryFetch(activity, out result)); + Assert.Equal(activity.DisplayName, result); - Assert.True(fetch.TryFetch(new PayloadTypeA(), out string propertyValue)); - Assert.Equal("A", propertyValue); + Assert.Equal(1, fetch.NumberOfInnerFetchers); + } - Assert.True(fetch.TryFetch(new PayloadTypeB(), out propertyValue)); - Assert.Equal("B", propertyValue); + [Fact] + public void FetchInvalidProperty() + { + using var activity = new Activity("test"); + var fetch = new PropertyFetcher("DisplayName2"); + Assert.False(fetch.TryFetch(activity, out string result)); - Assert.False(fetch.TryFetch(new PayloadTypeC(), out _)); + var fetchInt = new PropertyFetcher("DisplayName2"); + Assert.False(fetchInt.TryFetch(activity, out int resultInt)); - Assert.False(fetch.TryFetch(null, out _)); - } + Assert.Equal(default, result); + Assert.Equal(default, resultInt); + } - [Fact] - public void FetchPropertyMultiplePayloadTypes_IgnoreTypesWithoutExpectedPropertyName() - { - var fetch = new PropertyFetcher("Property"); + [Fact] + public void FetchNullProperty() + { + var fetch = new PropertyFetcher("null"); + Assert.False(fetch.TryFetch(null, out _)); + } - Assert.False(fetch.TryFetch(new PayloadTypeC(), out _)); + [Fact] + public void FetchPropertyMultiplePayloadTypes() + { + var fetch = new PropertyFetcher("Property"); - Assert.True(fetch.TryFetch(new PayloadTypeA(), out string propertyValue)); - Assert.Equal("A", propertyValue); - } + Assert.Equal(0, fetch.NumberOfInnerFetchers); - private class PayloadTypeA - { - public string Property { get; set; } = "A"; - } + Assert.True(fetch.TryFetch(new PayloadTypeA(), out string propertyValue)); + Assert.Equal("A", propertyValue); - private class PayloadTypeB - { - public string Property { get; set; } = "B"; - } + Assert.Equal(1, fetch.NumberOfInnerFetchers); + + Assert.True(fetch.TryFetch(new PayloadTypeB(), out propertyValue)); + Assert.Equal("B", propertyValue); + + Assert.Equal(2, fetch.NumberOfInnerFetchers); + + Assert.False(fetch.TryFetch(new PayloadTypeC(), out _)); + + Assert.Equal(2, fetch.NumberOfInnerFetchers); + + Assert.False(fetch.TryFetch(null, out _)); - private class PayloadTypeC + Assert.Equal(2, fetch.NumberOfInnerFetchers); + } + + [Fact] + public void FetchPropertyMultiplePayloadTypes_IgnoreTypesWithoutExpectedPropertyName() + { + var fetch = new PropertyFetcher("Property"); + + Assert.False(fetch.TryFetch(new PayloadTypeC(), out _)); + + Assert.True(fetch.TryFetch(new PayloadTypeA(), out string propertyValue)); + Assert.Equal("A", propertyValue); + } + + [Fact] + public void FetchPropertyWithDerivedInstanceType() + { + var fetch = new PropertyFetcher("Property"); + + Assert.True(fetch.TryFetch(new PayloadTypeWithBaseType(), out BaseType value)); + Assert.IsType(value); + } + + [Fact] + public void FetchPropertyWithDerivedDeclaredType() + { + var fetch = new PropertyFetcher("Property"); + + Assert.True(fetch.TryFetch(new PayloadTypeWithDerivedType(), out BaseType value)); + Assert.IsType(value); + } + + [Fact] + public void FetchPropertyWhenPayloadIsValueType() + { + var fetch = new PropertyFetcher("Property"); + var ex = Assert.Throws(() => fetch.TryFetch(new PayloadTypeIsValueType(), out BaseType value)); + Assert.Contains("PropertyFetcher can only operate on reference payload types.", ex.Message); + } + + private struct PayloadTypeIsValueType + { + public PayloadTypeIsValueType() { } + + public DerivedType Property { get; set; } = new DerivedType(); + } + + private class PayloadTypeA + { + public string Property { get; set; } = "A"; + } + + private class PayloadTypeB + { + public string Property { get; set; } = "B"; + } + + private class PayloadTypeC + { + } + + private class BaseType + { + } + + private class DerivedType : BaseType + { + } + + private class PayloadTypeWithBaseType + { + public BaseType Property { get; set; } = new DerivedType(); + } + + private class PayloadTypeWithDerivedType + { + public DerivedType Property { get; set; } = new DerivedType(); } } diff --git a/test/OpenTelemetry.Tests/Internal/CircularBufferTest.cs b/test/OpenTelemetry.Tests/Internal/CircularBufferTest.cs index 065c0977086..2e0e4225526 100644 --- a/test/OpenTelemetry.Tests/Internal/CircularBufferTest.cs +++ b/test/OpenTelemetry.Tests/Internal/CircularBufferTest.cs @@ -1,173 +1,159 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Xunit; -namespace OpenTelemetry.Internal.Tests +namespace OpenTelemetry.Internal.Tests; + +public class CircularBufferTest { - public class CircularBufferTest + [Fact] + public void CheckInvalidArgument() { - [Fact] - public void CheckInvalidArgument() - { - Assert.Throws(() => new CircularBuffer(0)); - } + Assert.Throws(() => new CircularBuffer(0)); + } - [Fact] - public void CheckCapacity() - { - int capacity = 1; - var circularBuffer = new CircularBuffer(capacity); + [Fact] + public void CheckCapacity() + { + int capacity = 1; + var circularBuffer = new CircularBuffer(capacity); - Assert.Equal(capacity, circularBuffer.Capacity); - } + Assert.Equal(capacity, circularBuffer.Capacity); + } - [Fact] - public void CheckNullValueWhenAdding() - { - int capacity = 1; - var circularBuffer = new CircularBuffer(capacity); - Assert.Throws(() => circularBuffer.Add(null)); - } + [Fact] + public void CheckNullValueWhenAdding() + { + int capacity = 1; + var circularBuffer = new CircularBuffer(capacity); + Assert.Throws(() => circularBuffer.Add(null)); + } - [Fact] - public void CheckValueWhenAdding() - { - int capacity = 1; - var circularBuffer = new CircularBuffer(capacity); - var result = circularBuffer.Add("a"); - Assert.True(result); - Assert.Equal(1, circularBuffer.AddedCount); - Assert.Equal(1, circularBuffer.Count); - } + [Fact] + public void CheckValueWhenAdding() + { + int capacity = 1; + var circularBuffer = new CircularBuffer(capacity); + var result = circularBuffer.Add("a"); + Assert.True(result); + Assert.Equal(1, circularBuffer.AddedCount); + Assert.Equal(1, circularBuffer.Count); + } - [Fact] - public void CheckBufferFull() - { - int capacity = 1; - var circularBuffer = new CircularBuffer(capacity); - var result = circularBuffer.Add("a"); - Assert.True(result); - Assert.Equal(1, circularBuffer.AddedCount); - Assert.Equal(1, circularBuffer.Count); - - result = circularBuffer.Add("b"); - Assert.False(result); - Assert.Equal(1, circularBuffer.AddedCount); - Assert.Equal(1, circularBuffer.Count); - } + [Fact] + public void CheckBufferFull() + { + int capacity = 1; + var circularBuffer = new CircularBuffer(capacity); + var result = circularBuffer.Add("a"); + Assert.True(result); + Assert.Equal(1, circularBuffer.AddedCount); + Assert.Equal(1, circularBuffer.Count); + + result = circularBuffer.Add("b"); + Assert.False(result); + Assert.Equal(1, circularBuffer.AddedCount); + Assert.Equal(1, circularBuffer.Count); + } - [Fact] - public void CheckRead() - { - string value = "a"; - int capacity = 1; - var circularBuffer = new CircularBuffer(capacity); - var result = circularBuffer.Add(value); - Assert.True(result); - Assert.Equal(1, circularBuffer.AddedCount); - Assert.Equal(1, circularBuffer.Count); - - string read = circularBuffer.Read(); - Assert.Equal(value, read); - Assert.Equal(1, circularBuffer.AddedCount); - Assert.Equal(1, circularBuffer.RemovedCount); - Assert.Equal(0, circularBuffer.Count); - } + [Fact] + public void CheckRead() + { + string value = "a"; + int capacity = 1; + var circularBuffer = new CircularBuffer(capacity); + var result = circularBuffer.Add(value); + Assert.True(result); + Assert.Equal(1, circularBuffer.AddedCount); + Assert.Equal(1, circularBuffer.Count); + + string read = circularBuffer.Read(); + Assert.Equal(value, read); + Assert.Equal(1, circularBuffer.AddedCount); + Assert.Equal(1, circularBuffer.RemovedCount); + Assert.Equal(0, circularBuffer.Count); + } + + [Fact] + public void CheckAddedCountAndCount() + { + int capacity = 2; + var circularBuffer = new CircularBuffer(capacity); + var result = circularBuffer.Add("a"); + Assert.True(result); + Assert.Equal(1, circularBuffer.AddedCount); + Assert.Equal(1, circularBuffer.Count); + + result = circularBuffer.Add("a"); + Assert.True(result); + Assert.Equal(2, circularBuffer.AddedCount); + Assert.Equal(2, circularBuffer.Count); + + _ = circularBuffer.Read(); + Assert.Equal(2, circularBuffer.AddedCount); + Assert.Equal(1, circularBuffer.RemovedCount); + Assert.Equal(1, circularBuffer.Count); + } - [Fact] - public void CheckAddedCountAndCount() + [Fact] + public async Task CpuPressureTest() + { + if (Environment.ProcessorCount < 2) { - int capacity = 2; - var circularBuffer = new CircularBuffer(capacity); - var result = circularBuffer.Add("a"); - Assert.True(result); - Assert.Equal(1, circularBuffer.AddedCount); - Assert.Equal(1, circularBuffer.Count); - - result = circularBuffer.Add("a"); - Assert.True(result); - Assert.Equal(2, circularBuffer.AddedCount); - Assert.Equal(2, circularBuffer.Count); - - _ = circularBuffer.Read(); - Assert.Equal(2, circularBuffer.AddedCount); - Assert.Equal(1, circularBuffer.RemovedCount); - Assert.Equal(1, circularBuffer.Count); + return; } - [Fact] - public async Task CpuPressureTest() - { - if (Environment.ProcessorCount < 2) - { - return; - } + var circularBuffer = new CircularBuffer(2048); - var circularBuffer = new CircularBuffer(2048); + List tasks = new(); - List tasks = new(); + int numberOfItemsPerWorker = 100_000; - int numberOfItemsPerWorker = 100_000; + for (int i = 0; i < Environment.ProcessorCount; i++) + { + int tid = i; - for (int i = 0; i < Environment.ProcessorCount; i++) + tasks.Add(Task.Run(async () => { - int tid = i; + await Task.Delay(2000); - tasks.Add(Task.Run(async () => + if (tid == 0) { - await Task.Delay(2000).ConfigureAwait(false); - - if (tid == 0) + for (int i = 0; i < numberOfItemsPerWorker * (Environment.ProcessorCount - 1); i++) { - for (int i = 0; i < numberOfItemsPerWorker * (Environment.ProcessorCount - 1); i++) + SpinWait wait = default; + while (true) { - SpinWait wait = default; - while (true) + if (circularBuffer.Count > 0) { - if (circularBuffer.Count > 0) - { - circularBuffer.Read(); - break; - } - - wait.SpinOnce(); + circularBuffer.Read(); + break; } + + wait.SpinOnce(); } } - else + } + else + { + for (int i = 0; i < numberOfItemsPerWorker; i++) { - for (int i = 0; i < numberOfItemsPerWorker; i++) + SpinWait wait = default; + while (true) { - SpinWait wait = default; - while (true) + if (circularBuffer.Add("item")) { - if (circularBuffer.Add("item")) - { - break; - } - - wait.SpinOnce(); + break; } + + wait.SpinOnce(); } } - })); - } - - await Task.WhenAll(tasks).ConfigureAwait(false); + } + })); } + + await Task.WhenAll(tasks); } } diff --git a/test/OpenTelemetry.Tests/Internal/MathHelperTest.cs b/test/OpenTelemetry.Tests/Internal/MathHelperTest.cs index 35725d5bdb3..a0d1c6fa9fc 100644 --- a/test/OpenTelemetry.Tests/Internal/MathHelperTest.cs +++ b/test/OpenTelemetry.Tests/Internal/MathHelperTest.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Xunit; diff --git a/test/OpenTelemetry.Tests/Internal/PeriodicExportingMetricReaderHelperTests.cs b/test/OpenTelemetry.Tests/Internal/PeriodicExportingMetricReaderHelperTests.cs index e206433e40d..4b8847a17c3 100644 --- a/test/OpenTelemetry.Tests/Internal/PeriodicExportingMetricReaderHelperTests.cs +++ b/test/OpenTelemetry.Tests/Internal/PeriodicExportingMetricReaderHelperTests.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #nullable enable @@ -21,133 +8,132 @@ using OpenTelemetry.Metrics; using Xunit; -namespace OpenTelemetry.Internal.Tests +namespace OpenTelemetry.Internal.Tests; + +public sealed class PeriodicExportingMetricReaderHelperTests : IDisposable { - public sealed class PeriodicExportingMetricReaderHelperTests : IDisposable + public PeriodicExportingMetricReaderHelperTests() { - public PeriodicExportingMetricReaderHelperTests() - { - ClearEnvVars(); - } + ClearEnvVars(); + } - public void Dispose() - { - ClearEnvVars(); - } + public void Dispose() + { + ClearEnvVars(); + } - [Fact] - public void CreatePeriodicExportingMetricReader_Defaults() - { - var reader = CreatePeriodicExportingMetricReader(); + [Fact] + public void CreatePeriodicExportingMetricReader_Defaults() + { + var reader = CreatePeriodicExportingMetricReader(); - Assert.Equal(60000, reader.ExportIntervalMilliseconds); - Assert.Equal(30000, reader.ExportTimeoutMilliseconds); - Assert.Equal(MetricReaderTemporalityPreference.Cumulative, reader.TemporalityPreference); - } + Assert.Equal(60000, reader.ExportIntervalMilliseconds); + Assert.Equal(30000, reader.ExportTimeoutMilliseconds); + Assert.Equal(MetricReaderTemporalityPreference.Cumulative, reader.TemporalityPreference); + } - [Fact] - public void CreatePeriodicExportingMetricReader_TemporalityPreference_FromOptions() + [Fact] + public void CreatePeriodicExportingMetricReader_TemporalityPreference_FromOptions() + { + var value = MetricReaderTemporalityPreference.Delta; + var reader = CreatePeriodicExportingMetricReader(new() { - var value = MetricReaderTemporalityPreference.Delta; - var reader = CreatePeriodicExportingMetricReader(new() - { - TemporalityPreference = value, - }); + TemporalityPreference = value, + }); - Assert.Equal(value, reader.TemporalityPreference); - } + Assert.Equal(value, reader.TemporalityPreference); + } - [Fact] - public void CreatePeriodicExportingMetricReader_ExportIntervalMilliseconds_FromOptions() + [Fact] + public void CreatePeriodicExportingMetricReader_ExportIntervalMilliseconds_FromOptions() + { + Environment.SetEnvironmentVariable(PeriodicExportingMetricReaderOptions.OTelMetricExportIntervalEnvVarKey, "88888"); // should be ignored, as value set via options has higher priority + var value = 123; + var reader = CreatePeriodicExportingMetricReader(new() { - Environment.SetEnvironmentVariable(PeriodicExportingMetricReaderOptions.OTelMetricExportIntervalEnvVarKey, "88888"); // should be ignored, as value set via options has higher priority - var value = 123; - var reader = CreatePeriodicExportingMetricReader(new() + PeriodicExportingMetricReaderOptions = new() { - PeriodicExportingMetricReaderOptions = new() - { - ExportIntervalMilliseconds = value, - }, - }); + ExportIntervalMilliseconds = value, + }, + }); - Assert.Equal(value, reader.ExportIntervalMilliseconds); - } + Assert.Equal(value, reader.ExportIntervalMilliseconds); + } - [Fact] - public void CreatePeriodicExportingMetricReader_ExportTimeoutMilliseconds_FromOptions() + [Fact] + public void CreatePeriodicExportingMetricReader_ExportTimeoutMilliseconds_FromOptions() + { + Environment.SetEnvironmentVariable(PeriodicExportingMetricReaderOptions.OTelMetricExportTimeoutEnvVarKey, "99999"); // should be ignored, as value set via options has higher priority + var value = 456; + var reader = CreatePeriodicExportingMetricReader(new() { - Environment.SetEnvironmentVariable(PeriodicExportingMetricReaderOptions.OTelMetricExportTimeoutEnvVarKey, "99999"); // should be ignored, as value set via options has higher priority - var value = 456; - var reader = CreatePeriodicExportingMetricReader(new() + PeriodicExportingMetricReaderOptions = new() { - PeriodicExportingMetricReaderOptions = new() - { - ExportTimeoutMilliseconds = value, - }, - }); + ExportTimeoutMilliseconds = value, + }, + }); - Assert.Equal(value, reader.ExportTimeoutMilliseconds); - } + Assert.Equal(value, reader.ExportTimeoutMilliseconds); + } - [Fact] - public void CreatePeriodicExportingMetricReader_ExportIntervalMilliseconds_FromEnvVar() - { - var value = 789; - Environment.SetEnvironmentVariable(PeriodicExportingMetricReaderOptions.OTelMetricExportIntervalEnvVarKey, value.ToString()); - var reader = CreatePeriodicExportingMetricReader(); + [Fact] + public void CreatePeriodicExportingMetricReader_ExportIntervalMilliseconds_FromEnvVar() + { + var value = 789; + Environment.SetEnvironmentVariable(PeriodicExportingMetricReaderOptions.OTelMetricExportIntervalEnvVarKey, value.ToString()); + var reader = CreatePeriodicExportingMetricReader(); - Assert.Equal(value, reader.ExportIntervalMilliseconds); - } + Assert.Equal(value, reader.ExportIntervalMilliseconds); + } - [Fact] - public void CreatePeriodicExportingMetricReader_ExportTimeoutMilliseconds_FromEnvVar() - { - var value = 246; - Environment.SetEnvironmentVariable(PeriodicExportingMetricReaderOptions.OTelMetricExportTimeoutEnvVarKey, value.ToString()); - var reader = CreatePeriodicExportingMetricReader(); + [Fact] + public void CreatePeriodicExportingMetricReader_ExportTimeoutMilliseconds_FromEnvVar() + { + var value = 246; + Environment.SetEnvironmentVariable(PeriodicExportingMetricReaderOptions.OTelMetricExportTimeoutEnvVarKey, value.ToString()); + var reader = CreatePeriodicExportingMetricReader(); - Assert.Equal(value, reader.ExportTimeoutMilliseconds); - } + Assert.Equal(value, reader.ExportTimeoutMilliseconds); + } - [Fact] - public void CreatePeriodicExportingMetricReader_FromIConfiguration() + [Fact] + public void CreatePeriodicExportingMetricReader_FromIConfiguration() + { + var values = new Dictionary() { - var values = new Dictionary() - { - [PeriodicExportingMetricReaderOptions.OTelMetricExportIntervalEnvVarKey] = "18", - [PeriodicExportingMetricReaderOptions.OTelMetricExportTimeoutEnvVarKey] = "19", - }; + [PeriodicExportingMetricReaderOptions.OTelMetricExportIntervalEnvVarKey] = "18", + [PeriodicExportingMetricReaderOptions.OTelMetricExportTimeoutEnvVarKey] = "19", + }; - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(values) - .Build(); + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(values) + .Build(); - var options = new MetricReaderOptions(configuration); + var options = new MetricReaderOptions(configuration); - Assert.Equal(18, options.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds); - Assert.Equal(19, options.PeriodicExportingMetricReaderOptions.ExportTimeoutMilliseconds); - } + Assert.Equal(18, options.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds); + Assert.Equal(19, options.PeriodicExportingMetricReaderOptions.ExportTimeoutMilliseconds); + } - [Fact] - public void EnvironmentVariableNames() - { - Assert.Equal("OTEL_METRIC_EXPORT_INTERVAL", PeriodicExportingMetricReaderOptions.OTelMetricExportIntervalEnvVarKey); - Assert.Equal("OTEL_METRIC_EXPORT_TIMEOUT", PeriodicExportingMetricReaderOptions.OTelMetricExportTimeoutEnvVarKey); - } + [Fact] + public void EnvironmentVariableNames() + { + Assert.Equal("OTEL_METRIC_EXPORT_INTERVAL", PeriodicExportingMetricReaderOptions.OTelMetricExportIntervalEnvVarKey); + Assert.Equal("OTEL_METRIC_EXPORT_TIMEOUT", PeriodicExportingMetricReaderOptions.OTelMetricExportTimeoutEnvVarKey); + } - private static void ClearEnvVars() - { - Environment.SetEnvironmentVariable(PeriodicExportingMetricReaderOptions.OTelMetricExportIntervalEnvVarKey, null); - Environment.SetEnvironmentVariable(PeriodicExportingMetricReaderOptions.OTelMetricExportTimeoutEnvVarKey, null); - } + private static void ClearEnvVars() + { + Environment.SetEnvironmentVariable(PeriodicExportingMetricReaderOptions.OTelMetricExportIntervalEnvVarKey, null); + Environment.SetEnvironmentVariable(PeriodicExportingMetricReaderOptions.OTelMetricExportTimeoutEnvVarKey, null); + } - private static PeriodicExportingMetricReader CreatePeriodicExportingMetricReader( - MetricReaderOptions? options = null) - { - options ??= new(); + private static PeriodicExportingMetricReader CreatePeriodicExportingMetricReader( + MetricReaderOptions? options = null) + { + options ??= new(); - var dummyMetricExporter = new InMemoryExporter(Array.Empty()); - return PeriodicExportingMetricReaderHelper.CreatePeriodicExportingMetricReader(dummyMetricExporter, options); - } + var dummyMetricExporter = new InMemoryExporter(Array.Empty()); + return PeriodicExportingMetricReaderHelper.CreatePeriodicExportingMetricReader(dummyMetricExporter, options); } } diff --git a/test/OpenTelemetry.Tests/Internal/PooledListTest.cs b/test/OpenTelemetry.Tests/Internal/PooledListTest.cs index 44f917899f0..eccbef450ad 100644 --- a/test/OpenTelemetry.Tests/Internal/PooledListTest.cs +++ b/test/OpenTelemetry.Tests/Internal/PooledListTest.cs @@ -1,112 +1,98 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Collections; using System.Reflection; using Xunit; -namespace OpenTelemetry.Internal.Tests +namespace OpenTelemetry.Internal.Tests; + +public class PooledListTest { - public class PooledListTest + [Fact] + public void Verify_ICollectionExplicitProperties() { - [Fact] - public void Verify_ICollectionExplicitProperties() - { - var pooledList = PooledList.Create(); - var icollection = (ICollection)pooledList; - Assert.False(icollection.IsSynchronized); - Assert.Equal(icollection, icollection.SyncRoot); - } + var pooledList = PooledList.Create(); + var icollection = (ICollection)pooledList; + Assert.False(icollection.IsSynchronized); + Assert.Equal(icollection, icollection.SyncRoot); + } - [Fact] - public void Verify_CreateAddClear() - { - var pooledList = PooledList.Create(); - Assert.Empty(pooledList); - Assert.True(pooledList.IsEmpty); + [Fact] + public void Verify_CreateAddClear() + { + var pooledList = PooledList.Create(); + Assert.Empty(pooledList); + Assert.True(pooledList.IsEmpty); - PooledList.Add(ref pooledList, 1); - Assert.Single(pooledList); - Assert.False(pooledList.IsEmpty); + PooledList.Add(ref pooledList, 1); + Assert.Single(pooledList); + Assert.False(pooledList.IsEmpty); - PooledList.Add(ref pooledList, 2); - Assert.Equal(2, pooledList.Count); + PooledList.Add(ref pooledList, 2); + Assert.Equal(2, pooledList.Count); - Assert.Equal(1, pooledList[0]); - Assert.Equal(2, pooledList[1]); + Assert.Equal(1, pooledList[0]); + Assert.Equal(2, pooledList[1]); - PooledList.Clear(ref pooledList); - Assert.Empty(pooledList); - Assert.True(pooledList.IsEmpty); - } + PooledList.Clear(ref pooledList); + Assert.Empty(pooledList); + Assert.True(pooledList.IsEmpty); + } - [Fact] - public void Verify_AllocatedSize() + [Fact] + public void Verify_AllocatedSize() + { + int GetLastAllocatedSize(PooledList pooledList) { - int GetLastAllocatedSize(PooledList pooledList) - { - var value = typeof(PooledList) - .GetField("lastAllocatedSize", BindingFlags.NonPublic | BindingFlags.Static) - .GetValue(pooledList); - return (int)value; - } - - var pooledList = PooledList.Create(); - - var size = GetLastAllocatedSize(pooledList); - Assert.Equal(64, size); - - // The Add() method has a condition to double the size of the buffer - // when the Count exceeds the buffer size. - // This for loop is meant to trigger that condition. - for (int i = 0; i <= size; i++) - { - PooledList.Add(ref pooledList, i); - } - - size = GetLastAllocatedSize(pooledList); - Assert.Equal(128, size); + var value = typeof(PooledList) + .GetField("lastAllocatedSize", BindingFlags.NonPublic | BindingFlags.Static) + .GetValue(pooledList); + return (int)value; } - [Fact] - public void Verify_Enumerator() + var pooledList = PooledList.Create(); + + var size = GetLastAllocatedSize(pooledList); + Assert.Equal(64, size); + + // The Add() method has a condition to double the size of the buffer + // when the Count exceeds the buffer size. + // This for loop is meant to trigger that condition. + for (int i = 0; i <= size; i++) { - var pooledList = PooledList.Create(); - PooledList.Add(ref pooledList, 1); - PooledList.Add(ref pooledList, 2); - PooledList.Add(ref pooledList, 3); + PooledList.Add(ref pooledList, i); + } + + size = GetLastAllocatedSize(pooledList); + Assert.Equal(128, size); + } + + [Fact] + public void Verify_Enumerator() + { + var pooledList = PooledList.Create(); + PooledList.Add(ref pooledList, 1); + PooledList.Add(ref pooledList, 2); + PooledList.Add(ref pooledList, 3); - var enumerator = pooledList.GetEnumerator(); + var enumerator = pooledList.GetEnumerator(); - Assert.Equal(default, enumerator.Current); + Assert.Equal(default, enumerator.Current); - Assert.True(enumerator.MoveNext()); - Assert.Equal(1, enumerator.Current); - Assert.True(enumerator.MoveNext()); - Assert.Equal(2, enumerator.Current); - Assert.True(enumerator.MoveNext()); - Assert.Equal(3, enumerator.Current); + Assert.True(enumerator.MoveNext()); + Assert.Equal(1, enumerator.Current); + Assert.True(enumerator.MoveNext()); + Assert.Equal(2, enumerator.Current); + Assert.True(enumerator.MoveNext()); + Assert.Equal(3, enumerator.Current); - Assert.False(enumerator.MoveNext()); - Assert.Equal(default, enumerator.Current); + Assert.False(enumerator.MoveNext()); + Assert.Equal(default, enumerator.Current); - var ienumerator = (IEnumerator)enumerator; - ienumerator.Reset(); - Assert.Equal(default, enumerator.Current); - } + var ienumerator = (IEnumerator)enumerator; + ienumerator.Reset(); + Assert.Equal(default, enumerator.Current); } } diff --git a/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigParserTest.cs b/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigParserTest.cs index ec80df5c24c..9249728690d 100644 --- a/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigParserTest.cs +++ b/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigParserTest.cs @@ -1,89 +1,75 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Xunit; -namespace OpenTelemetry.Internal.Tests +namespace OpenTelemetry.Internal.Tests; + +public class SelfDiagnosticsConfigParserTest { - public class SelfDiagnosticsConfigParserTest + [Fact] + public void SelfDiagnosticsConfigParser_TryParseFilePath_Success() { - [Fact] - public void SelfDiagnosticsConfigParser_TryParseFilePath_Success() - { - string configJson = "{ \t \n " - + "\t \"LogDirectory\" \t : \"Diagnostics\", \n" - + "FileSize \t : \t \n" - + " 1024 \n}\n"; - Assert.True(SelfDiagnosticsConfigParser.TryParseLogDirectory(configJson, out string logDirectory)); - Assert.Equal("Diagnostics", logDirectory); - } + string configJson = "{ \t \n " + + "\t \"LogDirectory\" \t : \"Diagnostics\", \n" + + "FileSize \t : \t \n" + + " 1024 \n}\n"; + Assert.True(SelfDiagnosticsConfigParser.TryParseLogDirectory(configJson, out string logDirectory)); + Assert.Equal("Diagnostics", logDirectory); + } - [Fact] - public void SelfDiagnosticsConfigParser_TryParseFilePath_MissingField() - { - string configJson = @"{ + [Fact] + public void SelfDiagnosticsConfigParser_TryParseFilePath_MissingField() + { + string configJson = @"{ ""path"": ""Diagnostics"", ""FileSize"": 1024 }"; - Assert.False(SelfDiagnosticsConfigParser.TryParseLogDirectory(configJson, out _)); - } + Assert.False(SelfDiagnosticsConfigParser.TryParseLogDirectory(configJson, out _)); + } - [Fact] - public void SelfDiagnosticsConfigParser_TryParseFileSize() - { - string configJson = @"{ + [Fact] + public void SelfDiagnosticsConfigParser_TryParseFileSize() + { + string configJson = @"{ ""LogDirectory"": ""Diagnostics"", ""FileSize"": 1024 }"; - Assert.True(SelfDiagnosticsConfigParser.TryParseFileSize(configJson, out int fileSize)); - Assert.Equal(1024, fileSize); - } + Assert.True(SelfDiagnosticsConfigParser.TryParseFileSize(configJson, out int fileSize)); + Assert.Equal(1024, fileSize); + } - [Fact] - public void SelfDiagnosticsConfigParser_TryParseFileSize_CaseInsensitive() - { - string configJson = @"{ + [Fact] + public void SelfDiagnosticsConfigParser_TryParseFileSize_CaseInsensitive() + { + string configJson = @"{ ""LogDirectory"": ""Diagnostics"", ""fileSize"" : 2048 }"; - Assert.True(SelfDiagnosticsConfigParser.TryParseFileSize(configJson, out int fileSize)); - Assert.Equal(2048, fileSize); - } + Assert.True(SelfDiagnosticsConfigParser.TryParseFileSize(configJson, out int fileSize)); + Assert.Equal(2048, fileSize); + } - [Fact] - public void SelfDiagnosticsConfigParser_TryParseFileSize_MissingField() - { - string configJson = @"{ + [Fact] + public void SelfDiagnosticsConfigParser_TryParseFileSize_MissingField() + { + string configJson = @"{ ""LogDirectory"": ""Diagnostics"", ""size"": 1024 }"; - Assert.False(SelfDiagnosticsConfigParser.TryParseFileSize(configJson, out _)); - } + Assert.False(SelfDiagnosticsConfigParser.TryParseFileSize(configJson, out _)); + } - [Fact] - public void SelfDiagnosticsConfigParser_TryParseLogLevel() - { - string configJson = @"{ + [Fact] + public void SelfDiagnosticsConfigParser_TryParseLogLevel() + { + string configJson = @"{ ""LogDirectory"": ""Diagnostics"", ""FileSize"": 1024, ""LogLevel"": ""Error"" }"; - Assert.True(SelfDiagnosticsConfigParser.TryParseLogLevel(configJson, out string logLevelString)); - Assert.Equal("Error", logLevelString); - } + Assert.True(SelfDiagnosticsConfigParser.TryParseLogLevel(configJson, out string logLevelString)); + Assert.Equal("Error", logLevelString); } } diff --git a/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigRefresherTest.cs b/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigRefresherTest.cs index 92ac0700826..10c0be18fa2 100644 --- a/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigRefresherTest.cs +++ b/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigRefresherTest.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #nullable enable @@ -22,117 +9,116 @@ using Xunit; using Xunit.Abstractions; -namespace OpenTelemetry.Internal.Tests +namespace OpenTelemetry.Internal.Tests; + +public class SelfDiagnosticsConfigRefresherTest { - public class SelfDiagnosticsConfigRefresherTest - { - private static readonly string ConfigFilePath = SelfDiagnosticsConfigParser.ConfigFileName; - private static readonly byte[] MessageOnNewFile = SelfDiagnosticsConfigRefresher.MessageOnNewFile; - private static readonly string MessageOnNewFileString = Encoding.UTF8.GetString(SelfDiagnosticsConfigRefresher.MessageOnNewFile); + private static readonly string ConfigFilePath = SelfDiagnosticsConfigParser.ConfigFileName; + private static readonly byte[] MessageOnNewFile = SelfDiagnosticsConfigRefresher.MessageOnNewFile; + private static readonly string MessageOnNewFileString = Encoding.UTF8.GetString(SelfDiagnosticsConfigRefresher.MessageOnNewFile); - private readonly ITestOutputHelper output; + private readonly ITestOutputHelper output; - public SelfDiagnosticsConfigRefresherTest(ITestOutputHelper output) - { - this.output = output; - } + public SelfDiagnosticsConfigRefresherTest(ITestOutputHelper output) + { + this.output = output; + } - [Fact] - public void SelfDiagnosticsConfigRefresher_OmitAsConfigured() + [Fact] + public void SelfDiagnosticsConfigRefresher_OmitAsConfigured() + { + try { - try - { - string logDirectory = Utils.GetCurrentMethodName(); - CreateConfigFile(logDirectory); - using var configRefresher = new SelfDiagnosticsConfigRefresher(); - - // Emitting event of EventLevel.Warning - OpenTelemetrySdkEventSource.Log.ObservableInstrumentCallbackException("exception"); - - int bufferSize = 512; - byte[] actualBytes = ReadFile(logDirectory, bufferSize); - string logText = Encoding.UTF8.GetString(actualBytes); - this.output.WriteLine(logText); // for debugging in case the test fails - Assert.StartsWith(MessageOnNewFileString, logText); - - // The event was omitted - Assert.Equal('\0', (char)actualBytes[MessageOnNewFile.Length]); - } - finally - { - CleanupConfigFile(); - } - } + string logDirectory = Utils.GetCurrentMethodName(); + CreateConfigFile(logDirectory); + using var configRefresher = new SelfDiagnosticsConfigRefresher(); + + // Emitting event of EventLevel.Warning + OpenTelemetrySdkEventSource.Log.ObservableInstrumentCallbackException("exception"); - [Fact] - public void SelfDiagnosticsConfigRefresher_CaptureAsConfigured() + int bufferSize = 512; + byte[] actualBytes = ReadFile(logDirectory, bufferSize); + string logText = Encoding.UTF8.GetString(actualBytes); + this.output.WriteLine(logText); // for debugging in case the test fails + Assert.StartsWith(MessageOnNewFileString, logText); + + // The event was omitted + Assert.Equal('\0', (char)actualBytes[MessageOnNewFile.Length]); + } + finally { - try - { - string logDirectory = Utils.GetCurrentMethodName(); - CreateConfigFile(logDirectory); - using var configRefresher = new SelfDiagnosticsConfigRefresher(); - - // Emitting event of EventLevel.Error - OpenTelemetrySdkEventSource.Log.TracerProviderException("Event string sample", "Exception string sample"); - string expectedMessage = "Unknown error in TracerProvider '{0}': '{1}'.{Event string sample}{Exception string sample}"; - - int bufferSize = 2 * (MessageOnNewFileString.Length + expectedMessage.Length); - byte[] actualBytes = ReadFile(logDirectory, bufferSize); - string logText = Encoding.UTF8.GetString(actualBytes); - Assert.StartsWith(MessageOnNewFileString, logText); - - // The event was captured - string logLine = logText.Substring(MessageOnNewFileString.Length); - string logMessage = ParseLogMessage(logLine); - Assert.StartsWith(expectedMessage, logMessage); - } - finally - { - CleanupConfigFile(); - } + CleanupConfigFile(); } + } - private static string ParseLogMessage(string logLine) + [Fact] + public void SelfDiagnosticsConfigRefresher_CaptureAsConfigured() + { + try { - int timestampPrefixLength = "2020-08-14T20:33:24.4788109Z:".Length; - Assert.Matches(@"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{7}Z:", logLine.Substring(0, timestampPrefixLength)); - return logLine.Substring(timestampPrefixLength); + string logDirectory = Utils.GetCurrentMethodName(); + CreateConfigFile(logDirectory); + using var configRefresher = new SelfDiagnosticsConfigRefresher(); + + // Emitting event of EventLevel.Error + OpenTelemetrySdkEventSource.Log.TracerProviderException("Event string sample", "Exception string sample"); + string expectedMessage = "Unknown error in TracerProvider '{0}': '{1}'.{Event string sample}{Exception string sample}"; + + int bufferSize = 2 * (MessageOnNewFileString.Length + expectedMessage.Length); + byte[] actualBytes = ReadFile(logDirectory, bufferSize); + string logText = Encoding.UTF8.GetString(actualBytes); + Assert.StartsWith(MessageOnNewFileString, logText); + + // The event was captured + string logLine = logText.Substring(MessageOnNewFileString.Length); + string logMessage = ParseLogMessage(logLine); + Assert.StartsWith(expectedMessage, logMessage); } - - private static byte[] ReadFile(string logDirectory, int byteCount) + finally { - var outputFileName = Path.GetFileName(Process.GetCurrentProcess().MainModule?.FileName) + "." - + Process.GetCurrentProcess().Id + ".log"; - var outputFilePath = Path.Combine(logDirectory, outputFileName); - using var file = File.Open(outputFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - byte[] actualBytes = new byte[byteCount]; - _ = file.Read(actualBytes, 0, byteCount); - return actualBytes; + CleanupConfigFile(); } + } - private static void CreateConfigFile(string logDirectory) - { - string configJson = $@"{{ + private static string ParseLogMessage(string logLine) + { + int timestampPrefixLength = "2020-08-14T20:33:24.4788109Z:".Length; + Assert.Matches(@"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{7}Z:", logLine.Substring(0, timestampPrefixLength)); + return logLine.Substring(timestampPrefixLength); + } + + private static byte[] ReadFile(string logDirectory, int byteCount) + { + var outputFileName = Path.GetFileName(Process.GetCurrentProcess().MainModule?.FileName) + "." + + Process.GetCurrentProcess().Id + ".log"; + var outputFilePath = Path.Combine(logDirectory, outputFileName); + using var file = File.Open(outputFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + byte[] actualBytes = new byte[byteCount]; + _ = file.Read(actualBytes, 0, byteCount); + return actualBytes; + } + + private static void CreateConfigFile(string logDirectory) + { + string configJson = $@"{{ ""LogDirectory"": ""{logDirectory}"", ""FileSize"": 1024, ""LogLevel"": ""Error"" }}"; - using FileStream file = File.Open(ConfigFilePath, FileMode.Create, FileAccess.Write); - byte[] configBytes = Encoding.UTF8.GetBytes(configJson); - file.Write(configBytes, 0, configBytes.Length); - } + using FileStream file = File.Open(ConfigFilePath, FileMode.Create, FileAccess.Write); + byte[] configBytes = Encoding.UTF8.GetBytes(configJson); + file.Write(configBytes, 0, configBytes.Length); + } - private static void CleanupConfigFile() + private static void CleanupConfigFile() + { + try + { + File.Delete(ConfigFilePath); + } + catch { - try - { - File.Delete(ConfigFilePath); - } - catch - { - // ignore any exceptions while removing files - } + // ignore any exceptions while removing files } } } diff --git a/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsEventListenerTest.cs b/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsEventListenerTest.cs index ce0b15d1d4a..58777971b5d 100644 --- a/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsEventListenerTest.cs +++ b/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsEventListenerTest.cs @@ -1,305 +1,280 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Tracing; using System.IO.MemoryMappedFiles; using System.Text; -using Moq; +using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Internal.Tests +namespace OpenTelemetry.Internal.Tests; + +public class SelfDiagnosticsEventListenerTest { - public class SelfDiagnosticsEventListenerTest - { - private const string LOGFILEPATH = "Diagnostics.log"; - private const string Ellipses = "...\n"; - private const string EllipsesWithBrackets = "{...}\n"; + private const string LOGFILEPATH = "Diagnostics.log"; + private const string Ellipses = "...\n"; + private const string EllipsesWithBrackets = "{...}\n"; - [Fact] - public void SelfDiagnosticsEventListener_constructor_Invalid_Input() + [Fact] + public void SelfDiagnosticsEventListener_constructor_Invalid_Input() + { + // no configRefresher object + Assert.Throws(() => { - // no configRefresher object - Assert.Throws(() => - { - _ = new SelfDiagnosticsEventListener(EventLevel.Error, null); - }); - } + _ = new SelfDiagnosticsEventListener(EventLevel.Error, null); + }); + } - [Fact] - public void SelfDiagnosticsEventListener_EventSourceSetup_LowerSeverity() - { - var configRefresherMock = new Mock(); - var listener = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresherMock.Object); + [Fact] + public void SelfDiagnosticsEventListener_EventSourceSetup_LowerSeverity() + { + var configRefresher = new TestSelfDiagnosticsConfigRefresher(); + _ = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresher); - // Emitting a Verbose event. Or any EventSource event with lower severity than Error. - OpenTelemetrySdkEventSource.Log.ActivityStarted("Activity started", "1"); - configRefresherMock.Verify(refresher => refresher.TryGetLogStream(It.IsAny(), out It.Ref.IsAny, out It.Ref.IsAny), Times.Never()); - } + // Emitting a Verbose event. Or any EventSource event with lower severity than Error. + OpenTelemetrySdkEventSource.Log.ActivityStarted("Activity started", "1"); + Assert.False(configRefresher.TryGetLogStreamCalled); + } - [Fact] - public void SelfDiagnosticsEventListener_EventSourceSetup_HigherSeverity() - { - var configRefresherMock = new Mock(); - configRefresherMock.Setup(configRefresher => configRefresher.TryGetLogStream(It.IsAny(), out It.Ref.IsAny, out It.Ref.IsAny)) - .Returns(true); - var listener = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresherMock.Object); - - // Emitting an Error event. Or any EventSource event with higher than or equal to to Error severity. - OpenTelemetrySdkEventSource.Log.TracerProviderException("TestEvent", "Exception Details"); - configRefresherMock.Verify(refresher => refresher.TryGetLogStream(It.IsAny(), out It.Ref.IsAny, out It.Ref.IsAny)); - } + [Fact] + public void SelfDiagnosticsEventListener_EventSourceSetup_HigherSeverity() + { + var configRefresher = new TestSelfDiagnosticsConfigRefresher(); + _ = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresher); - [Fact] - public void SelfDiagnosticsEventListener_WriteEvent() - { - // Arrange - var configRefresherMock = new Mock(); - var memoryMappedFile = MemoryMappedFile.CreateFromFile(LOGFILEPATH, FileMode.Create, null, 1024); - Stream stream = memoryMappedFile.CreateViewStream(); - string eventMessage = "Event Message"; - int timestampPrefixLength = "2020-08-14T20:33:24.4788109Z:".Length; - byte[] bytes = Encoding.UTF8.GetBytes(eventMessage); - int availableByteCount = 100; - configRefresherMock.Setup(configRefresher => configRefresher.TryGetLogStream(timestampPrefixLength + bytes.Length + 1, out stream, out availableByteCount)) - .Returns(true); - var listener = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresherMock.Object); - - // Act: call WriteEvent method directly - listener.WriteEvent(eventMessage, null); - - // Assert - configRefresherMock.Verify(refresher => refresher.TryGetLogStream(timestampPrefixLength + bytes.Length + 1, out stream, out availableByteCount)); - stream.Dispose(); - memoryMappedFile.Dispose(); - AssertFileOutput(LOGFILEPATH, eventMessage); - } + // Emitting an Error event. Or any EventSource event with higher than or equal to to Error severity. + OpenTelemetrySdkEventSource.Log.TracerProviderException("TestEvent", "Exception Details"); + Assert.True(configRefresher.TryGetLogStreamCalled); + } - [Fact] - public void SelfDiagnosticsEventListener_DateTimeGetBytes() - { - var configRefresherMock = new Mock(); - var listener = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresherMock.Object); - - // Check DateTimeKind of Utc, Local, and Unspecified - DateTime[] datetimes = new DateTime[] - { - DateTime.SpecifyKind(DateTime.Parse("1996-12-01T14:02:31.1234567-08:00"), DateTimeKind.Utc), - DateTime.SpecifyKind(DateTime.Parse("1996-12-01T14:02:31.1234567-08:00"), DateTimeKind.Local), - DateTime.SpecifyKind(DateTime.Parse("1996-12-01T14:02:31.1234567-08:00"), DateTimeKind.Unspecified), - DateTime.UtcNow, - DateTime.Now, - }; - - // Expect to match output string from DateTime.ToString("O") - string[] expected = new string[datetimes.Length]; - for (int i = 0; i < datetimes.Length; i++) - { - expected[i] = datetimes[i].ToString("O"); - } - - byte[] buffer = new byte[40 * datetimes.Length]; - int pos = 0; - - // Get string after DateTimeGetBytes() write into a buffer - string[] results = new string[datetimes.Length]; - for (int i = 0; i < datetimes.Length; i++) - { - int len = listener.DateTimeGetBytes(datetimes[i], buffer, pos); - results[i] = Encoding.Default.GetString(buffer, pos, len); - pos += len; - } - - Assert.Equal(expected, results); - } + [Fact] + public void SelfDiagnosticsEventListener_WriteEvent() + { + // Arrange + var memoryMappedFile = MemoryMappedFile.CreateFromFile(LOGFILEPATH, FileMode.Create, null, 1024); + Stream stream = memoryMappedFile.CreateViewStream(); + var configRefresher = new TestSelfDiagnosticsConfigRefresher(stream); + string eventMessage = "Event Message"; + var listener = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresher); + + // Act: call WriteEvent method directly + listener.WriteEvent(eventMessage, null); + + // Assert + Assert.True(configRefresher.TryGetLogStreamCalled); + stream.Dispose(); + memoryMappedFile.Dispose(); + AssertFileOutput(LOGFILEPATH, eventMessage); + } - [Fact] - public void SelfDiagnosticsEventListener_EmitEvent_OmitAsConfigured() + [Fact] + public void SelfDiagnosticsEventListener_DateTimeGetBytes() + { + var configRefresher = new TestSelfDiagnosticsConfigRefresher(); + var listener = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresher); + + // Check DateTimeKind of Utc, Local, and Unspecified + DateTime[] datetimes = + [ + DateTime.SpecifyKind(DateTime.Parse("1996-12-01T14:02:31.1234567-08:00"), DateTimeKind.Utc), + DateTime.SpecifyKind(DateTime.Parse("1996-12-01T14:02:31.1234567-08:00"), DateTimeKind.Local), + DateTime.SpecifyKind(DateTime.Parse("1996-12-01T14:02:31.1234567-08:00"), DateTimeKind.Unspecified), + DateTime.UtcNow, + DateTime.Now, + ]; + + // Expect to match output string from DateTime.ToString("O") + string[] expected = new string[datetimes.Length]; + for (int i = 0; i < datetimes.Length; i++) { - // Arrange - var configRefresherMock = new Mock(); - var memoryMappedFile = MemoryMappedFile.CreateFromFile(LOGFILEPATH, FileMode.Create, null, 1024); - Stream stream = memoryMappedFile.CreateViewStream(); - configRefresherMock.Setup(configRefresher => configRefresher.TryGetLogStream(It.IsAny(), out stream, out It.Ref.IsAny)) - .Returns(true); - var listener = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresherMock.Object); - - // Act: emit an event with severity lower than configured - OpenTelemetrySdkEventSource.Log.ActivityStarted("ActivityStart", "123"); - - // Assert - configRefresherMock.Verify(refresher => refresher.TryGetLogStream(It.IsAny(), out stream, out It.Ref.IsAny), Times.Never()); - stream.Dispose(); - memoryMappedFile.Dispose(); - - using FileStream file = File.Open(LOGFILEPATH, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); - var buffer = new byte[256]; - file.Read(buffer, 0, buffer.Length); - Assert.Equal('\0', (char)buffer[0]); + expected[i] = datetimes[i].ToString("O"); } - [Fact] - public void SelfDiagnosticsEventListener_EmitEvent_CaptureAsConfigured() - { - // Arrange - var configRefresherMock = new Mock(); - var memoryMappedFile = MemoryMappedFile.CreateFromFile(LOGFILEPATH, FileMode.Create, null, 1024); - Stream stream = memoryMappedFile.CreateViewStream(); - configRefresherMock.Setup(configRefresher => configRefresher.TryGetLogStream(It.IsAny(), out stream, out It.Ref.IsAny)) - .Returns(true); - var listener = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresherMock.Object); - - // Act: emit an event with severity equal to configured - OpenTelemetrySdkEventSource.Log.TracerProviderException("TestEvent", "Exception Details"); - - // Assert - configRefresherMock.Verify(refresher => refresher.TryGetLogStream(It.IsAny(), out stream, out It.Ref.IsAny)); - stream.Dispose(); - memoryMappedFile.Dispose(); - - var expectedLog = "Unknown error in TracerProvider '{0}': '{1}'.{TestEvent}{Exception Details}"; - AssertFileOutput(LOGFILEPATH, expectedLog); - } + byte[] buffer = new byte[40 * datetimes.Length]; + int pos = 0; - [Fact] - public void SelfDiagnosticsEventListener_EncodeInBuffer_Null() + // Get string after DateTimeGetBytes() write into a buffer + string[] results = new string[datetimes.Length]; + for (int i = 0; i < datetimes.Length; i++) { - byte[] buffer = new byte[20]; - int startPos = 0; - int endPos = SelfDiagnosticsEventListener.EncodeInBuffer(null, false, buffer, startPos); - Assert.Equal(startPos, endPos); + int len = listener.DateTimeGetBytes(datetimes[i], buffer, pos); + results[i] = Encoding.Default.GetString(buffer, pos, len); + pos += len; } - [Fact] - public void SelfDiagnosticsEventListener_EncodeInBuffer_Empty() - { - byte[] buffer = new byte[20]; - int startPos = 0; - int endPos = SelfDiagnosticsEventListener.EncodeInBuffer(string.Empty, false, buffer, startPos); - byte[] expected = Encoding.UTF8.GetBytes(string.Empty); - AssertBufferOutput(expected, buffer, startPos, endPos); - } + Assert.Equal(expected, results); + } - [Fact] - public void SelfDiagnosticsEventListener_EncodeInBuffer_EnoughSpace() - { - byte[] buffer = new byte[20]; - int startPos = buffer.Length - Ellipses.Length - 6; // Just enough space for "abc" even if "...\n" needs to be added. - int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", false, buffer, startPos); - - // '\n' will be appended to the original string "abc" after EncodeInBuffer is called. - // The byte where '\n' will be placed should not be touched within EncodeInBuffer, so it stays as '\0'. - byte[] expected = Encoding.UTF8.GetBytes("abc\0"); - AssertBufferOutput(expected, buffer, startPos, endPos + 1); - } + [Fact] + public void SelfDiagnosticsEventListener_EmitEvent_OmitAsConfigured() + { + // Arrange + var configRefresher = new TestSelfDiagnosticsConfigRefresher(); + var memoryMappedFile = MemoryMappedFile.CreateFromFile(LOGFILEPATH, FileMode.Create, null, 1024); + Stream stream = memoryMappedFile.CreateViewStream(); + _ = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresher); + + // Act: emit an event with severity lower than configured + OpenTelemetrySdkEventSource.Log.ActivityStarted("ActivityStart", "123"); + + // Assert + Assert.False(configRefresher.TryGetLogStreamCalled); + stream.Dispose(); + memoryMappedFile.Dispose(); + + using FileStream file = File.Open(LOGFILEPATH, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); + var buffer = new byte[256]; + file.Read(buffer, 0, buffer.Length); + Assert.Equal('\0', (char)buffer[0]); + } - [Fact] - public void SelfDiagnosticsEventListener_EncodeInBuffer_NotEnoughSpaceForFullString() - { - byte[] buffer = new byte[20]; - int startPos = buffer.Length - Ellipses.Length - 5; // Just not space for "abc" if "...\n" needs to be added. - - // It's a quick estimate by assumption that most Unicode characters takes up to 2 16-bit UTF-16 chars, - // which can be up to 4 bytes when encoded in UTF-8. - int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", false, buffer, startPos); - byte[] expected = Encoding.UTF8.GetBytes("ab...\0"); - AssertBufferOutput(expected, buffer, startPos, endPos + 1); - } + [Fact] + public void SelfDiagnosticsEventListener_EmitEvent_CaptureAsConfigured() + { + // Arrange + var memoryMappedFile = MemoryMappedFile.CreateFromFile(LOGFILEPATH, FileMode.Create, null, 1024); + Stream stream = memoryMappedFile.CreateViewStream(); + var configRefresher = new TestSelfDiagnosticsConfigRefresher(stream); + _ = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresher); + + // Act: emit an event with severity equal to configured + OpenTelemetrySdkEventSource.Log.TracerProviderException("TestEvent", "Exception Details"); + + // Assert + Assert.True(configRefresher.TryGetLogStreamCalled); + stream.Dispose(); + memoryMappedFile.Dispose(); + + var expectedLog = "Unknown error in TracerProvider '{0}': '{1}'.{TestEvent}{Exception Details}"; + AssertFileOutput(LOGFILEPATH, expectedLog); + } - [Fact] - public void SelfDiagnosticsEventListener_EncodeInBuffer_NotEvenSpaceForTruncatedString() - { - byte[] buffer = new byte[20]; - int startPos = buffer.Length - Ellipses.Length; // Just enough space for "...\n". - int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", false, buffer, startPos); - byte[] expected = Encoding.UTF8.GetBytes("...\0"); - AssertBufferOutput(expected, buffer, startPos, endPos + 1); - } + [Fact] + public void SelfDiagnosticsEventListener_EncodeInBuffer_Null() + { + byte[] buffer = new byte[20]; + int startPos = 0; + int endPos = SelfDiagnosticsEventListener.EncodeInBuffer(null, false, buffer, startPos); + Assert.Equal(startPos, endPos); + } - [Fact] - public void SelfDiagnosticsEventListener_EncodeInBuffer_NotEvenSpaceForTruncationEllipses() - { - byte[] buffer = new byte[20]; - int startPos = buffer.Length - Ellipses.Length + 1; // Not enough space for "...\n". - int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", false, buffer, startPos); - Assert.Equal(startPos, endPos); - } + [Fact] + public void SelfDiagnosticsEventListener_EncodeInBuffer_Empty() + { + byte[] buffer = new byte[20]; + int startPos = 0; + int endPos = SelfDiagnosticsEventListener.EncodeInBuffer(string.Empty, false, buffer, startPos); + byte[] expected = Encoding.UTF8.GetBytes(string.Empty); + AssertBufferOutput(expected, buffer, startPos, endPos); + } - [Fact] - public void SelfDiagnosticsEventListener_EncodeInBuffer_IsParameter_EnoughSpace() - { - byte[] buffer = new byte[20]; - int startPos = buffer.Length - EllipsesWithBrackets.Length - 6; // Just enough space for "abc" even if "...\n" need to be added. - int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", true, buffer, startPos); - byte[] expected = Encoding.UTF8.GetBytes("{abc}\0"); - AssertBufferOutput(expected, buffer, startPos, endPos + 1); - } + [Fact] + public void SelfDiagnosticsEventListener_EncodeInBuffer_EnoughSpace() + { + byte[] buffer = new byte[20]; + int startPos = buffer.Length - Ellipses.Length - 6; // Just enough space for "abc" even if "...\n" needs to be added. + int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", false, buffer, startPos); + + // '\n' will be appended to the original string "abc" after EncodeInBuffer is called. + // The byte where '\n' will be placed should not be touched within EncodeInBuffer, so it stays as '\0'. + byte[] expected = Encoding.UTF8.GetBytes("abc\0"); + AssertBufferOutput(expected, buffer, startPos, endPos + 1); + } - [Fact] - public void SelfDiagnosticsEventListener_EncodeInBuffer_IsParameter_NotEnoughSpaceForFullString() - { - byte[] buffer = new byte[20]; - int startPos = buffer.Length - EllipsesWithBrackets.Length - 5; // Just not space for "...\n". - int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", true, buffer, startPos); - byte[] expected = Encoding.UTF8.GetBytes("{ab...}\0"); - AssertBufferOutput(expected, buffer, startPos, endPos + 1); - } + [Fact] + public void SelfDiagnosticsEventListener_EncodeInBuffer_NotEnoughSpaceForFullString() + { + byte[] buffer = new byte[20]; + int startPos = buffer.Length - Ellipses.Length - 5; // Just not space for "abc" if "...\n" needs to be added. + + // It's a quick estimate by assumption that most Unicode characters takes up to 2 16-bit UTF-16 chars, + // which can be up to 4 bytes when encoded in UTF-8. + int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", false, buffer, startPos); + byte[] expected = Encoding.UTF8.GetBytes("ab...\0"); + AssertBufferOutput(expected, buffer, startPos, endPos + 1); + } - [Fact] - public void SelfDiagnosticsEventListener_EncodeInBuffer_IsParameter_NotEvenSpaceForTruncatedString() - { - byte[] buffer = new byte[20]; - int startPos = buffer.Length - EllipsesWithBrackets.Length; // Just enough space for "{...}\n". - int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", true, buffer, startPos); - byte[] expected = Encoding.UTF8.GetBytes("{...}\0"); - AssertBufferOutput(expected, buffer, startPos, endPos + 1); - } + [Fact] + public void SelfDiagnosticsEventListener_EncodeInBuffer_NotEvenSpaceForTruncatedString() + { + byte[] buffer = new byte[20]; + int startPos = buffer.Length - Ellipses.Length; // Just enough space for "...\n". + int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", false, buffer, startPos); + byte[] expected = Encoding.UTF8.GetBytes("...\0"); + AssertBufferOutput(expected, buffer, startPos, endPos + 1); + } - [Fact] - public void SelfDiagnosticsEventListener_EncodeInBuffer_IsParameter_NotEvenSpaceForTruncationEllipses() - { - byte[] buffer = new byte[20]; - int startPos = buffer.Length - EllipsesWithBrackets.Length + 1; // Not enough space for "{...}\n". - int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", true, buffer, startPos); - Assert.Equal(startPos, endPos); - } + [Fact] + public void SelfDiagnosticsEventListener_EncodeInBuffer_NotEvenSpaceForTruncationEllipses() + { + byte[] buffer = new byte[20]; + int startPos = buffer.Length - Ellipses.Length + 1; // Not enough space for "...\n". + int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", false, buffer, startPos); + Assert.Equal(startPos, endPos); + } - private static void AssertFileOutput(string filePath, string eventMessage) - { - using FileStream file = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); - var buffer = new byte[256]; - file.Read(buffer, 0, buffer.Length); - string logLine = Encoding.UTF8.GetString(buffer); - string logMessage = ParseLogMessage(logLine); - Assert.StartsWith(eventMessage, logMessage); - } + [Fact] + public void SelfDiagnosticsEventListener_EncodeInBuffer_IsParameter_EnoughSpace() + { + byte[] buffer = new byte[20]; + int startPos = buffer.Length - EllipsesWithBrackets.Length - 6; // Just enough space for "abc" even if "...\n" need to be added. + int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", true, buffer, startPos); + byte[] expected = Encoding.UTF8.GetBytes("{abc}\0"); + AssertBufferOutput(expected, buffer, startPos, endPos + 1); + } - private static string ParseLogMessage(string logLine) - { - int timestampPrefixLength = "2020-08-14T20:33:24.4788109Z:".Length; - Assert.Matches(@"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{7}Z:", logLine.Substring(0, timestampPrefixLength)); - return logLine.Substring(timestampPrefixLength); - } + [Fact] + public void SelfDiagnosticsEventListener_EncodeInBuffer_IsParameter_NotEnoughSpaceForFullString() + { + byte[] buffer = new byte[20]; + int startPos = buffer.Length - EllipsesWithBrackets.Length - 5; // Just not space for "...\n". + int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", true, buffer, startPos); + byte[] expected = Encoding.UTF8.GetBytes("{ab...}\0"); + AssertBufferOutput(expected, buffer, startPos, endPos + 1); + } - private static void AssertBufferOutput(byte[] expected, byte[] buffer, int startPos, int endPos) + [Fact] + public void SelfDiagnosticsEventListener_EncodeInBuffer_IsParameter_NotEvenSpaceForTruncatedString() + { + byte[] buffer = new byte[20]; + int startPos = buffer.Length - EllipsesWithBrackets.Length; // Just enough space for "{...}\n". + int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", true, buffer, startPos); + byte[] expected = Encoding.UTF8.GetBytes("{...}\0"); + AssertBufferOutput(expected, buffer, startPos, endPos + 1); + } + + [Fact] + public void SelfDiagnosticsEventListener_EncodeInBuffer_IsParameter_NotEvenSpaceForTruncationEllipses() + { + byte[] buffer = new byte[20]; + int startPos = buffer.Length - EllipsesWithBrackets.Length + 1; // Not enough space for "{...}\n". + int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", true, buffer, startPos); + Assert.Equal(startPos, endPos); + } + + private static void AssertFileOutput(string filePath, string eventMessage) + { + using FileStream file = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); + var buffer = new byte[256]; + file.Read(buffer, 0, buffer.Length); + string logLine = Encoding.UTF8.GetString(buffer); + string logMessage = ParseLogMessage(logLine); + Assert.StartsWith(eventMessage, logMessage); + } + + private static string ParseLogMessage(string logLine) + { + int timestampPrefixLength = "2020-08-14T20:33:24.4788109Z:".Length; + Assert.Matches(@"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{7}Z:", logLine.Substring(0, timestampPrefixLength)); + return logLine.Substring(timestampPrefixLength); + } + + private static void AssertBufferOutput(byte[] expected, byte[] buffer, int startPos, int endPos) + { + Assert.Equal(expected.Length, endPos - startPos); + for (int i = 0, j = startPos; j < endPos; ++i, ++j) { - Assert.Equal(expected.Length, endPos - startPos); - for (int i = 0, j = startPos; j < endPos; ++i, ++j) - { - Assert.Equal(expected[i], buffer[j]); - } + Assert.Equal(expected[i], buffer[j]); } } } diff --git a/test/OpenTelemetry.Tests/Internal/WildcardHelperTests.cs b/test/OpenTelemetry.Tests/Internal/WildcardHelperTests.cs index 2f114f52584..9b079580b87 100644 --- a/test/OpenTelemetry.Tests/Internal/WildcardHelperTests.cs +++ b/test/OpenTelemetry.Tests/Internal/WildcardHelperTests.cs @@ -1,51 +1,37 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Xunit; -namespace OpenTelemetry.Internal.Tests +namespace OpenTelemetry.Internal.Tests; + +public class WildcardHelperTests { - public class WildcardHelperTests + [Theory] + [InlineData(new[] { "a" }, "a", true)] + [InlineData(new[] { "a.*" }, "a.b", true)] + [InlineData(new[] { "a" }, "a.b", false)] + [InlineData(new[] { "a", "x.*" }, "x.y", true)] + [InlineData(new[] { "a", "x.*" }, "a.b", false)] + [InlineData(new[] { "a", "x", "y" }, "abbbt", false)] + [InlineData(new[] { "a", "x", "y" }, "ccxccc", false)] + [InlineData(new[] { "a", "x", "y" }, "wecgy", false)] + public void WildcardRegex_ShouldMatch(string[] patterns, string matchWith, bool isMatch) { - [Theory] - [InlineData(new[] { "a" }, "a", true)] - [InlineData(new[] { "a.*" }, "a.b", true)] - [InlineData(new[] { "a" }, "a.b", false)] - [InlineData(new[] { "a", "x.*" }, "x.y", true)] - [InlineData(new[] { "a", "x.*" }, "a.b", false)] - [InlineData(new[] { "a", "x", "y" }, "abbbt", false)] - [InlineData(new[] { "a", "x", "y" }, "ccxccc", false)] - [InlineData(new[] { "a", "x", "y" }, "wecgy", false)] - public void WildcardRegex_ShouldMatch(string[] patterns, string matchWith, bool isMatch) - { - var regex = WildcardHelper.GetWildcardRegex(patterns); + var regex = WildcardHelper.GetWildcardRegex(patterns); - var result = regex.IsMatch(matchWith); + var result = regex.IsMatch(matchWith); - Assert.True(result == isMatch); - } + Assert.True(result == isMatch); + } - [Theory] - [InlineData(null, false)] - [InlineData("a", false)] - [InlineData("a.*", true)] - [InlineData("a.?", true)] - public void Verify_ContainsWildcard(string pattern, bool expected) - { - Assert.Equal(expected, WildcardHelper.ContainsWildcard(pattern)); - } + [Theory] + [InlineData(null, false)] + [InlineData("a", false)] + [InlineData("a.*", true)] + [InlineData("a.?", true)] + public void Verify_ContainsWildcard(string pattern, bool expected) + { + Assert.Equal(expected, WildcardHelper.ContainsWildcard(pattern)); } } diff --git a/test/OpenTelemetry.Tests/Logs/BatchExportLogRecordProcessorOptionsTest.cs b/test/OpenTelemetry.Tests/Logs/BatchExportLogRecordProcessorOptionsTest.cs index 35e89e97b52..2030e402c24 100644 --- a/test/OpenTelemetry.Tests/Logs/BatchExportLogRecordProcessorOptionsTest.cs +++ b/test/OpenTelemetry.Tests/Logs/BatchExportLogRecordProcessorOptionsTest.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #nullable enable @@ -64,7 +51,7 @@ public void BatchExportLogRecordProcessorOptions_EnvironmentVariableOverride() [Fact] public void ExportLogRecordProcessorOptions_UsingIConfiguration() { - var values = new Dictionary() + var values = new Dictionary() { [BatchExportLogRecordProcessorOptions.MaxQueueSizeEnvVarKey] = "1", [BatchExportLogRecordProcessorOptions.MaxExportBatchSizeEnvVarKey] = "2", diff --git a/test/OpenTelemetry.Tests/Logs/BatchLogRecordExportProcessorTests.cs b/test/OpenTelemetry.Tests/Logs/BatchLogRecordExportProcessorTests.cs index f032e61a252..d285302fcfd 100644 --- a/test/OpenTelemetry.Tests/Logs/BatchLogRecordExportProcessorTests.cs +++ b/test/OpenTelemetry.Tests/Logs/BatchLogRecordExportProcessorTests.cs @@ -1,111 +1,141 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #if !NETFRAMEWORK using Microsoft.Extensions.Logging; using OpenTelemetry.Exporter; using Xunit; -namespace OpenTelemetry.Logs.Tests +namespace OpenTelemetry.Logs.Tests; + +public sealed class BatchLogRecordExportProcessorTests { - public sealed class BatchLogRecordExportProcessorTests + [Fact] + public void StateValuesAndScopeBufferingTest() { - [Fact] - public void StateValuesAndScopeBufferingTest() - { - var scopeProvider = new LoggerExternalScopeProvider(); + var scopeProvider = new LoggerExternalScopeProvider(); - List exportedItems = new(); + List exportedItems = new(); - using var processor = new BatchLogRecordExportProcessor( - new InMemoryExporter(exportedItems), - scheduledDelayMilliseconds: int.MaxValue); + using var processor = new BatchLogRecordExportProcessor( + new InMemoryExporter(exportedItems), + scheduledDelayMilliseconds: int.MaxValue); - using var scope = scopeProvider.Push(exportedItems); + using var scope = scopeProvider.Push(exportedItems); - var logRecord = new LogRecord(); + var pool = LogRecordSharedPool.Current; - var state = new LogRecordTest.DisposingState("Hello world"); + var logRecord = pool.Rent(); - logRecord.ILoggerData.ScopeProvider = scopeProvider; - logRecord.StateValues = state; + var state = new LogRecordTest.DisposingState("Hello world"); - processor.OnEnd(logRecord); + logRecord.ILoggerData.ScopeProvider = scopeProvider; + logRecord.StateValues = state; - state.Dispose(); + processor.OnEnd(logRecord); - Assert.Empty(exportedItems); + state.Dispose(); - Assert.Null(logRecord.ILoggerData.ScopeProvider); - Assert.False(ReferenceEquals(state, logRecord.StateValues)); - Assert.NotNull(logRecord.AttributeStorage); - Assert.NotNull(logRecord.ILoggerData.BufferedScopes); + Assert.Empty(exportedItems); - KeyValuePair actualState = logRecord.StateValues[0]; + Assert.Null(logRecord.ILoggerData.ScopeProvider); + Assert.False(ReferenceEquals(state, logRecord.StateValues)); + Assert.NotNull(logRecord.AttributeStorage); + Assert.NotNull(logRecord.ILoggerData.BufferedScopes); - Assert.Same("Value", actualState.Key); - Assert.Same("Hello world", actualState.Value); + KeyValuePair actualState = logRecord.StateValues[0]; - bool foundScope = false; + Assert.Same("Value", actualState.Key); + Assert.Same("Hello world", actualState.Value); - logRecord.ForEachScope( - (s, o) => - { - foundScope = ReferenceEquals(s.Scope, exportedItems); - }, - null); + bool foundScope = false; - Assert.True(foundScope); + logRecord.ForEachScope( + (s, o) => + { + foundScope = ReferenceEquals(s.Scope, exportedItems); + }, + null); - processor.Shutdown(); + Assert.True(foundScope); - Assert.Single(exportedItems); - } + processor.Shutdown(); - [Fact] - public void StateBufferingTest() - { - // LogRecord.State is never inspected or buffered. Accessing it - // after OnEnd may throw. This test verifies that behavior. TODO: - // Investigate this. Potentially obsolete logRecord.State and force - // StateValues/ParseStateValues behavior. - List exportedItems = new(); + Assert.Single(exportedItems); + Assert.Same(logRecord, exportedItems[0]); + } + + [Fact] + public void StateBufferingTest() + { + // LogRecord.State is never inspected or buffered. Accessing it + // after OnEnd may throw. This test verifies that behavior. TODO: + // Investigate this. Potentially obsolete logRecord.State and force + // StateValues/ParseStateValues behavior. + List exportedItems = new(); - using var processor = new BatchLogRecordExportProcessor( - new InMemoryExporter(exportedItems)); + using var processor = new BatchLogRecordExportProcessor( + new InMemoryExporter(exportedItems)); - var logRecord = new LogRecord(); + var pool = LogRecordSharedPool.Current; - var state = new LogRecordTest.DisposingState("Hello world"); - logRecord.State = state; + var logRecord = pool.Rent(); - processor.OnEnd(logRecord); - processor.Shutdown(); + var state = new LogRecordTest.DisposingState("Hello world"); + logRecord.State = state; - state.Dispose(); + processor.OnEnd(logRecord); + processor.Shutdown(); - Assert.Throws(() => + Assert.Single(exportedItems); + Assert.Same(logRecord, exportedItems[0]); + + state.Dispose(); + + Assert.Throws(() => + { + IReadOnlyList> state = (IReadOnlyList>)logRecord.State; + + foreach (var kvp in state) { - IReadOnlyList> state = (IReadOnlyList>)logRecord.State; + } + }); + } + + [Fact] + public void CopyMadeWhenLogRecordIsFromThreadStaticPoolTest() + { + List exportedItems = new(); + + using var processor = new BatchLogRecordExportProcessor( + new InMemoryExporter(exportedItems)); + + var pool = LogRecordThreadStaticPool.Instance; + + var logRecord = pool.Rent(); + + processor.OnEnd(logRecord); + processor.Shutdown(); + + Assert.Single(exportedItems); + Assert.NotSame(logRecord, exportedItems[0]); + } + + [Fact] + public void LogRecordAddedToBatchIfNotFromAnyPoolTest() + { + List exportedItems = new(); + + using var processor = new BatchLogRecordExportProcessor( + new InMemoryExporter(exportedItems)); + + var logRecord = new LogRecord(); + + processor.OnEnd(logRecord); + processor.Shutdown(); - foreach (var kvp in state) - { - } - }); - } + Assert.Single(exportedItems); + Assert.Same(logRecord, exportedItems[0]); } } #endif diff --git a/test/OpenTelemetry.Tests/Logs/LogRecordSharedPoolTests.cs b/test/OpenTelemetry.Tests/Logs/LogRecordSharedPoolTests.cs index 23fe71718b1..d4be69397b1 100644 --- a/test/OpenTelemetry.Tests/Logs/LogRecordSharedPoolTests.cs +++ b/test/OpenTelemetry.Tests/Logs/LogRecordSharedPoolTests.cs @@ -1,275 +1,264 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #nullable enable using Xunit; -namespace OpenTelemetry.Logs.Tests +namespace OpenTelemetry.Logs.Tests; + +public sealed class LogRecordSharedPoolTests { - public sealed class LogRecordSharedPoolTests + [Fact] + public void ResizeTests() { - [Fact] - public void ResizeTests() - { - LogRecordSharedPool.Resize(LogRecordSharedPool.DefaultMaxPoolSize); - Assert.NotNull(LogRecordSharedPool.Current); - Assert.Equal(LogRecordSharedPool.DefaultMaxPoolSize, LogRecordSharedPool.Current.Capacity); + LogRecordSharedPool.Resize(LogRecordSharedPool.DefaultMaxPoolSize); + Assert.NotNull(LogRecordSharedPool.Current); + Assert.Equal(LogRecordSharedPool.DefaultMaxPoolSize, LogRecordSharedPool.Current.Capacity); - Assert.Throws(() => LogRecordSharedPool.Resize(0)); + Assert.Throws(() => LogRecordSharedPool.Resize(0)); - var beforePool = LogRecordSharedPool.Current; + var beforePool = LogRecordSharedPool.Current; - LogRecordSharedPool.Resize(1); - - Assert.NotNull(LogRecordSharedPool.Current); - Assert.Equal(1, LogRecordSharedPool.Current.Capacity); - Assert.NotEqual(beforePool, LogRecordSharedPool.Current); - } + LogRecordSharedPool.Resize(1); - [Fact] - public void RentReturnTests() - { - LogRecordSharedPool.Resize(2); + Assert.NotNull(LogRecordSharedPool.Current); + Assert.Equal(1, LogRecordSharedPool.Current.Capacity); + Assert.NotEqual(beforePool, LogRecordSharedPool.Current); + } - var pool = LogRecordSharedPool.Current; + [Fact] + public void RentReturnTests() + { + LogRecordSharedPool.Resize(2); - var logRecord1 = pool.Rent(); - Assert.NotNull(logRecord1); + var pool = LogRecordSharedPool.Current; - var logRecord2 = pool.Rent(); - Assert.NotNull(logRecord1); + var logRecord1 = pool.Rent(); + Assert.NotNull(logRecord1); - pool.Return(logRecord1); + var logRecord2 = pool.Rent(); + Assert.NotNull(logRecord1); - Assert.Equal(1, pool.Count); + pool.Return(logRecord1); - // Note: This is ignored because logRecord manually created has PoolReferenceCount = int.MaxValue. - LogRecord manualRecord = new(); - Assert.Equal(int.MaxValue, manualRecord.PoolReferenceCount); - pool.Return(manualRecord); + Assert.Equal(1, pool.Count); - Assert.Equal(1, pool.Count); + var logRecordWithReferencesAdded = pool.Rent(); - pool.Return(logRecord2); + // Note: This record won't be returned to the pool because we add a reference to it. + logRecordWithReferencesAdded.AddReference(); - Assert.Equal(2, pool.Count); + Assert.Equal(2, logRecordWithReferencesAdded.PoolReferenceCount); + pool.Return(logRecordWithReferencesAdded); - logRecord1 = pool.Rent(); - Assert.NotNull(logRecord1); - Assert.Equal(1, pool.Count); + Assert.Equal(0, pool.Count); - logRecord2 = pool.Rent(); - Assert.NotNull(logRecord2); - Assert.Equal(0, pool.Count); + pool.Return(logRecord2); - var logRecord3 = pool.Rent(); - var logRecord4 = pool.Rent(); - Assert.NotNull(logRecord3); - Assert.NotNull(logRecord4); + Assert.Equal(1, pool.Count); - pool.Return(logRecord1); - pool.Return(logRecord2); - pool.Return(logRecord3); - pool.Return(logRecord4); // <- Discarded due to pool size of 2 + logRecord1 = pool.Rent(); + Assert.NotNull(logRecord1); + Assert.Equal(0, pool.Count); - Assert.Equal(2, pool.Count); - } + logRecord2 = pool.Rent(); + Assert.NotNull(logRecord2); + Assert.Equal(0, pool.Count); - [Fact] - public void TrackReferenceTests() - { - LogRecordSharedPool.Resize(2); + var logRecord3 = pool.Rent(); + var logRecord4 = pool.Rent(); + Assert.NotNull(logRecord3); + Assert.NotNull(logRecord4); - var pool = LogRecordSharedPool.Current; + pool.Return(logRecord1); + pool.Return(logRecord2); + pool.Return(logRecord3); // <- Discarded due to pool size of 2 + pool.Return(logRecord4); // <- Discarded due to pool size of 2 - var logRecord1 = pool.Rent(); - Assert.NotNull(logRecord1); + Assert.Equal(2, pool.Count); + } - Assert.Equal(1, logRecord1.PoolReferenceCount); + [Fact] + public void TrackReferenceTests() + { + LogRecordSharedPool.Resize(2); - logRecord1.AddReference(); + var pool = LogRecordSharedPool.Current; - Assert.Equal(2, logRecord1.PoolReferenceCount); + var logRecord1 = pool.Rent(); + Assert.NotNull(logRecord1); - pool.Return(logRecord1); + Assert.Equal(1, logRecord1.PoolReferenceCount); - Assert.Equal(1, logRecord1.PoolReferenceCount); + logRecord1.AddReference(); - pool.Return(logRecord1); + Assert.Equal(2, logRecord1.PoolReferenceCount); - Assert.Equal(1, pool.Count); - Assert.Equal(0, logRecord1.PoolReferenceCount); + pool.Return(logRecord1); - pool.Return(logRecord1); + Assert.Equal(1, logRecord1.PoolReferenceCount); - Assert.Equal(-1, logRecord1.PoolReferenceCount); - Assert.Equal(1, pool.Count); // Record was not returned because PoolReferences was negative. - } + pool.Return(logRecord1); - [Fact] - public void ClearTests() - { - LogRecordSharedPool.Resize(LogRecordSharedPool.DefaultMaxPoolSize); + Assert.Equal(1, pool.Count); + Assert.Equal(0, logRecord1.PoolReferenceCount); - var pool = LogRecordSharedPool.Current; + pool.Return(logRecord1); - var logRecord1 = pool.Rent(); - logRecord1.AttributeStorage = new List>(16) - { - new KeyValuePair("key1", "value1"), - new KeyValuePair("key2", "value2"), - }; - logRecord1.ScopeStorage = new List(8) { null, null }; + Assert.Equal(-1, logRecord1.PoolReferenceCount); + Assert.Equal(1, pool.Count); // Record was not returned because PoolReferences was negative. + } - pool.Return(logRecord1); + [Fact] + public void ClearTests() + { + LogRecordSharedPool.Resize(LogRecordSharedPool.DefaultMaxPoolSize); - Assert.Empty(logRecord1.AttributeStorage); - Assert.Equal(16, logRecord1.AttributeStorage.Capacity); - Assert.Empty(logRecord1.ScopeStorage); - Assert.Equal(8, logRecord1.ScopeStorage.Capacity); + var pool = LogRecordSharedPool.Current; - logRecord1 = pool.Rent(); + var logRecord1 = pool.Rent(); + logRecord1.AttributeStorage = new List>(16) + { + new KeyValuePair("key1", "value1"), + new KeyValuePair("key2", "value2"), + }; + logRecord1.ScopeStorage = new List(8) { null, null }; - Assert.NotNull(logRecord1.AttributeStorage); - Assert.NotNull(logRecord1.ScopeStorage); + pool.Return(logRecord1); - for (int i = 0; i <= LogRecordPoolHelper.DefaultMaxNumberOfAttributes; i++) - { - logRecord1.AttributeStorage!.Add(new KeyValuePair("key", "value")); - } + Assert.Empty(logRecord1.AttributeStorage); + Assert.Equal(16, logRecord1.AttributeStorage.Capacity); + Assert.Empty(logRecord1.ScopeStorage); + Assert.Equal(8, logRecord1.ScopeStorage.Capacity); - for (int i = 0; i <= LogRecordPoolHelper.DefaultMaxNumberOfScopes; i++) - { - logRecord1.ScopeStorage!.Add(null); - } + logRecord1 = pool.Rent(); - pool.Return(logRecord1); + Assert.NotNull(logRecord1.AttributeStorage); + Assert.NotNull(logRecord1.ScopeStorage); - Assert.Null(logRecord1.AttributeStorage); - Assert.Null(logRecord1.ScopeStorage); + for (int i = 0; i <= LogRecordPoolHelper.DefaultMaxNumberOfAttributes; i++) + { + logRecord1.AttributeStorage!.Add(new KeyValuePair("key", "value")); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task ExportTest(bool warmup) + for (int i = 0; i <= LogRecordPoolHelper.DefaultMaxNumberOfScopes; i++) { - LogRecordSharedPool.Resize(LogRecordSharedPool.DefaultMaxPoolSize); + logRecord1.ScopeStorage!.Add(null); + } + + pool.Return(logRecord1); + + Assert.Null(logRecord1.AttributeStorage); + Assert.Null(logRecord1.ScopeStorage); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExportTest(bool warmup) + { + LogRecordSharedPool.Resize(LogRecordSharedPool.DefaultMaxPoolSize); - var pool = LogRecordSharedPool.Current; + var pool = LogRecordSharedPool.Current; - if (warmup) + if (warmup) + { + for (int i = 0; i < LogRecordSharedPool.DefaultMaxPoolSize; i++) { - for (int i = 0; i < LogRecordSharedPool.DefaultMaxPoolSize; i++) - { - pool.Return(new LogRecord { PoolReferenceCount = 1 }); - } + pool.Return(new LogRecord { Source = LogRecord.LogRecordSource.FromSharedPool, PoolReferenceCount = 1 }); } + } - using BatchLogRecordExportProcessor processor = new(new NoopExporter()); + using BatchLogRecordExportProcessor processor = new(new NoopExporter()); - List tasks = new(); + List tasks = new(); - for (int i = 0; i < Environment.ProcessorCount; i++) + for (int i = 0; i < Environment.ProcessorCount; i++) + { + tasks.Add(Task.Run(async () => { - tasks.Add(Task.Run(async () => - { - Random random = new Random(); - - await Task.Delay(random.Next(100, 150)).ConfigureAwait(false); + Random random = new Random(); - for (int i = 0; i < 1000; i++) - { - var logRecord = pool.Rent(); + await Task.Delay(random.Next(100, 150)); - processor.OnEnd(logRecord); + for (int i = 0; i < 1000; i++) + { + var logRecord = pool.Rent(); - // This should no-op mostly. - pool.Return(logRecord); + processor.OnEnd(logRecord); - await Task.Delay(random.Next(0, 20)).ConfigureAwait(false); - } - })); - } + // This should no-op mostly. + pool.Return(logRecord); - await Task.WhenAll(tasks).ConfigureAwait(false); + await Task.Delay(random.Next(0, 20)); + } + })); + } - processor.ForceFlush(); + await Task.WhenAll(tasks); - if (warmup) - { - Assert.Equal(LogRecordSharedPool.DefaultMaxPoolSize, pool.Count); - } + processor.ForceFlush(); - Assert.True(pool.Count <= LogRecordSharedPool.DefaultMaxPoolSize); + if (warmup) + { + Assert.Equal(LogRecordSharedPool.DefaultMaxPoolSize, pool.Count); } - [Fact] - public async Task DeadlockTest() + Assert.True(pool.Count <= LogRecordSharedPool.DefaultMaxPoolSize); + } + + [Fact] + public async Task DeadlockTest() + { + /* + * The way the LogRecordPool works is it maintains two counters one + * for readers and one for writers. The counters always increment + * and point to an index in the pool array by way of a modulus on + * the size of the array (index = counter % capacity). Under very + * heavy load it is possible for a reader to receive an index and + * then be yielded. When waking up that index may no longer be valid + * if other threads caused the counters to loop around. There is + * protection for this case in the pool, this test verifies it is + * working. + * + * This is considered a corner case. Many threads have to be renting + * & returning logs in a tight loop for this to happen. Real + * applications should be logging based on logic firing which should + * have more natural back-off time. + */ + + LogRecordSharedPool.Resize(LogRecordSharedPool.DefaultMaxPoolSize); + + var pool = LogRecordSharedPool.Current; + + List tasks = new(); + + for (int i = 0; i < Environment.ProcessorCount; i++) { - /* - * The way the LogRecordPool works is it maintains two counters one - * for readers and one for writers. The counters always increment - * and point to an index in the pool array by way of a modulus on - * the size of the array (index = counter % capacity). Under very - * heavy load it is possible for a reader to receive an index and - * then be yielded. When waking up that index may no longer be valid - * if other threads caused the counters to loop around. There is - * protection for this case in the pool, this test verifies it is - * working. - * - * This is considered a corner case. Many threads have to be renting - * & returning logs in a tight loop for this to happen. Real - * applications should be logging based on logic firing which should - * have more natural back-off time. - */ - - LogRecordSharedPool.Resize(LogRecordSharedPool.DefaultMaxPoolSize); - - var pool = LogRecordSharedPool.Current; - - List tasks = new(); - - for (int i = 0; i < Environment.ProcessorCount; i++) + tasks.Add(Task.Run(async () => { - tasks.Add(Task.Run(async () => - { - await Task.Delay(2000).ConfigureAwait(false); + await Task.Delay(2000); - for (int i = 0; i < 100_000; i++) - { - var logRecord = pool.Rent(); + for (int i = 0; i < 100_000; i++) + { + var logRecord = pool.Rent(); - pool.Return(logRecord); - } - })); - } + pool.Return(logRecord); + } + })); + } - await Task.WhenAll(tasks).ConfigureAwait(false); + await Task.WhenAll(tasks); - Assert.True(pool.Count <= LogRecordSharedPool.DefaultMaxPoolSize); - } + Assert.True(pool.Count <= LogRecordSharedPool.DefaultMaxPoolSize); + } - private sealed class NoopExporter : BaseExporter + private sealed class NoopExporter : BaseExporter + { + public override ExportResult Export(in Batch batch) { - public override ExportResult Export(in Batch batch) - { - return ExportResult.Success; - } + return ExportResult.Success; } } } diff --git a/test/OpenTelemetry.Tests/Logs/LogRecordStateProcessorTests.cs b/test/OpenTelemetry.Tests/Logs/LogRecordStateProcessorTests.cs new file mode 100644 index 00000000000..24acd8869a7 --- /dev/null +++ b/test/OpenTelemetry.Tests/Logs/LogRecordStateProcessorTests.cs @@ -0,0 +1,266 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace OpenTelemetry.Logs.Tests; + +public class LogRecordStateProcessorTests +{ + [Theory] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + [InlineData(false, false)] + public void LogProcessorSetStateTest(bool includeAttributes, bool parseStateValues) + { + List exportedItems = new(); + + using (var loggerFactory = CreateLoggerFactory(includeAttributes, parseStateValues, exportedItems, OnEnd)) + { + var logger = loggerFactory.CreateLogger("TestLogger"); + + logger.LogInformation("Hello world {data}", 1234); + } + + Assert.Single(exportedItems); + + AssertStateAndAttributes( + exportedItems[0], + attributesExpectedCount: !includeAttributes ? 0 : parseStateValues ? 1 : 3, + stateExpectedCount: !includeAttributes || parseStateValues ? 1 : 3, + out var state, + out var attributes); + + void OnEnd(LogRecord logRecord) + { + AssertStateAndAttributes( + logRecord, + attributesExpectedCount: includeAttributes ? 2 : 0, + stateExpectedCount: !includeAttributes || parseStateValues ? 0 : 2, + out var state, + out var attributes); + + logRecord.State = new List>(state) + { + new("enrichedData", "OTel"), + }; + } + } + + [Theory] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + [InlineData(false, false)] + public void LogProcessorSetStateToUnsupportedTypeTest(bool includeAttributes, bool parseStateValues) + { + List exportedItems = new(); + + using (var loggerFactory = CreateLoggerFactory(includeAttributes, parseStateValues, exportedItems, OnEnd)) + { + var logger = loggerFactory.CreateLogger("TestLogger"); + + logger.LogInformation("Hello world {data}", 1234); + } + + Assert.Single(exportedItems); + + AssertStateAndAttributes( + exportedItems[0], + attributesExpectedCount: 0, + stateExpectedCount: 0, + out var state, + out var attributes); + + Assert.True(exportedItems[0].State is CustomState); + + void OnEnd(LogRecord logRecord) + { + AssertStateAndAttributes( + logRecord, + attributesExpectedCount: includeAttributes ? 2 : 0, + stateExpectedCount: !includeAttributes || parseStateValues ? 0 : 2, + out var state, + out var attributes); + + logRecord.State = new CustomState("OTel"); + } + } + + [Theory] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + [InlineData(false, false)] + public void LogProcessorSetAttributesTest(bool includeAttributes, bool parseStateValues) + { + List exportedItems = new(); + + using (var loggerFactory = CreateLoggerFactory(includeAttributes, parseStateValues, exportedItems, OnEnd)) + { + var logger = loggerFactory.CreateLogger("TestLogger"); + + logger.LogInformation("Hello world {data}", 1234); + } + + Assert.Single(exportedItems); + + AssertStateAndAttributes( + exportedItems[0], + attributesExpectedCount: !includeAttributes ? 1 : 3, + stateExpectedCount: !includeAttributes || parseStateValues ? 0 : 3, + out var state, + out var attributes); + + void OnEnd(LogRecord logRecord) + { + AssertStateAndAttributes( + logRecord, + attributesExpectedCount: includeAttributes ? 2 : 0, + stateExpectedCount: !includeAttributes || parseStateValues ? 0 : 2, + out var state, + out var attributes); + + logRecord.Attributes = new List>(attributes) + { + new("enrichedData", "OTel"), + }; + } + } + + [Theory] + [InlineData(true, false, 0)] + [InlineData(false, true, 0)] + [InlineData(true, true, 0)] + [InlineData(false, false, 0)] + [InlineData(true, false, 1)] + [InlineData(false, true, 1)] + [InlineData(true, true, 1)] + [InlineData(false, false, 1)] + public void LogProcessorSetAttributesAndStateMixedTest(bool includeAttributes, bool parseStateValues, int order) + { + List exportedItems = new(); + + using (var loggerFactory = CreateLoggerFactory(includeAttributes, parseStateValues, exportedItems, OnEnd)) + { + var logger = loggerFactory.CreateLogger("TestLogger"); + + logger.LogInformation("Hello world {data}", 1234); + } + + Assert.Single(exportedItems); + + AssertStateAndAttributes( + exportedItems[0], + attributesExpectedCount: !includeAttributes ? 1 : 3, + stateExpectedCount: !includeAttributes ? 1 : 3, + out var state, + out var attributes); + + void OnEnd(LogRecord logRecord) + { + AssertStateAndAttributes( + logRecord, + attributesExpectedCount: includeAttributes ? 2 : 0, + stateExpectedCount: !includeAttributes || parseStateValues ? 0 : 2, + out var state, + out var attributes); + + if (order == 0) + { + logRecord.State = logRecord.Attributes = new List>(attributes) + { + new("enrichedData", "OTel"), + }; + } + else + { + var newState = new List>(attributes) + { + new("enrichedData", "OTel"), + }; + + logRecord.State = newState; + logRecord.Attributes = newState; + } + } + } + + private static ILoggerFactory CreateLoggerFactory( + bool includeAttributes, + bool parseStateValues, + List exportedItems, + Action onEndAction) + { + return LoggerFactory.Create(logging => logging + .AddOpenTelemetry(options => + { + options.IncludeAttributes = includeAttributes; + options.ParseStateValues = parseStateValues; + + options + .AddProcessor(new LogRecordStateProcessor(onEndAction)) + .AddInMemoryExporter(exportedItems); + })); + } + + private static void AssertStateAndAttributes( + LogRecord logRecord, + int attributesExpectedCount, + int stateExpectedCount, + [NotNull] out IReadOnlyList>? state, + [NotNull] out IReadOnlyList>? attributes) + { + state = logRecord.State as IReadOnlyList>; + attributes = logRecord.Attributes; + + if (stateExpectedCount > 0) + { + Assert.NotNull(state); + Assert.Equal(stateExpectedCount, state.Count); + } + else + { + Assert.Null(state); + state = Array.Empty>(); + } + + if (attributesExpectedCount > 0) + { + Assert.NotNull(attributes); + Assert.Equal(attributesExpectedCount, attributes.Count); + } + else + { + Assert.Null(attributes); + attributes = Array.Empty>(); + } + } + + private sealed class LogRecordStateProcessor : BaseProcessor + { + private readonly Action onEndAction; + + public LogRecordStateProcessor(Action onEndAction) + { + this.onEndAction = onEndAction; + } + + public override void OnEnd(LogRecord data) + { + this.onEndAction(data); + + base.OnEnd(data); + } + } + + private sealed class CustomState(string enrichedData) + { + public string EnrichedData { get; } = enrichedData; + } +} diff --git a/test/OpenTelemetry.Tests/Logs/LogRecordTest.cs b/test/OpenTelemetry.Tests/Logs/LogRecordTest.cs index a828e7af63e..138af7ac695 100644 --- a/test/OpenTelemetry.Tests/Logs/LogRecordTest.cs +++ b/test/OpenTelemetry.Tests/Logs/LogRecordTest.cs @@ -1,27 +1,9 @@ -// // 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. -// - -#if !NETFRAMEWORK -using System; +// SPDX-License-Identifier: Apache-2.0 + using System.Collections; -using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; using Microsoft.Extensions.Logging; using OpenTelemetry.Exporter; using OpenTelemetry.Logs; @@ -29,1136 +11,1185 @@ using OpenTelemetry.Trace; using Xunit; -namespace OpenTelemetry.Logs.Tests +namespace OpenTelemetry.Logs.Tests; + +public sealed class LogRecordTest { - public sealed class LogRecordTest + private enum Field { - private enum Field - { - FormattedMessage, - State, - StateValues, - } + FormattedMessage, + State, + StateValues, + } - [Fact] - public void CheckCategoryNameForLog() - { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null); - var logger = loggerFactory.CreateLogger(); + [Fact] + public void CheckCategoryNameForLog() + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null); + var logger = loggerFactory.CreateLogger(); - logger.LogInformation("Log"); - var categoryName = exportedItems[0].CategoryName; + logger.LogInformation("Log"); + var categoryName = exportedItems[0].CategoryName; - Assert.Equal(typeof(LogRecordTest).FullName, categoryName); - } + Assert.Equal(typeof(LogRecordTest).FullName, categoryName); + } - [Theory] - [InlineData(LogLevel.Trace)] - [InlineData(LogLevel.Debug)] - [InlineData(LogLevel.Information)] - [InlineData(LogLevel.Warning)] - [InlineData(LogLevel.Error)] - [InlineData(LogLevel.Critical)] - public void CheckLogLevel(LogLevel logLevel) - { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null); - var logger = loggerFactory.CreateLogger(); + [Theory] + [InlineData(LogLevel.Trace)] + [InlineData(LogLevel.Debug)] + [InlineData(LogLevel.Information)] + [InlineData(LogLevel.Warning)] + [InlineData(LogLevel.Error)] + [InlineData(LogLevel.Critical)] + public void CheckLogLevel(LogLevel logLevel) + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null); + var logger = loggerFactory.CreateLogger(); - const string message = "Log {logLevel}"; - logger.Log(logLevel, message, logLevel); + const string message = "Log {logLevel}"; + logger.Log(logLevel, message, logLevel); - var logLevelRecorded = exportedItems[0].LogLevel; - Assert.Equal(logLevel, logLevelRecorded); - } + var logLevelRecorded = exportedItems[0].LogLevel; + Assert.Equal(logLevel, logLevelRecorded); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void CheckStateForUnstructuredLog(bool includeFormattedMessage) - { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: o => o.IncludeFormattedMessage = includeFormattedMessage); - var logger = loggerFactory.CreateLogger(); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CheckStateForUnstructuredLog(bool includeFormattedMessage) + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: o => o.IncludeFormattedMessage = includeFormattedMessage); + var logger = loggerFactory.CreateLogger(); - const string message = "Hello, World!"; - logger.LogInformation(message); + const string message = "Hello, World!"; + logger.LogInformation(message); - Assert.NotNull(exportedItems[0].State); + Assert.NotNull(exportedItems[0].State); - var attributes = exportedItems[0].Attributes; - Assert.NotNull(attributes); + var attributes = exportedItems[0].Attributes; + Assert.NotNull(attributes); - // state only has {OriginalFormat} - Assert.Equal(1, attributes.Count); + // state only has {OriginalFormat} + Assert.Single(attributes); - Assert.Equal(message, exportedItems[0].Body); - if (includeFormattedMessage) - { - Assert.Equal(message, exportedItems[0].FormattedMessage); - } - else - { - Assert.Null(exportedItems[0].FormattedMessage); - } + Assert.Equal(message, exportedItems[0].Body); + if (includeFormattedMessage) + { + Assert.Equal(message, exportedItems[0].FormattedMessage); } - - [Theory] - [InlineData(true)] - [InlineData(false)] - [SuppressMessage("CA2254", "CA2254", Justification = "While you shouldn't use interpolation in a log message, this test verifies things work with it anyway.")] - public void CheckStateForUnstructuredLogWithStringInterpolation(bool includeFormattedMessage) + else { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: o => o.IncludeFormattedMessage = includeFormattedMessage); - var logger = loggerFactory.CreateLogger(); + Assert.Null(exportedItems[0].FormattedMessage); + } + } - var message = $"Hello from potato {0.99}."; - logger.LogInformation(message); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CheckStateForUnstructuredLogWithStringInterpolation(bool includeFormattedMessage) + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: o => o.IncludeFormattedMessage = includeFormattedMessage); + var logger = loggerFactory.CreateLogger(); - Assert.NotNull(exportedItems[0].State); +#pragma warning disable CA2254 // Template should be a static expression + var message = $"Hello from potato {0.99}."; + logger.LogInformation(message); +#pragma warning restore CA2254 // Template should be a static expression - var attributes = exportedItems[0].Attributes; - Assert.NotNull(attributes); + Assert.NotNull(exportedItems[0].State); - // state only has {OriginalFormat} - Assert.Equal(1, attributes.Count); + var attributes = exportedItems[0].Attributes; + Assert.NotNull(attributes); - Assert.Equal(message, exportedItems[0].Body); - if (includeFormattedMessage) - { - Assert.Equal(message, exportedItems[0].FormattedMessage); - } - else - { - Assert.Null(exportedItems[0].FormattedMessage); - } - } + // state only has {OriginalFormat} + Assert.Single(attributes); - [Theory] - [InlineData(true)] - [InlineData(false)] - public void CheckStateForStructuredLogWithTemplate(bool includeFormattedMessage) + Assert.Equal(message, exportedItems[0].Body); + if (includeFormattedMessage) + { + Assert.Equal(message, exportedItems[0].FormattedMessage); + } + else { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: o => o.IncludeFormattedMessage = includeFormattedMessage); - var logger = loggerFactory.CreateLogger(); + Assert.Null(exportedItems[0].FormattedMessage); + } + } - const string message = "Hello from {name} {price}."; - logger.LogInformation(message, "tomato", 2.99); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CheckStateForStructuredLogWithTemplate(bool includeFormattedMessage) + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: o => o.IncludeFormattedMessage = includeFormattedMessage); + var logger = loggerFactory.CreateLogger(); - Assert.NotNull(exportedItems[0].State); + const string message = "Hello from {name} {price}."; + logger.LogInformation(message, "tomato", 2.99); - var attributes = exportedItems[0].Attributes; - Assert.NotNull(attributes); + Assert.NotNull(exportedItems[0].State); - // state has name, price and {OriginalFormat} - Assert.Equal(3, attributes.Count); + var attributes = exportedItems[0].Attributes; + Assert.NotNull(attributes); - // Check if state has name - Assert.Contains(attributes, item => item.Key == "name"); - Assert.Equal("tomato", attributes.First(item => item.Key == "name").Value); + // state has name, price and {OriginalFormat} + Assert.Equal(3, attributes.Count); - // Check if state has price - Assert.Contains(attributes, item => item.Key == "price"); - Assert.Equal(2.99, attributes.First(item => item.Key == "price").Value); + // Check if state has name + Assert.Contains(attributes, item => item.Key == "name"); + Assert.Equal("tomato", attributes.First(item => item.Key == "name").Value); - // Check if state has OriginalFormat - Assert.Contains(attributes, item => item.Key == "{OriginalFormat}"); - Assert.Equal(message, attributes.First(item => item.Key == "{OriginalFormat}").Value); + // Check if state has price + Assert.Contains(attributes, item => item.Key == "price"); + Assert.Equal(2.99, attributes.First(item => item.Key == "price").Value); - Assert.Equal(message, exportedItems[0].Body); - if (includeFormattedMessage) - { - Assert.Equal("Hello from tomato 2.99.", exportedItems[0].FormattedMessage); - } - else - { - Assert.Null(exportedItems[0].FormattedMessage); - } - } + // Check if state has OriginalFormat + Assert.Contains(attributes, item => item.Key == "{OriginalFormat}"); + Assert.Equal(message, attributes.First(item => item.Key == "{OriginalFormat}").Value); - [Theory] - [InlineData(true)] - [InlineData(false)] - public void CheckStateForStructuredLogWithStrongType(bool includeFormattedMessage) + Assert.Equal(message, exportedItems[0].Body); + if (includeFormattedMessage) { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: o => o.IncludeFormattedMessage = includeFormattedMessage); - var logger = loggerFactory.CreateLogger(); + Assert.Equal("Hello from tomato 2.99.", exportedItems[0].FormattedMessage); + } + else + { + Assert.Null(exportedItems[0].FormattedMessage); + } + } - var food = new Food { Name = "artichoke", Price = 3.99 }; - logger.LogInformation("{food}", food); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CheckStateForStructuredLogWithStrongType(bool includeFormattedMessage) + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: o => o.IncludeFormattedMessage = includeFormattedMessage); + var logger = loggerFactory.CreateLogger(); - Assert.NotNull(exportedItems[0].State); + var food = new Food { Name = "artichoke", Price = 3.99 }; + logger.LogInformation("{food}", food); - var attributes = exportedItems[0].Attributes; - Assert.NotNull(attributes); + Assert.NotNull(exportedItems[0].State); - // state has food and {OriginalFormat} - Assert.Equal(2, attributes.Count); + var attributes = exportedItems[0].Attributes; + Assert.NotNull(attributes); - // Check if state has food - Assert.Contains(attributes, item => item.Key == "food"); + // state has food and {OriginalFormat} + Assert.Equal(2, attributes.Count); - var foodParameter = (Food)attributes.First(item => item.Key == "food").Value; - Assert.Equal(food.Name, foodParameter.Name); - Assert.Equal(food.Price, foodParameter.Price); + // Check if state has food + Assert.Contains(attributes, item => item.Key == "food"); - // Check if state has OriginalFormat - Assert.Contains(attributes, item => item.Key == "{OriginalFormat}"); - Assert.Equal("{food}", attributes.First(item => item.Key == "{OriginalFormat}").Value); + var foodParameter = (Food)attributes.First(item => item.Key == "food").Value; + Assert.Equal(food.Name, foodParameter.Name); + Assert.Equal(food.Price, foodParameter.Price); - Assert.Equal("{food}", exportedItems[0].Body); - if (includeFormattedMessage) - { - Assert.Equal(food.ToString(), exportedItems[0].FormattedMessage); - } - else - { - Assert.Null(exportedItems[0].FormattedMessage); - } - } + // Check if state has OriginalFormat + Assert.Contains(attributes, item => item.Key == "{OriginalFormat}"); + Assert.Equal("{food}", attributes.First(item => item.Key == "{OriginalFormat}").Value); - [Theory] - [InlineData(true)] - [InlineData(false)] - public void CheckStateForStructuredLogWithAnonymousType(bool includeFormattedMessage) + Assert.Equal("{food}", exportedItems[0].Body); + if (includeFormattedMessage) + { + Assert.Equal(food.ToString(), exportedItems[0].FormattedMessage); + } + else { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: o => o.IncludeFormattedMessage = includeFormattedMessage); - var logger = loggerFactory.CreateLogger(); + Assert.Null(exportedItems[0].FormattedMessage); + } + } - var anonymousType = new { Name = "pumpkin", Price = 5.99 }; - logger.LogInformation("{food}", anonymousType); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CheckStateForStructuredLogWithAnonymousType(bool includeFormattedMessage) + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: o => o.IncludeFormattedMessage = includeFormattedMessage); + var logger = loggerFactory.CreateLogger(); - Assert.NotNull(exportedItems[0].State); + var anonymousType = new { Name = "pumpkin", Price = 5.99 }; + logger.LogInformation("{food}", anonymousType); - var attributes = exportedItems[0].Attributes; - Assert.NotNull(attributes); + Assert.NotNull(exportedItems[0].State); - // state has food and {OriginalFormat} - Assert.Equal(2, attributes.Count); + var attributes = exportedItems[0].Attributes; + Assert.NotNull(attributes); - // Check if state has food - Assert.Contains(attributes, item => item.Key == "food"); + // state has food and {OriginalFormat} + Assert.Equal(2, attributes.Count); - var foodParameter = attributes.First(item => item.Key == "food").Value as dynamic; - Assert.Equal(anonymousType.Name, foodParameter.Name); - Assert.Equal(anonymousType.Price, foodParameter.Price); + // Check if state has food + Assert.Contains(attributes, item => item.Key == "food"); - // Check if state has OriginalFormat - Assert.Contains(attributes, item => item.Key == "{OriginalFormat}"); - Assert.Equal("{food}", attributes.First(item => item.Key == "{OriginalFormat}").Value); + var foodParameter = attributes.First(item => item.Key == "food").Value as dynamic; + Assert.Equal(anonymousType.Name, foodParameter.Name); + Assert.Equal(anonymousType.Price, foodParameter.Price); - Assert.Equal("{food}", exportedItems[0].Body); - if (includeFormattedMessage) - { - Assert.Equal(anonymousType.ToString(), exportedItems[0].FormattedMessage); - } - else - { - Assert.Null(exportedItems[0].FormattedMessage); - } - } + // Check if state has OriginalFormat + Assert.Contains(attributes, item => item.Key == "{OriginalFormat}"); + Assert.Equal("{food}", attributes.First(item => item.Key == "{OriginalFormat}").Value); - [Theory] - [InlineData(true)] - [InlineData(false)] - public void CheckStateForStructuredLogWithGeneralType(bool includeFormattedMessage) + Assert.Equal("{food}", exportedItems[0].Body); + if (includeFormattedMessage) + { + Assert.Equal(anonymousType.ToString(), exportedItems[0].FormattedMessage); + } + else { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: o => o.IncludeFormattedMessage = includeFormattedMessage); - var logger = loggerFactory.CreateLogger(); + Assert.Null(exportedItems[0].FormattedMessage); + } + } - var food = new Dictionary - { - ["Name"] = "truffle", - ["Price"] = 299.99, - }; - logger.LogInformation("{food}", food); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CheckStateForStructuredLogWithGeneralType(bool includeFormattedMessage) + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: o => o.IncludeFormattedMessage = includeFormattedMessage); + var logger = loggerFactory.CreateLogger(); + + var food = new Dictionary + { + ["Name"] = "truffle", + ["Price"] = 299.99, + }; + logger.LogInformation("{food}", food); - Assert.NotNull(exportedItems[0].State); + Assert.NotNull(exportedItems[0].State); - var attributes = exportedItems[0].Attributes; - Assert.NotNull(attributes); + var attributes = exportedItems[0].Attributes; + Assert.NotNull(attributes); - // state only has food and {OriginalFormat} - Assert.Equal(2, attributes.Count); + // state only has food and {OriginalFormat} + Assert.Equal(2, attributes.Count); - // Check if state has food - Assert.Contains(attributes, item => item.Key == "food"); + // Check if state has food + Assert.Contains(attributes, item => item.Key == "food"); - var foodParameter = attributes.First(item => item.Key == "food").Value as Dictionary; - Assert.True(food.Count == foodParameter.Count && !food.Except(foodParameter).Any()); + var foodParameter = attributes.First(item => item.Key == "food").Value as Dictionary; + Assert.True(food.Count == foodParameter.Count && !food.Except(foodParameter).Any()); - // Check if state has OriginalFormat - Assert.Contains(attributes, item => item.Key == "{OriginalFormat}"); - Assert.Equal("{food}", attributes.First(item => item.Key == "{OriginalFormat}").Value); + // Check if state has OriginalFormat + Assert.Contains(attributes, item => item.Key == "{OriginalFormat}"); + Assert.Equal("{food}", attributes.First(item => item.Key == "{OriginalFormat}").Value); - Assert.Equal("{food}", exportedItems[0].Body); - if (includeFormattedMessage) + Assert.Equal("{food}", exportedItems[0].Body); + if (includeFormattedMessage) + { + var prevCulture = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; + try { - var prevCulture = CultureInfo.CurrentCulture; - CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; - try - { - Assert.Equal("[Name, truffle], [Price, 299.99]", exportedItems[0].FormattedMessage); - } - finally - { - CultureInfo.CurrentCulture = prevCulture; - } + Assert.Equal("[Name, truffle], [Price, 299.99]", exportedItems[0].FormattedMessage); } - else + finally { - Assert.Null(exportedItems[0].FormattedMessage); + CultureInfo.CurrentCulture = prevCulture; } } - - [Fact] - public void CheckStateForExceptionLogged() + else { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null); - var logger = loggerFactory.CreateLogger(); + Assert.Null(exportedItems[0].FormattedMessage); + } + } + + [Fact] + public void CheckStateForExceptionLogged() + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null); + var logger = loggerFactory.CreateLogger(); - var exceptionMessage = "Exception Message"; - var exception = new Exception(exceptionMessage); + var exceptionMessage = "Exception Message"; + var exception = new Exception(exceptionMessage); - const string message = "Exception Occurred"; - logger.LogInformation(exception, message); + const string message = "Exception Occurred"; + logger.LogInformation(exception, message); - Assert.NotNull(exportedItems[0].State); + Assert.NotNull(exportedItems[0].State); - var state = exportedItems[0].State; - var itemCount = state.GetType().GetProperty("Count").GetValue(state); + var state = exportedItems[0].State; + var itemCount = state.GetType().GetProperty("Count").GetValue(state); - // state only has {OriginalFormat} - Assert.Equal(1, itemCount); + // state only has {OriginalFormat} + Assert.Equal(1, itemCount); - var attributes = exportedItems[0].Attributes; - Assert.NotNull(attributes); + var attributes = exportedItems[0].Attributes; + Assert.NotNull(attributes); - // state only has {OriginalFormat} - Assert.Equal(1, attributes.Count); + // state only has {OriginalFormat} + Assert.Single(attributes); - var loggedException = exportedItems[0].Exception; - Assert.NotNull(loggedException); - Assert.Equal(exceptionMessage, loggedException.Message); + var loggedException = exportedItems[0].Exception; + Assert.NotNull(loggedException); + Assert.Equal(exceptionMessage, loggedException.Message); - Assert.Equal(message, exportedItems[0].Body); - Assert.Equal(message, state.ToString()); - Assert.Null(exportedItems[0].FormattedMessage); - } + Assert.Equal(message, exportedItems[0].Body); + Assert.Equal(message, state.ToString()); + Assert.Null(exportedItems[0].FormattedMessage); + } - [Fact] - public void CheckStateCanBeSet() - { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null); - var logger = loggerFactory.CreateLogger(); + [Fact] + public void CheckStateCanBeSet() + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null); + var logger = loggerFactory.CreateLogger(); - logger.LogInformation("This does not matter."); + logger.LogInformation("This does not matter."); - var logRecord = exportedItems[0]; - logRecord.State = "newState"; + var logRecord = exportedItems[0]; + logRecord.State = "newState"; - var expectedState = "newState"; - Assert.Equal(expectedState, logRecord.State); - } + var expectedState = "newState"; + Assert.Equal(expectedState, logRecord.State); + } - [Fact] - public void CheckStateValuesCanBeSet() - { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.ParseStateValues = true); - var logger = loggerFactory.CreateLogger(); + [Fact] + public void CheckStateValuesCanBeSet() + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.ParseStateValues = true); + var logger = loggerFactory.CreateLogger(); - logger.Log( - LogLevel.Information, - 0, - new List> { new KeyValuePair("Key1", "Value1") }, - null, - (s, e) => "OpenTelemetry!"); + logger.Log( + LogLevel.Information, + 0, + new List> { new KeyValuePair("Key1", "Value1") }, + null, + (s, e) => "OpenTelemetry!"); - var logRecord = exportedItems[0]; - var expectedStateValues = new List> { new KeyValuePair("Key2", "Value2") }; - logRecord.StateValues = expectedStateValues; + var logRecord = exportedItems[0]; + var expectedStateValues = new List> { new KeyValuePair("Key2", "Value2") }; + logRecord.StateValues = expectedStateValues; - Assert.Equal(expectedStateValues, logRecord.StateValues); - } + Assert.Equal(expectedStateValues, logRecord.StateValues); + } - [Fact] - public void CheckFormattedMessageCanBeSet() - { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.IncludeFormattedMessage = true); - var logger = loggerFactory.CreateLogger(); + [Fact] + public void CheckFormattedMessageCanBeSet() + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.IncludeFormattedMessage = true); + var logger = loggerFactory.CreateLogger(); - logger.LogInformation("OpenTelemetry {Greeting} {Subject}!", "Hello", "World"); - var logRecord = exportedItems[0]; - var expectedFormattedMessage = "OpenTelemetry Good Night!"; - logRecord.FormattedMessage = expectedFormattedMessage; + logger.LogInformation("OpenTelemetry {Greeting} {Subject}!", "Hello", "World"); + var logRecord = exportedItems[0]; + var expectedFormattedMessage = "OpenTelemetry Good Night!"; + logRecord.FormattedMessage = expectedFormattedMessage; - Assert.Equal(expectedFormattedMessage, logRecord.FormattedMessage); - } + Assert.Equal(expectedFormattedMessage, logRecord.FormattedMessage); + } - [Fact] - public void CheckStateCanBeSetByProcessor() + [Fact] + public void CheckStateCanBeSetByProcessor() + { + var exportedItems = new List(); + var exporter = new InMemoryExporter(exportedItems); + using var loggerFactory = LoggerFactory.Create(builder => { - var exportedItems = new List(); - var exporter = new InMemoryExporter(exportedItems); - using var loggerFactory = LoggerFactory.Create(builder => + builder.AddOpenTelemetry(options => { - builder.AddOpenTelemetry(options => - { - options.AddProcessor(new RedactionProcessor(Field.State)); - options.AddInMemoryExporter(exportedItems); - }); + options.AddProcessor(new RedactionProcessor(Field.State)); + options.AddInMemoryExporter(exportedItems); }); + }); - var logger = loggerFactory.CreateLogger(); - logger.LogInformation($"This does not matter."); + var logger = loggerFactory.CreateLogger(); + logger.LogInformation($"This does not matter."); - var state = exportedItems[0].State as IReadOnlyList>; - Assert.Equal("newStateKey", state[0].Key.ToString()); - Assert.Equal("newStateValue", state[0].Value.ToString()); - } + var state = exportedItems[0].State as IReadOnlyList>; + Assert.Equal("newStateKey", state[0].Key.ToString()); + Assert.Equal("newStateValue", state[0].Value.ToString()); + } - [Fact] - public void CheckStateValuesCanBeSetByProcessor() + [Fact] + public void CheckStateValuesCanBeSetByProcessor() + { + var exportedItems = new List(); + var exporter = new InMemoryExporter(exportedItems); + using var loggerFactory = LoggerFactory.Create(builder => { - var exportedItems = new List(); - var exporter = new InMemoryExporter(exportedItems); - using var loggerFactory = LoggerFactory.Create(builder => + builder.AddOpenTelemetry(options => { - builder.AddOpenTelemetry(options => - { - options.AddProcessor(new RedactionProcessor(Field.StateValues)); - options.AddInMemoryExporter(exportedItems); - options.ParseStateValues = true; - }); + options.AddProcessor(new RedactionProcessor(Field.StateValues)); + options.AddInMemoryExporter(exportedItems); + options.ParseStateValues = true; }); + }); - var logger = loggerFactory.CreateLogger(); - logger.LogInformation("This does not matter."); + var logger = loggerFactory.CreateLogger(); + logger.LogInformation("This does not matter."); - var stateValue = exportedItems[0]; - Assert.Equal(new KeyValuePair("newStateValueKey", "newStateValueValue"), stateValue.StateValues[0]); - } + var stateValue = exportedItems[0]; + Assert.Equal(new KeyValuePair("newStateValueKey", "newStateValueValue"), stateValue.StateValues[0]); + } - [Fact] - public void CheckFormattedMessageCanBeSetByProcessor() + [Fact] + public void CheckFormattedMessageCanBeSetByProcessor() + { + var exportedItems = new List(); + var exporter = new InMemoryExporter(exportedItems); + using var loggerFactory = LoggerFactory.Create(builder => { - var exportedItems = new List(); - var exporter = new InMemoryExporter(exportedItems); - using var loggerFactory = LoggerFactory.Create(builder => + builder.AddOpenTelemetry(options => { - builder.AddOpenTelemetry(options => - { - options.AddProcessor(new RedactionProcessor(Field.FormattedMessage)); - options.AddInMemoryExporter(exportedItems); - options.IncludeFormattedMessage = true; - }); + options.AddProcessor(new RedactionProcessor(Field.FormattedMessage)); + options.AddInMemoryExporter(exportedItems); + options.IncludeFormattedMessage = true; }); + }); - var logger = loggerFactory.CreateLogger(); - logger.LogInformation("OpenTelemetry {Greeting} {Subject}!", "Hello", "World"); + var logger = loggerFactory.CreateLogger(); + logger.LogInformation("OpenTelemetry {Greeting} {Subject}!", "Hello", "World"); - var item = exportedItems[0]; - Assert.Equal("OpenTelemetry Good Night!", item.FormattedMessage); - } + var item = exportedItems[0]; + Assert.Equal("OpenTelemetry Good Night!", item.FormattedMessage); + } - [Fact] - public void CheckTraceIdForLogWithinDroppedActivity() - { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null); - var logger = loggerFactory.CreateLogger(); + [Fact] + public void CheckTraceIdForLogWithinDroppedActivity() + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null); + var logger = loggerFactory.CreateLogger(); - logger.LogInformation("Log within a dropped activity"); - var logRecord = exportedItems[0]; + logger.LogInformation("Log within a dropped activity"); + var logRecord = exportedItems[0]; - Assert.Null(Activity.Current); - Assert.Equal(default, logRecord.TraceId); - Assert.Equal(default, logRecord.SpanId); - Assert.Equal(default, logRecord.TraceFlags); - } + Assert.Null(Activity.Current); + Assert.Equal(default, logRecord.TraceId); + Assert.Equal(default, logRecord.SpanId); + Assert.Equal(default, logRecord.TraceFlags); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void CheckTraceIdForLogWithinActivityMarkedAsRecordOnly(bool includeTraceState) + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CheckTraceIdForLogWithinActivityMarkedAsRecordOnly(bool includeTraceState) + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: o => o.IncludeTraceState = includeTraceState); + var logger = loggerFactory.CreateLogger(); + + var sampler = new RecordOnlySampler(); + var exportedActivityList = new List(); + var activitySourceName = Utils.GetCurrentMethodName(); + using var activitySource = new ActivitySource(activitySourceName); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .SetSampler(sampler) + .AddInMemoryExporter(exportedActivityList) + .Build(); + + using var activity = activitySource.StartActivity("Activity"); + activity.TraceStateString = "key1=value1"; + + logger.LogInformation("Log within activity marked as RecordOnly"); + var logRecord = exportedItems[0]; + + var currentActivity = Activity.Current; + Assert.NotNull(Activity.Current); + Assert.Equal(currentActivity.TraceId, logRecord.TraceId); + Assert.Equal(currentActivity.SpanId, logRecord.SpanId); + Assert.Equal(currentActivity.ActivityTraceFlags, logRecord.TraceFlags); + + if (includeTraceState) { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: o => o.IncludeTraceState = includeTraceState); - var logger = loggerFactory.CreateLogger(); - - var sampler = new RecordOnlySampler(); - var exportedActivityList = new List(); - var activitySourceName = Utils.GetCurrentMethodName(); - using var activitySource = new ActivitySource(activitySourceName); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .SetSampler(sampler) - .AddInMemoryExporter(exportedActivityList) - .Build(); - - using var activity = activitySource.StartActivity("Activity"); - activity.TraceStateString = "key1=value1"; - - logger.LogInformation("Log within activity marked as RecordOnly"); - var logRecord = exportedItems[0]; - - var currentActivity = Activity.Current; - Assert.NotNull(Activity.Current); - Assert.Equal(currentActivity.TraceId, logRecord.TraceId); - Assert.Equal(currentActivity.SpanId, logRecord.SpanId); - Assert.Equal(currentActivity.ActivityTraceFlags, logRecord.TraceFlags); - - if (includeTraceState) - { - Assert.Equal(currentActivity.TraceStateString, logRecord.TraceState); - } - else - { - Assert.Null(logRecord.TraceState); - } + Assert.Equal(currentActivity.TraceStateString, logRecord.TraceState); } - - [Fact] - public void CheckTraceIdForLogWithinActivityMarkedAsRecordAndSample() + else { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null); - var logger = loggerFactory.CreateLogger(); - - var sampler = new AlwaysOnSampler(); - var exportedActivityList = new List(); - var activitySourceName = Utils.GetCurrentMethodName(); - using var activitySource = new ActivitySource(activitySourceName); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .SetSampler(sampler) - .AddInMemoryExporter(exportedActivityList) - .Build(); - - using var activity = activitySource.StartActivity("Activity"); - - logger.LogInformation("Log within activity marked as RecordAndSample"); - var logRecord = exportedItems[0]; - - var currentActivity = Activity.Current; - Assert.NotNull(Activity.Current); - Assert.Equal(currentActivity.TraceId, logRecord.TraceId); - Assert.Equal(currentActivity.SpanId, logRecord.SpanId); - Assert.Equal(currentActivity.ActivityTraceFlags, logRecord.TraceFlags); + Assert.Null(logRecord.TraceState); } + } - [Fact] - public void VerifyIncludeFormattedMessage_False() - { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.IncludeFormattedMessage = false); - var logger = loggerFactory.CreateLogger(); - - logger.LogInformation("OpenTelemetry!"); - var logRecord = exportedItems[0]; - Assert.Null(logRecord.FormattedMessage); - } + [Fact] + public void CheckTraceIdForLogWithinActivityMarkedAsRecordAndSample() + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null); + var logger = loggerFactory.CreateLogger(); + + var sampler = new AlwaysOnSampler(); + var exportedActivityList = new List(); + var activitySourceName = Utils.GetCurrentMethodName(); + using var activitySource = new ActivitySource(activitySourceName); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .SetSampler(sampler) + .AddInMemoryExporter(exportedActivityList) + .Build(); + + using var activity = activitySource.StartActivity("Activity"); + + logger.LogInformation("Log within activity marked as RecordAndSample"); + var logRecord = exportedItems[0]; + + var currentActivity = Activity.Current; + Assert.NotNull(Activity.Current); + Assert.Equal(currentActivity.TraceId, logRecord.TraceId); + Assert.Equal(currentActivity.SpanId, logRecord.SpanId); + Assert.Equal(currentActivity.ActivityTraceFlags, logRecord.TraceFlags); + } - [Fact] - public void VerifyIncludeFormattedMessage_True() - { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.IncludeFormattedMessage = true); - var logger = loggerFactory.CreateLogger(); + [Fact] + public void VerifyIncludeFormattedMessage_False() + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.IncludeFormattedMessage = false); + var logger = loggerFactory.CreateLogger(); - logger.LogInformation("OpenTelemetry!"); - var logRecord = exportedItems[0]; - Assert.Equal("OpenTelemetry!", logRecord.FormattedMessage); + logger.LogInformation("OpenTelemetry!"); + var logRecord = exportedItems[0]; + Assert.Null(logRecord.FormattedMessage); + } - logger.LogInformation("OpenTelemetry {Greeting} {Subject}!", "Hello", "World"); - logRecord = exportedItems[1]; - Assert.Equal("OpenTelemetry Hello World!", logRecord.FormattedMessage); - } + [Fact] + public void VerifyIncludeFormattedMessage_True() + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.IncludeFormattedMessage = true); + var logger = loggerFactory.CreateLogger(); - [Fact] - public void IncludeFormattedMessageTestWhenFormatterNull() - { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.IncludeFormattedMessage = true); - var logger = loggerFactory.CreateLogger(); - - logger.Log(LogLevel.Information, default, "Hello World!", null, null); - var logRecord = exportedItems[0]; - Assert.Equal("Hello World!", logRecord.FormattedMessage); - Assert.Equal("Hello World!", logRecord.Body); - - logger.Log(LogLevel.Information, default, new CustomState(), null, null); - logRecord = exportedItems[1]; - Assert.Equal(CustomState.ToStringValue, logRecord.FormattedMessage); - Assert.Equal(CustomState.ToStringValue, logRecord.Body); - - var expectedFormattedMessage = "formatted message"; - logger.Log(LogLevel.Information, default, "Hello World!", null, (state, ex) => expectedFormattedMessage); - logRecord = exportedItems[2]; - Assert.Equal(expectedFormattedMessage, logRecord.FormattedMessage); - Assert.Equal(expectedFormattedMessage, logRecord.Body); - } + logger.LogInformation("OpenTelemetry!"); + var logRecord = exportedItems[0]; + Assert.Equal("OpenTelemetry!", logRecord.FormattedMessage); - [Fact] - public void VerifyIncludeScopes_False() - { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.IncludeScopes = false); - var logger = loggerFactory.CreateLogger(); + logger.LogInformation("OpenTelemetry {Greeting} {Subject}!", "Hello", "World"); + logRecord = exportedItems[1]; + Assert.Equal("OpenTelemetry Hello World!", logRecord.FormattedMessage); + } - using var scope = logger.BeginScope("string_scope"); + [Fact] + public void IncludeFormattedMessageTestWhenFormatterNull() + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.IncludeFormattedMessage = true); + var logger = loggerFactory.CreateLogger(); + + logger.Log(LogLevel.Information, default, "Hello World!", null, null); + var logRecord = exportedItems[0]; + Assert.Equal("Hello World!", logRecord.FormattedMessage); + Assert.Equal("Hello World!", logRecord.Body); + + logger.Log(LogLevel.Information, default, new CustomState(), null, null); + logRecord = exportedItems[1]; + Assert.Equal(CustomState.ToStringValue, logRecord.FormattedMessage); + Assert.Equal(CustomState.ToStringValue, logRecord.Body); + + var expectedFormattedMessage = "formatted message"; + logger.Log(LogLevel.Information, default, "Hello World!", null, (state, ex) => expectedFormattedMessage); + logRecord = exportedItems[2]; + Assert.Equal(expectedFormattedMessage, logRecord.FormattedMessage); + Assert.Equal(expectedFormattedMessage, logRecord.Body); + } - logger.LogInformation("OpenTelemetry!"); - var logRecord = exportedItems[0]; + [Fact] + public void VerifyIncludeScopes_False() + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.IncludeScopes = false); + var logger = loggerFactory.CreateLogger(); - List scopes = new List(); - logRecord.ForEachScope((scope, state) => scopes.Add(scope.Scope), null); - Assert.Empty(scopes); - } + using var scope = logger.BeginScope("string_scope"); - [Fact] - public void VerifyIncludeScopes_True() - { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.IncludeScopes = true); - var logger = loggerFactory.CreateLogger(); + logger.LogInformation("OpenTelemetry!"); + var logRecord = exportedItems[0]; - using var scope = logger.BeginScope("string_scope"); + List scopes = new List(); + logRecord.ForEachScope((scope, state) => scopes.Add(scope.Scope), null); + Assert.Empty(scopes); + } - logger.LogInformation("OpenTelemetry!"); - var logRecord = exportedItems[0]; + [Fact] + public void VerifyIncludeScopes_True() + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.IncludeScopes = true); + var logger = loggerFactory.CreateLogger(); - List scopes = new List(); + using var scope = logger.BeginScope("string_scope"); - logger.LogInformation("OpenTelemetry!"); - logRecord = exportedItems[1]; + logger.LogInformation("OpenTelemetry!"); + var logRecord = exportedItems[0]; - int reachedDepth = -1; - logRecord.ForEachScope( - (scope, state) => - { - reachedDepth++; - scopes.Add(scope.Scope); - foreach (KeyValuePair item in scope) - { - Assert.Equal(string.Empty, item.Key); - Assert.Equal("string_scope", item.Value); - } - }, - null); - Assert.Single(scopes); - Assert.Equal(0, reachedDepth); - Assert.Equal("string_scope", scopes[0]); + List scopes = new List(); - scopes.Clear(); + logger.LogInformation("OpenTelemetry!"); + logRecord = exportedItems[1]; - List> expectedScope2 = new List> + int reachedDepth = -1; + logRecord.ForEachScope( + (scope, state) => { - new KeyValuePair("item1", "value1"), - new KeyValuePair("item2", "value2"), - }; - using var scope2 = logger.BeginScope(expectedScope2); - - logger.LogInformation("OpenTelemetry!"); - logRecord = exportedItems[2]; - - reachedDepth = -1; - logRecord.ForEachScope( - (scope, state) => + reachedDepth++; + scopes.Add(scope.Scope); + foreach (KeyValuePair item in scope) { - scopes.Add(scope.Scope); - if (reachedDepth++ == 1) - { - foreach (KeyValuePair item in scope) - { - Assert.Contains(item, expectedScope2); - } - } - }, - null); - Assert.Equal(2, scopes.Count); - Assert.Equal(1, reachedDepth); - Assert.Equal("string_scope", scopes[0]); - Assert.Same(expectedScope2, scopes[1]); + Assert.Equal(string.Empty, item.Key); + Assert.Equal("string_scope", item.Value); + } + }, + null); + Assert.Single(scopes); + Assert.Equal(0, reachedDepth); + Assert.Equal("string_scope", scopes[0]); - scopes.Clear(); + scopes.Clear(); - KeyValuePair[] expectedScope3 = new KeyValuePair[] - { - new KeyValuePair("item3", "value3"), - new KeyValuePair("item4", "value4"), - }; - using var scope3 = logger.BeginScope(expectedScope3); + List> expectedScope2 = new List> + { + new KeyValuePair("item1", "value1"), + new KeyValuePair("item2", "value2"), + }; + using var scope2 = logger.BeginScope(expectedScope2); - logger.LogInformation("OpenTelemetry!"); - logRecord = exportedItems[3]; + logger.LogInformation("OpenTelemetry!"); + logRecord = exportedItems[2]; - reachedDepth = -1; - logRecord.ForEachScope( - (scope, state) => + reachedDepth = -1; + logRecord.ForEachScope( + (scope, state) => + { + scopes.Add(scope.Scope); + if (reachedDepth++ == 1) { - scopes.Add(scope.Scope); - if (reachedDepth++ == 2) + foreach (KeyValuePair item in scope) { - foreach (KeyValuePair item in scope) - { - Assert.Contains(item, expectedScope3); - } + Assert.Contains(item, expectedScope2); } - }, - null); - Assert.Equal(3, scopes.Count); - Assert.Equal(2, reachedDepth); - Assert.Equal("string_scope", scopes[0]); - Assert.Same(expectedScope2, scopes[1]); - Assert.Same(expectedScope3, scopes[2]); - } + } + }, + null); + Assert.Equal(2, scopes.Count); + Assert.Equal(1, reachedDepth); + Assert.Equal("string_scope", scopes[0]); + Assert.Same(expectedScope2, scopes[1]); - [Theory] - [InlineData(true)] - [InlineData(false)] - public void VerifyParseStateValues_UsingStandardExtensions(bool parseStateValues) - { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.ParseStateValues = parseStateValues); - var logger = loggerFactory.CreateLogger(); + scopes.Clear(); - // Tests state parsing with standard extensions. + KeyValuePair[] expectedScope3 = new KeyValuePair[] + { + new KeyValuePair("item3", "value3"), + new KeyValuePair("item4", "value4"), + }; + using var scope3 = logger.BeginScope(expectedScope3); - logger.LogInformation("{Product} {Year}!", "OpenTelemetry", 2021); - var logRecord = exportedItems[0]; + logger.LogInformation("OpenTelemetry!"); + logRecord = exportedItems[3]; - Assert.NotNull(logRecord.StateValues); - if (parseStateValues) + reachedDepth = -1; + logRecord.ForEachScope( + (scope, state) => { - Assert.Null(logRecord.State); - } - else - { - Assert.NotNull(logRecord.State); - } - - Assert.NotNull(logRecord.StateValues); - Assert.Equal(3, logRecord.StateValues.Count); - Assert.Equal(new KeyValuePair("Product", "OpenTelemetry"), logRecord.StateValues[0]); - Assert.Equal(new KeyValuePair("Year", 2021), logRecord.StateValues[1]); - Assert.Equal(new KeyValuePair("{OriginalFormat}", "{Product} {Year}!"), logRecord.StateValues[2]); - - var complex = new { Property = "Value" }; + scopes.Add(scope.Scope); + if (reachedDepth++ == 2) + { + foreach (KeyValuePair item in scope) + { + Assert.Contains(item, expectedScope3); + } + } + }, + null); + Assert.Equal(3, scopes.Count); + Assert.Equal(2, reachedDepth); + Assert.Equal("string_scope", scopes[0]); + Assert.Same(expectedScope2, scopes[1]); + Assert.Same(expectedScope3, scopes[2]); + } - logger.LogInformation("{Product} {Year} {Complex}!", "OpenTelemetry", 2021, complex); - logRecord = exportedItems[1]; + [Theory] + [InlineData(true)] + [InlineData(false)] + public void VerifyParseStateValues_UsingStandardExtensions(bool parseStateValues) + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.ParseStateValues = parseStateValues); + var logger = loggerFactory.CreateLogger(); - Assert.NotNull(logRecord.StateValues); - if (parseStateValues) - { - Assert.Null(logRecord.State); - } - else - { - Assert.NotNull(logRecord.State); - } + // Tests state parsing with standard extensions. - Assert.NotNull(logRecord.StateValues); - Assert.Equal(4, logRecord.StateValues.Count); - Assert.Equal(new KeyValuePair("Product", "OpenTelemetry"), logRecord.StateValues[0]); - Assert.Equal(new KeyValuePair("Year", 2021), logRecord.StateValues[1]); - Assert.Equal(new KeyValuePair("{OriginalFormat}", "{Product} {Year} {Complex}!"), logRecord.StateValues[3]); + logger.LogInformation("{Product} {Year}!", "OpenTelemetry", 2021); + var logRecord = exportedItems[0]; - KeyValuePair actualComplex = logRecord.StateValues[2]; - Assert.Equal("Complex", actualComplex.Key); - Assert.Same(complex, actualComplex.Value); + Assert.NotNull(logRecord.StateValues); + if (parseStateValues) + { + Assert.Null(logRecord.State); } - - [Fact] - public void ParseStateValuesUsingStructTest() + else { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.ParseStateValues = true); - var logger = loggerFactory.CreateLogger(); + Assert.NotNull(logRecord.State); + } + + Assert.NotNull(logRecord.StateValues); + Assert.Equal(3, logRecord.StateValues.Count); + Assert.Equal(new KeyValuePair("Product", "OpenTelemetry"), logRecord.StateValues[0]); + Assert.Equal(new KeyValuePair("Year", 2021), logRecord.StateValues[1]); + Assert.Equal(new KeyValuePair("{OriginalFormat}", "{Product} {Year}!"), logRecord.StateValues[2]); - // Tests struct IReadOnlyList> parse path. + var complex = new { Property = "Value" }; - logger.Log( - LogLevel.Information, - 0, - new StructState(new KeyValuePair("Key1", "Value1")), - null, - (s, e) => "OpenTelemetry!"); - var logRecord = exportedItems[0]; + logger.LogInformation("{Product} {Year} {Complex}!", "OpenTelemetry", 2021, complex); + logRecord = exportedItems[1]; + Assert.NotNull(logRecord.StateValues); + if (parseStateValues) + { Assert.Null(logRecord.State); - Assert.NotNull(logRecord.StateValues); - Assert.Equal(1, logRecord.StateValues.Count); - Assert.Equal(new KeyValuePair("Key1", "Value1"), logRecord.StateValues[0]); } - - [Fact] - public void ParseStateValuesUsingListTest() + else { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.ParseStateValues = true); - var logger = loggerFactory.CreateLogger(); + Assert.NotNull(logRecord.State); + } - // Tests ref IReadOnlyList> parse path. + Assert.NotNull(logRecord.StateValues); + Assert.Equal(4, logRecord.StateValues.Count); + Assert.Equal(new KeyValuePair("Product", "OpenTelemetry"), logRecord.StateValues[0]); + Assert.Equal(new KeyValuePair("Year", 2021), logRecord.StateValues[1]); + Assert.Equal(new KeyValuePair("{OriginalFormat}", "{Product} {Year} {Complex}!"), logRecord.StateValues[3]); - logger.Log( - LogLevel.Information, - 0, - new List> { new KeyValuePair("Key1", "Value1") }, - null, - (s, e) => "OpenTelemetry!"); - var logRecord = exportedItems[0]; + KeyValuePair actualComplex = logRecord.StateValues[2]; + Assert.Equal("Complex", actualComplex.Key); + Assert.Same(complex, actualComplex.Value); + } - Assert.Null(logRecord.State); - Assert.NotNull(logRecord.StateValues); - Assert.Equal(1, logRecord.StateValues.Count); - Assert.Equal(new KeyValuePair("Key1", "Value1"), logRecord.StateValues[0]); - } + [Fact] + public void ParseStateValuesUsingStructTest() + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.ParseStateValues = true); + var logger = loggerFactory.CreateLogger(); + + // Tests struct IReadOnlyList> parse path. + + logger.Log( + LogLevel.Information, + 0, + new StructState(new KeyValuePair("Key1", "Value1")), + null, + (s, e) => "OpenTelemetry!"); + var logRecord = exportedItems[0]; + + Assert.Null(logRecord.State); + Assert.NotNull(logRecord.StateValues); + Assert.Single(logRecord.StateValues); + Assert.Equal(new KeyValuePair("Key1", "Value1"), logRecord.StateValues[0]); + } - [Fact] - public void ParseStateValuesUsingIEnumerableTest() - { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.ParseStateValues = true); - var logger = loggerFactory.CreateLogger(); + [Fact] + public void ParseStateValuesUsingListTest() + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.ParseStateValues = true); + var logger = loggerFactory.CreateLogger(); + + // Tests ref IReadOnlyList> parse path. + + logger.Log( + LogLevel.Information, + 0, + new List> { new KeyValuePair("Key1", "Value1") }, + null, + (s, e) => "OpenTelemetry!"); + var logRecord = exportedItems[0]; + + Assert.Null(logRecord.State); + Assert.NotNull(logRecord.StateValues); + Assert.Single(logRecord.StateValues); + Assert.Equal(new KeyValuePair("Key1", "Value1"), logRecord.StateValues[0]); + } - // Tests IEnumerable> parse path. + [Fact] + public void ParseStateValuesUsingIEnumerableTest() + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.ParseStateValues = true); + var logger = loggerFactory.CreateLogger(); + + // Tests IEnumerable> parse path. + + logger.Log( + LogLevel.Information, + 0, + new ListState(new KeyValuePair("Key1", "Value1")), + null, + (s, e) => "OpenTelemetry!"); + var logRecord = exportedItems[0]; + + Assert.Null(logRecord.State); + Assert.NotNull(logRecord.StateValues); + Assert.Single(logRecord.StateValues); + Assert.Equal(new KeyValuePair("Key1", "Value1"), logRecord.StateValues[0]); + } - logger.Log( - LogLevel.Information, - 0, - new ListState(new KeyValuePair("Key1", "Value1")), - null, - (s, e) => "OpenTelemetry!"); - var logRecord = exportedItems[0]; + [Fact] + public void ParseStateValuesUsingNonconformingCustomTypeTest() + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.ParseStateValues = true); + var logger = loggerFactory.CreateLogger(); - Assert.Null(logRecord.State); - Assert.NotNull(logRecord.StateValues); - Assert.Equal(1, logRecord.StateValues.Count); - Assert.Equal(new KeyValuePair("Key1", "Value1"), logRecord.StateValues[0]); - } + // Tests unknown state parse path. - [Fact] - public void ParseStateValuesUsingNonconformingCustomTypeTest() + CustomState state = new CustomState { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.ParseStateValues = true); - var logger = loggerFactory.CreateLogger(); - - // Tests unknown state parse path. + Property = "Value", + }; + + logger.Log( + LogLevel.Information, + 0, + state, + null, + (s, e) => "OpenTelemetry!"); + var logRecord = exportedItems[0]; + + Assert.Null(logRecord.State); + Assert.NotNull(logRecord.StateValues); + + // Note: We currently do not support parsing custom states which do + // not implement the standard interfaces. We return empty attributes + // for these. + Assert.Empty(logRecord.StateValues); + } - CustomState state = new CustomState - { - Property = "Value", - }; + [Fact] + public void DisposingStateTest() + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.ParseStateValues = true); + var logger = loggerFactory.CreateLogger(); - logger.Log( - LogLevel.Information, - 0, - state, - null, - (s, e) => "OpenTelemetry!"); - var logRecord = exportedItems[0]; + DisposingState state = new DisposingState("Hello world"); - Assert.Null(logRecord.State); - Assert.NotNull(logRecord.StateValues); + logger.Log( + LogLevel.Information, + 0, + state, + null, + (s, e) => "OpenTelemetry!"); + var logRecord = exportedItems[0]; - // Note: We currently do not support parsing custom states which do - // not implement the standard interfaces. We return empty attributes - // for these. - Assert.Empty(logRecord.StateValues); - } + state.Dispose(); - [Fact] - public void DisposingStateTest() - { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.ParseStateValues = true); - var logger = loggerFactory.CreateLogger(); + Assert.Null(logRecord.State); + Assert.NotNull(logRecord.StateValues); + Assert.Single(logRecord.StateValues); - DisposingState state = new DisposingState("Hello world"); + KeyValuePair actualState = logRecord.StateValues[0]; - logger.Log( - LogLevel.Information, - 0, - state, - null, - (s, e) => "OpenTelemetry!"); - var logRecord = exportedItems[0]; + Assert.Same("Value", actualState.Key); + Assert.Same("Hello world", actualState.Value); + } - state.Dispose(); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ReusedLogRecordScopeTest(bool buffer) + { + var processor = new ScopeProcessor(buffer); - Assert.Null(logRecord.State); - Assert.NotNull(logRecord.StateValues); - Assert.Equal(1, logRecord.StateValues.Count); + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddOpenTelemetry(options => + { + options.IncludeScopes = true; + options.AddProcessor(processor); + }); + }); - KeyValuePair actualState = logRecord.StateValues[0]; + var logger = loggerFactory.CreateLogger("TestLogger"); - Assert.Same("Value", actualState.Key); - Assert.Same("Hello world", actualState.Value); + using (var scope1 = logger.BeginScope("scope1")) + { + logger.LogInformation("message1"); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ReusedLogRecordScopeTest(bool buffer) + using (var scope2 = logger.BeginScope("scope2")) { - var processor = new ScopeProcessor(buffer); + logger.LogInformation("message2"); + } + + Assert.Equal(2, processor.Scopes.Count); + Assert.Equal("scope1", processor.Scopes[0]); + Assert.Equal("scope2", processor.Scopes[1]); + } - using var loggerFactory = LoggerFactory.Create(builder => + [Fact] + public void IncludeStateTest() + { + using var loggerFactory = InitializeLoggerFactory( + out List exportedItems, configure: options => { - builder.AddOpenTelemetry(options => - { - options.IncludeScopes = true; - options.AddProcessor(processor); - }); + options.IncludeAttributes = false; }); + var logger = loggerFactory.CreateLogger(); - var logger = loggerFactory.CreateLogger("TestLogger"); + logger.LogInformation("Hello {world}", "earth"); - using (var scope1 = logger.BeginScope("scope1")) - { - logger.LogInformation("message1"); - } + var logRecord = exportedItems[0]; - using (var scope2 = logger.BeginScope("scope2")) - { - logger.LogInformation("message2"); - } + Assert.Null(logRecord.State); + Assert.Null(logRecord.Attributes); - Assert.Equal(2, processor.Scopes.Count); - Assert.Equal("scope1", processor.Scopes[0]); - Assert.Equal("scope2", processor.Scopes[1]); - } + Assert.Equal("Hello earth", logRecord.Body); + } - [Fact] - public void IncludeStateTest() - { - using var loggerFactory = InitializeLoggerFactory( - out List exportedItems, configure: options => - { - options.IncludeAttributes = false; - }); - var logger = loggerFactory.CreateLogger(); + [Theory] + [InlineData((int)LogRecordSeverity.Unspecified, LogLevel.Trace)] + [InlineData(int.MaxValue, LogLevel.Trace)] + [InlineData((int)LogRecordSeverity.Trace, LogLevel.Trace, (int)LogRecordSeverity.Trace)] + [InlineData((int)LogRecordSeverity.Trace2, LogLevel.Trace, (int)LogRecordSeverity.Trace)] + [InlineData((int)LogRecordSeverity.Trace3, LogLevel.Trace, (int)LogRecordSeverity.Trace)] + [InlineData((int)LogRecordSeverity.Trace4, LogLevel.Trace, (int)LogRecordSeverity.Trace)] + [InlineData((int)LogRecordSeverity.Debug, LogLevel.Debug, (int)LogRecordSeverity.Debug)] + [InlineData((int)LogRecordSeverity.Debug2, LogLevel.Debug, (int)LogRecordSeverity.Debug)] + [InlineData((int)LogRecordSeverity.Debug3, LogLevel.Debug, (int)LogRecordSeverity.Debug)] + [InlineData((int)LogRecordSeverity.Debug4, LogLevel.Debug, (int)LogRecordSeverity.Debug)] + [InlineData((int)LogRecordSeverity.Info, LogLevel.Information, (int)LogRecordSeverity.Info)] + [InlineData((int)LogRecordSeverity.Info2, LogLevel.Information, (int)LogRecordSeverity.Info)] + [InlineData((int)LogRecordSeverity.Info3, LogLevel.Information, (int)LogRecordSeverity.Info)] + [InlineData((int)LogRecordSeverity.Info4, LogLevel.Information, (int)LogRecordSeverity.Info)] + [InlineData((int)LogRecordSeverity.Warn, LogLevel.Warning, (int)LogRecordSeverity.Warn)] + [InlineData((int)LogRecordSeverity.Warn2, LogLevel.Warning, (int)LogRecordSeverity.Warn)] + [InlineData((int)LogRecordSeverity.Warn3, LogLevel.Warning, (int)LogRecordSeverity.Warn)] + [InlineData((int)LogRecordSeverity.Warn4, LogLevel.Warning, (int)LogRecordSeverity.Warn)] + [InlineData((int)LogRecordSeverity.Error, LogLevel.Error, (int)LogRecordSeverity.Error)] + [InlineData((int)LogRecordSeverity.Error2, LogLevel.Error, (int)LogRecordSeverity.Error)] + [InlineData((int)LogRecordSeverity.Error3, LogLevel.Error, (int)LogRecordSeverity.Error)] + [InlineData((int)LogRecordSeverity.Error4, LogLevel.Error, (int)LogRecordSeverity.Error)] + [InlineData((int)LogRecordSeverity.Fatal, LogLevel.Critical, (int)LogRecordSeverity.Fatal)] + [InlineData((int)LogRecordSeverity.Fatal2, LogLevel.Critical, (int)LogRecordSeverity.Fatal)] + [InlineData((int)LogRecordSeverity.Fatal3, LogLevel.Critical, (int)LogRecordSeverity.Fatal)] + [InlineData((int)LogRecordSeverity.Fatal4, LogLevel.Critical, (int)LogRecordSeverity.Fatal)] + public void SeverityLogLevelTest(int logSeverity, LogLevel logLevel, int? transformedLogSeverity = null) + { + var severity = (LogRecordSeverity)logSeverity; - logger.LogInformation("Hello {world}", "earth"); + var logRecord = new LogRecord + { + Severity = severity, + }; - var logRecord = exportedItems[0]; + Assert.Equal(logLevel, logRecord.LogLevel); - Assert.Null(logRecord.State); - Assert.Null(logRecord.Attributes); + if (transformedLogSeverity.HasValue) + { + logRecord.LogLevel = logLevel; - Assert.Equal("Hello earth", logRecord.Body); + Assert.Equal((LogRecordSeverity)transformedLogSeverity.Value, logRecord.Severity); + Assert.Equal(logLevel.ToString(), logRecord.SeverityText); } + } - [Theory] - [InlineData((int)LogRecordSeverity.Unspecified, LogLevel.Trace)] - [InlineData(int.MaxValue, LogLevel.Trace)] - [InlineData((int)LogRecordSeverity.Trace, LogLevel.Trace, (int)LogRecordSeverity.Trace)] - [InlineData((int)LogRecordSeverity.Trace2, LogLevel.Trace, (int)LogRecordSeverity.Trace)] - [InlineData((int)LogRecordSeverity.Trace3, LogLevel.Trace, (int)LogRecordSeverity.Trace)] - [InlineData((int)LogRecordSeverity.Trace4, LogLevel.Trace, (int)LogRecordSeverity.Trace)] - [InlineData((int)LogRecordSeverity.Debug, LogLevel.Debug, (int)LogRecordSeverity.Debug)] - [InlineData((int)LogRecordSeverity.Debug2, LogLevel.Debug, (int)LogRecordSeverity.Debug)] - [InlineData((int)LogRecordSeverity.Debug3, LogLevel.Debug, (int)LogRecordSeverity.Debug)] - [InlineData((int)LogRecordSeverity.Debug4, LogLevel.Debug, (int)LogRecordSeverity.Debug)] - [InlineData((int)LogRecordSeverity.Info, LogLevel.Information, (int)LogRecordSeverity.Info)] - [InlineData((int)LogRecordSeverity.Info2, LogLevel.Information, (int)LogRecordSeverity.Info)] - [InlineData((int)LogRecordSeverity.Info3, LogLevel.Information, (int)LogRecordSeverity.Info)] - [InlineData((int)LogRecordSeverity.Info4, LogLevel.Information, (int)LogRecordSeverity.Info)] - [InlineData((int)LogRecordSeverity.Warn, LogLevel.Warning, (int)LogRecordSeverity.Warn)] - [InlineData((int)LogRecordSeverity.Warn2, LogLevel.Warning, (int)LogRecordSeverity.Warn)] - [InlineData((int)LogRecordSeverity.Warn3, LogLevel.Warning, (int)LogRecordSeverity.Warn)] - [InlineData((int)LogRecordSeverity.Warn4, LogLevel.Warning, (int)LogRecordSeverity.Warn)] - [InlineData((int)LogRecordSeverity.Error, LogLevel.Error, (int)LogRecordSeverity.Error)] - [InlineData((int)LogRecordSeverity.Error2, LogLevel.Error, (int)LogRecordSeverity.Error)] - [InlineData((int)LogRecordSeverity.Error3, LogLevel.Error, (int)LogRecordSeverity.Error)] - [InlineData((int)LogRecordSeverity.Error4, LogLevel.Error, (int)LogRecordSeverity.Error)] - [InlineData((int)LogRecordSeverity.Fatal, LogLevel.Critical, (int)LogRecordSeverity.Fatal)] - [InlineData((int)LogRecordSeverity.Fatal2, LogLevel.Critical, (int)LogRecordSeverity.Fatal)] - [InlineData((int)LogRecordSeverity.Fatal3, LogLevel.Critical, (int)LogRecordSeverity.Fatal)] - [InlineData((int)LogRecordSeverity.Fatal4, LogLevel.Critical, (int)LogRecordSeverity.Fatal)] - public void SeverityLogLevelTest(int logSeverity, LogLevel logLevel, int? transformedLogSeverity = null) - { - var severity = (LogRecordSeverity)logSeverity; + [Fact] + public void LogRecordInstrumentationScopeTest() + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems); - var logRecord = new LogRecord - { - Severity = severity, - }; + var logger = loggerFactory.CreateLogger(); - Assert.Equal(logLevel, logRecord.LogLevel); + logger.LogInformation("Hello world!"); - if (transformedLogSeverity.HasValue) - { - logRecord.LogLevel = logLevel; + var logRecord = exportedItems.FirstOrDefault(); - Assert.Equal((LogRecordSeverity)transformedLogSeverity.Value, logRecord.Severity); - Assert.Equal(logLevel.ToString(), logRecord.SeverityText); - } - } + Assert.NotNull(logRecord); + Assert.NotNull(logRecord.Logger); + Assert.Equal("OpenTelemetry.Logs.Tests.LogRecordTest", logRecord.Logger.Name); + Assert.Null(logRecord.Logger.Version); + } - private static ILoggerFactory InitializeLoggerFactory(out List exportedItems, Action configure = null) - { - var items = exportedItems = new List(); + [Fact] + public void LogRecordCategoryNameAliasForInstrumentationScopeTests() + { + LogRecord logRecord = new(); - return LoggerFactory.Create(builder => - { - builder.AddOpenTelemetry(options => - { - configure?.Invoke(options); - options.AddInMemoryExporter(items); - }); - builder.AddFilter(typeof(LogRecordTest).FullName, LogLevel.Trace); - }); - } + Assert.Equal(string.Empty, logRecord.CategoryName); + Assert.Equal(logRecord.CategoryName, logRecord.Logger.Name); - internal struct Food - { - public string Name { get; set; } + logRecord.CategoryName = "Testing"; - public double Price { get; set; } - } + Assert.Equal("Testing", logRecord.CategoryName); + Assert.Equal(logRecord.CategoryName, logRecord.Logger.Name); + + logRecord.CategoryName = null; - private struct StructState : IReadOnlyList> + Assert.Equal(string.Empty, logRecord.CategoryName); + Assert.Equal(logRecord.CategoryName, logRecord.Logger.Name); + + var exportedItems = new List(); + using (var loggerProvider = Sdk.CreateLoggerProviderBuilder() + .AddProcessor(new BatchLogRecordExportProcessor(new InMemoryExporter(exportedItems))) + .Build()) { - private readonly List> list; + var logger = loggerProvider.GetLogger("TestName"); + logger.EmitLog(default); + } - public StructState(params KeyValuePair[] items) - { - this.list = new List>(items); - } + Assert.Single(exportedItems); - public int Count => this.list.Count; + Assert.Equal("TestName", exportedItems[0].CategoryName); + Assert.Equal(exportedItems[0].CategoryName, exportedItems[0].Logger.Name); + } - public KeyValuePair this[int index] => this.list[index]; + private static ILoggerFactory InitializeLoggerFactory(out List exportedItems, Action configure = null) + { + var items = exportedItems = new List(); - public IEnumerator> GetEnumerator() + return LoggerFactory.Create(builder => + { + builder.AddOpenTelemetry(options => { - return this.list.GetEnumerator(); - } + configure?.Invoke(options); + options.AddInMemoryExporter(items); + }); + builder.AddFilter(typeof(LogRecordTest).FullName, LogLevel.Trace); + }); + } - IEnumerator IEnumerable.GetEnumerator() - { - return this.list.GetEnumerator(); - } - } + internal struct Food + { + public string Name { get; set; } + + public double Price { get; set; } + } - internal sealed class DisposingState : IReadOnlyList>, IDisposable + private readonly struct StructState : IReadOnlyList> + { + private readonly List> list; + + public StructState(params KeyValuePair[] items) { - private string value; - private bool disposed; + this.list = new List>(items); + } - public DisposingState(string value) - { - this.Value = value; - } + public int Count => this.list.Count; - public int Count => 1; + public KeyValuePair this[int index] => this.list[index]; - public string Value - { - get - { - if (this.disposed) - { - throw new ObjectDisposedException(nameof(DisposingState)); - } + public IEnumerator> GetEnumerator() + { + return this.list.GetEnumerator(); + } - return this.value; - } - private set => this.value = value; - } + IEnumerator IEnumerable.GetEnumerator() + { + return this.list.GetEnumerator(); + } + } - public KeyValuePair this[int index] => index switch - { - 0 => new KeyValuePair(nameof(this.Value), this.Value), - _ => throw new IndexOutOfRangeException(nameof(index)), - }; + internal sealed class DisposingState : IReadOnlyList>, IDisposable + { + private string value; + private bool disposed; - public void Dispose() - { - this.disposed = true; - } + public DisposingState(string value) + { + this.Value = value; + } - public IEnumerator> GetEnumerator() + public int Count => 1; + + public string Value + { + get { - for (var i = 0; i < this.Count; i++) + if (this.disposed) { - yield return this[i]; + throw new ObjectDisposedException(nameof(DisposingState)); } - } - IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + return this.value; + } + private set => this.value = value; } - private class RedactionProcessor : BaseProcessor + public KeyValuePair this[int index] => index switch { - private readonly Field fieldToUpdate; + 0 => new KeyValuePair(nameof(this.Value), this.Value), + _ => throw new IndexOutOfRangeException(nameof(index)), + }; - public RedactionProcessor(Field fieldToUpdate) - { - this.fieldToUpdate = fieldToUpdate; - } + public void Dispose() + { + this.disposed = true; + } - public override void OnEnd(LogRecord logRecord) + public IEnumerator> GetEnumerator() + { + for (var i = 0; i < this.Count; i++) { - if (this.fieldToUpdate == Field.State) - { - logRecord.State = new List> { new KeyValuePair("newStateKey", "newStateValue") }; - } - else if (this.fieldToUpdate == Field.StateValues) - { - logRecord.StateValues = new List> { new KeyValuePair("newStateValueKey", "newStateValueValue") }; - } - else - { - logRecord.FormattedMessage = "OpenTelemetry Good Night!"; - } + yield return this[i]; } } - private class ListState : IEnumerable> + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + } + + private class RedactionProcessor : BaseProcessor + { + private readonly Field fieldToUpdate; + + public RedactionProcessor(Field fieldToUpdate) { - private readonly List> list; + this.fieldToUpdate = fieldToUpdate; + } - public ListState(params KeyValuePair[] items) + public override void OnEnd(LogRecord logRecord) + { + if (this.fieldToUpdate == Field.State) { - this.list = new List>(items); + logRecord.State = new List> { new KeyValuePair("newStateKey", "newStateValue") }; } - - public IEnumerator> GetEnumerator() + else if (this.fieldToUpdate == Field.StateValues) { - return this.list.GetEnumerator(); + logRecord.StateValues = new List> { new KeyValuePair("newStateValueKey", "newStateValueValue") }; } - - IEnumerator IEnumerable.GetEnumerator() + else { - return this.list.GetEnumerator(); + logRecord.FormattedMessage = "OpenTelemetry Good Night!"; } } + } - private class CustomState - { - public const string ToStringValue = "CustomState.ToString"; + private class ListState : IEnumerable> + { + private readonly List> list; - public string Property { get; set; } + public ListState(params KeyValuePair[] items) + { + this.list = new List>(items); + } - public override string ToString() - => ToStringValue; + public IEnumerator> GetEnumerator() + { + return this.list.GetEnumerator(); } - private class ScopeProcessor : BaseProcessor + IEnumerator IEnumerable.GetEnumerator() { - private readonly bool buffer; + return this.list.GetEnumerator(); + } + } - public ScopeProcessor(bool buffer) - { - this.buffer = buffer; - } + private class CustomState + { + public const string ToStringValue = "CustomState.ToString"; - public List Scopes { get; } = new(); + public string Property { get; set; } - public override void OnEnd(LogRecord data) - { - data.ForEachScope( - (scope, state) => - { - this.Scopes.Add(scope.Scope); - }, - null); + public override string ToString() + => ToStringValue; + } - if (this.buffer) + private class ScopeProcessor : BaseProcessor + { + private readonly bool buffer; + + public ScopeProcessor(bool buffer) + { + this.buffer = buffer; + } + + public List Scopes { get; } = new(); + + public override void OnEnd(LogRecord data) + { + data.ForEachScope( + (scope, state) => { - data.Buffer(); - } + this.Scopes.Add(scope.Scope); + }, + null); + + if (this.buffer) + { + data.Buffer(); } } } } -#endif diff --git a/test/OpenTelemetry.Tests/Logs/LogRecordThreadStaticPoolTests.cs b/test/OpenTelemetry.Tests/Logs/LogRecordThreadStaticPoolTests.cs index df244db0ec5..59c0b53454e 100644 --- a/test/OpenTelemetry.Tests/Logs/LogRecordThreadStaticPoolTests.cs +++ b/test/OpenTelemetry.Tests/Logs/LogRecordThreadStaticPoolTests.cs @@ -1,89 +1,76 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #nullable enable using Xunit; -namespace OpenTelemetry.Logs.Tests +namespace OpenTelemetry.Logs.Tests; + +public sealed class LogRecordThreadStaticPoolTests { - public sealed class LogRecordThreadStaticPoolTests + [Fact] + public void RentReturnTests() { - [Fact] - public void RentReturnTests() - { - LogRecordThreadStaticPool.Storage = null; + LogRecordThreadStaticPool.Storage = null; - var logRecord = LogRecordThreadStaticPool.Instance.Rent(); - Assert.NotNull(logRecord); - Assert.Null(LogRecordThreadStaticPool.Storage); + var logRecord = LogRecordThreadStaticPool.Instance.Rent(); + Assert.NotNull(logRecord); + Assert.Null(LogRecordThreadStaticPool.Storage); - LogRecordThreadStaticPool.Instance.Return(logRecord); - Assert.NotNull(LogRecordThreadStaticPool.Storage); - Assert.Equal(logRecord, LogRecordThreadStaticPool.Storage); + LogRecordThreadStaticPool.Instance.Return(logRecord); + Assert.NotNull(LogRecordThreadStaticPool.Storage); + Assert.Equal(logRecord, LogRecordThreadStaticPool.Storage); - LogRecordThreadStaticPool.Instance.Return(new()); - Assert.NotNull(LogRecordThreadStaticPool.Storage); - Assert.Equal(logRecord, LogRecordThreadStaticPool.Storage); + // Note: This record will be ignored because there is already something in the ThreadStatic storage. + LogRecordThreadStaticPool.Instance.Return(new() { Source = LogRecord.LogRecordSource.FromThreadStaticPool }); + Assert.NotNull(LogRecordThreadStaticPool.Storage); + Assert.Equal(logRecord, LogRecordThreadStaticPool.Storage); - LogRecordThreadStaticPool.Storage = null; + LogRecordThreadStaticPool.Storage = null; - var manual = new LogRecord(); - LogRecordThreadStaticPool.Instance.Return(manual); - Assert.NotNull(LogRecordThreadStaticPool.Storage); - Assert.Equal(manual, LogRecordThreadStaticPool.Storage); - } + var newLogRecord = new LogRecord() { Source = LogRecord.LogRecordSource.FromThreadStaticPool }; + LogRecordThreadStaticPool.Instance.Return(newLogRecord); + Assert.NotNull(LogRecordThreadStaticPool.Storage); + Assert.Equal(newLogRecord, LogRecordThreadStaticPool.Storage); + } - [Fact] - public void ClearTests() + [Fact] + public void ClearTests() + { + var logRecord1 = LogRecordThreadStaticPool.Instance.Rent(); + logRecord1.AttributeStorage = new List>(16) { - var logRecord1 = LogRecordThreadStaticPool.Instance.Rent(); - logRecord1.AttributeStorage = new List>(16) - { - new KeyValuePair("key1", "value1"), - new KeyValuePair("key2", "value2"), - }; - logRecord1.ScopeStorage = new List(8) { null, null }; + new KeyValuePair("key1", "value1"), + new KeyValuePair("key2", "value2"), + }; + logRecord1.ScopeStorage = new List(8) { null, null }; - LogRecordThreadStaticPool.Instance.Return(logRecord1); + LogRecordThreadStaticPool.Instance.Return(logRecord1); - Assert.Empty(logRecord1.AttributeStorage); - Assert.Equal(16, logRecord1.AttributeStorage.Capacity); - Assert.Empty(logRecord1.ScopeStorage); - Assert.Equal(8, logRecord1.ScopeStorage.Capacity); + Assert.Empty(logRecord1.AttributeStorage); + Assert.Equal(16, logRecord1.AttributeStorage.Capacity); + Assert.Empty(logRecord1.ScopeStorage); + Assert.Equal(8, logRecord1.ScopeStorage.Capacity); - logRecord1 = LogRecordThreadStaticPool.Instance.Rent(); + logRecord1 = LogRecordThreadStaticPool.Instance.Rent(); - Assert.NotNull(logRecord1.AttributeStorage); - Assert.NotNull(logRecord1.ScopeStorage); + Assert.NotNull(logRecord1.AttributeStorage); + Assert.NotNull(logRecord1.ScopeStorage); - for (int i = 0; i <= LogRecordPoolHelper.DefaultMaxNumberOfAttributes; i++) - { - logRecord1.AttributeStorage!.Add(new KeyValuePair("key", "value")); - } + for (int i = 0; i <= LogRecordPoolHelper.DefaultMaxNumberOfAttributes; i++) + { + logRecord1.AttributeStorage!.Add(new KeyValuePair("key", "value")); + } - for (int i = 0; i <= LogRecordPoolHelper.DefaultMaxNumberOfScopes; i++) - { - logRecord1.ScopeStorage!.Add(null); - } + for (int i = 0; i <= LogRecordPoolHelper.DefaultMaxNumberOfScopes; i++) + { + logRecord1.ScopeStorage!.Add(null); + } - LogRecordThreadStaticPool.Instance.Return(logRecord1); + LogRecordThreadStaticPool.Instance.Return(logRecord1); - Assert.Null(logRecord1.AttributeStorage); - Assert.Null(logRecord1.ScopeStorage); - } + Assert.Null(logRecord1.AttributeStorage); + Assert.Null(logRecord1.ScopeStorage); } } diff --git a/test/OpenTelemetry.Tests/Logs/LoggerFactoryAndResourceBuilderTests.cs b/test/OpenTelemetry.Tests/Logs/LoggerFactoryAndResourceBuilderTests.cs index 4d824dd0761..073160b6e5c 100644 --- a/test/OpenTelemetry.Tests/Logs/LoggerFactoryAndResourceBuilderTests.cs +++ b/test/OpenTelemetry.Tests/Logs/LoggerFactoryAndResourceBuilderTests.cs @@ -1,79 +1,65 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.Logging; using OpenTelemetry.Exporter; using OpenTelemetry.Resources; using Xunit; -namespace OpenTelemetry.Logs.Tests +namespace OpenTelemetry.Logs.Tests; + +public sealed class LoggerFactoryAndResourceBuilderTests { - public sealed class LoggerFactoryAndResourceBuilderTests + [Fact] + public void TestLogExporterCanAccessResource() + { + VerifyResourceBuilder( + assert: (Resource resource) => + { + Assert.Contains(resource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.ToString().Contains("unknown_service")); + }); + } + + [Fact] + public void VerifyResourceBuilder_WithServiceNameEnVar() { - [Fact] - public void TestLogExporterCanAccessResource() + try { + Environment.SetEnvironmentVariable(OtelServiceNameEnvVarDetector.EnvVarKey, "MyService"); + VerifyResourceBuilder( assert: (Resource resource) => { - Assert.Contains(resource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.ToString().Contains("unknown_service")); + Assert.Contains(resource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.Equals("MyService")); }); } - - [Fact] - public void VerifyResourceBuilder_WithServiceNameEnVar() + finally { - try - { - Environment.SetEnvironmentVariable(OtelServiceNameEnvVarDetector.EnvVarKey, "MyService"); - - VerifyResourceBuilder( - assert: (Resource resource) => - { - Assert.Contains(resource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.Equals("MyService")); - }); - } - finally - { - Environment.SetEnvironmentVariable(OtelServiceNameEnvVarDetector.EnvVarKey, null); - } + Environment.SetEnvironmentVariable(OtelServiceNameEnvVarDetector.EnvVarKey, null); } + } - private static void VerifyResourceBuilder( - Action assert) + private static void VerifyResourceBuilder( + Action assert) + { + // Setup + using var exporter = new InMemoryExporter(new List()); + using var loggerFactory = LoggerFactory.Create(builder => { - // Setup - using var exporter = new InMemoryExporter(new List()); - using var loggerFactory = LoggerFactory.Create(builder => + builder.AddOpenTelemetry(options => { - builder.AddOpenTelemetry(options => - { - options.AddProcessor(new SimpleLogRecordExportProcessor(exporter)); - }); + options.AddProcessor(new SimpleLogRecordExportProcessor(exporter)); }); + }); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); - Assert.NotNull(exporter.ParentProvider); + Assert.NotNull(exporter.ParentProvider); - var resource = exporter.ParentProvider.GetResource(); - Assert.NotNull(resource); + var resource = exporter.ParentProvider.GetResource(); + Assert.NotNull(resource); - // Verify - assert.Invoke(resource); - } + // Verify + assert.Invoke(resource); } } diff --git a/test/OpenTelemetry.Tests/Logs/LoggerProviderBuilderExtensionsTests.cs b/test/OpenTelemetry.Tests/Logs/LoggerProviderBuilderExtensionsTests.cs index 022f0c5bc01..e8a40af4f1b 100644 --- a/test/OpenTelemetry.Tests/Logs/LoggerProviderBuilderExtensionsTests.cs +++ b/test/OpenTelemetry.Tests/Logs/LoggerProviderBuilderExtensionsTests.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #nullable enable @@ -33,6 +20,7 @@ public void LoggerProviderBuilderAddInstrumentationTest() .AddInstrumentation() .AddInstrumentation((sp, provider) => new CustomInstrumentation() { Provider = provider }) .AddInstrumentation(new CustomInstrumentation()) + .AddInstrumentation(() => (object?)null) .Build() as LoggerProviderSdk) { Assert.NotNull(provider); @@ -178,6 +166,14 @@ public void LoggerProviderBuilderAddProcessorTest() } } + [Fact] + public void LoggerProviderBuilderCustomImplementationBuildTest() + { + var builder = new CustomLoggerProviderBuilder(); + + Assert.Throws(() => builder.Build()); + } + private sealed class CustomInstrumentation : IDisposable { public bool Disposed; @@ -208,4 +204,12 @@ public override ExportResult Export(in Batch batch) return ExportResult.Success; } } + + private sealed class CustomLoggerProviderBuilder : LoggerProviderBuilder + { + public override LoggerProviderBuilder AddInstrumentation(Func instrumentationFactory) + { + throw new NotImplementedException(); + } + } } diff --git a/test/OpenTelemetry.Tests/Logs/LoggerProviderExtensionsTests.cs b/test/OpenTelemetry.Tests/Logs/LoggerProviderExtensionsTests.cs index 52e8ddf7065..0324027f7f2 100644 --- a/test/OpenTelemetry.Tests/Logs/LoggerProviderExtensionsTests.cs +++ b/test/OpenTelemetry.Tests/Logs/LoggerProviderExtensionsTests.cs @@ -1,21 +1,9 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #nullable enable +using OpenTelemetry.Exporter; using Xunit; namespace OpenTelemetry.Logs.Tests; @@ -44,12 +32,11 @@ public void AddProcessorTest() [Fact] public void ForceFlushTest() { - var exporter = new TestExporter(); - + List exportedItems = new(); using var provider = Sdk.CreateLoggerProviderBuilder() .AddProcessor( new BatchLogRecordExportProcessor( - exporter, + new InMemoryExporter(exportedItems), scheduledDelayMilliseconds: int.MaxValue)) .Build(); @@ -65,11 +52,11 @@ public void ForceFlushTest() logger.EmitLog(new LogRecordData { Body = "Hello world" }); - Assert.Empty(exporter.LogRecords); + Assert.Empty(exportedItems); Assert.True(provider.ForceFlush()); - Assert.Single(exporter.LogRecords); + Assert.Single(exportedItems); } [Fact] @@ -94,19 +81,4 @@ public void ShutdownTest() private sealed class TestProcessor : BaseProcessor { } - - private sealed class TestExporter : BaseExporter - { - public List LogRecords { get; } = new(); - - public override ExportResult Export(in Batch batch) - { - foreach (var logRecord in batch) - { - this.LogRecords.Add(logRecord); - } - - return ExportResult.Success; - } - } } diff --git a/test/OpenTelemetry.Tests/Logs/LoggerProviderSdkTests.cs b/test/OpenTelemetry.Tests/Logs/LoggerProviderSdkTests.cs index dc8ace58797..7122dd089e1 100644 --- a/test/OpenTelemetry.Tests/Logs/LoggerProviderSdkTests.cs +++ b/test/OpenTelemetry.Tests/Logs/LoggerProviderSdkTests.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #nullable enable @@ -32,7 +19,7 @@ public void ResourceDetectionUsingIConfigurationTest() .ConfigureServices(services => { services.AddSingleton( - new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { ["OTEL_SERVICE_NAME"] = "TestServiceName" }).Build()); + new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { ["OTEL_SERVICE_NAME"] = "TestServiceName" }).Build()); }) .Build() as LoggerProviderSdk; diff --git a/test/OpenTelemetry.Tests/Logs/OpenTelemetryLoggerProviderTests.cs b/test/OpenTelemetry.Tests/Logs/OpenTelemetryLoggerProviderTests.cs index a72a9086e15..e40d8d8b60a 100644 --- a/test/OpenTelemetry.Tests/Logs/OpenTelemetryLoggerProviderTests.cs +++ b/test/OpenTelemetry.Tests/Logs/OpenTelemetryLoggerProviderTests.cs @@ -1,88 +1,74 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Xunit; -namespace OpenTelemetry.Logs.Tests +namespace OpenTelemetry.Logs.Tests; + +public sealed class OpenTelemetryLoggerProviderTests { - public sealed class OpenTelemetryLoggerProviderTests + [Fact] + public void DefaultCtorTests() { - [Fact] - public void DefaultCtorTests() - { - var services = new ServiceCollection(); - services.AddOptions(); + var services = new ServiceCollection(); + services.AddOptions(); + + using var sp = services.BuildServiceProvider(); - using var sp = services.BuildServiceProvider(); + OpenTelemetryLoggerOptions defaults = new(); - OpenTelemetryLoggerOptions defaults = new(); + using OpenTelemetryLoggerProvider provider = new(sp.GetRequiredService>()); - using OpenTelemetryLoggerProvider provider = new(sp.GetRequiredService>()); + Assert.Equal(defaults.IncludeScopes, provider.Options.IncludeScopes); + Assert.Equal(defaults.IncludeFormattedMessage, provider.Options.IncludeFormattedMessage); + Assert.Equal(defaults.ParseStateValues, provider.Options.ParseStateValues); - Assert.Equal(defaults.IncludeScopes, provider.Options.IncludeScopes); - Assert.Equal(defaults.IncludeFormattedMessage, provider.Options.IncludeFormattedMessage); - Assert.Equal(defaults.ParseStateValues, provider.Options.ParseStateValues); + var providerSdk = provider.Provider as LoggerProviderSdk; - var providerSdk = provider.Provider as LoggerProviderSdk; + Assert.NotNull(providerSdk); + Assert.Null(providerSdk.Processor); + Assert.NotNull(providerSdk.Resource); + } - Assert.NotNull(providerSdk); - Assert.Null(providerSdk.Processor); - Assert.NotNull(providerSdk.Resource); - } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void VerifyOptionsCannotBeChangedAfterInit(bool initialValue) + { + var services = new ServiceCollection(); - [Theory] - [InlineData(true)] - [InlineData(false)] - public void VerifyOptionsCannotBeChangedAfterInit(bool initialValue) + services.AddOptions().Configure(o => { - var services = new ServiceCollection(); - - services.AddOptions().Configure(o => - { - o.IncludeFormattedMessage = initialValue; - o.IncludeScopes = initialValue; - o.ParseStateValues = initialValue; - }); + o.IncludeFormattedMessage = initialValue; + o.IncludeScopes = initialValue; + o.ParseStateValues = initialValue; + }); - using var sp = services.BuildServiceProvider(); + using var sp = services.BuildServiceProvider(); - var optionsMonitor = sp.GetRequiredService>(); + var optionsMonitor = sp.GetRequiredService>(); - var provider = new OpenTelemetryLoggerProvider(optionsMonitor); + var provider = new OpenTelemetryLoggerProvider(optionsMonitor); - // Verify initial set - Assert.Equal(initialValue, provider.Options.IncludeFormattedMessage); - Assert.Equal(initialValue, provider.Options.IncludeScopes); - Assert.Equal(initialValue, provider.Options.ParseStateValues); + // Verify initial set + Assert.Equal(initialValue, provider.Options.IncludeFormattedMessage); + Assert.Equal(initialValue, provider.Options.IncludeScopes); + Assert.Equal(initialValue, provider.Options.ParseStateValues); - var options = optionsMonitor.CurrentValue; + var options = optionsMonitor.CurrentValue; - Assert.NotNull(options); + Assert.NotNull(options); - // Attempt to change value - options.IncludeFormattedMessage = !initialValue; - options.IncludeScopes = !initialValue; - options.ParseStateValues = !initialValue; + // Attempt to change value + options.IncludeFormattedMessage = !initialValue; + options.IncludeScopes = !initialValue; + options.ParseStateValues = !initialValue; - // Verify processor is unchanged - Assert.Equal(initialValue, provider.Options.IncludeFormattedMessage); - Assert.Equal(initialValue, provider.Options.IncludeScopes); - Assert.Equal(initialValue, provider.Options.ParseStateValues); - } + // Verify processor is unchanged + Assert.Equal(initialValue, provider.Options.IncludeFormattedMessage); + Assert.Equal(initialValue, provider.Options.IncludeScopes); + Assert.Equal(initialValue, provider.Options.ParseStateValues); } } diff --git a/test/OpenTelemetry.Tests/Logs/OpenTelemetryLoggingExtensionsTests.cs b/test/OpenTelemetry.Tests/Logs/OpenTelemetryLoggingExtensionsTests.cs index c6a6aa5a893..cdebc61f176 100644 --- a/test/OpenTelemetry.Tests/Logs/OpenTelemetryLoggingExtensionsTests.cs +++ b/test/OpenTelemetry.Tests/Logs/OpenTelemetryLoggingExtensionsTests.cs @@ -1,21 +1,10 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 #nullable enable +using System.Reflection; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Xunit; @@ -24,16 +13,25 @@ namespace OpenTelemetry.Logs.Tests; public sealed class OpenTelemetryLoggingExtensionsTests { - [Fact] - public void ServiceCollectionAddOpenTelemetryNoParametersTest() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ServiceCollectionAddOpenTelemetryNoParametersTest(bool callUseExtension) { bool optionsCallbackInvoked = false; var serviceCollection = new ServiceCollection(); - serviceCollection.AddLogging(configure => + serviceCollection.AddLogging(logging => { - configure.AddOpenTelemetry(); + if (callUseExtension) + { + logging.UseOpenTelemetry(); + } + else + { + logging.AddOpenTelemetry(); + } }); serviceCollection.Configure(options => @@ -51,10 +49,16 @@ public void ServiceCollectionAddOpenTelemetryNoParametersTest() } [Theory] - [InlineData(1, 0)] - [InlineData(1, 1)] - [InlineData(5, 5)] - public void ServiceCollectionAddOpenTelemetryConfigureActionTests(int numberOfBuilderRegistrations, int numberOfOptionsRegistrations) + [InlineData(false, 1, 0)] + [InlineData(false, 1, 1)] + [InlineData(false, 5, 5)] + [InlineData(true, 1, 0)] + [InlineData(true, 1, 1)] + [InlineData(true, 5, 5)] + public void ServiceCollectionAddOpenTelemetryConfigureActionTests( + bool callUseExtension, + int numberOfBuilderRegistrations, + int numberOfOptionsRegistrations) { int configureCallbackInvocations = 0; int optionsCallbackInvocations = 0; @@ -62,11 +66,18 @@ public void ServiceCollectionAddOpenTelemetryConfigureActionTests(int numberOfBu var serviceCollection = new ServiceCollection(); - serviceCollection.AddLogging(configure => + serviceCollection.AddLogging(logging => { for (int i = 0; i < numberOfBuilderRegistrations; i++) { - configure.AddOpenTelemetry(ConfigureCallback); + if (callUseExtension) + { + logging.UseOpenTelemetry(configureBuilder: null, configureOptions: ConfigureCallback); + } + else + { + logging.AddOpenTelemetry(ConfigureCallback); + } } }); @@ -114,4 +125,201 @@ void OptionsCallback(OpenTelemetryLoggerOptions options) optionsCallbackInvocations++; } } + + [Fact] + public void UseOpenTelemetryDependencyInjectionTest() + { + var serviceCollection = new ServiceCollection(); + + serviceCollection.AddLogging(logging => + { + logging.UseOpenTelemetry(builder => + { + builder.ConfigureServices(services => + { + services.AddSingleton(); + }); + + builder.ConfigureBuilder((sp, builder) => + { + builder.AddProcessor( + sp.GetRequiredService()); + }); + }); + }); + + using var sp = serviceCollection.BuildServiceProvider(); + + var loggerProvider = sp.GetRequiredService() as LoggerProviderSdk; + + Assert.NotNull(loggerProvider); + + Assert.NotNull(loggerProvider.Processor); + + Assert.True(loggerProvider.Processor is TestLogProcessor); + } + + [Fact] + public void UseOpenTelemetryOptionsOrderingTest() + { + int currentIndex = -1; + int beforeDelegateIndex = -1; + int extensionDelegateIndex = -1; + int afterDelegateIndex = -1; + + var serviceCollection = new ServiceCollection(); + + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { ["Logging:OpenTelemetry:IncludeFormattedMessage"] = "true" }) + .Build(); + + serviceCollection.Configure(o => + { + // Verify this fires BEFORE options are bound + Assert.False(o.IncludeFormattedMessage); + + beforeDelegateIndex = ++currentIndex; + }); + + serviceCollection.AddLogging(logging => + { + // Note: Typically the host binds logging configuration to the + // "Logging" section but since we aren't using a host we do this + // manually. + logging.AddConfiguration(config.GetSection("Logging")); + + logging.UseOpenTelemetry( + configureBuilder: null, + configureOptions: o => + { + // Verify this fires AFTER options are bound + Assert.True(o.IncludeFormattedMessage); + + extensionDelegateIndex = ++currentIndex; + }); + }); + + serviceCollection.Configure(o => afterDelegateIndex = ++currentIndex); + + using var sp = serviceCollection.BuildServiceProvider(); + + var loggerProvider = sp.GetRequiredService() as LoggerProviderSdk; + + Assert.NotNull(loggerProvider); + + Assert.Equal(0, beforeDelegateIndex); + Assert.Equal(1, extensionDelegateIndex); + Assert.Equal(2, afterDelegateIndex); + } + + // This test validates that the OpenTelemetryLoggerOptions contains only primitive type properties. + // This is necessary to ensure trim correctness since that class is effectively deserialized from + // configuration. The top level properties are ensured via annotation on the RegisterProviderOptions API + // but if there was any complex type property, members of the complex type would not be preserved + // and could lead to incompatibilities with trimming. + [Fact] + public void TestTrimmingCorrectnessOfOpenTelemetryLoggerOptions() + { + foreach (var prop in typeof(OpenTelemetryLoggerOptions).GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + { + Assert.True(prop.PropertyType.IsPrimitive, $"Property OpenTelemetryLoggerOptions.{prop.Name} doesn't have a primitive type. This is potentially a trim compatibility issue."); + } + } + + [Fact] + public void VerifyAddProcessorOverloadWithImplementationFactory() + { + // arrange + var services = new ServiceCollection(); + + services.AddSingleton(); + + services.AddLogging(logging => + logging.AddOpenTelemetry( + o => o.AddProcessor(sp => sp.GetRequiredService()))); + + // act + using var sp = services.BuildServiceProvider(); + + var loggerProvider = sp.GetRequiredService() as LoggerProviderSdk; + + // assert + Assert.NotNull(loggerProvider); + Assert.NotNull(loggerProvider.Processor); + Assert.True(loggerProvider.Processor is TestLogProcessor); + } + + [Fact] + public void VerifyExceptionIsThrownWhenImplementationFactoryIsNull() + { + // arrange + var services = new ServiceCollection(); + + services.AddLogging(logging => + logging.AddOpenTelemetry( + o => o.AddProcessor(implementationFactory: null!))); + + // act + using var sp = services.BuildServiceProvider(); + + // assert + Assert.Throws(() => sp.GetRequiredService() as LoggerProviderSdk); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CircularReferenceTest(bool requestLoggerProviderDirectly) + { + var services = new ServiceCollection(); + + services.AddLogging(logging => logging.AddOpenTelemetry()); + + services.ConfigureOpenTelemetryLoggerProvider(builder => builder.AddProcessor()); + + using var sp = services.BuildServiceProvider(); + + if (requestLoggerProviderDirectly) + { + var provider = sp.GetRequiredService(); + Assert.NotNull(provider); + } + else + { + var factory = sp.GetRequiredService(); + Assert.NotNull(factory); + } + + var loggerProvider = sp.GetRequiredService() as LoggerProviderSdk; + + Assert.NotNull(loggerProvider); + + Assert.True(loggerProvider.Processor is TestLogProcessorWithILoggerFactoryDependency); + } + + private class TestLogProcessor : BaseProcessor + { + } + + private class TestLogProcessorWithILoggerFactoryDependency : BaseProcessor + { + private readonly ILogger logger; + + public TestLogProcessorWithILoggerFactoryDependency(ILoggerFactory loggerFactory) + { + // Note: It is NOT recommended to log from inside a processor. This + // test is meant to mirror someone injecting IHttpClientFactory + // (which itself uses ILoggerFactory) as part of an exporter. That + // is a more realistic scenario but needs a dependency to do that so + // here we approximate the graph. + this.logger = loggerFactory.CreateLogger("MyLogger"); + } + + protected override void Dispose(bool disposing) + { + this.logger.LogInformation("Dispose called"); + + base.Dispose(disposing); + } + } } diff --git a/test/OpenTelemetry.Tests/Metrics/AggregatorTest.cs b/test/OpenTelemetry.Tests/Metrics/AggregatorTest.cs deleted file mode 100644 index 94ce11b76a8..00000000000 --- a/test/OpenTelemetry.Tests/Metrics/AggregatorTest.cs +++ /dev/null @@ -1,466 +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 Xunit; - -namespace OpenTelemetry.Metrics.Tests -{ - public class AggregatorTest - { - private static readonly Meter Meter = new("testMeter"); - private static readonly Instrument Instrument = Meter.CreateHistogram("testInstrument"); - private static readonly ExplicitBucketHistogramConfiguration HistogramConfiguration = new() { Boundaries = Metric.DefaultHistogramBounds }; - private static readonly MetricStreamIdentity MetricStreamIdentity = new(Instrument, HistogramConfiguration); - private readonly AggregatorStore aggregatorStore = new(MetricStreamIdentity, AggregationType.HistogramWithBuckets, AggregationTemporality.Cumulative, 1024); - - [Fact] - public void HistogramDistributeToAllBucketsDefault() - { - var histogramPoint = new MetricPoint(this.aggregatorStore, AggregationType.HistogramWithBuckets, null, Metric.DefaultHistogramBounds, Metric.DefaultExponentialHistogramMaxBuckets, Metric.DefaultExponentialHistogramMaxScale); - histogramPoint.Update(-1); - histogramPoint.Update(0); - histogramPoint.Update(2); - histogramPoint.Update(5); - histogramPoint.Update(8); - histogramPoint.Update(10); - histogramPoint.Update(11); - histogramPoint.Update(25); - histogramPoint.Update(40); - histogramPoint.Update(50); - histogramPoint.Update(70); - histogramPoint.Update(75); - histogramPoint.Update(99); - histogramPoint.Update(100); - histogramPoint.Update(246); - histogramPoint.Update(250); - histogramPoint.Update(499); - histogramPoint.Update(500); - histogramPoint.Update(501); - histogramPoint.Update(750); - histogramPoint.Update(751); - histogramPoint.Update(1000); - histogramPoint.Update(1001); - histogramPoint.Update(2500); - histogramPoint.Update(2501); - histogramPoint.Update(5000); - histogramPoint.Update(5001); - histogramPoint.Update(7500); - histogramPoint.Update(7501); - histogramPoint.Update(10000); - histogramPoint.Update(10001); - histogramPoint.Update(10000000); - histogramPoint.TakeSnapshot(true); - - var count = histogramPoint.GetHistogramCount(); - - Assert.Equal(32, count); - - int actualCount = 0; - foreach (var histogramMeasurement in histogramPoint.GetHistogramBuckets()) - { - Assert.Equal(2, histogramMeasurement.BucketCount); - actualCount++; - } - } - - [Fact] - public void HistogramDistributeToAllBucketsCustom() - { - var boundaries = new double[] { 10, 20 }; - var histogramPoint = new MetricPoint(this.aggregatorStore, AggregationType.HistogramWithBuckets, null, boundaries, Metric.DefaultExponentialHistogramMaxBuckets, Metric.DefaultExponentialHistogramMaxScale); - - // 5 recordings <=10 - histogramPoint.Update(-10); - histogramPoint.Update(0); - histogramPoint.Update(1); - histogramPoint.Update(9); - histogramPoint.Update(10); - - // 2 recordings >10, <=20 - histogramPoint.Update(11); - histogramPoint.Update(19); - - histogramPoint.TakeSnapshot(true); - - var count = histogramPoint.GetHistogramCount(); - var sum = histogramPoint.GetHistogramSum(); - - // Sum of all recordings - Assert.Equal(40, sum); - - // Count = # of recordings - Assert.Equal(7, count); - - int index = 0; - int actualCount = 0; - var expectedBucketCounts = new long[] { 5, 2, 0 }; - foreach (var histogramMeasurement in histogramPoint.GetHistogramBuckets()) - { - Assert.Equal(expectedBucketCounts[index], histogramMeasurement.BucketCount); - index++; - actualCount++; - } - - Assert.Equal(boundaries.Length + 1, actualCount); - } - - [Fact] - public void HistogramBinaryBucketTest() - { - // Arrange - // Bounds = (-Inf, 0] (0, 1], ... (49, +Inf) - var boundaries = new double[HistogramBuckets.DefaultBoundaryCountForBinarySearch]; - for (var i = 0; i < boundaries.Length; i++) - { - boundaries[i] = i; - } - - var histogramPoint = new MetricPoint(this.aggregatorStore, AggregationType.HistogramWithBuckets, null, boundaries, Metric.DefaultExponentialHistogramMaxBuckets, Metric.DefaultExponentialHistogramMaxScale); - - // Act - histogramPoint.Update(-1); - histogramPoint.Update(boundaries[0]); - histogramPoint.Update(boundaries[boundaries.Length - 1]); - for (var i = 0.5; i < boundaries.Length; i++) - { - histogramPoint.Update(i); - } - - histogramPoint.TakeSnapshot(true); - - // Assert - var index = 0; - foreach (var histogramMeasurement in histogramPoint.GetHistogramBuckets()) - { - var expectedCount = 1; - - if (index == 0 || index == boundaries.Length - 1) - { - expectedCount = 2; - } - - Assert.Equal(expectedCount, histogramMeasurement.BucketCount); - index++; - } - } - - [Fact] - public void HistogramWithOnlySumCount() - { - var boundaries = Array.Empty(); - var histogramPoint = new MetricPoint(this.aggregatorStore, AggregationType.Histogram, null, boundaries, Metric.DefaultExponentialHistogramMaxBuckets, Metric.DefaultExponentialHistogramMaxScale); - - histogramPoint.Update(-10); - histogramPoint.Update(0); - histogramPoint.Update(1); - histogramPoint.Update(9); - histogramPoint.Update(10); - histogramPoint.Update(11); - histogramPoint.Update(19); - - histogramPoint.TakeSnapshot(true); - - var count = histogramPoint.GetHistogramCount(); - var sum = histogramPoint.GetHistogramSum(); - - // Sum of all recordings - Assert.Equal(40, sum); - - // Count = # of recordings - Assert.Equal(7, count); - - // There should be no enumeration of BucketCounts and ExplicitBounds for HistogramSumCount - var enumerator = histogramPoint.GetHistogramBuckets().GetEnumerator(); - Assert.False(enumerator.MoveNext()); - } - - [Fact] - public void MultiThreadedHistogramUpdateAndSnapShotTest() - { - var boundaries = Array.Empty(); - var histogramPoint = new MetricPoint(this.aggregatorStore, AggregationType.Histogram, null, boundaries, Metric.DefaultExponentialHistogramMaxBuckets, Metric.DefaultExponentialHistogramMaxScale); - var argsToThread = new ThreadArguments - { - HistogramPoint = histogramPoint, - MreToEnsureAllThreadsStart = new ManualResetEvent(false), - }; - - var numberOfThreads = 2; - var snapshotThread = new Thread(HistogramSnapshotThread); - Thread[] updateThreads = new Thread[numberOfThreads]; - for (int i = 0; i < numberOfThreads; ++i) - { - updateThreads[i] = new Thread(HistogramUpdateThread); - updateThreads[i].Start(argsToThread); - } - - snapshotThread.Start(argsToThread); - - for (int i = 0; i < numberOfThreads; ++i) - { - updateThreads[i].Join(); - } - - snapshotThread.Join(); - - // last snapshot - histogramPoint.TakeSnapshot(outputDelta: true); - - var lastDelta = histogramPoint.GetHistogramSum(); - Assert.Equal(200, argsToThread.SumOfDelta + lastDelta); - } - - internal static void AssertExponentialBucketsAreCorrect(Base2ExponentialBucketHistogram expectedHistogram, ExponentialHistogramData data) - { - Assert.Equal(expectedHistogram.Scale, data.Scale); - Assert.Equal(expectedHistogram.ZeroCount, data.ZeroCount); - Assert.Equal(expectedHistogram.PositiveBuckets.Offset, data.PositiveBuckets.Offset); - Assert.Equal(expectedHistogram.NegativeBuckets.Offset, data.NegativeBuckets.Offset); - - expectedHistogram.Snapshot(); - var expectedData = expectedHistogram.GetExponentialHistogramData(); - - var actual = new List(); - foreach (var bucketCount in data.PositiveBuckets) - { - actual.Add(bucketCount); - } - - var expected = new List(); - foreach (var bucketCount in expectedData.PositiveBuckets) - { - expected.Add(bucketCount); - } - - Assert.Equal(expected, actual); - - actual = new List(); - foreach (var bucketCount in data.NegativeBuckets) - { - actual.Add(bucketCount); - } - - expected = new List(); - foreach (var bucketCount in expectedData.NegativeBuckets) - { - expected.Add(bucketCount); - } - - Assert.Equal(expected, actual); - } - - [Theory] - [InlineData(AggregationType.Base2ExponentialHistogram, AggregationTemporality.Cumulative, true)] - [InlineData(AggregationType.Base2ExponentialHistogram, AggregationTemporality.Delta, true)] - [InlineData(AggregationType.Base2ExponentialHistogramWithMinMax, AggregationTemporality.Cumulative, true)] - [InlineData(AggregationType.Base2ExponentialHistogramWithMinMax, AggregationTemporality.Delta, true)] - [InlineData(AggregationType.Base2ExponentialHistogram, AggregationTemporality.Cumulative, false)] - [InlineData(AggregationType.Base2ExponentialHistogram, AggregationTemporality.Delta, false)] - [InlineData(AggregationType.Base2ExponentialHistogramWithMinMax, AggregationTemporality.Cumulative, false)] - [InlineData(AggregationType.Base2ExponentialHistogramWithMinMax, AggregationTemporality.Delta, false)] - internal void ExponentialHistogramTests(AggregationType aggregationType, AggregationTemporality aggregationTemporality, bool exemplarsEnabled) - { - var valuesToRecord = new[] { -10, 0, 1, 9, 10, 11, 19 }; - - var streamConfiguration = new Base2ExponentialBucketHistogramConfiguration(); - var metricStreamIdentity = new MetricStreamIdentity(Instrument, streamConfiguration); - - var aggregatorStore = new AggregatorStore( - metricStreamIdentity, - aggregationType, - aggregationTemporality, - maxMetricPoints: 1024, - exemplarsEnabled ? new AlwaysOnExemplarFilter() : null); - - var expectedHistogram = new Base2ExponentialBucketHistogram(); - - foreach (var value in valuesToRecord) - { - aggregatorStore.Update(value, Array.Empty>()); - - if (value >= 0) - { - expectedHistogram.Record(value); - } - } - - aggregatorStore.Snapshot(); - - var metricPoints = new List(); - - foreach (ref readonly var mp in aggregatorStore.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - Assert.Single(metricPoints); - var metricPoint = metricPoints[0]; - - var count = metricPoint.GetHistogramCount(); - var sum = metricPoint.GetHistogramSum(); - var hasMinMax = metricPoint.TryGetHistogramMinMaxValues(out var min, out var max); - - AssertExponentialBucketsAreCorrect(expectedHistogram, metricPoint.GetExponentialHistogramData()); - Assert.Equal(50, sum); - Assert.Equal(6, count); - - if (aggregationType == AggregationType.Base2ExponentialHistogramWithMinMax) - { - Assert.True(hasMinMax); - Assert.Equal(0, min); - Assert.Equal(19, max); - } - else - { - Assert.False(hasMinMax); - } - - metricPoint.TakeSnapshot(aggregationTemporality == AggregationTemporality.Delta); - - count = metricPoint.GetHistogramCount(); - sum = metricPoint.GetHistogramSum(); - hasMinMax = metricPoint.TryGetHistogramMinMaxValues(out min, out max); - - if (aggregationTemporality == AggregationTemporality.Cumulative) - { - AssertExponentialBucketsAreCorrect(expectedHistogram, metricPoint.GetExponentialHistogramData()); - Assert.Equal(50, sum); - Assert.Equal(6, count); - - if (aggregationType == AggregationType.Base2ExponentialHistogramWithMinMax) - { - Assert.True(hasMinMax); - Assert.Equal(0, min); - Assert.Equal(19, max); - } - else - { - Assert.False(hasMinMax); - } - } - else - { - expectedHistogram.Reset(); - AssertExponentialBucketsAreCorrect(expectedHistogram, metricPoint.GetExponentialHistogramData()); - Assert.Equal(0, sum); - Assert.Equal(0, count); - - if (aggregationType == AggregationType.Base2ExponentialHistogramWithMinMax) - { - Assert.True(hasMinMax); - Assert.Equal(double.PositiveInfinity, min); - Assert.Equal(double.NegativeInfinity, max); - } - else - { - Assert.False(hasMinMax); - } - } - } - - [Theory] - [InlineData(-5)] - [InlineData(0)] - [InlineData(5)] - [InlineData(null)] - internal void ExponentialMaxScaleConfigWorks(int? maxScale) - { - var streamConfiguration = new Base2ExponentialBucketHistogramConfiguration(); - if (maxScale.HasValue) - { - streamConfiguration.MaxScale = maxScale.Value; - } - - var metricStreamIdentity = new MetricStreamIdentity(Instrument, streamConfiguration); - - var aggregatorStore = new AggregatorStore( - metricStreamIdentity, - AggregationType.Base2ExponentialHistogram, - AggregationTemporality.Cumulative, - maxMetricPoints: 1024); - - aggregatorStore.Update(10, Array.Empty>()); - - aggregatorStore.Snapshot(); - - var metricPoints = new List(); - - foreach (ref readonly var mp in aggregatorStore.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - Assert.Single(metricPoints); - var metricPoint = metricPoints[0]; - - // After a single measurement there will not have been a scale down. - // Scale will equal MaxScale. - var expectedScale = maxScale.HasValue ? maxScale : Metric.DefaultExponentialHistogramMaxScale; - Assert.Equal(expectedScale, metricPoint.GetExponentialHistogramData().Scale); - } - - private static void HistogramSnapshotThread(object obj) - { - var args = obj as ThreadArguments; - var mreToEnsureAllThreadsStart = args.MreToEnsureAllThreadsStart; - - if (Interlocked.Increment(ref args.ThreadStartedCount) == 3) - { - mreToEnsureAllThreadsStart.Set(); - } - - mreToEnsureAllThreadsStart.WaitOne(); - - double curSnapshotDelta; - while (Interlocked.Read(ref args.ThreadsFinishedAllUpdatesCount) != 2) - { - args.HistogramPoint.TakeSnapshot(outputDelta: true); - curSnapshotDelta = args.HistogramPoint.GetHistogramSum(); - args.SumOfDelta += curSnapshotDelta; - } - } - - private static void HistogramUpdateThread(object obj) - { - var args = obj as ThreadArguments; - var mreToEnsureAllThreadsStart = args.MreToEnsureAllThreadsStart; - - if (Interlocked.Increment(ref args.ThreadStartedCount) == 3) - { - mreToEnsureAllThreadsStart.Set(); - } - - mreToEnsureAllThreadsStart.WaitOne(); - - for (int i = 0; i < 10; ++i) - { - args.HistogramPoint.Update(10); - } - - Interlocked.Increment(ref args.ThreadsFinishedAllUpdatesCount); - } - - private class ThreadArguments - { - public MetricPoint HistogramPoint; - public ManualResetEvent MreToEnsureAllThreadsStart; - public int ThreadStartedCount; - public long ThreadsFinishedAllUpdatesCount; - public double SumOfDelta; - } - } -} diff --git a/test/OpenTelemetry.Tests/Metrics/AggregatorTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/AggregatorTestsBase.cs new file mode 100644 index 00000000000..6bd96046d3f --- /dev/null +++ b/test/OpenTelemetry.Tests/Metrics/AggregatorTestsBase.cs @@ -0,0 +1,550 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics.Metrics; +using Xunit; + +namespace OpenTelemetry.Metrics.Tests; + +#pragma warning disable SA1402 + +public abstract class AggregatorTestsBase +{ + private static readonly Meter Meter = new("testMeter"); + private static readonly Instrument Instrument = Meter.CreateHistogram("testInstrument"); + private static readonly ExplicitBucketHistogramConfiguration HistogramConfiguration = new() { Boundaries = Metric.DefaultHistogramBounds }; + private static readonly MetricStreamIdentity MetricStreamIdentity = new(Instrument, HistogramConfiguration); + + private readonly bool emitOverflowAttribute; + private readonly bool shouldReclaimUnusedMetricPoints; + private readonly AggregatorStore aggregatorStore; + + protected AggregatorTestsBase(bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints) + { + this.emitOverflowAttribute = emitOverflowAttribute; + this.shouldReclaimUnusedMetricPoints = shouldReclaimUnusedMetricPoints; + + this.aggregatorStore = new(MetricStreamIdentity, AggregationType.HistogramWithBuckets, AggregationTemporality.Cumulative, 1024, emitOverflowAttribute, this.shouldReclaimUnusedMetricPoints); + } + + [Fact] + public void HistogramDistributeToAllBucketsDefault() + { + var histogramPoint = new MetricPoint(this.aggregatorStore, AggregationType.HistogramWithBuckets, null, Metric.DefaultHistogramBounds, Metric.DefaultExponentialHistogramMaxBuckets, Metric.DefaultExponentialHistogramMaxScale); + histogramPoint.Update(-1); + histogramPoint.Update(0); + histogramPoint.Update(2); + histogramPoint.Update(5); + histogramPoint.Update(8); + histogramPoint.Update(10); + histogramPoint.Update(11); + histogramPoint.Update(25); + histogramPoint.Update(40); + histogramPoint.Update(50); + histogramPoint.Update(70); + histogramPoint.Update(75); + histogramPoint.Update(99); + histogramPoint.Update(100); + histogramPoint.Update(246); + histogramPoint.Update(250); + histogramPoint.Update(499); + histogramPoint.Update(500); + histogramPoint.Update(501); + histogramPoint.Update(750); + histogramPoint.Update(751); + histogramPoint.Update(1000); + histogramPoint.Update(1001); + histogramPoint.Update(2500); + histogramPoint.Update(2501); + histogramPoint.Update(5000); + histogramPoint.Update(5001); + histogramPoint.Update(7500); + histogramPoint.Update(7501); + histogramPoint.Update(10000); + histogramPoint.Update(10001); + histogramPoint.Update(10000000); + histogramPoint.TakeSnapshot(true); + + var count = histogramPoint.GetHistogramCount(); + + Assert.Equal(32, count); + + int actualCount = 0; + foreach (var histogramMeasurement in histogramPoint.GetHistogramBuckets()) + { + Assert.Equal(2, histogramMeasurement.BucketCount); + actualCount++; + } + } + + [Fact] + public void HistogramDistributeToAllBucketsCustom() + { + var boundaries = new double[] { 10, 20 }; + var histogramPoint = new MetricPoint(this.aggregatorStore, AggregationType.HistogramWithBuckets, null, boundaries, Metric.DefaultExponentialHistogramMaxBuckets, Metric.DefaultExponentialHistogramMaxScale); + + // 5 recordings <=10 + histogramPoint.Update(-10); + histogramPoint.Update(0); + histogramPoint.Update(1); + histogramPoint.Update(9); + histogramPoint.Update(10); + + // 2 recordings >10, <=20 + histogramPoint.Update(11); + histogramPoint.Update(19); + + histogramPoint.TakeSnapshot(true); + + var count = histogramPoint.GetHistogramCount(); + var sum = histogramPoint.GetHistogramSum(); + + // Sum of all recordings + Assert.Equal(40, sum); + + // Count = # of recordings + Assert.Equal(7, count); + + int index = 0; + int actualCount = 0; + var expectedBucketCounts = new long[] { 5, 2, 0 }; + foreach (var histogramMeasurement in histogramPoint.GetHistogramBuckets()) + { + Assert.Equal(expectedBucketCounts[index], histogramMeasurement.BucketCount); + index++; + actualCount++; + } + + Assert.Equal(boundaries.Length + 1, actualCount); + } + + [Fact] + public void HistogramBinaryBucketTest() + { + // Arrange + // Bounds = (-Inf, 0] (0, 1], ... (49, +Inf) + var boundaries = new double[HistogramBuckets.DefaultBoundaryCountForBinarySearch]; + for (var i = 0; i < boundaries.Length; i++) + { + boundaries[i] = i; + } + + var histogramPoint = new MetricPoint(this.aggregatorStore, AggregationType.HistogramWithBuckets, null, boundaries, Metric.DefaultExponentialHistogramMaxBuckets, Metric.DefaultExponentialHistogramMaxScale); + + // Act + histogramPoint.Update(-1); + histogramPoint.Update(boundaries[0]); + histogramPoint.Update(boundaries[boundaries.Length - 1]); + for (var i = 0.5; i < boundaries.Length; i++) + { + histogramPoint.Update(i); + } + + histogramPoint.TakeSnapshot(true); + + // Assert + var index = 0; + foreach (var histogramMeasurement in histogramPoint.GetHistogramBuckets()) + { + var expectedCount = 1; + + if (index == 0 || index == boundaries.Length - 1) + { + expectedCount = 2; + } + + Assert.Equal(expectedCount, histogramMeasurement.BucketCount); + index++; + } + } + + [Fact] + public void HistogramWithOnlySumCount() + { + var boundaries = Array.Empty(); + var histogramPoint = new MetricPoint(this.aggregatorStore, AggregationType.Histogram, null, boundaries, Metric.DefaultExponentialHistogramMaxBuckets, Metric.DefaultExponentialHistogramMaxScale); + + histogramPoint.Update(-10); + histogramPoint.Update(0); + histogramPoint.Update(1); + histogramPoint.Update(9); + histogramPoint.Update(10); + histogramPoint.Update(11); + histogramPoint.Update(19); + + histogramPoint.TakeSnapshot(true); + + var count = histogramPoint.GetHistogramCount(); + var sum = histogramPoint.GetHistogramSum(); + + // Sum of all recordings + Assert.Equal(40, sum); + + // Count = # of recordings + Assert.Equal(7, count); + + // There should be no enumeration of BucketCounts and ExplicitBounds for HistogramSumCount + var enumerator = histogramPoint.GetHistogramBuckets().GetEnumerator(); + Assert.False(enumerator.MoveNext()); + } + + [Fact] + public void MultiThreadedHistogramUpdateAndSnapShotTest() + { + var boundaries = Array.Empty(); + var histogramPoint = new MetricPoint(this.aggregatorStore, AggregationType.Histogram, null, boundaries, Metric.DefaultExponentialHistogramMaxBuckets, Metric.DefaultExponentialHistogramMaxScale); + var argsToThread = new ThreadArguments + { + HistogramPoint = histogramPoint, + MreToEnsureAllThreadsStart = new ManualResetEvent(false), + }; + + var numberOfThreads = 2; + var snapshotThread = new Thread(HistogramSnapshotThread); + Thread[] updateThreads = new Thread[numberOfThreads]; + for (int i = 0; i < numberOfThreads; ++i) + { + updateThreads[i] = new Thread(HistogramUpdateThread); + updateThreads[i].Start(argsToThread); + } + + snapshotThread.Start(argsToThread); + + for (int i = 0; i < numberOfThreads; ++i) + { + updateThreads[i].Join(); + } + + snapshotThread.Join(); + + // last snapshot + histogramPoint.TakeSnapshot(outputDelta: true); + + var lastDelta = histogramPoint.GetHistogramSum(); + Assert.Equal(200, argsToThread.SumOfDelta + lastDelta); + } + + [Theory] + [InlineData("Microsoft.AspNetCore.Hosting", "http.server.request.duration", "s", KnownHistogramBuckets.DefaultShortSeconds)] + [InlineData("Microsoft.AspNetCore.Hosting", "http.server.request.duration", "ms", KnownHistogramBuckets.Default)] + [InlineData("Microsoft.AspNetCore.Hosting", "http.server.request.duration", "By", KnownHistogramBuckets.Default)] + [InlineData("Microsoft.AspNetCore.Hosting", "http.server.request.duration", null, KnownHistogramBuckets.Default)] + [InlineData("Microsoft.AspNetCore.Http.Connections", "signalr.server.connection.duration", "s", KnownHistogramBuckets.DefaultLongSeconds)] + [InlineData("Microsoft.AspNetCore.RateLimiting", "aspnetcore.rate_limiting.request_lease.duration", "s", KnownHistogramBuckets.DefaultShortSeconds)] + [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.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) + { + using var meter = new Meter(meterName); + + var instrument = meter.CreateHistogram(instrumentName, unit); + + var metricStreamIdentity = new MetricStreamIdentity(instrument, metricStreamConfiguration: null); + + AggregatorStore aggregatorStore = new( + metricStreamIdentity, + AggregationType.Histogram, + AggregationTemporality.Cumulative, + cardinalityLimit: 1024, + this.emitOverflowAttribute, + this.shouldReclaimUnusedMetricPoints); + + KnownHistogramBuckets actualHistogramBounds = KnownHistogramBuckets.Default; + if (aggregatorStore.HistogramBounds == Metric.DefaultHistogramBoundsShortSeconds) + { + actualHistogramBounds = KnownHistogramBuckets.DefaultShortSeconds; + } + else if (aggregatorStore.HistogramBounds == Metric.DefaultHistogramBoundsLongSeconds) + { + actualHistogramBounds = KnownHistogramBuckets.DefaultLongSeconds; + } + + Assert.NotNull(aggregatorStore.HistogramBounds); + Assert.Equal(expectedHistogramBuckets, actualHistogramBounds); + } + + internal static void AssertExponentialBucketsAreCorrect(Base2ExponentialBucketHistogram expectedHistogram, ExponentialHistogramData data) + { + Assert.Equal(expectedHistogram.Scale, data.Scale); + Assert.Equal(expectedHistogram.ZeroCount, data.ZeroCount); + Assert.Equal(expectedHistogram.PositiveBuckets.Offset, data.PositiveBuckets.Offset); + Assert.Equal(expectedHistogram.NegativeBuckets.Offset, data.NegativeBuckets.Offset); + + expectedHistogram.Snapshot(); + var expectedData = expectedHistogram.GetExponentialHistogramData(); + + var actual = new List(); + foreach (var bucketCount in data.PositiveBuckets) + { + actual.Add(bucketCount); + } + + var expected = new List(); + foreach (var bucketCount in expectedData.PositiveBuckets) + { + expected.Add(bucketCount); + } + + Assert.Equal(expected, actual); + + actual = new List(); + foreach (var bucketCount in data.NegativeBuckets) + { + actual.Add(bucketCount); + } + + expected = new List(); + foreach (var bucketCount in expectedData.NegativeBuckets) + { + expected.Add(bucketCount); + } + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(AggregationType.Base2ExponentialHistogram, AggregationTemporality.Cumulative, true)] + [InlineData(AggregationType.Base2ExponentialHistogram, AggregationTemporality.Delta, true)] + [InlineData(AggregationType.Base2ExponentialHistogramWithMinMax, AggregationTemporality.Cumulative, true)] + [InlineData(AggregationType.Base2ExponentialHistogramWithMinMax, AggregationTemporality.Delta, true)] + [InlineData(AggregationType.Base2ExponentialHistogram, AggregationTemporality.Cumulative, false)] + [InlineData(AggregationType.Base2ExponentialHistogram, AggregationTemporality.Delta, false)] + [InlineData(AggregationType.Base2ExponentialHistogramWithMinMax, AggregationTemporality.Cumulative, false)] + [InlineData(AggregationType.Base2ExponentialHistogramWithMinMax, AggregationTemporality.Delta, false)] + internal void ExponentialHistogramTests(AggregationType aggregationType, AggregationTemporality aggregationTemporality, bool exemplarsEnabled) + { + var valuesToRecord = new[] { -10, 0, 1, 9, 10, 11, 19 }; + + var streamConfiguration = new Base2ExponentialBucketHistogramConfiguration(); + var metricStreamIdentity = new MetricStreamIdentity(Instrument, streamConfiguration); + + var aggregatorStore = new AggregatorStore( + metricStreamIdentity, + aggregationType, + aggregationTemporality, + cardinalityLimit: 1024, + this.emitOverflowAttribute, + this.shouldReclaimUnusedMetricPoints, + exemplarsEnabled ? ExemplarFilterType.AlwaysOn : null); + + var expectedHistogram = new Base2ExponentialBucketHistogram(); + + foreach (var value in valuesToRecord) + { + aggregatorStore.Update(value, Array.Empty>()); + + if (value >= 0) + { + expectedHistogram.Record(value); + } + } + + aggregatorStore.Snapshot(); + + var metricPoints = new List(); + + foreach (ref readonly var mp in aggregatorStore.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + Assert.Single(metricPoints); + var metricPoint = metricPoints[0]; + + var count = metricPoint.GetHistogramCount(); + var sum = metricPoint.GetHistogramSum(); + var hasMinMax = metricPoint.TryGetHistogramMinMaxValues(out var min, out var max); + + AssertExponentialBucketsAreCorrect(expectedHistogram, metricPoint.GetExponentialHistogramData()); + Assert.Equal(50, sum); + Assert.Equal(6, count); + + if (aggregationType == AggregationType.Base2ExponentialHistogramWithMinMax) + { + Assert.True(hasMinMax); + Assert.Equal(0, min); + Assert.Equal(19, max); + } + else + { + Assert.False(hasMinMax); + } + + metricPoint.TakeSnapshot(aggregationTemporality == AggregationTemporality.Delta); + + count = metricPoint.GetHistogramCount(); + sum = metricPoint.GetHistogramSum(); + hasMinMax = metricPoint.TryGetHistogramMinMaxValues(out min, out max); + + if (aggregationTemporality == AggregationTemporality.Cumulative) + { + AssertExponentialBucketsAreCorrect(expectedHistogram, metricPoint.GetExponentialHistogramData()); + Assert.Equal(50, sum); + Assert.Equal(6, count); + + if (aggregationType == AggregationType.Base2ExponentialHistogramWithMinMax) + { + Assert.True(hasMinMax); + Assert.Equal(0, min); + Assert.Equal(19, max); + } + else + { + Assert.False(hasMinMax); + } + } + else + { + expectedHistogram.Reset(); + AssertExponentialBucketsAreCorrect(expectedHistogram, metricPoint.GetExponentialHistogramData()); + Assert.Equal(0, sum); + Assert.Equal(0, count); + + if (aggregationType == AggregationType.Base2ExponentialHistogramWithMinMax) + { + Assert.True(hasMinMax); + Assert.Equal(double.PositiveInfinity, min); + Assert.Equal(double.NegativeInfinity, max); + } + else + { + Assert.False(hasMinMax); + } + } + } + + [Theory] + [InlineData(-5)] + [InlineData(0)] + [InlineData(5)] + [InlineData(null)] + internal void ExponentialMaxScaleConfigWorks(int? maxScale) + { + var streamConfiguration = new Base2ExponentialBucketHistogramConfiguration(); + if (maxScale.HasValue) + { + streamConfiguration.MaxScale = maxScale.Value; + } + + var metricStreamIdentity = new MetricStreamIdentity(Instrument, streamConfiguration); + + var aggregatorStore = new AggregatorStore( + metricStreamIdentity, + AggregationType.Base2ExponentialHistogram, + AggregationTemporality.Cumulative, + cardinalityLimit: 1024, + this.emitOverflowAttribute, + this.shouldReclaimUnusedMetricPoints); + + aggregatorStore.Update(10, Array.Empty>()); + + aggregatorStore.Snapshot(); + + var metricPoints = new List(); + + foreach (ref readonly var mp in aggregatorStore.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + Assert.Single(metricPoints); + var metricPoint = metricPoints[0]; + + // After a single measurement there will not have been a scale down. + // Scale will equal MaxScale. + var expectedScale = maxScale.HasValue ? maxScale : Metric.DefaultExponentialHistogramMaxScale; + Assert.Equal(expectedScale, metricPoint.GetExponentialHistogramData().Scale); + } + + private static void HistogramSnapshotThread(object obj) + { + var args = obj as ThreadArguments; + var mreToEnsureAllThreadsStart = args.MreToEnsureAllThreadsStart; + + if (Interlocked.Increment(ref args.ThreadStartedCount) == 3) + { + mreToEnsureAllThreadsStart.Set(); + } + + mreToEnsureAllThreadsStart.WaitOne(); + + double curSnapshotDelta; + while (Interlocked.Read(ref args.ThreadsFinishedAllUpdatesCount) != 2) + { + args.HistogramPoint.TakeSnapshot(outputDelta: true); + curSnapshotDelta = args.HistogramPoint.GetHistogramSum(); + args.SumOfDelta += curSnapshotDelta; + } + } + + private static void HistogramUpdateThread(object obj) + { + var args = obj as ThreadArguments; + var mreToEnsureAllThreadsStart = args.MreToEnsureAllThreadsStart; + + if (Interlocked.Increment(ref args.ThreadStartedCount) == 3) + { + mreToEnsureAllThreadsStart.Set(); + } + + mreToEnsureAllThreadsStart.WaitOne(); + + for (int i = 0; i < 10; ++i) + { + args.HistogramPoint.Update(10); + } + + Interlocked.Increment(ref args.ThreadsFinishedAllUpdatesCount); + } + + private class ThreadArguments + { + public MetricPoint HistogramPoint; + public ManualResetEvent MreToEnsureAllThreadsStart; + public int ThreadStartedCount; + public long ThreadsFinishedAllUpdatesCount; + public double SumOfDelta; + } +} + +public class AggregatorTests : AggregatorTestsBase +{ + public AggregatorTests() + : base(emitOverflowAttribute: false, shouldReclaimUnusedMetricPoints: false) + { + } +} + +public class AggregatorTestsWithOverflowAttribute : AggregatorTestsBase +{ + public AggregatorTestsWithOverflowAttribute() + : 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/Base2ExponentialBucketHistogram.LowerBoundary.Test.cs b/test/OpenTelemetry.Tests/Metrics/Base2ExponentialBucketHistogramHelperTests.cs similarity index 66% rename from test/OpenTelemetry.Tests/Metrics/Base2ExponentialBucketHistogram.LowerBoundary.Test.cs rename to test/OpenTelemetry.Tests/Metrics/Base2ExponentialBucketHistogramHelperTests.cs index 192f03edfc7..0af08fffc11 100644 --- a/test/OpenTelemetry.Tests/Metrics/Base2ExponentialBucketHistogram.LowerBoundary.Test.cs +++ b/test/OpenTelemetry.Tests/Metrics/Base2ExponentialBucketHistogramHelperTests.cs @@ -1,29 +1,17 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +using OpenTelemetry.Tests; using Xunit; using Xunit.Abstractions; namespace OpenTelemetry.Metrics.Tests; -public partial class Base2ExponentialBucketHistogramTest +public class Base2ExponentialBucketHistogramHelperTests { private readonly ITestOutputHelper output; - public Base2ExponentialBucketHistogramTest(ITestOutputHelper output) + public Base2ExponentialBucketHistogramHelperTests(ITestOutputHelper output) { this.output = output; } @@ -54,7 +42,7 @@ public void TestNonPositiveScalesLowerBoundaryRoundTrip(int scale) for (var index = minIndex; index <= maxIndex; ++index) { - var lowerBound = Base2ExponentialBucketHistogram.LowerBoundary(index, scale); + var lowerBound = Base2ExponentialBucketHistogramHelper.CalculateLowerBoundary(index, scale); var roundTrip = histogram.MapToIndex(lowerBound); if (lowerBound == double.Epsilon) @@ -86,7 +74,7 @@ These are unique cases in that these buckets near the // is exclusive. That is: // MapToIndex(LowerBoundary(index)) == index - 1 Assert.Equal(index - 1, roundTrip); - roundTrip = histogram.MapToIndex(BitIncrement(lowerBound)); + roundTrip = histogram.MapToIndex(MathHelper.BitIncrement(lowerBound)); Assert.Equal(index, roundTrip); } } @@ -119,7 +107,7 @@ off by one relative to LowerBoundary by performing a "round trip". for (var index = -indexesPerPowerOf2; index > minIndex; index -= indexesPerPowerOf2) { - var lowerBound = Base2ExponentialBucketHistogram.LowerBoundary(index, scale); + var lowerBound = Base2ExponentialBucketHistogramHelper.CalculateLowerBoundary(index, scale); var roundTrip = histogram.MapToIndex(lowerBound); // The round trip is off by one. @@ -144,7 +132,7 @@ off by one relative to LowerBoundary by performing a "round trip". for (var index = indexesPerPowerOf2; index < maxIndex; index += indexesPerPowerOf2) { - var lowerBound = Base2ExponentialBucketHistogram.LowerBoundary(index, scale); + var lowerBound = Base2ExponentialBucketHistogramHelper.CalculateLowerBoundary(index, scale); var roundTrip = histogram.MapToIndex(lowerBound); // The round trip is never off by one for positive indexes near powers of two. @@ -162,7 +150,7 @@ public void TestPositiveScalesLowerBoundaryMaxIndex(int scale) var histogram = new Base2ExponentialBucketHistogram(scale: scale); var maxIndex = histogram.MapToIndex(double.MaxValue); - var lowerBoundary = Base2ExponentialBucketHistogram.LowerBoundary(maxIndex, scale); + var lowerBoundary = Base2ExponentialBucketHistogramHelper.CalculateLowerBoundary(maxIndex, scale); var roundTrip = histogram.MapToIndex(lowerBoundary); Assert.Equal(maxIndex - 1, roundTrip); } @@ -174,7 +162,7 @@ public void TestPositiveScalesLowerBoundaryMinIndex(int scale) var histogram = new Base2ExponentialBucketHistogram(scale: scale); var minIndex = histogram.MapToIndex(double.Epsilon); - var lowerBoundary = Base2ExponentialBucketHistogram.LowerBoundary(minIndex, scale); + var lowerBoundary = Base2ExponentialBucketHistogramHelper.CalculateLowerBoundary(minIndex, scale); var roundTrip = histogram.MapToIndex(lowerBoundary); Assert.Equal(minIndex, roundTrip); } @@ -198,7 +186,7 @@ private void DisplayMarginOfError(bool displayDebugInfo, int scale, int index) } var histogram = new Base2ExponentialBucketHistogram(scale: scale); - var lowerBound = Base2ExponentialBucketHistogram.LowerBoundary(index, scale); + var lowerBound = Base2ExponentialBucketHistogramHelper.CalculateLowerBoundary(index, scale); var roundTrip = histogram.MapToIndex(lowerBound); Assert.True((index == roundTrip) || (index - 1 == roundTrip)); @@ -212,7 +200,7 @@ private void DisplayMarginOfError(bool displayDebugInfo, int scale, int index) { for (; newRoundTrip != index - 1;) { - preciseLowerBound = BitDecrement(preciseLowerBound); + preciseLowerBound = MathHelper.BitDecrement(preciseLowerBound); newRoundTrip = histogram.MapToIndex(preciseLowerBound); ++increments; } @@ -221,7 +209,7 @@ private void DisplayMarginOfError(bool displayDebugInfo, int scale, int index) { for (; newRoundTrip < index;) { - var newLowerBound = BitIncrement(preciseLowerBound); + var newLowerBound = MathHelper.BitIncrement(preciseLowerBound); newRoundTrip = histogram.MapToIndex(newLowerBound); if (newRoundTrip < index) @@ -243,70 +231,4 @@ private void DisplayMarginOfError(bool displayDebugInfo, int scale, int index) var marginOfError = lowerBoundDelta / lowerBound; this.output.WriteLine($"{scale},{index},{unusedIndex},{lowerBound},{roundTrip},{preciseLowerBound},{lowerBoundDelta},{marginOfError},{increments}"); } - - // Math.BitIncrement was introduced in .NET Core 3.0. - // This is the implementation from: - // https://github.com/dotnet/runtime/blob/v7.0.0/src/libraries/System.Private.CoreLib/src/System/Math.cs#L259 -#pragma warning disable SA1204 // Static members should appear before non-static members -#pragma warning disable SA1119 // Statement should not use unnecessary parenthesis - private static double BitIncrement(double x) - { -#if NET6_0_OR_GREATER - return Math.BitIncrement(x); -#else - long bits = BitConverter.DoubleToInt64Bits(x); - - if (((bits >> 32) & 0x7FF00000) >= 0x7FF00000) - { - // NaN returns NaN - // -Infinity returns double.MinValue - // +Infinity returns +Infinity - - return (bits == unchecked((long)(0xFFF00000_00000000))) ? double.MinValue : x; - } - - if (bits == unchecked((long)(0x80000000_00000000))) - { - // -0.0 returns double.Epsilon - return double.Epsilon; - } - - // Negative values need to be decremented - // Positive values need to be incremented - - bits += ((bits < 0) ? -1 : +1); - return BitConverter.Int64BitsToDouble(bits); -#endif - } - - private static double BitDecrement(double x) - { -#if NET6_0_OR_GREATER - return Math.BitDecrement(x); -#else - long bits = BitConverter.DoubleToInt64Bits(x); - - if (((bits >> 32) & 0x7FF00000) >= 0x7FF00000) - { - // NaN returns NaN - // -Infinity returns -Infinity - // +Infinity returns double.MaxValue - return (bits == 0x7FF00000_00000000) ? double.MaxValue : x; - } - - if (bits == 0x00000000_00000000) - { - // +0.0 returns -double.Epsilon - return -double.Epsilon; - } - - // Negative values need to be incremented - // Positive values need to be decremented - - bits += ((bits < 0) ? +1 : -1); - return BitConverter.Int64BitsToDouble(bits); -#endif - } -#pragma warning restore SA1119 // Statement should not use unnecessary parenthesis -#pragma warning restore SA1204 // Static members should appear before non-static members } diff --git a/test/OpenTelemetry.Tests/Metrics/Base2ExponentialBucketHistogramTest.cs b/test/OpenTelemetry.Tests/Metrics/Base2ExponentialBucketHistogramTest.cs index fbcdfb13aa0..a40beac0aa2 100644 --- a/test/OpenTelemetry.Tests/Metrics/Base2ExponentialBucketHistogramTest.cs +++ b/test/OpenTelemetry.Tests/Metrics/Base2ExponentialBucketHistogramTest.cs @@ -1,26 +1,21 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Tests; using Xunit; +using Xunit.Abstractions; namespace OpenTelemetry.Metrics.Tests; -public partial class Base2ExponentialBucketHistogramTest +public class Base2ExponentialBucketHistogramTest { + private readonly ITestOutputHelper output; + + public Base2ExponentialBucketHistogramTest(ITestOutputHelper output) + { + this.output = output; + } + [Fact] public void ScalingFactorCalculation() { @@ -537,7 +532,7 @@ are mapped to an index off by one. The range of these incorrectly mapped // Knowing that MapToIndex is imprecise near bucket boundaries, // the following produces an analysis of the magnitude of imprecision. - var incremented = BitIncrement(lowerBound); + var incremented = MathHelper.BitIncrement(lowerBound); if (index == histogram.MapToIndex(incremented)) { @@ -554,7 +549,7 @@ are mapped to an index off by one. The range of these incorrectly mapped var increments = 1; while (index != histogram.MapToIndex(incremented)) { - incremented = BitIncrement(incremented); + incremented = MathHelper.BitIncrement(incremented); increments++; } diff --git a/test/OpenTelemetry.Tests/Metrics/CircularBufferBucketsTest.cs b/test/OpenTelemetry.Tests/Metrics/CircularBufferBucketsTest.cs index 7c57cb4da6a..7666ab1a5df 100644 --- a/test/OpenTelemetry.Tests/Metrics/CircularBufferBucketsTest.cs +++ b/test/OpenTelemetry.Tests/Metrics/CircularBufferBucketsTest.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Xunit; diff --git a/test/OpenTelemetry.Tests/Metrics/InMemoryExporterTests.cs b/test/OpenTelemetry.Tests/Metrics/InMemoryExporterTests.cs index ca300a7a10a..bb44505bd9c 100644 --- a/test/OpenTelemetry.Tests/Metrics/InMemoryExporterTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/InMemoryExporterTests.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Metrics; @@ -20,49 +7,48 @@ using Xunit; -namespace OpenTelemetry.Metrics.Tests +namespace OpenTelemetry.Metrics.Tests; + +public class InMemoryExporterTests { - public class InMemoryExporterTests + [Fact] + public void InMemoryExporterShouldDeepCopyMetricPoints() { - [Fact] - public void InMemoryExporterShouldDeepCopyMetricPoints() - { - var exportedItems = new List(); + var exportedItems = new List(); - using var meter = new Meter(Utils.GetCurrentMethodName()); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems, metricReaderOptions => - { - metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta; - }) - .Build(); + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta; + }) + .Build(); - var counter = meter.CreateCounter("meter"); + var counter = meter.CreateCounter("meter"); - // TEST 1: Emit 10 for the MetricPoint with a single key-vaue pair: ("tag1", "value1") - counter.Add(10, new KeyValuePair("tag1", "value1")); + // TEST 1: Emit 10 for the MetricPoint with a single key-vaue pair: ("tag1", "value1") + counter.Add(10, new KeyValuePair("tag1", "value1")); - meterProvider.ForceFlush(); + meterProvider.ForceFlush(); - Assert.Single(exportedItems); - var metric1 = exportedItems[0]; // Only one Metric object is added to the collection at this point - Assert.Single(metric1.MetricPoints); - Assert.Equal(10, metric1.MetricPoints[0].GetSumLong()); + Assert.Single(exportedItems); + var metric1 = exportedItems[0]; // Only one Metric object is added to the collection at this point + Assert.Single(metric1.MetricPoints); + Assert.Equal(10, metric1.MetricPoints[0].GetSumLong()); - // TEST 2: Emit 25 for the MetricPoint with a single key-vaue pair: ("tag1", "value1") - counter.Add(25, new KeyValuePair("tag1", "value1")); + // TEST 2: Emit 25 for the MetricPoint with a single key-vaue pair: ("tag1", "value1") + counter.Add(25, new KeyValuePair("tag1", "value1")); - meterProvider.ForceFlush(); + meterProvider.ForceFlush(); - Assert.Equal(2, exportedItems.Count); - var metric2 = exportedItems[1]; // Second Metric object is added to the collection at this point - Assert.Single(metric2.MetricPoints); - Assert.Equal(25, metric2.MetricPoints[0].GetSumLong()); + Assert.Equal(2, exportedItems.Count); + var metric2 = exportedItems[1]; // Second Metric object is added to the collection at this point + Assert.Single(metric2.MetricPoints); + Assert.Equal(25, metric2.MetricPoints[0].GetSumLong()); - // TEST 3: Verify first exported metric is unchanged - // MetricPoint.LongValue for the first exported metric should still be 10 - Assert.Equal(10, metric1.MetricPoints[0].GetSumLong()); - } + // TEST 3: Verify first exported metric is unchanged + // MetricPoint.LongValue for the first exported metric should still be 10 + Assert.Equal(10, metric1.MetricPoints[0].GetSumLong()); } } diff --git a/test/OpenTelemetry.Tests/Metrics/KnownHistogramBuckets.cs b/test/OpenTelemetry.Tests/Metrics/KnownHistogramBuckets.cs new file mode 100644 index 00000000000..b63b4c2651b --- /dev/null +++ b/test/OpenTelemetry.Tests/Metrics/KnownHistogramBuckets.cs @@ -0,0 +1,22 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Metrics.Tests; + +public enum KnownHistogramBuckets +{ + /// + /// Default OpenTelemetry semantic convention buckets. + /// + Default, + + /// + /// Buckets for up to 10 seconds. + /// + DefaultShortSeconds, + + /// + /// Buckets for up to 300 seconds. + /// + DefaultLongSeconds, +} diff --git a/test/OpenTelemetry.Tests/Metrics/MemoryEfficiencyTests.cs b/test/OpenTelemetry.Tests/Metrics/MemoryEfficiencyTests.cs index 8abfa15be13..3965aa0599c 100644 --- a/test/OpenTelemetry.Tests/Metrics/MemoryEfficiencyTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MemoryEfficiencyTests.cs @@ -1,60 +1,46 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Metrics; using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Metrics.Tests +namespace OpenTelemetry.Metrics.Tests; + +public class MemoryEfficiencyTests { - public class MemoryEfficiencyTests + [Theory] + [InlineData(MetricReaderTemporalityPreference.Cumulative)] + [InlineData(MetricReaderTemporalityPreference.Delta)] + public void ExportOnlyWhenPointChanged(MetricReaderTemporalityPreference temporality) { - [Theory] - [InlineData(MetricReaderTemporalityPreference.Cumulative)] - [InlineData(MetricReaderTemporalityPreference.Delta)] - public void ExportOnlyWhenPointChanged(MetricReaderTemporalityPreference temporality) - { - using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{temporality}"); + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{temporality}"); - var exportedItems = new List(); + var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems, metricReaderOptions => - { - metricReaderOptions.TemporalityPreference = temporality; - }) - .Build(); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = temporality; + }) + .Build(); - var counter = meter.CreateCounter("meter"); + var counter = meter.CreateCounter("meter"); - counter.Add(10, new KeyValuePair("tag1", "value1")); - meterProvider.ForceFlush(); - Assert.Single(exportedItems); + counter.Add(10, new KeyValuePair("tag1", "value1")); + meterProvider.ForceFlush(); + Assert.Single(exportedItems); - exportedItems.Clear(); - meterProvider.ForceFlush(); - if (temporality == MetricReaderTemporalityPreference.Cumulative) - { - Assert.Single(exportedItems); - } - else - { - Assert.Empty(exportedItems); - } + exportedItems.Clear(); + meterProvider.ForceFlush(); + if (temporality == MetricReaderTemporalityPreference.Cumulative) + { + Assert.Single(exportedItems); + } + else + { + Assert.Empty(exportedItems); } } } diff --git a/test/OpenTelemetry.Tests/Metrics/MeterProviderBuilderExtensionsTests.cs b/test/OpenTelemetry.Tests/Metrics/MeterProviderBuilderExtensionsTests.cs index 6e52ed000ef..5ddd39fee0d 100644 --- a/test/OpenTelemetry.Tests/Metrics/MeterProviderBuilderExtensionsTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MeterProviderBuilderExtensionsTests.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -20,367 +7,388 @@ using OpenTelemetry.Resources; using Xunit; -namespace OpenTelemetry.Metrics.Tests +namespace OpenTelemetry.Metrics.Tests; + +public class MeterProviderBuilderExtensionsTests { - public class MeterProviderBuilderExtensionsTests + [Fact] + public void ServiceLifecycleAvailableToSDKBuilderTest() { - [Fact] - public void ServiceLifecycleAvailableToSDKBuilderTest() - { - var builder = Sdk.CreateMeterProviderBuilder(); + var builder = Sdk.CreateMeterProviderBuilder(); - MyInstrumentation myInstrumentation = null; + MyInstrumentation myInstrumentation = null; - RunBuilderServiceLifecycleTest( - builder, - () => - { - var provider = builder.Build() as MeterProviderSdk; + RunBuilderServiceLifecycleTest( + builder, + () => + { + var provider = builder.Build() as MeterProviderSdk; - // Note: Build can only be called once - Assert.Throws(() => builder.Build()); + // Note: Build can only be called once + Assert.Throws(() => builder.Build()); - Assert.NotNull(provider); - Assert.NotNull(provider.OwnedServiceProvider); + Assert.NotNull(provider); + Assert.NotNull(provider.OwnedServiceProvider); - myInstrumentation = ((IServiceProvider)provider.OwnedServiceProvider).GetRequiredService(); + myInstrumentation = ((IServiceProvider)provider.OwnedServiceProvider).GetRequiredService(); - return provider; - }, - provider => - { - provider.Dispose(); - }); + return provider; + }, + provider => + { + provider.Dispose(); + }); - Assert.NotNull(myInstrumentation); - Assert.True(myInstrumentation.Disposed); - } + Assert.NotNull(myInstrumentation); + Assert.True(myInstrumentation.Disposed); + } - [Fact] - public void AddReaderUsingDependencyInjectionTest() - { - var builder = Sdk.CreateMeterProviderBuilder(); + [Fact] + public void AddReaderUsingDependencyInjectionTest() + { + var builder = Sdk.CreateMeterProviderBuilder(); - builder.AddReader(); - builder.AddReader(); + builder.AddReader(); + builder.AddReader(); - using var provider = builder.Build() as MeterProviderSdk; + using var provider = builder.Build() as MeterProviderSdk; - Assert.NotNull(provider); + Assert.NotNull(provider); - var readers = ((IServiceProvider)provider.OwnedServiceProvider).GetServices(); + var readers = ((IServiceProvider)provider.OwnedServiceProvider).GetServices(); - // Note: Two "Add" calls but it is a singleton so only a single registration is produced - Assert.Single(readers); + // Note: Two "Add" calls but it is a singleton so only a single registration is produced + Assert.Single(readers); - var reader = provider.Reader as CompositeMetricReader; + var reader = provider.Reader as CompositeMetricReader; - Assert.NotNull(reader); + Assert.NotNull(reader); - // Note: Two "Add" calls due yield two readers added to provider, even though they are the same - Assert.True(reader.Head.Value is MyReader); - Assert.True(reader.Head.Next?.Value is MyReader); - } + // Note: Two "Add" calls due yield two readers added to provider, even though they are the same + Assert.True(reader.Head.Value is MyReader); + Assert.True(reader.Head.Next?.Value is MyReader); + } - [Fact] - public void AddInstrumentationTest() + [Fact] + public void AddInstrumentationTest() + { + List instrumentation = null; + + using (var provider = Sdk.CreateMeterProviderBuilder() + .AddInstrumentation() + .AddInstrumentation((sp, provider) => new MyInstrumentation() { Provider = provider }) + .AddInstrumentation(new MyInstrumentation()) + .AddInstrumentation(() => (object)null) + .Build() as MeterProviderSdk) { - List instrumentation = null; + Assert.NotNull(provider); - using (var provider = Sdk.CreateMeterProviderBuilder() - .AddInstrumentation() - .AddInstrumentation((sp, provider) => new MyInstrumentation() { Provider = provider }) - .AddInstrumentation(new MyInstrumentation()) - .Build() as MeterProviderSdk) - { - Assert.NotNull(provider); + Assert.Equal(3, provider.Instrumentations.Count); - Assert.Equal(3, provider.Instrumentations.Count); + Assert.Null(((MyInstrumentation)provider.Instrumentations[0]).Provider); + Assert.False(((MyInstrumentation)provider.Instrumentations[0]).Disposed); - Assert.Null(((MyInstrumentation)provider.Instrumentations[0]).Provider); - Assert.False(((MyInstrumentation)provider.Instrumentations[0]).Disposed); + Assert.NotNull(((MyInstrumentation)provider.Instrumentations[1]).Provider); + Assert.False(((MyInstrumentation)provider.Instrumentations[1]).Disposed); - Assert.NotNull(((MyInstrumentation)provider.Instrumentations[1]).Provider); - Assert.False(((MyInstrumentation)provider.Instrumentations[1]).Disposed); + Assert.Null(((MyInstrumentation)provider.Instrumentations[2]).Provider); + Assert.False(((MyInstrumentation)provider.Instrumentations[2]).Disposed); - Assert.Null(((MyInstrumentation)provider.Instrumentations[2]).Provider); - Assert.False(((MyInstrumentation)provider.Instrumentations[2]).Disposed); + instrumentation = new List(provider.Instrumentations); + } - instrumentation = new List(provider.Instrumentations); - } + Assert.NotNull(instrumentation); + Assert.True(((MyInstrumentation)instrumentation[0]).Disposed); + Assert.True(((MyInstrumentation)instrumentation[1]).Disposed); + Assert.True(((MyInstrumentation)instrumentation[2]).Disposed); + } - Assert.NotNull(instrumentation); - Assert.True(((MyInstrumentation)instrumentation[0]).Disposed); - Assert.True(((MyInstrumentation)instrumentation[1]).Disposed); - Assert.True(((MyInstrumentation)instrumentation[2]).Disposed); - } + [Fact] + public void SetAndConfigureResourceTest() + { + var builder = Sdk.CreateMeterProviderBuilder(); + + int configureInvocations = 0; + bool serviceProviderTestExecuted = false; - [Fact] - public void SetAndConfigureResourceTest() + builder.SetResourceBuilder(ResourceBuilder.CreateEmpty().AddService("Test")); + builder.ConfigureResource(builder => { - var builder = Sdk.CreateMeterProviderBuilder(); + configureInvocations++; - int configureInvocations = 0; - bool serviceProviderTestExecuted = false; + Assert.Single(builder.ResourceDetectors); - builder.SetResourceBuilder(ResourceBuilder.CreateEmpty().AddService("Test")); - builder.ConfigureResource(builder => - { - configureInvocations++; + builder.AddAttributes(new Dictionary() { ["key1"] = "value1" }); - Assert.Single(builder.ResourceDetectors); + Assert.Equal(2, builder.ResourceDetectors.Count); + }); + builder.SetResourceBuilder(ResourceBuilder.CreateEmpty()); + builder.ConfigureResource(builder => + { + configureInvocations++; - builder.AddAttributes(new Dictionary() { ["key1"] = "value1" }); + Assert.Empty(builder.ResourceDetectors); - Assert.Equal(2, builder.ResourceDetectors.Count); - }); - builder.SetResourceBuilder(ResourceBuilder.CreateEmpty()); - builder.ConfigureResource(builder => + builder.AddDetectorInternal(sp => { - configureInvocations++; + serviceProviderTestExecuted = true; + Assert.NotNull(sp); + return new ResourceBuilder.WrapperResourceDetector(new Resource(new Dictionary() { ["key2"] = "value2" })); + }); - Assert.Empty(builder.ResourceDetectors); + Assert.Single(builder.ResourceDetectors); + }); - builder.AddDetectorInternal(sp => - { - serviceProviderTestExecuted = true; - Assert.NotNull(sp); - return new ResourceBuilder.WrapperResourceDetector(new Resource(new Dictionary() { ["key2"] = "value2" })); - }); + using var provider = builder.Build() as MeterProviderSdk; - Assert.Single(builder.ResourceDetectors); - }); + Assert.True(serviceProviderTestExecuted); + Assert.Equal(2, configureInvocations); - using var provider = builder.Build() as MeterProviderSdk; + Assert.Single(provider.Resource.Attributes); + Assert.Contains(provider.Resource.Attributes, kvp => kvp.Key == "key2" && (string)kvp.Value == "value2"); + } - Assert.True(serviceProviderTestExecuted); - Assert.Equal(2, configureInvocations); + [Fact] + public void ConfigureBuilderIConfigurationAvailableTest() + { + Environment.SetEnvironmentVariable("TEST_KEY", "TEST_KEY_VALUE"); - Assert.Single(provider.Resource.Attributes); - Assert.Contains(provider.Resource.Attributes, kvp => kvp.Key == "key2" && (string)kvp.Value == "value2"); - } + bool configureBuilderCalled = false; - [Fact] - public void ConfigureBuilderIConfigurationAvailableTest() - { - Environment.SetEnvironmentVariable("TEST_KEY", "TEST_KEY_VALUE"); + using var provider = Sdk.CreateMeterProviderBuilder() + .ConfigureBuilder((sp, builder) => + { + var configuration = sp.GetRequiredService(); - bool configureBuilderCalled = false; + configureBuilderCalled = true; - using var provider = Sdk.CreateMeterProviderBuilder() - .ConfigureBuilder((sp, builder) => - { - var configuration = sp.GetRequiredService(); + var testKeyValue = configuration.GetValue("TEST_KEY", null); - configureBuilderCalled = true; + Assert.Equal("TEST_KEY_VALUE", testKeyValue); + }) + .Build(); - var testKeyValue = configuration.GetValue("TEST_KEY", null); + Assert.True(configureBuilderCalled); - Assert.Equal("TEST_KEY_VALUE", testKeyValue); - }) - .Build(); + Environment.SetEnvironmentVariable("TEST_KEY", null); + } - Assert.True(configureBuilderCalled); + [Fact] + public void ConfigureBuilderIConfigurationModifiableTest() + { + bool configureBuilderCalled = false; - Environment.SetEnvironmentVariable("TEST_KEY", null); - } + using var provider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { ["TEST_KEY_2"] = "TEST_KEY_2_VALUE" }) + .Build(); - [Fact] - public void ConfigureBuilderIConfigurationModifiableTest() - { - bool configureBuilderCalled = false; + services.AddSingleton(configuration); + }) + .ConfigureBuilder((sp, builder) => + { + var configuration = sp.GetRequiredService(); - using var provider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { ["TEST_KEY_2"] = "TEST_KEY_2_VALUE" }) - .Build(); + configureBuilderCalled = true; - services.AddSingleton(configuration); - }) - .ConfigureBuilder((sp, builder) => - { - var configuration = sp.GetRequiredService(); + var testKey2Value = configuration.GetValue("TEST_KEY_2", null); - configureBuilderCalled = true; + Assert.Equal("TEST_KEY_2_VALUE", testKey2Value); + }) + .Build(); - var testKey2Value = configuration.GetValue("TEST_KEY_2", null); + Assert.True(configureBuilderCalled); + } - Assert.Equal("TEST_KEY_2_VALUE", testKey2Value); - }) - .Build(); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void MeterProviderNestedResolutionUsingBuilderTest(bool callNestedConfigure) + { + bool innerConfigureBuilderTestExecuted = false; + bool innerConfigureOpenTelemetryLoggerProviderTestExecuted = false; + bool innerConfigureOpenTelemetryLoggerProviderTestWithServiceProviderExecuted = false; - Assert.True(configureBuilderCalled); - } + using var provider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + if (callNestedConfigure) + { + services.ConfigureOpenTelemetryMeterProvider( + builder => + { + innerConfigureOpenTelemetryLoggerProviderTestExecuted = true; + builder.AddInstrumentation(); + }); + services.ConfigureOpenTelemetryMeterProvider( + (sp, builder) => + { + innerConfigureOpenTelemetryLoggerProviderTestWithServiceProviderExecuted = true; + Assert.Throws(() => builder.AddInstrumentation()); + }); + } + }) + .ConfigureBuilder((sp, builder) => + { + innerConfigureBuilderTestExecuted = true; + Assert.Throws(() => sp.GetService()); + }) + .Build() as MeterProviderSdk; - [Theory] - [InlineData(true)] - [InlineData(false)] - public void MeterProviderNestedResolutionUsingBuilderTest(bool callNestedConfigure) - { - bool innerConfigureBuilderTestExecuted = false; - bool innerConfigureOpenTelemetryLoggerProviderTestExecuted = false; - bool innerConfigureOpenTelemetryLoggerProviderTestWithServiceProviderExecuted = false; + Assert.NotNull(provider); - using var provider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - if (callNestedConfigure) - { - services.ConfigureOpenTelemetryMeterProvider( - builder => - { - innerConfigureOpenTelemetryLoggerProviderTestExecuted = true; - builder.AddInstrumentation(); - }); - services.ConfigureOpenTelemetryMeterProvider( - (sp, builder) => - { - innerConfigureOpenTelemetryLoggerProviderTestWithServiceProviderExecuted = true; - Assert.Throws(() => builder.AddInstrumentation()); - }); - } - }) - .ConfigureBuilder((sp, builder) => - { - innerConfigureBuilderTestExecuted = true; - Assert.Throws(() => sp.GetService()); - }) - .Build() as MeterProviderSdk; + Assert.True(innerConfigureBuilderTestExecuted); + Assert.Equal(callNestedConfigure, innerConfigureOpenTelemetryLoggerProviderTestExecuted); + Assert.Equal(callNestedConfigure, innerConfigureOpenTelemetryLoggerProviderTestWithServiceProviderExecuted); - Assert.NotNull(provider); + if (callNestedConfigure) + { + Assert.Single(provider.Instrumentations); + } + else + { + Assert.Empty(provider.Instrumentations); + } - Assert.True(innerConfigureBuilderTestExecuted); - Assert.Equal(callNestedConfigure, innerConfigureOpenTelemetryLoggerProviderTestExecuted); - Assert.Equal(callNestedConfigure, innerConfigureOpenTelemetryLoggerProviderTestWithServiceProviderExecuted); + Assert.Throws(() => provider.GetServiceProvider()?.GetService()); + } - if (callNestedConfigure) - { - Assert.Single(provider.Instrumentations); - } - else - { - Assert.Empty(provider.Instrumentations); - } + [Fact] + public void MeterProviderAddReaderFactoryTest() + { + bool factoryInvoked = false; - Assert.Throws(() => provider.GetServiceProvider()?.GetService()); - } + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddReader(sp => + { + factoryInvoked = true; - [Fact] - public void MeterProviderAddReaderFactoryTest() - { - bool factoryInvoked = false; + Assert.NotNull(sp); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddReader(sp => - { - factoryInvoked = true; + return new MyReader(); + }) + .Build() as MeterProviderSdk; - Assert.NotNull(sp); + Assert.True(factoryInvoked); - return new MyReader(); - }) - .Build() as MeterProviderSdk; + Assert.NotNull(meterProvider); + Assert.True(meterProvider.Reader is MyReader); + } - Assert.True(factoryInvoked); + [Fact] + public void MeterProviderBuilderCustomImplementationBuildTest() + { + var builder = new MyMeterProviderBuilder(); - Assert.NotNull(meterProvider); - Assert.True(meterProvider.Reader is MyReader); - } + Assert.Throws(() => builder.Build()); + } - private static void RunBuilderServiceLifecycleTest( - MeterProviderBuilder builder, - Func buildFunc, - Action postAction) - { - var baseBuilder = builder as MeterProviderBuilderBase; + private static void RunBuilderServiceLifecycleTest( + MeterProviderBuilder builder, + Func buildFunc, + Action postAction) + { + var baseBuilder = builder as MeterProviderBuilderBase; - builder.AddMeter("TestSource"); + builder.AddMeter("TestSource"); - bool configureServicesCalled = false; - builder.ConfigureServices(services => - { - configureServicesCalled = true; + bool configureServicesCalled = false; + builder.ConfigureServices(services => + { + configureServicesCalled = true; - Assert.NotNull(services); + Assert.NotNull(services); - services.TryAddSingleton(); - services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); - // Note: This is strange to call ConfigureOpenTelemetryMeterProvider here, but supported - services.ConfigureOpenTelemetryMeterProvider((sp, b) => - { - Assert.Throws(() => b.ConfigureServices(services => { })); + // Note: This is strange to call ConfigureOpenTelemetryMeterProvider here, but supported + services.ConfigureOpenTelemetryMeterProvider((sp, b) => + { + Assert.Throws(() => b.ConfigureServices(services => { })); - b.AddInstrumentation(sp.GetRequiredService()); - }); + b.AddInstrumentation(sp.GetRequiredService()); }); + }); - int configureBuilderInvocations = 0; - builder.ConfigureBuilder((sp, builder) => - { - configureBuilderInvocations++; + int configureBuilderInvocations = 0; + builder.ConfigureBuilder((sp, builder) => + { + configureBuilderInvocations++; - var sdkBuilder = builder as MeterProviderBuilderSdk; - Assert.NotNull(sdkBuilder); + var sdkBuilder = builder as MeterProviderBuilderSdk; + Assert.NotNull(sdkBuilder); - builder.AddMeter("TestSource2"); + builder.AddMeter("TestSource2"); - Assert.Contains(sdkBuilder.MeterSources, s => s == "TestSource"); - Assert.Contains(sdkBuilder.MeterSources, s => s == "TestSource2"); + Assert.Contains(sdkBuilder.MeterSources, s => s == "TestSource"); + Assert.Contains(sdkBuilder.MeterSources, s => s == "TestSource2"); - // Note: Services can't be configured at this stage - Assert.Throws( - () => builder.ConfigureServices(services => services.TryAddSingleton())); + // Note: Services can't be configured at this stage + Assert.Throws( + () => builder.ConfigureServices(services => services.TryAddSingleton())); - builder.AddReader(sp.GetRequiredService()); + builder.AddReader(sp.GetRequiredService()); - builder.ConfigureBuilder((_, b) => + builder.ConfigureBuilder((_, b) => + { + // Note: ConfigureBuilder calls can be nested, this is supported + configureBuilderInvocations++; + + b.ConfigureBuilder((_, _) => { - // Note: ConfigureBuilder calls can be nested, this is supported configureBuilderInvocations++; - - b.ConfigureBuilder((_, _) => - { - configureBuilderInvocations++; - }); }); }); + }); - var provider = buildFunc(); + var provider = buildFunc(); - Assert.True(configureServicesCalled); - Assert.Equal(3, configureBuilderInvocations); + Assert.True(configureServicesCalled); + Assert.Equal(3, configureBuilderInvocations); - Assert.Single(provider.Instrumentations); - Assert.True(provider.Instrumentations[0] is MyInstrumentation); - Assert.True(provider.Reader is MyReader); + Assert.Single(provider.Instrumentations); + Assert.True(provider.Instrumentations[0] is MyInstrumentation); + Assert.True(provider.Reader is MyReader); - postAction(provider); - } + postAction(provider); + } - private sealed class MyInstrumentation : IDisposable + private sealed class MyInstrumentation : IDisposable + { + internal MeterProvider Provider; + internal bool Disposed; + + public void Dispose() { - internal MeterProvider Provider; - internal bool Disposed; + this.Disposed = true; + } + } - public void Dispose() - { - this.Disposed = true; - } + private sealed class MyReader : MetricReader + { + } + + private sealed class MyExporter : BaseExporter + { + public override ExportResult Export(in Batch batch) + { + return ExportResult.Success; } + } - private sealed class MyReader : MetricReader + private sealed class MyMeterProviderBuilder : MeterProviderBuilder + { + public override MeterProviderBuilder AddInstrumentation(Func instrumentationFactory) { + throw new NotImplementedException(); } - private sealed class MyExporter : BaseExporter + public override MeterProviderBuilder AddMeter(params string[] names) { - public override ExportResult Export(in Batch batch) - { - return ExportResult.Success; - } + throw new NotImplementedException(); } } } diff --git a/test/OpenTelemetry.Tests/Metrics/MeterProviderSdkTest.cs b/test/OpenTelemetry.Tests/Metrics/MeterProviderSdkTest.cs index 9a980e12ea4..bce959764f2 100644 --- a/test/OpenTelemetry.Tests/Metrics/MeterProviderSdkTest.cs +++ b/test/OpenTelemetry.Tests/Metrics/MeterProviderSdkTest.cs @@ -1,19 +1,9 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics.Metrics; +using OpenTelemetry.Internal; +using OpenTelemetry.Tests; using Xunit; namespace OpenTelemetry.Metrics.Tests; @@ -45,4 +35,73 @@ 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); + } + + var metricInstrumentIgnoredEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 33 && e.Payload[1] as string == meterName); + + Assert.Single(metricInstrumentIgnoredEvents); + + 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/MeterProviderTests.cs b/test/OpenTelemetry.Tests/Metrics/MeterProviderTests.cs index 4a0e42dbcdf..d179363c035 100644 --- a/test/OpenTelemetry.Tests/Metrics/MeterProviderTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MeterProviderTests.cs @@ -1,44 +1,30 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Exporter; using Xunit; -namespace OpenTelemetry.Metrics.Tests +namespace OpenTelemetry.Metrics.Tests; + +public class MeterProviderTests { - public class MeterProviderTests + [Fact] + public void MeterProviderFindExporterTest() { - [Fact] - public void MeterProviderFindExporterTest() - { - var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddInMemoryExporter(exportedItems) - .Build(); + var exportedItems = new List(); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddInMemoryExporter(exportedItems) + .Build(); - Assert.True(meterProvider.TryFindExporter(out InMemoryExporter inMemoryExporter)); - Assert.False(meterProvider.TryFindExporter(out MyExporter myExporter)); - } + Assert.True(meterProvider.TryFindExporter(out InMemoryExporter inMemoryExporter)); + Assert.False(meterProvider.TryFindExporter(out MyExporter myExporter)); + } - private class MyExporter : BaseExporter + private class MyExporter : BaseExporter + { + public override ExportResult Export(in Batch batch) { - public override ExportResult Export(in Batch batch) - { - return ExportResult.Success; - } + return ExportResult.Success; } } } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricAPITest.cs b/test/OpenTelemetry.Tests/Metrics/MetricAPITest.cs deleted file mode 100644 index 498efc1fbe5..00000000000 --- a/test/OpenTelemetry.Tests/Metrics/MetricAPITest.cs +++ /dev/null @@ -1,1692 +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 System.Diagnostics.Metrics; -using OpenTelemetry.Exporter; -using OpenTelemetry.Internal; -using OpenTelemetry.Tests; -using Xunit; -using Xunit.Abstractions; - -namespace OpenTelemetry.Metrics.Tests -{ - public class MetricApiTest : MetricTestsBase - { - private const int MaxTimeToAllowForFlush = 10000; - private static readonly int NumberOfThreads = Environment.ProcessorCount; - private static readonly long DeltaLongValueUpdatedByEachCall = 10; - private static readonly double DeltaDoubleValueUpdatedByEachCall = 11.987; - private static readonly int NumberOfMetricUpdateByEachThread = 100000; - private readonly ITestOutputHelper output; - - public MetricApiTest(ITestOutputHelper output) - { - this.output = output; - } - - [Fact] - public void MeasurementWithNullValuedTag() - { - using var meter = new Meter(Utils.GetCurrentMethodName()); - var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems) - .Build(); - - var counter = meter.CreateCounter("myCounter"); - counter.Add(100, new KeyValuePair("tagWithNullValue", null)); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Single(exportedItems); - var metric = exportedItems[0]; - Assert.Equal("myCounter", metric.Name); - List metricPoints = new List(); - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - Assert.Single(metricPoints); - var metricPoint = metricPoints[0]; - Assert.Equal(100, metricPoint.GetSumLong()); - Assert.Equal(1, metricPoint.Tags.Count); - var tagEnumerator = metricPoint.Tags.GetEnumerator(); - tagEnumerator.MoveNext(); - Assert.Equal("tagWithNullValue", tagEnumerator.Current.Key); - Assert.Null(tagEnumerator.Current.Value); - } - - [Fact] - public void ObserverCallbackTest() - { - using var meter = new Meter(Utils.GetCurrentMethodName()); - var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems) - .Build(); - - var measurement = new Measurement(100, new("name", "apple"), new("color", "red")); - meter.CreateObservableGauge("myGauge", () => measurement); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Single(exportedItems); - var metric = exportedItems[0]; - Assert.Equal("myGauge", metric.Name); - List metricPoints = new List(); - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - Assert.Single(metricPoints); - var metricPoint = metricPoints[0]; - Assert.Equal(100, metricPoint.GetGaugeLastValueLong()); - Assert.True(metricPoint.Tags.Count > 0); - } - - [Fact] - public void ObserverCallbackExceptionTest() - { - using var meter = new Meter(Utils.GetCurrentMethodName()); - var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems) - .Build(); - - var measurement = new Measurement(100, new("name", "apple"), new("color", "red")); - meter.CreateObservableGauge("myGauge", () => measurement); - meter.CreateObservableGauge("myBadGauge", observeValues: () => throw new Exception("gauge read error")); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Single(exportedItems); - var metric = exportedItems[0]; - Assert.Equal("myGauge", metric.Name); - List metricPoints = new List(); - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - Assert.Single(metricPoints); - var metricPoint = metricPoints[0]; - Assert.Equal(100, metricPoint.GetGaugeLastValueLong()); - Assert.True(metricPoint.Tags.Count > 0); - } - - [Theory] - [InlineData("unit")] - [InlineData("")] - [InlineData(null)] - 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(); - - var counter = meter.CreateCounter("name1", unit); - counter.Add(10); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Single(exportedItems); - var metric = exportedItems[0]; - Assert.Equal(unit ?? string.Empty, metric.Unit); - } - - [Theory] - [InlineData("description")] - [InlineData("")] - [InlineData(null)] - 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(); - - var counter = meter.CreateCounter("name1", null, description); - counter.Add(10); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Single(exportedItems); - var metric = exportedItems[0]; - Assert.Equal(description ?? string.Empty, metric.Description); - } - - [Fact] - 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(); - - var instrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); - var duplicateInstrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); - - instrument.Add(10); - duplicateInstrument.Add(20); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Single(exportedItems); - - var metric = exportedItems[0]; - Assert.Equal("instrumentName", metric.Name); - List metricPoints = new List(); - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - Assert.Single(metricPoints); - var metricPoint1 = metricPoints[0]; - Assert.Equal(30, metricPoint1.GetSumLong()); - } - - [Fact] - public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_DifferentDescription() - { - 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(); - - var instrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription1"); - var duplicateInstrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription2"); - - instrument.Add(10); - duplicateInstrument.Add(20); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Equal(2, exportedItems.Count); - - var metric1 = exportedItems[0]; - var metric2 = exportedItems[1]; - Assert.Equal("instrumentDescription1", metric1.Description); - Assert.Equal("instrumentDescription2", metric2.Description); - - List metric1MetricPoints = new List(); - foreach (ref readonly var mp in metric1.GetMetricPoints()) - { - metric1MetricPoints.Add(mp); - } - - Assert.Single(metric1MetricPoints); - var metricPoint1 = metric1MetricPoints[0]; - Assert.Equal(10, metricPoint1.GetSumLong()); - - List metric2MetricPoints = new List(); - foreach (ref readonly var mp in metric2.GetMetricPoints()) - { - metric2MetricPoints.Add(mp); - } - - Assert.Single(metric2MetricPoints); - var metricPoint2 = metric2MetricPoints[0]; - Assert.Equal(20, metricPoint2.GetSumLong()); - } - - [Fact] - public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_DifferentUnit() - { - 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(); - - var instrument = meter.CreateCounter("instrumentName", "instrumentUnit1", "instrumentDescription"); - var duplicateInstrument = meter.CreateCounter("instrumentName", "instrumentUnit2", "instrumentDescription"); - - instrument.Add(10); - duplicateInstrument.Add(20); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Equal(2, exportedItems.Count); - - var metric1 = exportedItems[0]; - var metric2 = exportedItems[1]; - Assert.Equal("instrumentUnit1", metric1.Unit); - Assert.Equal("instrumentUnit2", metric2.Unit); - - List metric1MetricPoints = new List(); - foreach (ref readonly var mp in metric1.GetMetricPoints()) - { - metric1MetricPoints.Add(mp); - } - - Assert.Single(metric1MetricPoints); - var metricPoint1 = metric1MetricPoints[0]; - Assert.Equal(10, metricPoint1.GetSumLong()); - - List metric2MetricPoints = new List(); - foreach (ref readonly var mp in metric2.GetMetricPoints()) - { - metric2MetricPoints.Add(mp); - } - - Assert.Single(metric2MetricPoints); - var metricPoint2 = metric2MetricPoints[0]; - Assert.Equal(20, metricPoint2.GetSumLong()); - } - - [Fact] - public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_DifferentDataType() - { - 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(); - - var instrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); - var duplicateInstrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); - - instrument.Add(10); - duplicateInstrument.Add(20); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Equal(2, exportedItems.Count); - - var metric1 = exportedItems[0]; - var metric2 = exportedItems[1]; - - List metric1MetricPoints = new List(); - foreach (ref readonly var mp in metric1.GetMetricPoints()) - { - metric1MetricPoints.Add(mp); - } - - Assert.Single(metric1MetricPoints); - var metricPoint1 = metric1MetricPoints[0]; - Assert.Equal(10, metricPoint1.GetSumLong()); - - List metric2MetricPoints = new List(); - foreach (ref readonly var mp in metric2.GetMetricPoints()) - { - metric2MetricPoints.Add(mp); - } - - Assert.Single(metric2MetricPoints); - var metricPoint2 = metric2MetricPoints[0]; - Assert.Equal(20D, metricPoint2.GetSumDouble()); - } - - [Fact] - public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_DifferentInstrumentType() - { - 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(); - - var instrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); - var duplicateInstrument = meter.CreateHistogram("instrumentName", "instrumentUnit", "instrumentDescription"); - - instrument.Add(10); - duplicateInstrument.Record(20); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Equal(2, exportedItems.Count); - - var metric1 = exportedItems[0]; - var metric2 = exportedItems[1]; - - List metric1MetricPoints = new List(); - foreach (ref readonly var mp in metric1.GetMetricPoints()) - { - metric1MetricPoints.Add(mp); - } - - Assert.Single(metric1MetricPoints); - var metricPoint1 = metric1MetricPoints[0]; - Assert.Equal(10, metricPoint1.GetSumLong()); - - List metric2MetricPoints = new List(); - foreach (ref readonly var mp in metric2.GetMetricPoints()) - { - metric2MetricPoints.Add(mp); - } - - Assert.Single(metric2MetricPoints); - var metricPoint2 = metric2MetricPoints[0]; - Assert.Equal(1, metricPoint2.GetHistogramCount()); - Assert.Equal(20D, metricPoint2.GetHistogramSum()); - } - - [Fact] - public void DuplicateInstrumentNamesFromDifferentMetersWithSameNameDifferentVersion() - { - var exportedItems = new List(); - - using var meter1 = new Meter($"{Utils.GetCurrentMethodName()}", "1.0"); - using var meter2 = new Meter($"{Utils.GetCurrentMethodName()}", "2.0"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter1.Name) - .AddMeter(meter2.Name) - .AddInMemoryExporter(exportedItems); - - using var meterProvider = meterProviderBuilder.Build(); - - // Expecting one metric stream. - var counterLong = meter1.CreateCounter("name1"); - counterLong.Add(10); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Single(exportedItems); - - // Expeecting another metric stream since the meter differs by version - var anotherCounterSameNameDiffMeter = meter2.CreateCounter("name1"); - anotherCounterSameNameDiffMeter.Add(10); - counterLong.Add(10); - exportedItems.Clear(); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Equal(2, exportedItems.Count); - } - - [Theory] - [InlineData(MetricReaderTemporalityPreference.Cumulative, true)] - [InlineData(MetricReaderTemporalityPreference.Cumulative, false)] - [InlineData(MetricReaderTemporalityPreference.Delta, true)] - [InlineData(MetricReaderTemporalityPreference.Delta, false)] - public void DuplicateInstrumentNamesFromDifferentMetersAreAllowed(MetricReaderTemporalityPreference temporality, bool hasView) - { - var exportedItems = new List(); - - 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) - { - meterProviderBuilder.AddView("name1", new MetricStreamConfiguration() { Description = "description" }); - } - - using var meterProvider = meterProviderBuilder.Build(); - - // Expecting one metric stream. - var counterLong = meter1.CreateCounter("name1"); - counterLong.Add(10); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Single(exportedItems); - - // The following will not be ignored - // as it is the same metric name but different meter. - var anotherCounterSameNameDiffMeter = meter2.CreateCounter("name1"); - anotherCounterSameNameDiffMeter.Add(10); - counterLong.Add(10); - exportedItems.Clear(); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Equal(2, exportedItems.Count); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void MeterSourcesWildcardSupportMatchTest(bool hasView) - { - using var meter1 = new Meter("AbcCompany.XyzProduct.ComponentA"); - using var meter2 = new Meter("abcCompany.xYzProduct.componentC"); // Wildcard match is case insensitive. - using var meter3 = new Meter("DefCompany.AbcProduct.ComponentC"); - using var meter4 = new Meter("DefCompany.XyzProduct.ComponentC"); // Wildcard match supports matching multiple patterns. - using var meter5 = new Meter("GhiCompany.qweProduct.ComponentN"); - 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) - { - meterProviderBuilder.AddView("myGauge1", "newName"); - } - - using var meterProvider = meterProviderBuilder.Build(); - - var measurement = new Measurement(100, new("name", "apple"), new("color", "red")); - meter1.CreateObservableGauge("myGauge1", () => measurement); - meter2.CreateObservableGauge("myGauge2", () => measurement); - meter3.CreateObservableGauge("myGauge3", () => measurement); - meter4.CreateObservableGauge("myGauge4", () => measurement); - meter5.CreateObservableGauge("myGauge5", () => measurement); - meter6.CreateObservableGauge("myGauge6", () => measurement); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - - Assert.True(exportedItems.Count == 5); // "SomeCompany.SomeProduct.SomeComponent" will not be subscribed. - - if (hasView) - { - Assert.Equal("newName", exportedItems[0].Name); - } - else - { - Assert.Equal("myGauge1", exportedItems[0].Name); - } - - Assert.Equal("myGauge2", exportedItems[1].Name); - Assert.Equal("myGauge3", exportedItems[2].Name); - Assert.Equal("myGauge4", exportedItems[3].Name); - Assert.Equal("myGauge5", exportedItems[4].Name); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void MeterSourcesWildcardSupportNegativeTestNoMeterAdded(bool hasView) - { - using var meter1 = new Meter($"AbcCompany.XyzProduct.ComponentA.{hasView}"); - using var meter2 = new Meter($"abcCompany.xYzProduct.componentC.{hasView}"); - - var exportedItems = new List(); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .AddInMemoryExporter(exportedItems); - - if (hasView) - { - meterProviderBuilder.AddView("gauge1", "renamed"); - } - - using var meterProvider = meterProviderBuilder.Build(); - var measurement = new Measurement(100, new("name", "apple"), new("color", "red")); - - meter1.CreateObservableGauge("myGauge1", () => measurement); - meter2.CreateObservableGauge("myGauge2", () => measurement); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.True(exportedItems.Count == 0); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void CounterAggregationTest(bool exportDelta) - { - DateTime testStartTime = DateTime.UtcNow; - - var exportedItems = new List(); - - using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); - var counterLong = meter.CreateCounter("mycounter"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems, metricReaderOptions => - { - metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }) - .Build(); - - counterLong.Add(10); - counterLong.Add(10); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - long sumReceived = GetLongSum(exportedItems); - Assert.Equal(20, sumReceived); - - var metricPoint = GetFirstMetricPoint(exportedItems); - Assert.NotNull(metricPoint); - Assert.True(metricPoint.Value.StartTime >= testStartTime); - Assert.True(metricPoint.Value.EndTime != default); - - DateTimeOffset firstRunStartTime = metricPoint.Value.StartTime; - DateTimeOffset firstRunEndTime = metricPoint.Value.EndTime; - - exportedItems.Clear(); - -#if NETFRAMEWORK - Thread.Sleep(10); // Compensates for low resolution timing in netfx. -#endif - - counterLong.Add(10); - counterLong.Add(10); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - sumReceived = GetLongSum(exportedItems); - if (exportDelta) - { - Assert.Equal(20, sumReceived); - } - else - { - Assert.Equal(40, sumReceived); - } - - metricPoint = GetFirstMetricPoint(exportedItems); - Assert.NotNull(metricPoint); - Assert.True(metricPoint.Value.StartTime >= testStartTime); - Assert.True(metricPoint.Value.EndTime != default); - if (exportDelta) - { - Assert.True(metricPoint.Value.StartTime == firstRunEndTime); - } - else - { - Assert.Equal(firstRunStartTime, metricPoint.Value.StartTime); - } - - Assert.True(metricPoint.Value.EndTime > firstRunEndTime); - - exportedItems.Clear(); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - sumReceived = GetLongSum(exportedItems); - if (exportDelta) - { - Assert.Equal(0, sumReceived); - } - else - { - Assert.Equal(40, sumReceived); - } - - exportedItems.Clear(); - counterLong.Add(40); - counterLong.Add(20); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - sumReceived = GetLongSum(exportedItems); - if (exportDelta) - { - Assert.Equal(60, sumReceived); - } - else - { - Assert.Equal(100, sumReceived); - } - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ObservableCounterAggregationTest(bool exportDelta) - { - var exportedItems = new List(); - - using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); - int i = 1; - var counterLong = meter.CreateObservableCounter( - "observable-counter", - () => - { - return new List>() - { - new Measurement(i++ * 10), - }; - }); - - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems, metricReaderOptions => - { - metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }) - .Build(); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - long sumReceived = GetLongSum(exportedItems); - Assert.Equal(10, sumReceived); - - exportedItems.Clear(); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - sumReceived = GetLongSum(exportedItems); - if (exportDelta) - { - Assert.Equal(10, sumReceived); - } - else - { - Assert.Equal(20, sumReceived); - } - - exportedItems.Clear(); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - sumReceived = GetLongSum(exportedItems); - if (exportDelta) - { - Assert.Equal(10, sumReceived); - } - else - { - Assert.Equal(30, sumReceived); - } - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ObservableCounterWithTagsAggregationTest(bool exportDelta) - { - var exportedItems = new List(); - var tags1 = new List> - { - new("statusCode", 200), - new("verb", "get"), - }; - - var tags2 = new List> - { - new("statusCode", 200), - new("verb", "post"), - }; - - var tags3 = new List> - { - new("statusCode", 500), - new("verb", "get"), - }; - - using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); - var counterLong = meter.CreateObservableCounter( - "observable-counter", - () => - { - return new List>() - { - new Measurement(10, tags1), - new Measurement(10, tags2), - new Measurement(10, tags3), - }; - }); - - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems, metricReaderOptions => - { - metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }) - .Build(); - - // Export 1 - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Single(exportedItems); - var metric = exportedItems[0]; - Assert.Equal("observable-counter", metric.Name); - List metricPoints = new List(); - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - Assert.Equal(3, metricPoints.Count); - - var metricPoint1 = metricPoints[0]; - Assert.Equal(10, metricPoint1.GetSumLong()); - ValidateMetricPointTags(tags1, metricPoint1.Tags); - - var metricPoint2 = metricPoints[1]; - Assert.Equal(10, metricPoint2.GetSumLong()); - ValidateMetricPointTags(tags2, metricPoint2.Tags); - - var metricPoint3 = metricPoints[2]; - Assert.Equal(10, metricPoint3.GetSumLong()); - ValidateMetricPointTags(tags3, metricPoint3.Tags); - - // Export 2 - exportedItems.Clear(); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Single(exportedItems); - metric = exportedItems[0]; - Assert.Equal("observable-counter", metric.Name); - metricPoints.Clear(); - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - Assert.Equal(3, metricPoints.Count); - - metricPoint1 = metricPoints[0]; - Assert.Equal(exportDelta ? 0 : 10, metricPoint1.GetSumLong()); - ValidateMetricPointTags(tags1, metricPoint1.Tags); - - metricPoint2 = metricPoints[1]; - Assert.Equal(exportDelta ? 0 : 10, metricPoint2.GetSumLong()); - ValidateMetricPointTags(tags2, metricPoint2.Tags); - - metricPoint3 = metricPoints[2]; - Assert.Equal(exportDelta ? 0 : 10, metricPoint3.GetSumLong()); - ValidateMetricPointTags(tags3, metricPoint3.Tags); - } - - [Theory(Skip = "Known issue.")] - [InlineData(true)] - [InlineData(false)] - public void ObservableCounterSpatialAggregationTest(bool exportDelta) - { - var exportedItems = new List(); - var tags1 = new List> - { - new("statusCode", 200), - new("verb", "get"), - }; - - var tags2 = new List> - { - new("statusCode", 200), - new("verb", "post"), - }; - - var tags3 = new List> - { - new("statusCode", 500), - new("verb", "get"), - }; - - using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); - var counterLong = meter.CreateObservableCounter( - "requestCount", - () => - { - return new List>() - { - new Measurement(10, tags1), - new Measurement(10, tags2), - new Measurement(10, tags3), - }; - }); - - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems, metricReaderOptions => - { - metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }) - .AddView("requestCount", new MetricStreamConfiguration() { TagKeys = Array.Empty() }) - .Build(); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Single(exportedItems); - var metric = exportedItems[0]; - Assert.Equal("requestCount", metric.Name); - List metricPoints = new List(); - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - Assert.Single(metricPoints); - - var emptyTags = new List>(); - var metricPoint1 = metricPoints[0]; - ValidateMetricPointTags(emptyTags, metricPoint1.Tags); - - // This will fail, as SDK is not "spatially" aggregating the - // requestCount - Assert.Equal(30, metricPoint1.GetSumLong()); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void UpDownCounterAggregationTest(bool exportDelta) - { - DateTime testStartTime = DateTime.UtcNow; - - var exportedItems = new List(); - - using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); - var counterLong = meter.CreateUpDownCounter("mycounter"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems, metricReaderOptions => - { - metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }) - .Build(); - - counterLong.Add(10); - counterLong.Add(-5); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - long sumReceived = GetLongSum(exportedItems); - Assert.Equal(5, sumReceived); - - var metricPoint = GetFirstMetricPoint(exportedItems); - Assert.NotNull(metricPoint); - Assert.True(metricPoint.Value.StartTime >= testStartTime); - Assert.True(metricPoint.Value.EndTime != default); - - DateTimeOffset firstRunStartTime = metricPoint.Value.StartTime; - DateTimeOffset firstRunEndTime = metricPoint.Value.EndTime; - - exportedItems.Clear(); - -#if NETFRAMEWORK - Thread.Sleep(10); // Compensates for low resolution timing in netfx. -#endif - - counterLong.Add(10); - counterLong.Add(-5); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - sumReceived = GetLongSum(exportedItems); - - // Same for both cumulative and delta. MetricReaderTemporalityPreference.Delta implies cumulative for UpDownCounters. - Assert.Equal(10, sumReceived); - - metricPoint = GetFirstMetricPoint(exportedItems); - Assert.NotNull(metricPoint); - Assert.True(metricPoint.Value.StartTime >= testStartTime); - Assert.True(metricPoint.Value.EndTime != default); - - // Same for both cumulative and delta. MetricReaderTemporalityPreference.Delta implies cumulative for UpDownCounters. - Assert.Equal(firstRunStartTime, metricPoint.Value.StartTime); - - Assert.True(metricPoint.Value.EndTime > firstRunEndTime); - - exportedItems.Clear(); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - sumReceived = GetLongSum(exportedItems); - - // Same for both cumulative and delta. MetricReaderTemporalityPreference.Delta implies cumulative for UpDownCounters. - Assert.Equal(10, sumReceived); - - exportedItems.Clear(); - counterLong.Add(40); - counterLong.Add(-20); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - sumReceived = GetLongSum(exportedItems); - - // Same for both cumulative and delta. MetricReaderTemporalityPreference.Delta implies cumulative for UpDownCounters. - Assert.Equal(30, sumReceived); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ObservableUpDownCounterAggregationTest(bool exportDelta) - { - var exportedItems = new List(); - - using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); - int i = 1; - var counterLong = meter.CreateObservableUpDownCounter( - "observable-counter", - () => - { - return new List>() - { - new Measurement(i++ * 10), - }; - }); - - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems, metricReaderOptions => - { - metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }) - .Build(); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - long sumReceived = GetLongSum(exportedItems); - Assert.Equal(10, sumReceived); - - exportedItems.Clear(); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - sumReceived = GetLongSum(exportedItems); - - // Same for both cumulative and delta. MetricReaderTemporalityPreference.Delta implies cumulative for UpDownCounters. - Assert.Equal(20, sumReceived); - - exportedItems.Clear(); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - sumReceived = GetLongSum(exportedItems); - - // Same for both cumulative and delta. MetricReaderTemporalityPreference.Delta implies cumulative for UpDownCounters. - Assert.Equal(30, sumReceived); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ObservableUpDownCounterWithTagsAggregationTest(bool exportDelta) - { - var exportedItems = new List(); - var tags1 = new List> - { - new("statusCode", 200), - new("verb", "get"), - }; - - var tags2 = new List> - { - new("statusCode", 200), - new("verb", "post"), - }; - - var tags3 = new List> - { - new("statusCode", 500), - new("verb", "get"), - }; - - using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); - var counterLong = meter.CreateObservableUpDownCounter( - "observable-counter", - () => - { - return new List>() - { - new Measurement(10, tags1), - new Measurement(10, tags2), - new Measurement(10, tags3), - }; - }); - - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems, metricReaderOptions => - { - metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; - }) - .Build(); - - // Export 1 - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Single(exportedItems); - var metric = exportedItems[0]; - Assert.Equal("observable-counter", metric.Name); - List metricPoints = new List(); - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - Assert.Equal(3, metricPoints.Count); - - var metricPoint1 = metricPoints[0]; - Assert.Equal(10, metricPoint1.GetSumLong()); - ValidateMetricPointTags(tags1, metricPoint1.Tags); - - var metricPoint2 = metricPoints[1]; - Assert.Equal(10, metricPoint2.GetSumLong()); - ValidateMetricPointTags(tags2, metricPoint2.Tags); - - var metricPoint3 = metricPoints[2]; - Assert.Equal(10, metricPoint3.GetSumLong()); - ValidateMetricPointTags(tags3, metricPoint3.Tags); - - // Export 2 - exportedItems.Clear(); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Single(exportedItems); - metric = exportedItems[0]; - Assert.Equal("observable-counter", metric.Name); - metricPoints.Clear(); - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } - - Assert.Equal(3, metricPoints.Count); - - // Same for both cumulative and delta. MetricReaderTemporalityPreference.Delta implies cumulative for UpDownCounters. - metricPoint1 = metricPoints[0]; - Assert.Equal(10, metricPoint1.GetSumLong()); - ValidateMetricPointTags(tags1, metricPoint1.Tags); - - metricPoint2 = metricPoints[1]; - Assert.Equal(10, metricPoint2.GetSumLong()); - ValidateMetricPointTags(tags2, metricPoint2.Tags); - - metricPoint3 = metricPoints[2]; - Assert.Equal(10, metricPoint3.GetSumLong()); - ValidateMetricPointTags(tags3, metricPoint3.Tags); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void DimensionsAreOrderInsensitiveWithSortedKeysFirst(bool exportDelta) - { - var exportedItems = new List(); - - using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); - var counterLong = meter.CreateCounter("Counter"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .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")); - counterLong.Add(10, new("Key1", "Value1"), new("Key3", "Value3"), new("Key2", "Value2")); - counterLong.Add(10, new("Key2", "Value20"), new("Key1", "Value10"), new("Key3", "Value30")); - - // Emit a metric with different set of keys but the same set of values as one of the previous metric points - counterLong.Add(25, new("Key4", "Value1"), new("Key5", "Value3"), new("Key6", "Value2")); - counterLong.Add(25, new("Key4", "Value1"), new("Key6", "Value3"), new("Key5", "Value2")); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - - List> expectedTagsForFirstMetricPoint = new List>() - { - new("Key1", "Value1"), - new("Key2", "Value2"), - new("Key3", "Value3"), - }; - - List> expectedTagsForSecondMetricPoint = new List>() - { - new("Key1", "Value10"), - new("Key2", "Value20"), - new("Key3", "Value30"), - }; - - List> expectedTagsForThirdMetricPoint = new List>() - { - new("Key4", "Value1"), - new("Key5", "Value3"), - new("Key6", "Value2"), - }; - - List> expectedTagsForFourthMetricPoint = new List>() - { - new("Key4", "Value1"), - new("Key5", "Value2"), - new("Key6", "Value3"), - }; - - Assert.Equal(4, GetNumberOfMetricPoints(exportedItems)); - CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFirstMetricPoint, 1); - CheckTagsForNthMetricPoint(exportedItems, expectedTagsForSecondMetricPoint, 2); - CheckTagsForNthMetricPoint(exportedItems, expectedTagsForThirdMetricPoint, 3); - CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFourthMetricPoint, 4); - long sumReceived = GetLongSum(exportedItems); - Assert.Equal(75, sumReceived); - - exportedItems.Clear(); - - counterLong.Add(5, new("Key2", "Value2"), new("Key1", "Value1"), new("Key3", "Value3")); - counterLong.Add(5, new("Key2", "Value2"), new("Key1", "Value1"), new("Key3", "Value3")); - counterLong.Add(10, new("Key2", "Value2"), new("Key3", "Value3"), new("Key1", "Value1")); - counterLong.Add(10, new("Key2", "Value20"), new("Key3", "Value30"), new("Key1", "Value10")); - counterLong.Add(20, new("Key4", "Value1"), new("Key6", "Value2"), new("Key5", "Value3")); - counterLong.Add(20, new("Key4", "Value1"), new("Key5", "Value2"), new("Key6", "Value3")); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - - Assert.Equal(4, GetNumberOfMetricPoints(exportedItems)); - CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFirstMetricPoint, 1); - CheckTagsForNthMetricPoint(exportedItems, expectedTagsForSecondMetricPoint, 2); - CheckTagsForNthMetricPoint(exportedItems, expectedTagsForThirdMetricPoint, 3); - CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFourthMetricPoint, 4); - sumReceived = GetLongSum(exportedItems); - if (exportDelta) - { - Assert.Equal(70, sumReceived); - } - else - { - Assert.Equal(145, sumReceived); - } - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void DimensionsAreOrderInsensitiveWithUnsortedKeysFirst(bool exportDelta) - { - var exportedItems = new List(); - - using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); - var counterLong = meter.CreateCounter("Counter"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .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")); - counterLong.Add(10, new("Key1", "Value1"), new("Key2", "Value2"), new("Key3", "Value3")); - counterLong.Add(10, new("Key2", "Value20"), new("Key1", "Value10"), new("Key3", "Value30")); - - // Emit a metric with different set of keys but the same set of values as one of the previous metric points - counterLong.Add(25, new("Key4", "Value1"), new("Key5", "Value3"), new("Key6", "Value2")); - counterLong.Add(25, new("Key4", "Value1"), new("Key6", "Value3"), new("Key5", "Value2")); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - - List> expectedTagsForFirstMetricPoint = new List>() - { - new("Key1", "Value1"), - new("Key2", "Value2"), - new("Key3", "Value3"), - }; - - List> expectedTagsForSecondMetricPoint = new List>() - { - new("Key1", "Value10"), - new("Key2", "Value20"), - new("Key3", "Value30"), - }; - - List> expectedTagsForThirdMetricPoint = new List>() - { - new("Key4", "Value1"), - new("Key5", "Value3"), - new("Key6", "Value2"), - }; - - List> expectedTagsForFourthMetricPoint = new List>() - { - new("Key4", "Value1"), - new("Key5", "Value2"), - new("Key6", "Value3"), - }; - - Assert.Equal(4, GetNumberOfMetricPoints(exportedItems)); - CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFirstMetricPoint, 1); - CheckTagsForNthMetricPoint(exportedItems, expectedTagsForSecondMetricPoint, 2); - CheckTagsForNthMetricPoint(exportedItems, expectedTagsForThirdMetricPoint, 3); - CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFourthMetricPoint, 4); - long sumReceived = GetLongSum(exportedItems); - Assert.Equal(75, sumReceived); - - exportedItems.Clear(); - - counterLong.Add(5, new("Key2", "Value2"), new("Key1", "Value1"), new("Key3", "Value3")); - counterLong.Add(5, new("Key2", "Value2"), new("Key1", "Value1"), new("Key3", "Value3")); - counterLong.Add(10, new("Key2", "Value2"), new("Key3", "Value3"), new("Key1", "Value1")); - counterLong.Add(10, new("Key2", "Value20"), new("Key3", "Value30"), new("Key1", "Value10")); - counterLong.Add(20, new("Key4", "Value1"), new("Key6", "Value2"), new("Key5", "Value3")); - counterLong.Add(20, new("Key4", "Value1"), new("Key5", "Value2"), new("Key6", "Value3")); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - - Assert.Equal(4, GetNumberOfMetricPoints(exportedItems)); - CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFirstMetricPoint, 1); - CheckTagsForNthMetricPoint(exportedItems, expectedTagsForSecondMetricPoint, 2); - CheckTagsForNthMetricPoint(exportedItems, expectedTagsForThirdMetricPoint, 3); - CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFourthMetricPoint, 4); - sumReceived = GetLongSum(exportedItems); - if (exportDelta) - { - Assert.Equal(70, sumReceived); - } - else - { - Assert.Equal(145, sumReceived); - } - } - - [Theory] - [InlineData(MetricReaderTemporalityPreference.Cumulative)] - [InlineData(MetricReaderTemporalityPreference.Delta)] - public void TestInstrumentDisposal(MetricReaderTemporalityPreference temporality) - { - var exportedItems = new List(); - - var meter1 = new Meter($"{Utils.GetCurrentMethodName()}.{temporality}.1"); - var meter2 = new Meter($"{Utils.GetCurrentMethodName()}.{temporality}.2"); - var counter1 = meter1.CreateCounter("counterFromMeter1"); - var counter2 = meter2.CreateCounter("counterFromMeter2"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .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")); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Equal(2, exportedItems.Count); - exportedItems.Clear(); - - counter1.Add(10, new KeyValuePair("key", "value")); - counter2.Add(10, new KeyValuePair("key", "value")); - meter1.Dispose(); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Equal(2, exportedItems.Count); - exportedItems.Clear(); - - counter1.Add(10, new KeyValuePair("key", "value")); - counter2.Add(10, new KeyValuePair("key", "value")); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Single(exportedItems); - exportedItems.Clear(); - - counter1.Add(10, new KeyValuePair("key", "value")); - counter2.Add(10, new KeyValuePair("key", "value")); - meter2.Dispose(); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Single(exportedItems); - exportedItems.Clear(); - - counter1.Add(10, new KeyValuePair("key", "value")); - counter2.Add(10, new KeyValuePair("key", "value")); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Empty(exportedItems); - } - - [Theory] - [InlineData(MetricReaderTemporalityPreference.Cumulative)] - [InlineData(MetricReaderTemporalityPreference.Delta)] - public void TestMetricPointCap(MetricReaderTemporalityPreference temporality) - { - var exportedItems = new List(); - - int MetricPointCount() - { - var count = 0; - - foreach (var metric in exportedItems) - { - foreach (ref readonly var metricPoint in metric.GetMetricPoints()) - { - count++; - } - } - - return count; - } - - using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{temporality}"); - var counterLong = meter.CreateCounter("mycounterCapTest"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems, metricReaderOptions => - { - metricReaderOptions.TemporalityPreference = temporality; - }) - .Build(); - - // Make one Add with no tags. - // as currently we reserve 0th index - // for no tag point! - // This may be changed later. - counterLong.Add(10); - for (int i = 0; i < MeterProviderBuilderSdk.MaxMetricPointsPerMetricDefault + 1; i++) - { - counterLong.Add(10, new KeyValuePair("key", "value" + i)); - } - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Equal(MeterProviderBuilderSdk.MaxMetricPointsPerMetricDefault, MetricPointCount()); - - exportedItems.Clear(); - counterLong.Add(10); - for (int i = 0; i < MeterProviderBuilderSdk.MaxMetricPointsPerMetricDefault + 1; i++) - { - counterLong.Add(10, new KeyValuePair("key", "value" + i)); - } - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Equal(MeterProviderBuilderSdk.MaxMetricPointsPerMetricDefault, MetricPointCount()); - - counterLong.Add(10); - for (int i = 0; i < MeterProviderBuilderSdk.MaxMetricPointsPerMetricDefault + 1; i++) - { - counterLong.Add(10, new KeyValuePair("key", "value" + i)); - } - - // These updates would be dropped. - counterLong.Add(10, new KeyValuePair("key", "valueA")); - counterLong.Add(10, new KeyValuePair("key", "valueB")); - counterLong.Add(10, new KeyValuePair("key", "valueC")); - exportedItems.Clear(); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Equal(MeterProviderBuilderSdk.MaxMetricPointsPerMetricDefault, MetricPointCount()); - } - - [Fact] - public void MultithreadedLongCounterTest() - { - this.MultithreadedCounterTest(DeltaLongValueUpdatedByEachCall); - } - - [Fact] - public void MultithreadedDoubleCounterTest() - { - this.MultithreadedCounterTest(DeltaDoubleValueUpdatedByEachCall); - } - - [Fact] - public void MultithreadedLongHistogramTest() - { - var expected = new long[16]; - for (var i = 0; i < expected.Length; i++) - { - expected[i] = NumberOfThreads * NumberOfMetricUpdateByEachThread; - } - - // Metric.DefaultHistogramBounds: 0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000 - var values = new long[] { -1, 1, 6, 20, 40, 60, 80, 200, 300, 600, 800, 1001, 3000, 6000, 8000, 10001 }; - - this.MultithreadedHistogramTest(expected, values); - } - - [Fact] - public void MultithreadedDoubleHistogramTest() - { - var expected = new long[16]; - for (var i = 0; i < expected.Length; i++) - { - expected[i] = NumberOfThreads * NumberOfMetricUpdateByEachThread; - } - - // Metric.DefaultHistogramBounds: 0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000 - var values = new double[] { -1.0, 1.0, 6.0, 20.0, 40.0, 60.0, 80.0, 200.0, 300.0, 600.0, 800.0, 1001.0, 3000.0, 6000.0, 8000.0, 10001.0 }; - - this.MultithreadedHistogramTest(expected, values); - } - - [Theory] - [MemberData(nameof(MetricTestData.InvalidInstrumentNames), MemberType = typeof(MetricTestData))] - public void InstrumentWithInvalidNameIsIgnoredTest(string instrumentName) - { - var exportedItems = new List(); - - using var meter = new Meter("InstrumentWithInvalidNameIsIgnoredTest"); - - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems) - .Build(); - - var counterLong = meter.CreateCounter(instrumentName); - counterLong.Add(10); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - - // instrument should have been ignored - // as its name does not comply with the specification - Assert.Empty(exportedItems); - } - - [Theory] - [MemberData(nameof(MetricTestData.ValidInstrumentNames), MemberType = typeof(MetricTestData))] - public void InstrumentWithValidNameIsExportedTest(string name) - { - var exportedItems = new List(); - - using var meter = new Meter("InstrumentValidNameIsExportedTest"); - - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems) - .Build(); - - var counterLong = meter.CreateCounter(name); - counterLong.Add(10); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - - // Expecting one metric stream. - Assert.Single(exportedItems); - var metric = exportedItems[0]; - Assert.Equal(name, metric.Name); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - 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) - { - meterProviderBuilder.AddView("counter", "renamedCounter"); - } - - using var meterProvider = meterProviderBuilder.Build(); - - var counter = meter.CreateCounter("counter"); - - counter.Add(10, new KeyValuePair("key", "value")); - } - - [Fact] - 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 inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log)) - { - var counter = meter.CreateCounter("counter"); - counter.Add(1); - - // This validates that we log InstrumentIgnored event - // and not something else. - Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 33)); - } - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Empty(exportedItems); - } - - private static void CounterUpdateThread(object obj) - where T : struct, IComparable - { - if (obj is not UpdateThreadArguments arguments) - { - throw new Exception("Invalid args"); - } - - var mre = arguments.MreToBlockUpdateThread; - var mreToEnsureAllThreadsStart = arguments.MreToEnsureAllThreadsStart; - var counter = arguments.Instrument as Counter; - var valueToUpdate = arguments.ValuesToRecord[0]; - if (Interlocked.Increment(ref arguments.ThreadsStartedCount) == NumberOfThreads) - { - mreToEnsureAllThreadsStart.Set(); - } - - // Wait until signalled to start calling update on aggregator - mre.WaitOne(); - - for (int i = 0; i < NumberOfMetricUpdateByEachThread; i++) - { - counter.Add(valueToUpdate, new KeyValuePair("verb", "GET")); - } - } - - private static void HistogramUpdateThread(object obj) - where T : struct, IComparable - { - if (obj is not UpdateThreadArguments arguments) - { - throw new Exception("Invalid args"); - } - - var mre = arguments.MreToBlockUpdateThread; - var mreToEnsureAllThreadsStart = arguments.MreToEnsureAllThreadsStart; - var histogram = arguments.Instrument as Histogram; - - if (Interlocked.Increment(ref arguments.ThreadsStartedCount) == NumberOfThreads) - { - mreToEnsureAllThreadsStart.Set(); - } - - // Wait until signalled to start calling update on aggregator - mre.WaitOne(); - - for (int i = 0; i < NumberOfMetricUpdateByEachThread; i++) - { - for (int j = 0; j < arguments.ValuesToRecord.Length; j++) - { - histogram.Record(arguments.ValuesToRecord[j]); - } - } - } - - private void MultithreadedCounterTest(T deltaValueUpdatedByEachCall) - where T : struct, IComparable - { - var metricItems = new List(); - - using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{typeof(T).Name}.{deltaValueUpdatedByEachCall}"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(metricItems) - .Build(); - - var argToThread = new UpdateThreadArguments - { - ValuesToRecord = new T[] { deltaValueUpdatedByEachCall }, - Instrument = meter.CreateCounter("counter"), - MreToBlockUpdateThread = new ManualResetEvent(false), - MreToEnsureAllThreadsStart = new ManualResetEvent(false), - }; - - Thread[] t = new Thread[NumberOfThreads]; - for (int i = 0; i < NumberOfThreads; i++) - { - t[i] = new Thread(CounterUpdateThread); - t[i].Start(argToThread); - } - - argToThread.MreToEnsureAllThreadsStart.WaitOne(); - Stopwatch sw = Stopwatch.StartNew(); - argToThread.MreToBlockUpdateThread.Set(); - - for (int i = 0; i < NumberOfThreads; i++) - { - t[i].Join(); - } - - this.output.WriteLine($"Took {sw.ElapsedMilliseconds} msecs. Total threads: {NumberOfThreads}, each thread doing {NumberOfMetricUpdateByEachThread} recordings."); - - meterProvider.ForceFlush(); - - if (typeof(T) == typeof(long)) - { - var sumReceived = GetLongSum(metricItems); - var expectedSum = DeltaLongValueUpdatedByEachCall * NumberOfMetricUpdateByEachThread * NumberOfThreads; - Assert.Equal(expectedSum, sumReceived); - } - else if (typeof(T) == typeof(double)) - { - var sumReceived = GetDoubleSum(metricItems); - var expectedSum = DeltaDoubleValueUpdatedByEachCall * NumberOfMetricUpdateByEachThread * NumberOfThreads; - Assert.Equal(expectedSum, sumReceived, 2); - } - } - - private void MultithreadedHistogramTest(long[] expected, T[] values) - where T : struct, IComparable - { - var bucketCounts = new long[11]; - - var metrics = new List(); - var metricReader = new BaseExportingMetricReader(new InMemoryExporter(metrics)); - - using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{typeof(T).Name}"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddReader(metricReader) - .Build(); - - var argsToThread = new UpdateThreadArguments - { - Instrument = meter.CreateHistogram("histogram"), - MreToBlockUpdateThread = new ManualResetEvent(false), - MreToEnsureAllThreadsStart = new ManualResetEvent(false), - ValuesToRecord = values, - }; - - Thread[] t = new Thread[NumberOfThreads]; - for (int i = 0; i < NumberOfThreads; i++) - { - t[i] = new Thread(HistogramUpdateThread); - t[i].Start(argsToThread); - } - - argsToThread.MreToEnsureAllThreadsStart.WaitOne(); - Stopwatch sw = Stopwatch.StartNew(); - argsToThread.MreToBlockUpdateThread.Set(); - - for (int i = 0; i < NumberOfThreads; i++) - { - t[i].Join(); - } - - this.output.WriteLine($"Took {sw.ElapsedMilliseconds} msecs. Total threads: {NumberOfThreads}, each thread doing {NumberOfMetricUpdateByEachThread * values.Length} recordings."); - - metricReader.Collect(); - - foreach (var metric in metrics) - { - foreach (var metricPoint in metric.GetMetricPoints()) - { - bucketCounts = metricPoint.GetHistogramBuckets().RunningBucketCounts; - } - } - - Assert.Equal(expected, bucketCounts); - } - - private class UpdateThreadArguments - where T : struct, IComparable - { - public ManualResetEvent MreToBlockUpdateThread; - public ManualResetEvent MreToEnsureAllThreadsStart; - public int ThreadsStartedCount; - public Instrument Instrument; - public T[] ValuesToRecord; - } - } -} diff --git a/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs new file mode 100644 index 00000000000..00336d03663 --- /dev/null +++ b/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs @@ -0,0 +1,1822 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Configuration; +using OpenTelemetry.Exporter; +using OpenTelemetry.Internal; +using OpenTelemetry.Tests; +using Xunit; +using Xunit.Abstractions; + +namespace OpenTelemetry.Metrics.Tests; + +#pragma warning disable SA1402 + +public abstract class MetricApiTestsBase : MetricTestsBase +{ + private const int MaxTimeToAllowForFlush = 10000; + private static readonly int NumberOfThreads = Environment.ProcessorCount; + private static readonly long DeltaLongValueUpdatedByEachCall = 10; + private static readonly double DeltaDoubleValueUpdatedByEachCall = 11.987; + private static readonly int NumberOfMetricUpdateByEachThread = 100000; + private readonly ITestOutputHelper output; + + protected MetricApiTestsBase(ITestOutputHelper output, bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints) + : base(BuildConfiguration(emitOverflowAttribute, shouldReclaimUnusedMetricPoints)) + { + this.output = output; + } + + [Fact] + public void MeasurementWithNullValuedTag() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); + + var counter = meter.CreateCounter("myCounter"); + counter.Add(100, new KeyValuePair("tagWithNullValue", null)); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal("myCounter", metric.Name); + List metricPoints = new List(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + Assert.Single(metricPoints); + var metricPoint = metricPoints[0]; + Assert.Equal(100, metricPoint.GetSumLong()); + Assert.Equal(1, metricPoint.Tags.Count); + var tagEnumerator = metricPoint.Tags.GetEnumerator(); + tagEnumerator.MoveNext(); + Assert.Equal("tagWithNullValue", tagEnumerator.Current.Key); + Assert.Null(tagEnumerator.Current.Value); + } + + [Fact] + public void ObserverCallbackTest() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); + + var measurement = new Measurement(100, new("name", "apple"), new("color", "red")); + meter.CreateObservableGauge("myGauge", () => measurement); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal("myGauge", metric.Name); + List metricPoints = new List(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + Assert.Single(metricPoints); + var metricPoint = metricPoints[0]; + Assert.Equal(100, metricPoint.GetGaugeLastValueLong()); + Assert.True(metricPoint.Tags.Count > 0); + } + + [Fact] + public void ObserverCallbackExceptionTest() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); + + var measurement = new Measurement(100, new("name", "apple"), new("color", "red")); + meter.CreateObservableGauge("myGauge", () => measurement); + meter.CreateObservableGauge("myBadGauge", observeValues: () => throw new Exception("gauge read error")); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal("myGauge", metric.Name); + List metricPoints = new List(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + Assert.Single(metricPoints); + var metricPoint = metricPoints[0]; + Assert.Equal(100, metricPoint.GetGaugeLastValueLong()); + Assert.True(metricPoint.Tags.Count > 0); + } + + [Theory] + [InlineData("unit")] + [InlineData("")] + [InlineData(null)] + public void MetricUnitIsExportedCorrectly(string unit) + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); + + var counter = meter.CreateCounter("name1", unit); + counter.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal(unit ?? string.Empty, metric.Unit); + } + + [Theory] + [InlineData("description")] + [InlineData("")] + [InlineData(null)] + public void MetricDescriptionIsExportedCorrectly(string description) + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + + 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); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal(description ?? string.Empty, metric.Description); + } + + [Fact] + public void MetricInstrumentationScopeIsExportedCorrectly() + { + var exportedItems = new List(); + var meterName = Utils.GetCurrentMethodName(); + var meterVersion = "1.0"; + var meterTags = new List> + { + new( + "MeterTagKey", + "MeterTagValue"), + }; + using var meter = new Meter($"{meterName}", meterVersion, meterTags); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); + + var counter = meter.CreateCounter("name1"); + counter.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal(meterName, metric.MeterName); + Assert.Equal(meterVersion, metric.MeterVersion); + + Assert.Single(metric.MeterTags.Where(kvp => kvp.Key == meterTags[0].Key && kvp.Value == meterTags[0].Value)); + } + + [Fact] + public void MetricInstrumentationScopeAttributesAreNotTreatedAsIdentifyingProperty() + { + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#get-a-meter + // Meters are identified by name, version, and schema_url fields + // and not with tags. + var exportedItems = new List(); + var meterName = "MyMeter"; + var meterVersion = "1.0"; + var meterTags1 = new List> + { + new( + "Key1", + "Value1"), + }; + var meterTags2 = new List> + { + new( + "Key2", + "Value2"), + }; + using var meter1 = new Meter(meterName, meterVersion, meterTags1); + using var meter2 = new Meter(meterName, meterVersion, meterTags2); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meterName) + .AddInMemoryExporter(exportedItems)); + + var counter1 = meter1.CreateCounter("my-counter"); + counter1.Add(10); + var counter2 = meter2.CreateCounter("my-counter"); + counter2.Add(15); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + // The instruments differ only in the Meter.Tags, which is not an identifying property. + // The first instrument's Meter.Tags is exported. + // It is considered a user-error to create Meters with same name,version but with + // different tags. TODO: See if we can emit an internal log about this. + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal(meterName, metric.MeterName); + Assert.Equal(meterVersion, metric.MeterVersion); + + Assert.Single(metric.MeterTags.Where(kvp => kvp.Key == meterTags1[0].Key && kvp.Value == meterTags1[0].Value)); + Assert.Empty(metric.MeterTags.Where(kvp => kvp.Key == meterTags2[0].Key && kvp.Value == meterTags2[0].Value)); + + List metricPoints = new List(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + Assert.Single(metricPoints); + var metricPoint1 = metricPoints[0]; + Assert.Equal(25, metricPoint1.GetSumLong()); + } + + [Fact] + public void DuplicateInstrumentRegistration_NoViews_IdenticalInstruments() + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + + 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"); + + instrument.Add(10); + duplicateInstrument.Add(20); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + + var metric = exportedItems[0]; + Assert.Equal("instrumentName", metric.Name); + List metricPoints = new List(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + Assert.Single(metricPoints); + var metricPoint1 = metricPoints[0]; + Assert.Equal(30, metricPoint1.GetSumLong()); + } + + [Fact] + public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_DifferentDescription() + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + + 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"); + + instrument.Add(10); + duplicateInstrument.Add(20); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + + var metric1 = exportedItems[0]; + var metric2 = exportedItems[1]; + Assert.Equal("instrumentDescription1", metric1.Description); + Assert.Equal("instrumentDescription2", metric2.Description); + + List metric1MetricPoints = new List(); + foreach (ref readonly var mp in metric1.GetMetricPoints()) + { + metric1MetricPoints.Add(mp); + } + + Assert.Single(metric1MetricPoints); + var metricPoint1 = metric1MetricPoints[0]; + Assert.Equal(10, metricPoint1.GetSumLong()); + + List metric2MetricPoints = new List(); + foreach (ref readonly var mp in metric2.GetMetricPoints()) + { + metric2MetricPoints.Add(mp); + } + + Assert.Single(metric2MetricPoints); + var metricPoint2 = metric2MetricPoints[0]; + Assert.Equal(20, metricPoint2.GetSumLong()); + } + + [Fact] + public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_DifferentUnit() + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + + 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"); + + instrument.Add(10); + duplicateInstrument.Add(20); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + + var metric1 = exportedItems[0]; + var metric2 = exportedItems[1]; + Assert.Equal("instrumentUnit1", metric1.Unit); + Assert.Equal("instrumentUnit2", metric2.Unit); + + List metric1MetricPoints = new List(); + foreach (ref readonly var mp in metric1.GetMetricPoints()) + { + metric1MetricPoints.Add(mp); + } + + Assert.Single(metric1MetricPoints); + var metricPoint1 = metric1MetricPoints[0]; + Assert.Equal(10, metricPoint1.GetSumLong()); + + List metric2MetricPoints = new List(); + foreach (ref readonly var mp in metric2.GetMetricPoints()) + { + metric2MetricPoints.Add(mp); + } + + Assert.Single(metric2MetricPoints); + var metricPoint2 = metric2MetricPoints[0]; + Assert.Equal(20, metricPoint2.GetSumLong()); + } + + [Fact] + public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_DifferentDataType() + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + + 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"); + + instrument.Add(10); + duplicateInstrument.Add(20); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + + var metric1 = exportedItems[0]; + var metric2 = exportedItems[1]; + + List metric1MetricPoints = new List(); + foreach (ref readonly var mp in metric1.GetMetricPoints()) + { + metric1MetricPoints.Add(mp); + } + + Assert.Single(metric1MetricPoints); + var metricPoint1 = metric1MetricPoints[0]; + Assert.Equal(10, metricPoint1.GetSumLong()); + + List metric2MetricPoints = new List(); + foreach (ref readonly var mp in metric2.GetMetricPoints()) + { + metric2MetricPoints.Add(mp); + } + + Assert.Single(metric2MetricPoints); + var metricPoint2 = metric2MetricPoints[0]; + Assert.Equal(20D, metricPoint2.GetSumDouble()); + } + + [Fact] + public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_DifferentInstrumentType() + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + + 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"); + + instrument.Add(10); + duplicateInstrument.Record(20); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + + var metric1 = exportedItems[0]; + var metric2 = exportedItems[1]; + + List metric1MetricPoints = new List(); + foreach (ref readonly var mp in metric1.GetMetricPoints()) + { + metric1MetricPoints.Add(mp); + } + + Assert.Single(metric1MetricPoints); + var metricPoint1 = metric1MetricPoints[0]; + Assert.Equal(10, metricPoint1.GetSumLong()); + + List metric2MetricPoints = new List(); + foreach (ref readonly var mp in metric2.GetMetricPoints()) + { + metric2MetricPoints.Add(mp); + } + + Assert.Single(metric2MetricPoints); + var metricPoint2 = metric2MetricPoints[0]; + Assert.Equal(1, metricPoint2.GetHistogramCount()); + Assert.Equal(20D, metricPoint2.GetHistogramSum()); + } + + [Fact] + public void DuplicateInstrumentNamesFromDifferentMetersWithSameNameDifferentVersion() + { + var exportedItems = new List(); + + using var meter1 = new Meter($"{Utils.GetCurrentMethodName()}", "1.0"); + using var meter2 = new Meter($"{Utils.GetCurrentMethodName()}", "2.0"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter1.Name) + .AddMeter(meter2.Name) + .AddInMemoryExporter(exportedItems)); + + // Expecting one metric stream. + var counterLong = meter1.CreateCounter("name1"); + counterLong.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + + // Expeecting another metric stream since the meter differs by version + var anotherCounterSameNameDiffMeter = meter2.CreateCounter("name1"); + anotherCounterSameNameDiffMeter.Add(10); + counterLong.Add(10); + exportedItems.Clear(); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + } + + [Theory] + [InlineData(MetricReaderTemporalityPreference.Cumulative, true)] + [InlineData(MetricReaderTemporalityPreference.Cumulative, false)] + [InlineData(MetricReaderTemporalityPreference.Delta, true)] + [InlineData(MetricReaderTemporalityPreference.Delta, false)] + public void DuplicateInstrumentNamesFromDifferentMetersAreAllowed(MetricReaderTemporalityPreference temporality, bool hasView) + { + var exportedItems = new List(); + + using var meter1 = new Meter($"{Utils.GetCurrentMethodName()}.1.{temporality}"); + using var meter2 = new Meter($"{Utils.GetCurrentMethodName()}.2.{temporality}"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => + { + builder + .AddMeter(meter1.Name) + .AddMeter(meter2.Name) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = temporality; + }); + + if (hasView) + { + builder.AddView("name1", new MetricStreamConfiguration() { Description = "description" }); + } + }); + + // Expecting one metric stream. + var counterLong = meter1.CreateCounter("name1"); + counterLong.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + + // The following will not be ignored + // as it is the same metric name but different meter. + var anotherCounterSameNameDiffMeter = meter2.CreateCounter("name1"); + anotherCounterSameNameDiffMeter.Add(10); + counterLong.Add(10); + exportedItems.Clear(); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + } + +#if !BUILDING_HOSTING_TESTS + [Theory] + [InlineData(true)] + [InlineData(false)] + public void MeterSourcesWildcardSupportMatchTest(bool hasView) + { + using var meter1 = new Meter("AbcCompany.XyzProduct.ComponentA"); + using var meter2 = new Meter("abcCompany.xYzProduct.componentC"); // Wildcard match is case insensitive. + using var meter3 = new Meter("DefCompany.AbcProduct.ComponentC"); + using var meter4 = new Meter("DefCompany.XyzProduct.ComponentC"); // Wildcard match supports matching multiple patterns. + using var meter5 = new Meter("GhiCompany.qweProduct.ComponentN"); + using var meter6 = new Meter("SomeCompany.SomeProduct.SomeComponent"); + + var exportedItems = new List(); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => + { + builder + .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) + { + builder.AddView("myGauge1", "newName"); + } + }); + + var measurement = new Measurement(100, new("name", "apple"), new("color", "red")); + meter1.CreateObservableGauge("myGauge1", () => measurement); + meter2.CreateObservableGauge("myGauge2", () => measurement); + meter3.CreateObservableGauge("myGauge3", () => measurement); + meter4.CreateObservableGauge("myGauge4", () => measurement); + meter5.CreateObservableGauge("myGauge5", () => measurement); + meter6.CreateObservableGauge("myGauge6", () => measurement); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + Assert.True(exportedItems.Count == 5); // "SomeCompany.SomeProduct.SomeComponent" will not be subscribed. + + if (hasView) + { + Assert.Equal("newName", exportedItems[0].Name); + } + else + { + Assert.Equal("myGauge1", exportedItems[0].Name); + } + + Assert.Equal("myGauge2", exportedItems[1].Name); + Assert.Equal("myGauge3", exportedItems[2].Name); + Assert.Equal("myGauge4", exportedItems[3].Name); + Assert.Equal("myGauge5", exportedItems[4].Name); + } +#endif + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void MeterSourcesWildcardSupportNegativeTestNoMeterAdded(bool hasView) + { + using var meter1 = new Meter($"AbcCompany.XyzProduct.ComponentA.{hasView}"); + using var meter2 = new Meter($"abcCompany.xYzProduct.componentC.{hasView}"); + + var exportedItems = new List(); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => + { + builder + .AddInMemoryExporter(exportedItems); + + if (hasView) + { + builder.AddView("gauge1", "renamed"); + } + }); + + var measurement = new Measurement(100, new("name", "apple"), new("color", "red")); + + meter1.CreateObservableGauge("myGauge1", () => measurement); + meter2.CreateObservableGauge("myGauge2", () => measurement); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.True(exportedItems.Count == 0); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CounterAggregationTest(bool exportDelta) + { + DateTime testStartTime = DateTime.UtcNow; + + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); + var counterLong = meter.CreateCounter("mycounter"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; + })); + + counterLong.Add(10); + counterLong.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + long sumReceived = GetLongSum(exportedItems); + Assert.Equal(20, sumReceived); + + var metricPoint = GetFirstMetricPoint(exportedItems); + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + DateTimeOffset firstRunStartTime = metricPoint.Value.StartTime; + DateTimeOffset firstRunEndTime = metricPoint.Value.EndTime; + + exportedItems.Clear(); + +#if NETFRAMEWORK + Thread.Sleep(10); // Compensates for low resolution timing in netfx. +#endif + + counterLong.Add(10); + counterLong.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + sumReceived = GetLongSum(exportedItems); + if (exportDelta) + { + Assert.Equal(20, sumReceived); + } + else + { + Assert.Equal(40, sumReceived); + } + + metricPoint = GetFirstMetricPoint(exportedItems); + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + if (exportDelta) + { + Assert.True(metricPoint.Value.StartTime == firstRunEndTime); + } + else + { + Assert.Equal(firstRunStartTime, metricPoint.Value.StartTime); + } + + Assert.True(metricPoint.Value.EndTime > firstRunEndTime); + + exportedItems.Clear(); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + sumReceived = GetLongSum(exportedItems); + if (exportDelta) + { + Assert.Equal(0, sumReceived); + } + else + { + Assert.Equal(40, sumReceived); + } + + exportedItems.Clear(); + counterLong.Add(40); + counterLong.Add(20); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + sumReceived = GetLongSum(exportedItems); + if (exportDelta) + { + Assert.Equal(60, sumReceived); + } + else + { + Assert.Equal(100, sumReceived); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ObservableCounterAggregationTest(bool exportDelta) + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); + int i = 1; + var counterLong = meter.CreateObservableCounter( + "observable-counter", + () => + { + return new List>() + { + new Measurement(i++ * 10), + }; + }); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; + })); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + long sumReceived = GetLongSum(exportedItems); + Assert.Equal(10, sumReceived); + + exportedItems.Clear(); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + sumReceived = GetLongSum(exportedItems); + if (exportDelta) + { + Assert.Equal(10, sumReceived); + } + else + { + Assert.Equal(20, sumReceived); + } + + exportedItems.Clear(); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + sumReceived = GetLongSum(exportedItems); + if (exportDelta) + { + Assert.Equal(10, sumReceived); + } + else + { + Assert.Equal(30, sumReceived); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ObservableCounterWithTagsAggregationTest(bool exportDelta) + { + var exportedItems = new List(); + var tags1 = new List> + { + new("statusCode", 200), + new("verb", "get"), + }; + + var tags2 = new List> + { + new("statusCode", 200), + new("verb", "post"), + }; + + var tags3 = new List> + { + new("statusCode", 500), + new("verb", "get"), + }; + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); + var counterLong = meter.CreateObservableCounter( + "observable-counter", + () => + { + return new List>() + { + new Measurement(10, tags1), + new Measurement(10, tags2), + new Measurement(10, tags3), + }; + }); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; + })); + + // Export 1 + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal("observable-counter", metric.Name); + List metricPoints = new List(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + Assert.Equal(3, metricPoints.Count); + + var metricPoint1 = metricPoints[0]; + Assert.Equal(10, metricPoint1.GetSumLong()); + ValidateMetricPointTags(tags1, metricPoint1.Tags); + + var metricPoint2 = metricPoints[1]; + Assert.Equal(10, metricPoint2.GetSumLong()); + ValidateMetricPointTags(tags2, metricPoint2.Tags); + + var metricPoint3 = metricPoints[2]; + Assert.Equal(10, metricPoint3.GetSumLong()); + ValidateMetricPointTags(tags3, metricPoint3.Tags); + + // Export 2 + exportedItems.Clear(); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + metric = exportedItems[0]; + Assert.Equal("observable-counter", metric.Name); + metricPoints.Clear(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + Assert.Equal(3, metricPoints.Count); + + metricPoint1 = metricPoints[0]; + Assert.Equal(exportDelta ? 0 : 10, metricPoint1.GetSumLong()); + ValidateMetricPointTags(tags1, metricPoint1.Tags); + + metricPoint2 = metricPoints[1]; + Assert.Equal(exportDelta ? 0 : 10, metricPoint2.GetSumLong()); + ValidateMetricPointTags(tags2, metricPoint2.Tags); + + metricPoint3 = metricPoints[2]; + Assert.Equal(exportDelta ? 0 : 10, metricPoint3.GetSumLong()); + ValidateMetricPointTags(tags3, metricPoint3.Tags); + } + + [Theory(Skip = "Known issue.")] + [InlineData(true)] + [InlineData(false)] + public void ObservableCounterSpatialAggregationTest(bool exportDelta) + { + var exportedItems = new List(); + var tags1 = new List> + { + new("statusCode", 200), + new("verb", "get"), + }; + + var tags2 = new List> + { + new("statusCode", 200), + new("verb", "post"), + }; + + var tags3 = new List> + { + new("statusCode", 500), + new("verb", "get"), + }; + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); + var counterLong = meter.CreateObservableCounter( + "requestCount", + () => + { + return new List>() + { + new Measurement(10, tags1), + new Measurement(10, tags2), + new Measurement(10, tags3), + }; + }); + + 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() })); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal("requestCount", metric.Name); + List metricPoints = new List(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + Assert.Single(metricPoints); + + var emptyTags = new List>(); + var metricPoint1 = metricPoints[0]; + ValidateMetricPointTags(emptyTags, metricPoint1.Tags); + + // This will fail, as SDK is not "spatially" aggregating the + // requestCount + Assert.Equal(30, metricPoint1.GetSumLong()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void UpDownCounterAggregationTest(bool exportDelta) + { + DateTime testStartTime = DateTime.UtcNow; + + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); + var counterLong = meter.CreateUpDownCounter("mycounter"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; + })); + + counterLong.Add(10); + counterLong.Add(-5); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + long sumReceived = GetLongSum(exportedItems); + Assert.Equal(5, sumReceived); + + var metricPoint = GetFirstMetricPoint(exportedItems); + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + DateTimeOffset firstRunStartTime = metricPoint.Value.StartTime; + DateTimeOffset firstRunEndTime = metricPoint.Value.EndTime; + + exportedItems.Clear(); + +#if NETFRAMEWORK + Thread.Sleep(10); // Compensates for low resolution timing in netfx. +#endif + + counterLong.Add(10); + counterLong.Add(-5); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + sumReceived = GetLongSum(exportedItems); + + // Same for both cumulative and delta. MetricReaderTemporalityPreference.Delta implies cumulative for UpDownCounters. + Assert.Equal(10, sumReceived); + + metricPoint = GetFirstMetricPoint(exportedItems); + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + // Same for both cumulative and delta. MetricReaderTemporalityPreference.Delta implies cumulative for UpDownCounters. + Assert.Equal(firstRunStartTime, metricPoint.Value.StartTime); + + Assert.True(metricPoint.Value.EndTime > firstRunEndTime); + + exportedItems.Clear(); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + sumReceived = GetLongSum(exportedItems); + + // Same for both cumulative and delta. MetricReaderTemporalityPreference.Delta implies cumulative for UpDownCounters. + Assert.Equal(10, sumReceived); + + exportedItems.Clear(); + counterLong.Add(40); + counterLong.Add(-20); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + sumReceived = GetLongSum(exportedItems); + + // Same for both cumulative and delta. MetricReaderTemporalityPreference.Delta implies cumulative for UpDownCounters. + Assert.Equal(30, sumReceived); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ObservableUpDownCounterAggregationTest(bool exportDelta) + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); + int i = 1; + var counterLong = meter.CreateObservableUpDownCounter( + "observable-counter", + () => + { + return new List>() + { + new Measurement(i++ * 10), + }; + }); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; + })); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + long sumReceived = GetLongSum(exportedItems); + Assert.Equal(10, sumReceived); + + exportedItems.Clear(); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + sumReceived = GetLongSum(exportedItems); + + // Same for both cumulative and delta. MetricReaderTemporalityPreference.Delta implies cumulative for UpDownCounters. + Assert.Equal(20, sumReceived); + + exportedItems.Clear(); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + sumReceived = GetLongSum(exportedItems); + + // Same for both cumulative and delta. MetricReaderTemporalityPreference.Delta implies cumulative for UpDownCounters. + Assert.Equal(30, sumReceived); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ObservableUpDownCounterWithTagsAggregationTest(bool exportDelta) + { + var exportedItems = new List(); + var tags1 = new List> + { + new("statusCode", 200), + new("verb", "get"), + }; + + var tags2 = new List> + { + new("statusCode", 200), + new("verb", "post"), + }; + + var tags3 = new List> + { + new("statusCode", 500), + new("verb", "get"), + }; + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); + var counterLong = meter.CreateObservableUpDownCounter( + "observable-counter", + () => + { + return new List>() + { + new Measurement(10, tags1), + new Measurement(10, tags2), + new Measurement(10, tags3), + }; + }); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; + })); + + // Export 1 + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal("observable-counter", metric.Name); + List metricPoints = new List(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + Assert.Equal(3, metricPoints.Count); + + var metricPoint1 = metricPoints[0]; + Assert.Equal(10, metricPoint1.GetSumLong()); + ValidateMetricPointTags(tags1, metricPoint1.Tags); + + var metricPoint2 = metricPoints[1]; + Assert.Equal(10, metricPoint2.GetSumLong()); + ValidateMetricPointTags(tags2, metricPoint2.Tags); + + var metricPoint3 = metricPoints[2]; + Assert.Equal(10, metricPoint3.GetSumLong()); + ValidateMetricPointTags(tags3, metricPoint3.Tags); + + // Export 2 + exportedItems.Clear(); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + metric = exportedItems[0]; + Assert.Equal("observable-counter", metric.Name); + metricPoints.Clear(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + Assert.Equal(3, metricPoints.Count); + + // Same for both cumulative and delta. MetricReaderTemporalityPreference.Delta implies cumulative for UpDownCounters. + metricPoint1 = metricPoints[0]; + Assert.Equal(10, metricPoint1.GetSumLong()); + ValidateMetricPointTags(tags1, metricPoint1.Tags); + + metricPoint2 = metricPoints[1]; + Assert.Equal(10, metricPoint2.GetSumLong()); + ValidateMetricPointTags(tags2, metricPoint2.Tags); + + metricPoint3 = metricPoints[2]; + Assert.Equal(10, metricPoint3.GetSumLong()); + ValidateMetricPointTags(tags3, metricPoint3.Tags); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DimensionsAreOrderInsensitiveWithSortedKeysFirst(bool exportDelta) + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); + var counterLong = meter.CreateCounter("Counter"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; + })); + + // Emit the first metric with the sorted order of tag keys + counterLong.Add(5, new("Key1", "Value1"), new("Key2", "Value2"), new("Key3", "Value3")); + counterLong.Add(10, new("Key1", "Value1"), new("Key3", "Value3"), new("Key2", "Value2")); + counterLong.Add(10, new("Key2", "Value20"), new("Key1", "Value10"), new("Key3", "Value30")); + + // Emit a metric with different set of keys but the same set of values as one of the previous metric points + counterLong.Add(25, new("Key4", "Value1"), new("Key5", "Value3"), new("Key6", "Value2")); + counterLong.Add(25, new("Key4", "Value1"), new("Key6", "Value3"), new("Key5", "Value2")); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + List> expectedTagsForFirstMetricPoint = new List>() + { + new("Key1", "Value1"), + new("Key2", "Value2"), + new("Key3", "Value3"), + }; + + List> expectedTagsForSecondMetricPoint = new List>() + { + new("Key1", "Value10"), + new("Key2", "Value20"), + new("Key3", "Value30"), + }; + + List> expectedTagsForThirdMetricPoint = new List>() + { + new("Key4", "Value1"), + new("Key5", "Value3"), + new("Key6", "Value2"), + }; + + List> expectedTagsForFourthMetricPoint = new List>() + { + new("Key4", "Value1"), + new("Key5", "Value2"), + new("Key6", "Value3"), + }; + + Assert.Equal(4, GetNumberOfMetricPoints(exportedItems)); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFirstMetricPoint, 1); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForSecondMetricPoint, 2); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForThirdMetricPoint, 3); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFourthMetricPoint, 4); + long sumReceived = GetLongSum(exportedItems); + Assert.Equal(75, sumReceived); + + exportedItems.Clear(); + + counterLong.Add(5, new("Key2", "Value2"), new("Key1", "Value1"), new("Key3", "Value3")); + counterLong.Add(5, new("Key2", "Value2"), new("Key1", "Value1"), new("Key3", "Value3")); + counterLong.Add(10, new("Key2", "Value2"), new("Key3", "Value3"), new("Key1", "Value1")); + counterLong.Add(10, new("Key2", "Value20"), new("Key3", "Value30"), new("Key1", "Value10")); + counterLong.Add(20, new("Key4", "Value1"), new("Key6", "Value2"), new("Key5", "Value3")); + counterLong.Add(20, new("Key4", "Value1"), new("Key5", "Value2"), new("Key6", "Value3")); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + Assert.Equal(4, GetNumberOfMetricPoints(exportedItems)); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFirstMetricPoint, 1); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForSecondMetricPoint, 2); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForThirdMetricPoint, 3); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFourthMetricPoint, 4); + sumReceived = GetLongSum(exportedItems); + if (exportDelta) + { + Assert.Equal(70, sumReceived); + } + else + { + Assert.Equal(145, sumReceived); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DimensionsAreOrderInsensitiveWithUnsortedKeysFirst(bool exportDelta) + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); + var counterLong = meter.CreateCounter("Counter"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = exportDelta ? MetricReaderTemporalityPreference.Delta : MetricReaderTemporalityPreference.Cumulative; + })); + + // Emit the first metric with the unsorted order of tag keys + counterLong.Add(5, new("Key1", "Value1"), new("Key3", "Value3"), new("Key2", "Value2")); + counterLong.Add(10, new("Key1", "Value1"), new("Key2", "Value2"), new("Key3", "Value3")); + counterLong.Add(10, new("Key2", "Value20"), new("Key1", "Value10"), new("Key3", "Value30")); + + // Emit a metric with different set of keys but the same set of values as one of the previous metric points + counterLong.Add(25, new("Key4", "Value1"), new("Key5", "Value3"), new("Key6", "Value2")); + counterLong.Add(25, new("Key4", "Value1"), new("Key6", "Value3"), new("Key5", "Value2")); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + List> expectedTagsForFirstMetricPoint = new List>() + { + new("Key1", "Value1"), + new("Key2", "Value2"), + new("Key3", "Value3"), + }; + + List> expectedTagsForSecondMetricPoint = new List>() + { + new("Key1", "Value10"), + new("Key2", "Value20"), + new("Key3", "Value30"), + }; + + List> expectedTagsForThirdMetricPoint = new List>() + { + new("Key4", "Value1"), + new("Key5", "Value3"), + new("Key6", "Value2"), + }; + + List> expectedTagsForFourthMetricPoint = new List>() + { + new("Key4", "Value1"), + new("Key5", "Value2"), + new("Key6", "Value3"), + }; + + Assert.Equal(4, GetNumberOfMetricPoints(exportedItems)); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFirstMetricPoint, 1); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForSecondMetricPoint, 2); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForThirdMetricPoint, 3); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFourthMetricPoint, 4); + long sumReceived = GetLongSum(exportedItems); + Assert.Equal(75, sumReceived); + + exportedItems.Clear(); + + counterLong.Add(5, new("Key2", "Value2"), new("Key1", "Value1"), new("Key3", "Value3")); + counterLong.Add(5, new("Key2", "Value2"), new("Key1", "Value1"), new("Key3", "Value3")); + counterLong.Add(10, new("Key2", "Value2"), new("Key3", "Value3"), new("Key1", "Value1")); + counterLong.Add(10, new("Key2", "Value20"), new("Key3", "Value30"), new("Key1", "Value10")); + counterLong.Add(20, new("Key4", "Value1"), new("Key6", "Value2"), new("Key5", "Value3")); + counterLong.Add(20, new("Key4", "Value1"), new("Key5", "Value2"), new("Key6", "Value3")); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + Assert.Equal(4, GetNumberOfMetricPoints(exportedItems)); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFirstMetricPoint, 1); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForSecondMetricPoint, 2); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForThirdMetricPoint, 3); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFourthMetricPoint, 4); + sumReceived = GetLongSum(exportedItems); + if (exportDelta) + { + Assert.Equal(70, sumReceived); + } + else + { + Assert.Equal(145, sumReceived); + } + } + + [Theory] + [InlineData(MetricReaderTemporalityPreference.Cumulative)] + [InlineData(MetricReaderTemporalityPreference.Delta)] + public void TestInstrumentDisposal(MetricReaderTemporalityPreference temporality) + { + var exportedItems = new List(); + + var meter1 = new Meter($"{Utils.GetCurrentMethodName()}.{temporality}.1"); + var meter2 = new Meter($"{Utils.GetCurrentMethodName()}.{temporality}.2"); + var counter1 = meter1.CreateCounter("counterFromMeter1"); + var counter2 = meter2.CreateCounter("counterFromMeter2"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter1.Name) + .AddMeter(meter2.Name) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = temporality; + })); + + counter1.Add(10, new KeyValuePair("key", "value")); + counter2.Add(10, new KeyValuePair("key", "value")); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + exportedItems.Clear(); + + counter1.Add(10, new KeyValuePair("key", "value")); + counter2.Add(10, new KeyValuePair("key", "value")); + meter1.Dispose(); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + exportedItems.Clear(); + + counter1.Add(10, new KeyValuePair("key", "value")); + counter2.Add(10, new KeyValuePair("key", "value")); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + exportedItems.Clear(); + + counter1.Add(10, new KeyValuePair("key", "value")); + counter2.Add(10, new KeyValuePair("key", "value")); + meter2.Dispose(); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + exportedItems.Clear(); + + counter1.Add(10, new KeyValuePair("key", "value")); + counter2.Add(10, new KeyValuePair("key", "value")); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Empty(exportedItems); + } + + [Theory] + [InlineData(MetricReaderTemporalityPreference.Cumulative)] + [InlineData(MetricReaderTemporalityPreference.Delta)] + public void TestMetricPointCap(MetricReaderTemporalityPreference temporality) + { + var exportedItems = new List(); + + int MetricPointCount() + { + var count = 0; + + foreach (var metric in exportedItems) + { + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + { + count++; + } + } + + return count; + } + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{temporality}"); + var counterLong = meter.CreateCounter("mycounterCapTest"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = temporality; + })); + + // Make one Add with no tags. + // as currently we reserve 0th index + // for no tag point! + // This may be changed later. + counterLong.Add(10); + for (int i = 0; i < MeterProviderBuilderSdk.DefaultCardinalityLimit + 1; i++) + { + counterLong.Add(10, new KeyValuePair("key", "value" + i)); + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(MeterProviderBuilderSdk.DefaultCardinalityLimit, MetricPointCount()); + + exportedItems.Clear(); + counterLong.Add(10); + for (int i = 0; i < MeterProviderBuilderSdk.DefaultCardinalityLimit + 1; i++) + { + counterLong.Add(10, new KeyValuePair("key", "value" + i)); + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(MeterProviderBuilderSdk.DefaultCardinalityLimit, MetricPointCount()); + + counterLong.Add(10); + for (int i = 0; i < MeterProviderBuilderSdk.DefaultCardinalityLimit + 1; i++) + { + counterLong.Add(10, new KeyValuePair("key", "value" + i)); + } + + // These updates would be dropped. + counterLong.Add(10, new KeyValuePair("key", "valueA")); + counterLong.Add(10, new KeyValuePair("key", "valueB")); + counterLong.Add(10, new KeyValuePair("key", "valueC")); + exportedItems.Clear(); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(MeterProviderBuilderSdk.DefaultCardinalityLimit, MetricPointCount()); + } + + [Fact] + public void MultithreadedLongCounterTest() + { + this.MultithreadedCounterTest(DeltaLongValueUpdatedByEachCall); + } + + [Fact] + public void MultithreadedDoubleCounterTest() + { + this.MultithreadedCounterTest(DeltaDoubleValueUpdatedByEachCall); + } + + [Fact] + public void MultithreadedLongHistogramTest() + { + var expected = new long[16]; + for (var i = 0; i < expected.Length; i++) + { + expected[i] = NumberOfThreads * NumberOfMetricUpdateByEachThread; + } + + // Metric.DefaultHistogramBounds: 0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000 + var values = new long[] { -1, 1, 6, 20, 40, 60, 80, 200, 300, 600, 800, 1001, 3000, 6000, 8000, 10001 }; + + this.MultithreadedHistogramTest(expected, values); + } + + [Fact] + public void MultithreadedDoubleHistogramTest() + { + var expected = new long[16]; + for (var i = 0; i < expected.Length; i++) + { + expected[i] = NumberOfThreads * NumberOfMetricUpdateByEachThread; + } + + // Metric.DefaultHistogramBounds: 0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000 + var values = new double[] { -1.0, 1.0, 6.0, 20.0, 40.0, 60.0, 80.0, 200.0, 300.0, 600.0, 800.0, 1001.0, 3000.0, 6000.0, 8000.0, 10001.0 }; + + this.MultithreadedHistogramTest(expected, values); + } + + [Theory] + [MemberData(nameof(MetricTestData.InvalidInstrumentNames), MemberType = typeof(MetricTestData))] + public void InstrumentWithInvalidNameIsIgnoredTest(string instrumentName) + { + var exportedItems = new List(); + + using var meter = new Meter("InstrumentWithInvalidNameIsIgnoredTest"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); + + var counterLong = meter.CreateCounter(instrumentName); + counterLong.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + // instrument should have been ignored + // as its name does not comply with the specification + Assert.Empty(exportedItems); + } + + [Theory] + [MemberData(nameof(MetricTestData.ValidInstrumentNames), MemberType = typeof(MetricTestData))] + public void InstrumentWithValidNameIsExportedTest(string name) + { + var exportedItems = new List(); + + using var meter = new Meter("InstrumentValidNameIsExportedTest"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); + + var counterLong = meter.CreateCounter(name); + counterLong.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + // Expecting one metric stream. + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal(name, metric.Name); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + 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}"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => + { + builder + .AddMeter(meter.Name); + + if (hasViews) + { + builder.AddView("counter", "renamedCounter"); + } + }); + + var counter = meter.CreateCounter("counter"); + + counter.Add(10, new KeyValuePair("key", "value")); + } + + [Fact] + public void UnsupportedMetricInstrument() + { + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + var exportedItems = new List(); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); + + using (var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log)) + { + var counter = meter.CreateCounter("counter"); + counter.Add(1); + + // This validates that we log InstrumentIgnored event + // and not something else. + 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); + } + + internal static IConfiguration BuildConfiguration(bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints) + { + 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) + where T : struct, IComparable + { + if (obj is not UpdateThreadArguments arguments) + { + throw new Exception("Invalid args"); + } + + var mre = arguments.MreToBlockUpdateThread; + var mreToEnsureAllThreadsStart = arguments.MreToEnsureAllThreadsStart; + var counter = arguments.Instrument as Counter; + var valueToUpdate = arguments.ValuesToRecord[0]; + if (Interlocked.Increment(ref arguments.ThreadsStartedCount) == NumberOfThreads) + { + mreToEnsureAllThreadsStart.Set(); + } + + // Wait until signalled to start calling update on aggregator + mre.WaitOne(); + + for (int i = 0; i < NumberOfMetricUpdateByEachThread; i++) + { + counter.Add(valueToUpdate, new KeyValuePair("verb", "GET")); + } + } + + private static void HistogramUpdateThread(object obj) + where T : struct, IComparable + { + if (obj is not UpdateThreadArguments arguments) + { + throw new Exception("Invalid args"); + } + + var mre = arguments.MreToBlockUpdateThread; + var mreToEnsureAllThreadsStart = arguments.MreToEnsureAllThreadsStart; + var histogram = arguments.Instrument as Histogram; + + if (Interlocked.Increment(ref arguments.ThreadsStartedCount) == NumberOfThreads) + { + mreToEnsureAllThreadsStart.Set(); + } + + // Wait until signalled to start calling update on aggregator + mre.WaitOne(); + + for (int i = 0; i < NumberOfMetricUpdateByEachThread; i++) + { + for (int j = 0; j < arguments.ValuesToRecord.Length; j++) + { + histogram.Record(arguments.ValuesToRecord[j]); + } + } + } + + private void MultithreadedCounterTest(T deltaValueUpdatedByEachCall) + where T : struct, IComparable + { + var metricItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{typeof(T).Name}.{deltaValueUpdatedByEachCall}"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(metricItems)); + + var argToThread = new UpdateThreadArguments + { + ValuesToRecord = new T[] { deltaValueUpdatedByEachCall }, + Instrument = meter.CreateCounter("counter"), + MreToBlockUpdateThread = new ManualResetEvent(false), + MreToEnsureAllThreadsStart = new ManualResetEvent(false), + }; + + Thread[] t = new Thread[NumberOfThreads]; + for (int i = 0; i < NumberOfThreads; i++) + { + t[i] = new Thread(CounterUpdateThread); + t[i].Start(argToThread); + } + + argToThread.MreToEnsureAllThreadsStart.WaitOne(); + Stopwatch sw = Stopwatch.StartNew(); + argToThread.MreToBlockUpdateThread.Set(); + + for (int i = 0; i < NumberOfThreads; i++) + { + t[i].Join(); + } + + this.output.WriteLine($"Took {sw.ElapsedMilliseconds} msecs. Total threads: {NumberOfThreads}, each thread doing {NumberOfMetricUpdateByEachThread} recordings."); + + meterProvider.ForceFlush(); + + if (typeof(T) == typeof(long)) + { + var sumReceived = GetLongSum(metricItems); + var expectedSum = DeltaLongValueUpdatedByEachCall * NumberOfMetricUpdateByEachThread * NumberOfThreads; + Assert.Equal(expectedSum, sumReceived); + } + else if (typeof(T) == typeof(double)) + { + var sumReceived = GetDoubleSum(metricItems); + var expectedSum = DeltaDoubleValueUpdatedByEachCall * NumberOfMetricUpdateByEachThread * NumberOfThreads; + Assert.Equal(expectedSum, sumReceived, 2); + } + } + + private void MultithreadedHistogramTest(long[] expected, T[] values) + where T : struct, IComparable + { + var bucketCounts = new long[11]; + + var metrics = new List(); + var metricReader = new BaseExportingMetricReader(new InMemoryExporter(metrics)); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{typeof(T).Name}"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddReader(metricReader)); + + var argsToThread = new UpdateThreadArguments + { + Instrument = meter.CreateHistogram("histogram"), + MreToBlockUpdateThread = new ManualResetEvent(false), + MreToEnsureAllThreadsStart = new ManualResetEvent(false), + ValuesToRecord = values, + }; + + Thread[] t = new Thread[NumberOfThreads]; + for (int i = 0; i < NumberOfThreads; i++) + { + t[i] = new Thread(HistogramUpdateThread); + t[i].Start(argsToThread); + } + + argsToThread.MreToEnsureAllThreadsStart.WaitOne(); + Stopwatch sw = Stopwatch.StartNew(); + argsToThread.MreToBlockUpdateThread.Set(); + + for (int i = 0; i < NumberOfThreads; i++) + { + t[i].Join(); + } + + this.output.WriteLine($"Took {sw.ElapsedMilliseconds} msecs. Total threads: {NumberOfThreads}, each thread doing {NumberOfMetricUpdateByEachThread * values.Length} recordings."); + + metricReader.Collect(); + + foreach (var metric in metrics) + { + foreach (var metricPoint in metric.GetMetricPoints()) + { + bucketCounts = metricPoint.GetHistogramBuckets().BucketCounts.Select(v => v.RunningValue).ToArray(); + } + } + + Assert.Equal(expected, bucketCounts); + } + + private class UpdateThreadArguments + where T : struct, IComparable + { + public ManualResetEvent MreToBlockUpdateThread; + public ManualResetEvent MreToEnsureAllThreadsStart; + public int ThreadsStartedCount; + public Instrument Instrument; + public T[] ValuesToRecord; + } +} + +public class MetricApiTest : MetricApiTestsBase +{ + public MetricApiTest(ITestOutputHelper output) + : base(output, emitOverflowAttribute: false, shouldReclaimUnusedMetricPoints: false) + { + } +} + +public class MetricApiTestWithOverflowAttribute : MetricApiTestsBase +{ + public MetricApiTestWithOverflowAttribute(ITestOutputHelper output) + : 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 5595fc05cf4..2499c825b6a 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs @@ -1,214 +1,846 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable using System.Diagnostics; using System.Diagnostics.Metrics; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Tests; using Xunit; -using Xunit.Abstractions; -namespace OpenTelemetry.Metrics.Tests +namespace OpenTelemetry.Metrics.Tests; + +public class MetricExemplarTests : MetricTestsBase { - public class MetricExemplarTests : MetricTestsBase - { - private const int MaxTimeToAllowForFlush = 10000; - private readonly ITestOutputHelper output; + private const int MaxTimeToAllowForFlush = 10000; + private static readonly Func IsExemplarApiExposed = () => typeof(ExemplarFilterType).IsVisible; - public MetricExemplarTests(ITestOutputHelper output) + [SkipUnlessTrueTheory(typeof(MetricExemplarTests), nameof(IsExemplarApiExposed), "ExemplarFilter config tests skipped for stable builds")] + [InlineData(null, null, null)] + [InlineData(null, "always_off", (int)ExemplarFilterType.AlwaysOff)] + [InlineData(null, "ALWays_ON", (int)ExemplarFilterType.AlwaysOn)] + [InlineData(null, "trace_based", (int)ExemplarFilterType.TraceBased)] + [InlineData(null, "invalid", null)] + [InlineData((int)ExemplarFilterType.AlwaysOn, "trace_based", (int)ExemplarFilterType.AlwaysOn)] + public void TestExemplarFilterSetFromConfiguration( + int? programmaticValue, + string? configValue, + int? expectedValue) + { + var configBuilder = new ConfigurationBuilder(); + if (!string.IsNullOrEmpty(configValue)) { - this.output = output; + configBuilder.AddInMemoryCollection(new Dictionary + { + [MeterProviderSdk.ExemplarFilterConfigKey] = configValue, + }); } - [Fact] - public void TestExemplarsCounter() + using var container = this.BuildMeterProvider(out var meterProvider, b => { - DateTime testStartTime = DateTime.UtcNow; - var exportedItems = new List(); + b.ConfigureServices( + s => s.AddSingleton(configBuilder.Build())); + + if (programmaticValue.HasValue) + { + b.SetExemplarFilter(((ExemplarFilterType?)programmaticValue).Value); + } + }); + + var meterProviderSdk = meterProvider as MeterProviderSdk; - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var counter = meter.CreateCounter("testCounter"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .SetExemplarFilter(new AlwaysOnExemplarFilter()) - .AddInMemoryExporter(exportedItems, metricReaderOptions => + Assert.NotNull(meterProviderSdk); + Assert.Equal((ExemplarFilterType?)expectedValue, meterProviderSdk.ExemplarFilter); + } + + [Theory] + [InlineData(MetricReaderTemporalityPreference.Cumulative)] + [InlineData(MetricReaderTemporalityPreference.Delta)] + public void TestExemplarsCounter(MetricReaderTemporalityPreference temporality) + { + DateTime testStartTime = DateTime.UtcNow; + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + var counterDouble = meter.CreateCounter("testCounterDouble"); + var counterLong = meter.CreateCounter("testCounterLong"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .SetExemplarFilter(ExemplarFilterType.AlwaysOn) + .AddView(i => + { + if (i.Name.StartsWith("testCounter")) { - metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta; - }) - .Build(); + return new MetricStreamConfiguration + { + ExemplarReservoirFactory = () => new SimpleFixedSizeExemplarReservoir(3), + }; + } - var measurementValues = GenerateRandomValues(10); - foreach (var value in measurementValues) + return null; + }) + .AddInMemoryExporter(exportedItems, metricReaderOptions => { - counter.Add(value); - } + metricReaderOptions.TemporalityPreference = temporality; + })); + + var measurementValues = GenerateRandomValues(2, false, null); + foreach (var value in measurementValues) + { + counterDouble.Add(value.Value); + counterLong.Add((long)value.Value); + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + ValidateFirstPhase("testCounterDouble", testStartTime, exportedItems, measurementValues, e => e.DoubleValue); + ValidateFirstPhase("testCounterLong", testStartTime, exportedItems, measurementValues, e => e.LongValue); + + exportedItems.Clear(); + +#if NETFRAMEWORK + Thread.Sleep(10); // Compensates for low resolution timing in netfx. +#endif + + var secondMeasurementValues = GenerateRandomValues(1, true, measurementValues); + foreach (var value in secondMeasurementValues) + { + using var act = new Activity("test").Start(); + counterDouble.Add(value.Value); + counterLong.Add((long)value.Value); + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + ValidateSecondPhase("testCounterDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues, e => e.DoubleValue); + ValidateSecondPhase("testCounterLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues, e => e.LongValue); + + void ValidateFirstPhase( + string instrumentName, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] measurementValues, + Func getExemplarValueFunc) + { + var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName)); + + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + var exemplars = GetExemplars(metricPoint.Value); + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, getExemplarValueFunc); + } + + void ValidateSecondPhase( + string instrumentName, + MetricReaderTemporalityPreference temporality, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] firstMeasurementValues, + (double Value, bool ExpectTraceId)[] secondMeasurementValues, + Func getExemplarValueFunc) + { + var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName)); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - var metricPoint = GetFirstMetricPoint(exportedItems); Assert.NotNull(metricPoint); Assert.True(metricPoint.Value.StartTime >= testStartTime); Assert.True(metricPoint.Value.EndTime != default); + var exemplars = GetExemplars(metricPoint.Value); - ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, false); - exportedItems.Clear(); + if (temporality == MetricReaderTemporalityPreference.Cumulative) + { + // Current design: + // First collect we saw Exemplar A & B + // Second collect we saw Exemplar C but B remained in the reservoir + Assert.Equal(2, exemplars.Count); + secondMeasurementValues = secondMeasurementValues.Concat(firstMeasurementValues.Skip(1).Take(1)).ToArray(); + } + else + { + Assert.Single(exemplars); + } + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, secondMeasurementValues, getExemplarValueFunc); + } + } + + [Theory] + [InlineData(MetricReaderTemporalityPreference.Cumulative)] + [InlineData(MetricReaderTemporalityPreference.Delta)] + public void TestExemplarsObservable(MetricReaderTemporalityPreference temporality) + { + DateTime testStartTime = DateTime.UtcNow; + var exportedItems = new List(); + + (double Value, bool ExpectTraceId)[] measurementValues = new (double Value, bool ExpectTraceId)[] + { + (18D, false), + (19D, false), + }; + + int measurementIndex = 0; + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + var gaugeDouble = meter.CreateObservableGauge("testGaugeDouble", () => measurementValues[measurementIndex].Value); + var gaugeLong = meter.CreateObservableGauge("testGaugeLong", () => (long)measurementValues[measurementIndex].Value); + var counterDouble = meter.CreateObservableCounter("counterDouble", () => measurementValues[measurementIndex].Value); + var counterLong = meter.CreateObservableCounter("counterLong", () => (long)measurementValues[measurementIndex].Value); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .SetExemplarFilter(ExemplarFilterType.AlwaysOn) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = temporality; + })); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + ValidateFirstPhase("testGaugeDouble", testStartTime, exportedItems, measurementValues, e => e.DoubleValue); + ValidateFirstPhase("testGaugeLong", testStartTime, exportedItems, measurementValues, e => e.LongValue); + ValidateFirstPhase("counterDouble", testStartTime, exportedItems, measurementValues, e => e.DoubleValue); + ValidateFirstPhase("counterLong", testStartTime, exportedItems, measurementValues, e => e.LongValue); + + exportedItems.Clear(); + + measurementIndex++; #if NETFRAMEWORK - Thread.Sleep(10); // Compensates for low resolution timing in netfx. + Thread.Sleep(10); // Compensates for low resolution timing in netfx. #endif - measurementValues = GenerateRandomValues(10); - foreach (var value in measurementValues) - { - var act = new Activity("test").Start(); - counter.Add(value); - act.Stop(); - } + meterProvider.ForceFlush(MaxTimeToAllowForFlush); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - metricPoint = GetFirstMetricPoint(exportedItems); + ValidateSecondPhase("testGaugeDouble", testStartTime, exportedItems, measurementValues, e => e.DoubleValue); + ValidateSecondPhase("testGaugeLong", testStartTime, exportedItems, measurementValues, e => e.LongValue); + + void ValidateFirstPhase( + string instrumentName, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] measurementValues, + Func getExemplarValueFunc) + { + var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName)); Assert.NotNull(metricPoint); Assert.True(metricPoint.Value.StartTime >= testStartTime); Assert.True(metricPoint.Value.EndTime != default); - exemplars = GetExemplars(metricPoint.Value); - ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, true); + + var exemplars = GetExemplars(metricPoint.Value); + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues.Take(1), getExemplarValueFunc); } - [Fact] - public void TestExemplarsHistogram() + static void ValidateSecondPhase( + string instrumentName, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] measurementValues, + Func getExemplarValueFunc) { - DateTime testStartTime = DateTime.UtcNow; - var exportedItems = new List(); + var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName)); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var histogram = meter.CreateHistogram("testHistogram"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .SetExemplarFilter(new AlwaysOnExemplarFilter()) - .AddInMemoryExporter(exportedItems, metricReaderOptions => - { - metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta; - }) - .Build(); + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + var exemplars = GetExemplars(metricPoint.Value); + + // Note: Gauges are only observed when collection happens. For + // Cumulative & Delta the behavior will be the same. We will record the + // single measurement each time as the only exemplar. + + Assert.Single(exemplars); + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues.Skip(1), getExemplarValueFunc); + } + } - var measurementValues = GenerateRandomValues(10); - foreach (var value in measurementValues) + [Theory] + [InlineData(MetricReaderTemporalityPreference.Cumulative)] + [InlineData(MetricReaderTemporalityPreference.Delta)] + public void TestExemplarsHistogramWithBuckets(MetricReaderTemporalityPreference temporality) + { + DateTime testStartTime = DateTime.UtcNow; + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + var histogramWithBucketsAndMinMaxDouble = meter.CreateHistogram("histogramWithBucketsAndMinMaxDouble"); + var histogramWithBucketsDouble = meter.CreateHistogram("histogramWithBucketsDouble"); + var histogramWithBucketsAndMinMaxLong = meter.CreateHistogram("histogramWithBucketsAndMinMaxLong"); + var histogramWithBucketsLong = meter.CreateHistogram("histogramWithBucketsLong"); + + var buckets = new double[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .SetExemplarFilter(ExemplarFilterType.AlwaysOn) + .AddView(i => { - histogram.Record(value); - } + if (i.Name.StartsWith("histogramWithBucketsAndMinMax")) + { + return new ExplicitBucketHistogramConfiguration + { + Boundaries = buckets, + }; + } + else + { + return new ExplicitBucketHistogramConfiguration + { + Boundaries = buckets, + RecordMinMax = false, + }; + } + }) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = temporality; + })); + + var measurementValues = buckets + /* 2000 is here to test overflow measurement */ + .Concat(new double[] { 2000 }) + .Select(b => (Value: b, ExpectTraceId: false)) + .ToArray(); + foreach (var value in measurementValues) + { + histogramWithBucketsAndMinMaxDouble.Record(value.Value); + histogramWithBucketsDouble.Record(value.Value); + histogramWithBucketsAndMinMaxLong.Record((long)value.Value); + histogramWithBucketsLong.Record((long)value.Value); + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + ValidateFirstPhase("histogramWithBucketsAndMinMaxDouble", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("histogramWithBucketsDouble", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("histogramWithBucketsAndMinMaxLong", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("histogramWithBucketsLong", testStartTime, exportedItems, measurementValues); + + exportedItems.Clear(); + +#if NETFRAMEWORK + Thread.Sleep(10); // Compensates for low resolution timing in netfx. +#endif + + var secondMeasurementValues = buckets.Take(1).Select(b => (Value: b, ExpectTraceId: true)).ToArray(); + foreach (var value in secondMeasurementValues) + { + using var act = new Activity("test").Start(); + histogramWithBucketsAndMinMaxDouble.Record(value.Value); + histogramWithBucketsDouble.Record(value.Value); + histogramWithBucketsAndMinMaxLong.Record((long)value.Value); + histogramWithBucketsLong.Record((long)value.Value); + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + ValidateScondPhase("histogramWithBucketsAndMinMaxDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateScondPhase("histogramWithBucketsDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateScondPhase("histogramWithBucketsAndMinMaxLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateScondPhase("histogramWithBucketsLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + + static void ValidateFirstPhase( + string instrumentName, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] measurementValues) + { + var metricPoint = GetFirstMetricPoint(exportedItems.Where(n => n.Name == instrumentName)); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - var metricPoint = GetFirstMetricPoint(exportedItems); Assert.NotNull(metricPoint); Assert.True(metricPoint.Value.StartTime >= testStartTime); Assert.True(metricPoint.Value.EndTime != default); + var exemplars = GetExemplars(metricPoint.Value); - ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, false); - exportedItems.Clear(); + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, e => e.DoubleValue); + } -#if NETFRAMEWORK - Thread.Sleep(10); // Compensates for low resolution timing in netfx. -#endif + static void ValidateScondPhase( + string instrumentName, + MetricReaderTemporalityPreference temporality, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] firstMeasurementValues, + (double Value, bool ExpectTraceId)[] secondMeasurementValues) + { + var metricPoint = GetFirstMetricPoint(exportedItems.Where(n => n.Name == instrumentName)); + + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); - measurementValues = GenerateRandomValues(10); - foreach (var value in measurementValues) + var exemplars = GetExemplars(metricPoint.Value); + + if (temporality == MetricReaderTemporalityPreference.Cumulative) { - using var act = new Activity("test").Start(); - histogram.Record(value); + Assert.Equal(11, exemplars.Count); + secondMeasurementValues = secondMeasurementValues.Concat(firstMeasurementValues.Skip(1)).ToArray(); } + else + { + Assert.Single(exemplars); + } + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, secondMeasurementValues, e => e.DoubleValue); + } + } + + [Theory] + [InlineData(MetricReaderTemporalityPreference.Cumulative)] + [InlineData(MetricReaderTemporalityPreference.Delta)] + public void TestExemplarsHistogramWithoutBuckets(MetricReaderTemporalityPreference temporality) + { + DateTime testStartTime = DateTime.UtcNow; + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + var histogramWithoutBucketsAndMinMaxDouble = meter.CreateHistogram("histogramWithoutBucketsAndMinMaxDouble"); + var histogramWithoutBucketsDouble = meter.CreateHistogram("histogramWithoutBucketsDouble"); + var histogramWithoutBucketsAndMinMaxLong = meter.CreateHistogram("histogramWithoutBucketsAndMinMaxLong"); + var histogramWithoutBucketsLong = meter.CreateHistogram("histogramWithoutBucketsLong"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .SetExemplarFilter(ExemplarFilterType.AlwaysOn) + .AddView(i => + { + if (i.Name.StartsWith("histogramWithoutBucketsAndMinMax")) + { + return new ExplicitBucketHistogramConfiguration + { + Boundaries = Array.Empty(), + ExemplarReservoirFactory = () => new SimpleFixedSizeExemplarReservoir(3), + }; + } + else + { + return new ExplicitBucketHistogramConfiguration + { + Boundaries = Array.Empty(), + RecordMinMax = false, + ExemplarReservoirFactory = () => new SimpleFixedSizeExemplarReservoir(3), + }; + } + }) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = temporality; + })); + + var measurementValues = GenerateRandomValues(2, false, null); + foreach (var value in measurementValues) + { + histogramWithoutBucketsAndMinMaxDouble.Record(value.Value); + histogramWithoutBucketsDouble.Record(value.Value); + histogramWithoutBucketsAndMinMaxLong.Record((long)value.Value); + histogramWithoutBucketsLong.Record((long)value.Value); + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + ValidateFirstPhase("histogramWithoutBucketsAndMinMaxDouble", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("histogramWithoutBucketsDouble", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("histogramWithoutBucketsAndMinMaxLong", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("histogramWithoutBucketsLong", testStartTime, exportedItems, measurementValues); + + exportedItems.Clear(); + +#if NETFRAMEWORK + Thread.Sleep(10); // Compensates for low resolution timing in netfx. +#endif + + var secondMeasurementValues = GenerateRandomValues(1, true, measurementValues); + foreach (var value in secondMeasurementValues) + { + using var act = new Activity("test").Start(); + histogramWithoutBucketsAndMinMaxDouble.Record(value.Value); + histogramWithoutBucketsDouble.Record(value.Value); + histogramWithoutBucketsAndMinMaxLong.Record((long)value.Value); + histogramWithoutBucketsLong.Record((long)value.Value); + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + ValidateSecondPhase("histogramWithoutBucketsAndMinMaxDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateSecondPhase("histogramWithoutBucketsDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateSecondPhase("histogramWithoutBucketsAndMinMaxLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateSecondPhase("histogramWithoutBucketsLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + + static void ValidateFirstPhase( + string instrumentName, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] measurementValues) + { + var metricPoint = GetFirstMetricPoint(exportedItems.Where(n => n.Name == instrumentName)); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - metricPoint = GetFirstMetricPoint(exportedItems); Assert.NotNull(metricPoint); Assert.True(metricPoint.Value.StartTime >= testStartTime); Assert.True(metricPoint.Value.EndTime != default); - exemplars = GetExemplars(metricPoint.Value); - ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, true); + + var exemplars = GetExemplars(metricPoint.Value); + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, e => e.DoubleValue); } - [Fact] - public void TestExemplarsFilterTags() + static void ValidateSecondPhase( + string instrumentName, + MetricReaderTemporalityPreference temporality, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] firstMeasurementValues, + (double Value, bool ExpectTraceId)[] secondMeasurementValues) { - DateTime testStartTime = DateTime.UtcNow; - var exportedItems = new List(); + var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName)); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var histogram = meter.CreateHistogram("testHistogram"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .SetExemplarFilter(new AlwaysOnExemplarFilter()) - .AddView(histogram.Name, new MetricStreamConfiguration() { TagKeys = new string[] { "key1" } }) - .AddInMemoryExporter(exportedItems, metricReaderOptions => - { - metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta; - }) - .Build(); + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + var exemplars = GetExemplars(metricPoint.Value); - var measurementValues = GenerateRandomValues(10); - foreach (var value in measurementValues) + if (temporality == MetricReaderTemporalityPreference.Cumulative) { - histogram.Record(value, new("key1", "value1"), new("key2", "value1"), new("key3", "value1")); + Assert.Equal(2, exemplars.Count); + secondMeasurementValues = secondMeasurementValues.Concat(firstMeasurementValues.Skip(1)).ToArray(); } + else + { + Assert.Single(exemplars); + } + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, secondMeasurementValues, e => e.DoubleValue); + } + } + + [Theory] + [InlineData(MetricReaderTemporalityPreference.Cumulative)] + [InlineData(MetricReaderTemporalityPreference.Delta)] + public void TestExemplarsExponentialHistogram(MetricReaderTemporalityPreference temporality) + { + DateTime testStartTime = DateTime.UtcNow; + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + var exponentialHistogramWithMinMaxDouble = meter.CreateHistogram("exponentialHistogramWithMinMaxDouble"); + var exponentialHistogramDouble = meter.CreateHistogram("exponentialHistogramDouble"); + var exponentialHistogramWithMinMaxLong = meter.CreateHistogram("exponentialHistogramWithMinMaxLong"); + var exponentialHistogramLong = meter.CreateHistogram("exponentialHistogramLong"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .SetExemplarFilter(ExemplarFilterType.AlwaysOn) + .AddView(i => + { + if (i.Name.StartsWith("exponentialHistogramWithMinMax")) + { + return new Base2ExponentialBucketHistogramConfiguration(); + } + else + { + return new Base2ExponentialBucketHistogramConfiguration() + { + RecordMinMax = false, + }; + } + }) + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = temporality; + })); + + var measurementValues = GenerateRandomValues(20, false, null); + foreach (var value in measurementValues) + { + exponentialHistogramWithMinMaxDouble.Record(value.Value); + exponentialHistogramDouble.Record(value.Value); + exponentialHistogramWithMinMaxLong.Record((long)value.Value); + exponentialHistogramLong.Record((long)value.Value); + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + ValidateFirstPhase("exponentialHistogramWithMinMaxDouble", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("exponentialHistogramDouble", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("exponentialHistogramWithMinMaxLong", testStartTime, exportedItems, measurementValues); + ValidateFirstPhase("exponentialHistogramLong", testStartTime, exportedItems, measurementValues); + + exportedItems.Clear(); + +#if NETFRAMEWORK + Thread.Sleep(10); // Compensates for low resolution timing in netfx. +#endif + + var secondMeasurementValues = GenerateRandomValues(1, true, measurementValues); + foreach (var value in secondMeasurementValues) + { + using var act = new Activity("test").Start(); + exponentialHistogramWithMinMaxDouble.Record(value.Value); + exponentialHistogramDouble.Record(value.Value); + exponentialHistogramWithMinMaxLong.Record((long)value.Value); + exponentialHistogramLong.Record((long)value.Value); + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + ValidateSecondPhase("exponentialHistogramWithMinMaxDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateSecondPhase("exponentialHistogramDouble", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateSecondPhase("exponentialHistogramWithMinMaxLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + ValidateSecondPhase("exponentialHistogramLong", temporality, testStartTime, exportedItems, measurementValues, secondMeasurementValues); + + static void ValidateFirstPhase( + string instrumentName, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] measurementValues) + { + var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName)); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - var metricPoint = GetFirstMetricPoint(exportedItems); Assert.NotNull(metricPoint); Assert.True(metricPoint.Value.StartTime >= testStartTime); Assert.True(metricPoint.Value.EndTime != default); + var exemplars = GetExemplars(metricPoint.Value); - Assert.NotNull(exemplars); - foreach (var exemplar in exemplars) + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, e => e.DoubleValue); + } + + static void ValidateSecondPhase( + string instrumentName, + MetricReaderTemporalityPreference temporality, + DateTime testStartTime, + List exportedItems, + (double Value, bool ExpectTraceId)[] firstMeasurementValues, + (double Value, bool ExpectTraceId)[] secondMeasurementValues) + { + var metricPoint = GetFirstMetricPoint(exportedItems.Where(m => m.Name == instrumentName)); + + Assert.NotNull(metricPoint); + Assert.True(metricPoint.Value.StartTime >= testStartTime); + Assert.True(metricPoint.Value.EndTime != default); + + var exemplars = GetExemplars(metricPoint.Value); + + if (temporality == MetricReaderTemporalityPreference.Cumulative) + { + Assert.Equal(20, exemplars.Count); + secondMeasurementValues = secondMeasurementValues.Concat(firstMeasurementValues.Skip(1).Take(19)).ToArray(); + } + else + { + Assert.Single(exemplars); + } + + ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, secondMeasurementValues, e => e.DoubleValue); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TestTraceBasedExemplarFilter(bool enableTracing) + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + + var counter = meter.CreateCounter("testCounter"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .SetExemplarFilter(ExemplarFilterType.TraceBased) + .AddInMemoryExporter(exportedItems)); + + if (enableTracing) + { + using var act = new Activity("test").Start(); + act.ActivityTraceFlags = ActivityTraceFlags.Recorded; + counter.Add(18); + } + else + { + counter.Add(18); + } + + meterProvider.ForceFlush(); + + Assert.Single(exportedItems); + + var metricPoint = GetFirstMetricPoint(exportedItems); + + Assert.NotNull(metricPoint); + + var exemplars = GetExemplars(metricPoint.Value); + + if (enableTracing) + { + Assert.Single(exemplars); + } + else + { + Assert.Empty(exemplars); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TestExemplarsFilterTags(bool enableTagFiltering) + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + + var histogram = meter.CreateHistogram("testHistogram"); + + TestExemplarReservoir? testExemplarReservoir = null; + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .SetExemplarFilter(ExemplarFilterType.AlwaysOn) + .AddView( + histogram.Name, + new MetricStreamConfiguration() + { + TagKeys = enableTagFiltering ? new string[] { "key1" } : null, + ExemplarReservoirFactory = () => + { + if (testExemplarReservoir != null) + { + throw new InvalidOperationException(); + } + + return testExemplarReservoir = new TestExemplarReservoir(); + }, + }) + .AddInMemoryExporter(exportedItems)); + + histogram.Record( + 0, + new("key1", "value1"), + new("key2", "value2"), + new("key3", "value3")); + + meterProvider.ForceFlush(); + + Assert.NotNull(testExemplarReservoir); + Assert.NotNull(testExemplarReservoir.MeasurementTags); + Assert.Equal(3, testExemplarReservoir.MeasurementTags.Length); + Assert.Contains(testExemplarReservoir.MeasurementTags, t => t.Key == "key1" && (string?)t.Value == "value1"); + Assert.Contains(testExemplarReservoir.MeasurementTags, t => t.Key == "key2" && (string?)t.Value == "value2"); + Assert.Contains(testExemplarReservoir.MeasurementTags, t => t.Key == "key3" && (string?)t.Value == "value3"); + + var metricPoint = GetFirstMetricPoint(exportedItems); + + Assert.NotNull(metricPoint); + + var exemplars = GetExemplars(metricPoint.Value); + + Assert.NotNull(exemplars); + + foreach (var exemplar in exemplars) + { + if (!enableTagFiltering) { - Assert.NotNull(exemplar.FilteredTags); - Assert.Contains(new("key2", "value1"), exemplar.FilteredTags); - Assert.Contains(new("key3", "value1"), exemplar.FilteredTags); + Assert.Equal(0, exemplar.FilteredTags.MaximumCount); + } + else + { + Assert.Equal(3, exemplar.FilteredTags.MaximumCount); + + var filteredTags = exemplar.FilteredTags.ToReadOnlyList(); + + Assert.Equal(2, filteredTags.Count); + + Assert.Contains(new("key2", "value2"), filteredTags); + Assert.Contains(new("key3", "value3"), filteredTags); } } + } - private static double[] GenerateRandomValues(int count) + private static (double Value, bool ExpectTraceId)[] GenerateRandomValues( + int count, + bool expectTraceId, + (double Value, bool ExpectTraceId)[]? previousValues) + { + var random = new Random(); + var values = new (double, bool)[count]; + for (int i = 0; i < count; i++) { - var random = new Random(); - var values = new double[count]; - for (int i = 0; i < count; i++) + var nextValue = random.NextDouble() * 100_000; + if (values.Any(m => m.Item1 == nextValue || m.Item1 == (long)nextValue) + || previousValues?.Any(m => m.Value == nextValue || m.Value == (long)nextValue) == true) { - values[i] = random.NextDouble(); + i--; + continue; } - return values; + values[i] = (nextValue, expectTraceId); } - private static void ValidateExemplars(Exemplar[] exemplars, DateTimeOffset startTime, DateTimeOffset endTime, double[] measurementValues, bool traceContextExists) + return values; + } + + private static void ValidateExemplars( + IReadOnlyList exemplars, + DateTimeOffset startTime, + DateTimeOffset endTime, + IEnumerable<(double Value, bool ExpectTraceId)> measurementValues, + Func getExemplarValueFunc) + { + int count = 0; + + foreach (var exemplar in exemplars) { - Assert.NotNull(exemplars); - foreach (var exemplar in exemplars) + Assert.True(exemplar.Timestamp >= startTime && exemplar.Timestamp <= endTime, $"{startTime} < {exemplar.Timestamp} < {endTime}"); + Assert.Equal(0, exemplar.FilteredTags.MaximumCount); + + var measurement = measurementValues.FirstOrDefault(v => v.Value == getExemplarValueFunc(exemplar) + || (long)v.Value == getExemplarValueFunc(exemplar)); + Assert.NotEqual(default, measurement); + if (measurement.ExpectTraceId) { - Assert.True(exemplar.Timestamp >= startTime && exemplar.Timestamp <= endTime, $"{startTime} < {exemplar.Timestamp} < {endTime}"); - Assert.Contains(exemplar.DoubleValue, measurementValues); - Assert.Null(exemplar.FilteredTags); - if (traceContextExists) - { - Assert.NotEqual(default, exemplar.TraceId); - Assert.NotEqual(default, exemplar.SpanId); - } - else - { - Assert.Equal(default, exemplar.TraceId); - Assert.Equal(default, exemplar.SpanId); - } + Assert.NotEqual(default, exemplar.TraceId); + Assert.NotEqual(default, exemplar.SpanId); } + else + { + Assert.Equal(default, exemplar.TraceId); + Assert.Equal(default, exemplar.SpanId); + } + + count++; + } + + Assert.Equal(measurementValues.Count(), count); + } + + private sealed class TestExemplarReservoir : FixedSizeExemplarReservoir + { + public TestExemplarReservoir() + : base(1) + { + } + + public KeyValuePair[]? MeasurementTags { get; private set; } + + public override void Offer(in ExemplarMeasurement measurement) + { + this.MeasurementTags = measurement.Tags.ToArray(); + + this.UpdateExemplar(0, in measurement); + } + + public override void Offer(in ExemplarMeasurement measurement) + { + throw new NotSupportedException(); } } } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricExporterTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricExporterTests.cs index a773cedf616..185679acac9 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricExporterTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricExporterTests.cs @@ -1,102 +1,88 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Xunit; -namespace OpenTelemetry.Metrics.Tests +namespace OpenTelemetry.Metrics.Tests; + +public class MetricExporterTests { - public class MetricExporterTests + [Theory] + [InlineData(ExportModes.Push)] + [InlineData(ExportModes.Pull)] + [InlineData(ExportModes.Pull | ExportModes.Push)] + public void FlushMetricExporterTest(ExportModes mode) { - [Theory] - [InlineData(ExportModes.Push)] - [InlineData(ExportModes.Pull)] - [InlineData(ExportModes.Pull | ExportModes.Push)] - public void FlushMetricExporterTest(ExportModes mode) - { - BaseExporter exporter = null; + BaseExporter exporter = null; - switch (mode) - { - case ExportModes.Push: - exporter = new PushOnlyMetricExporter(); - break; - case ExportModes.Pull: - exporter = new PullOnlyMetricExporter(); - break; - case ExportModes.Pull | ExportModes.Push: - exporter = new PushPullMetricExporter(); - break; - } + switch (mode) + { + case ExportModes.Push: + exporter = new PushOnlyMetricExporter(); + break; + case ExportModes.Pull: + exporter = new PullOnlyMetricExporter(); + break; + case ExportModes.Pull | ExportModes.Push: + exporter = new PushPullMetricExporter(); + break; + } - var reader = new BaseExportingMetricReader(exporter); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddReader(reader) - .Build(); + var reader = new BaseExportingMetricReader(exporter); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddReader(reader) + .Build(); - switch (mode) - { - case ExportModes.Push: - Assert.True(reader.Collect()); - Assert.True(meterProvider.ForceFlush()); - break; - case ExportModes.Pull: - Assert.False(reader.Collect()); - Assert.False(meterProvider.ForceFlush()); - Assert.True((exporter as IPullMetricExporter).Collect(-1)); - break; - case ExportModes.Pull | ExportModes.Push: - Assert.True(reader.Collect()); - Assert.True(meterProvider.ForceFlush()); - break; - } + switch (mode) + { + case ExportModes.Push: + Assert.True(reader.Collect()); + Assert.True(meterProvider.ForceFlush()); + break; + case ExportModes.Pull: + Assert.False(reader.Collect()); + Assert.False(meterProvider.ForceFlush()); + Assert.True((exporter as IPullMetricExporter).Collect(-1)); + break; + case ExportModes.Pull | ExportModes.Push: + Assert.True(reader.Collect()); + Assert.True(meterProvider.ForceFlush()); + break; } + } - [ExportModes(ExportModes.Push)] - private class PushOnlyMetricExporter : BaseExporter + [ExportModes(ExportModes.Push)] + private class PushOnlyMetricExporter : BaseExporter + { + public override ExportResult Export(in Batch batch) { - public override ExportResult Export(in Batch batch) - { - return ExportResult.Success; - } + return ExportResult.Success; } + } - [ExportModes(ExportModes.Pull)] - private class PullOnlyMetricExporter : BaseExporter, IPullMetricExporter - { - private Func funcCollect; + [ExportModes(ExportModes.Pull)] + private class PullOnlyMetricExporter : BaseExporter, IPullMetricExporter + { + private Func funcCollect; - public Func Collect - { - get => this.funcCollect; - set { this.funcCollect = value; } - } + public Func Collect + { + get => this.funcCollect; + set { this.funcCollect = value; } + } - public override ExportResult Export(in Batch batch) - { - return ExportResult.Success; - } + public override ExportResult Export(in Batch batch) + { + return ExportResult.Success; } + } - [ExportModes(ExportModes.Pull | ExportModes.Push)] - private class PushPullMetricExporter : BaseExporter + [ExportModes(ExportModes.Pull | ExportModes.Push)] + private class PushPullMetricExporter : BaseExporter + { + public override ExportResult Export(in Batch batch) { - public override ExportResult Export(in Batch batch) - { - return ExportResult.Success; - } + return ExportResult.Success; } } } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTestsBase.cs new file mode 100644 index 00000000000..baff3b86d76 --- /dev/null +++ b/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTestsBase.cs @@ -0,0 +1,458 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics.Metrics; +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 +{ + 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[MetricTestsBase.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); + Assert.Equal(isEmitOverflowAttributeKeySet, exportedItems[0].AggregatorStore.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); + Assert.Equal(isEmitOverflowAttributeKeySet, exportedItems[0].AggregatorStore.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); + Assert.Equal(isEmitOverflowAttributeKeySet, exportedItems[0].AggregatorStore.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.DefaultCardinalityLimit - 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.DefaultCardinalityLimit - 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/MetricPointReclaimTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTestsBase.cs new file mode 100644 index 00000000000..fa33298643e --- /dev/null +++ b/test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTestsBase.cs @@ -0,0 +1,344 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Tests; +using Xunit; + +namespace OpenTelemetry.Metrics.Tests; + +#pragma warning disable SA1402 + +public abstract class MetricPointReclaimTestsBase +{ + private readonly Dictionary configurationData = new() + { + [MetricTestsBase.ReclaimUnusedMetricPointsConfigKey] = "true", + }; + + private readonly IConfiguration configuration; + + protected MetricPointReclaimTestsBase(bool emitOverflowAttribute) + { + if (emitOverflowAttribute) + { + this.configurationData[MetricTestsBase.EmitOverFlowAttributeConfigKey] = "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 TestReclaimAttributeConfigWithEnvVar(string value, bool isReclaimAttributeKeySet) + { + // Clear the environment variable value first + Environment.SetEnvironmentVariable(MetricTestsBase.ReclaimUnusedMetricPointsConfigKey, null); + + // Set the environment variable to the value provided in the test input + Environment.SetEnvironmentVariable(MetricTestsBase.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.ReclaimUnusedMetricPoints); + } + + [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 { [MetricTestsBase.ReclaimUnusedMetricPointsConfigKey] = value }) + .Build(); + + services.AddSingleton(configuration); + }) + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems) + .Build(); + + var meterProviderSdk = meterProvider as MeterProviderSdk; + Assert.Equal(isReclaimAttributeKeySet, meterProviderSdk.ReclaimUnusedMetricPoints); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void MeasurementsAreNotDropped(bool emitMetricWithNoDimensions) + { + var meter = new Meter(Utils.GetCurrentMethodName()); + var counter = meter.CreateCounter("MyFruitCounter"); + + int numberOfUpdateThreads = 25; + int maxNumberofDistinctMetricPoints = 4000; // Default max MetricPoints * 2 + + using var exporter = new CustomExporter(assertNoDroppedMeasurements: true); + using var metricReader = new PeriodicExportingMetricReader(exporter, exportIntervalMilliseconds: 10) + { + TemporalityPreference = MetricReaderTemporalityPreference.Delta, + }; + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) + .AddMeter(Utils.GetCurrentMethodName()) + .AddReader(metricReader) + .Build(); + + void EmitMetric(object obj) + { + var threadArguments = obj as ThreadArguments; + var random = new Random(); + while (true) + { + int i = Interlocked.Increment(ref threadArguments!.Counter); + if (i <= maxNumberofDistinctMetricPoints) + { + // Check for cases where a metric with no dimension is also emitted + if (emitMetricWithNoDimensions) + { + counter.Add(25); + } + + // There are separate code paths for single dimension vs multiple dimensions + if (random.Next(2) == 0) + { + counter.Add(100, new KeyValuePair("key", $"value{i}")); + } + else + { + counter.Add(100, new KeyValuePair("key", $"value{i}"), new KeyValuePair("dimensionKey", "dimensionValue")); + } + + Thread.Sleep(25); + } + else + { + break; + } + } + } + + var threads = new Thread[numberOfUpdateThreads]; + var threadArgs = new ThreadArguments(); + + for (int i = 0; i < threads.Length; i++) + { + threads[i] = new Thread(EmitMetric!); + threads[i].Start(threadArgs); + } + + for (int i = 0; i < threads.Length; i++) + { + threads[i].Join(); + } + + meterProvider.ForceFlush(); + + long expectedSum; + + if (emitMetricWithNoDimensions) + { + expectedSum = maxNumberofDistinctMetricPoints * (25 + 100); + } + else + { + expectedSum = maxNumberofDistinctMetricPoints * 100; + } + + Assert.Equal(expectedSum, exporter.Sum); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void MeasurementsAreAggregatedEvenAfterTheyAreDropped(bool emitMetricWithNoDimension) + { + var meter = new Meter(Utils.GetCurrentMethodName()); + var counter = meter.CreateCounter("MyFruitCounter"); + + long sum = 0; + var measurementValues = new long[] { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }; + + int numberOfUpdateThreads = 4; + int numberOfMeasurementsPerThread = 10; + + using var exporter = new CustomExporter(assertNoDroppedMeasurements: false); + using var metricReader = new PeriodicExportingMetricReader(exporter, exportIntervalMilliseconds: 10) + { + TemporalityPreference = MetricReaderTemporalityPreference.Delta, + }; + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.AddSingleton(this.configuration); + }) + .AddMeter(Utils.GetCurrentMethodName()) + .SetMaxMetricPointsPerMetricStream(10) // Set max MetricPoints limit to 10 + .AddReader(metricReader) + .Build(); + + // Add 10 distinct combinations of dimensions to surpass the max metric points limit of 10. + // Note that one MetricPoint is reserved for zero tags and one MetricPoint is optionally + // reserved for the overflow tag depending on the user's input. + // This would lead to dropping a few measurements. We want to make sure that they can still be + // aggregated later on when there are free MetricPoints available. + for (int i = 0; i < 10; i++) + { + counter.Add(100, new KeyValuePair("key", $"value{i}")); + } + + meterProvider.ForceFlush(); + meterProvider.ForceFlush(); + + exporter.Sum = 0; + + void EmitMetric() + { + int numberOfMeasurements = 0; + var random = new Random(); + while (true) + { + if (numberOfMeasurements < numberOfMeasurementsPerThread) + { + // Check for cases where a metric with no dimension is also emitted + if (emitMetricWithNoDimension) + { + counter.Add(25); + Interlocked.Add(ref sum, 25); + } + + var index = random.Next(measurementValues.Length); + var measurement = measurementValues[index]; + counter.Add(measurement, new KeyValuePair("key", $"value{index}")); + Interlocked.Add(ref sum, measurement); + + numberOfMeasurements++; + + Thread.Sleep(25); + } + else + { + break; + } + } + } + + var threads = new Thread[numberOfUpdateThreads]; + + for (int i = 0; i < threads.Length; i++) + { + threads[i] = new Thread(EmitMetric!); + threads[i].Start(); + } + + for (int i = 0; i < threads.Length; i++) + { + threads[i].Join(); + } + + meterProvider.ForceFlush(); + Assert.Equal(sum, exporter.Sum); + } + + private sealed class ThreadArguments + { + public int Counter; + } + + private sealed class CustomExporter : BaseExporter + { + public long Sum = 0; + + private readonly bool assertNoDroppedMeasurements; + + public CustomExporter(bool assertNoDroppedMeasurements) + { + this.assertNoDroppedMeasurements = assertNoDroppedMeasurements; + } + + public override ExportResult Export(in Batch batch) + { + foreach (var metric in batch) + { + var aggStore = metric.AggregatorStore; + var metricPointLookupDictionary = aggStore.TagsToMetricPointIndexDictionaryDelta; + var droppedMeasurements = aggStore.DroppedMeasurements; + + if (this.assertNoDroppedMeasurements) + { + Assert.Equal(0, droppedMeasurements); + } + + // This is to ensure that the lookup dictionary does not have unbounded growth + Assert.True(metricPointLookupDictionary.Count <= (MeterProviderBuilderSdk.DefaultCardinalityLimit * 2)); + + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + { + // Access the tags to ensure that this does not throw any exception due to + // any erroneous thread interactions. + foreach (var tag in metricPoint.Tags) + { + _ = tag.Key; + _ = tag.Value; + } + + if (metric.MetricType.IsSum()) + { + this.Sum += metricPoint.GetSumLong(); + } + } + } + + return ExportResult.Success; + } + } +} + +public class MetricPointReclaimTests : MetricPointReclaimTestsBase +{ + public MetricPointReclaimTests() + : base(emitOverflowAttribute: false) + { + } +} + +public class MetricPointReclaimTestsWithEmitOverflowAttribute : MetricPointReclaimTestsBase +{ + public MetricPointReclaimTestsWithEmitOverflowAttribute() + : base(emitOverflowAttribute: true) + { + } +} diff --git a/test/OpenTelemetry.Tests/Metrics/MetricPointTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricPointTests.cs index 1510fa44cc5..9e50c5354b5 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricPointTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricPointTests.cs @@ -1,98 +1,84 @@ -// // 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. -// ; +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Metrics; using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Metrics.Tests +namespace OpenTelemetry.Metrics.Tests; + +public sealed class MetricPointTests : IDisposable { - public sealed class MetricPointTests : IDisposable + private readonly Meter meter; + private readonly MeterProvider meterProvider; + private readonly Metric metric; + private readonly Histogram histogram; + private readonly double[] bounds; + private MetricPoint metricPoint; + + public MetricPointTests() { - private readonly Meter meter; - private readonly MeterProvider provider; - private readonly Metric metric; - private readonly Histogram histogram; - private readonly double[] bounds; - private MetricPoint metricPoint; - - public MetricPointTests() - { - this.meter = new Meter(Utils.GetCurrentMethodName()); - this.histogram = this.meter.CreateHistogram("histogram"); + this.meter = new Meter(Utils.GetCurrentMethodName()); + this.histogram = this.meter.CreateHistogram("histogram"); - // Evenly distribute the bound values over the range [0, MaxValue) - this.bounds = new double[10]; - for (int i = 0; i < this.bounds.Length; i++) - { - this.bounds[i] = i * 1000 / this.bounds.Length; - } + // Evenly distribute the bound values over the range [0, MaxValue) + this.bounds = new double[10]; + for (int i = 0; i < this.bounds.Length; i++) + { + this.bounds[i] = i * 1000 / this.bounds.Length; + } - var exportedItems = new List(); + var exportedItems = new List(); - this.provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(this.meter.Name) - .AddInMemoryExporter(exportedItems) - .AddView(this.histogram.Name, new ExplicitBucketHistogramConfiguration() { Boundaries = this.bounds }) - .Build(); + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(this.meter.Name) + .AddInMemoryExporter(exportedItems) + .AddView(this.histogram.Name, new ExplicitBucketHistogramConfiguration() { Boundaries = this.bounds }) + .Build(); - this.histogram.Record(500); + this.histogram.Record(500); - this.provider.ForceFlush(); + this.meterProvider.ForceFlush(); - this.metric = exportedItems[0]; - var metricPointsEnumerator = this.metric.GetMetricPoints().GetEnumerator(); - metricPointsEnumerator.MoveNext(); - this.metricPoint = metricPointsEnumerator.Current; - } + this.metric = exportedItems[0]; + var metricPointsEnumerator = this.metric.GetMetricPoints().GetEnumerator(); + metricPointsEnumerator.MoveNext(); + this.metricPoint = metricPointsEnumerator.Current; + } - public void Dispose() - { - this.meter?.Dispose(); - this.provider?.Dispose(); - } + public void Dispose() + { + this.meter?.Dispose(); + this.meterProvider.Dispose(); + } - [Fact] - public void VerifyMetricPointCopy() - { - var copy = this.metricPoint.Copy(); + [Fact] + public void VerifyMetricPointCopy() + { + var copy = this.metricPoint.Copy(); - // Verify these structs are unique instances. - Assert.NotEqual(copy, this.metricPoint); + // Verify these structs are unique instances. + Assert.NotEqual(copy, this.metricPoint); - // Verify properties are copied. - Assert.Equal(copy.Tags, this.metricPoint.Tags); - Assert.Equal(copy.StartTime, this.metricPoint.StartTime); - Assert.Equal(copy.EndTime, this.metricPoint.EndTime); - } + // Verify properties are copied. + Assert.Equal(copy.Tags, this.metricPoint.Tags); + Assert.Equal(copy.StartTime, this.metricPoint.StartTime); + Assert.Equal(copy.EndTime, this.metricPoint.EndTime); + } - [Fact] - public void VerifyHistogramBucketsCopy() - { - var histogramBuckets = this.metricPoint.GetHistogramBuckets(); - var copy = histogramBuckets.Copy(); + [Fact] + public void VerifyHistogramBucketsCopy() + { + var histogramBuckets = this.metricPoint.GetHistogramBuckets(); + var copy = histogramBuckets.Copy(); - // Verify these are unique instances. - Assert.False(ReferenceEquals(copy, histogramBuckets)); - Assert.NotSame(copy, histogramBuckets); + // Verify these are unique instances. + Assert.False(ReferenceEquals(copy, histogramBuckets)); + Assert.NotSame(copy, histogramBuckets); - // Verify fields are copied - Assert.NotSame(copy.SnapshotBucketCounts, histogramBuckets.SnapshotBucketCounts); - Assert.Equal(copy.SnapshotBucketCounts, histogramBuckets.SnapshotBucketCounts); - Assert.Equal(copy.SnapshotSum, histogramBuckets.SnapshotSum); - } + // Verify fields are copied + Assert.NotSame(copy.BucketCounts, histogramBuckets.BucketCounts); + Assert.Equal(copy.BucketCounts, histogramBuckets.BucketCounts); + Assert.Equal(copy.SnapshotSum, histogramBuckets.SnapshotSum); } } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricSnapshotTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricSnapshotTests.cs deleted file mode 100644 index f26ea07597d..00000000000 --- a/test/OpenTelemetry.Tests/Metrics/MetricSnapshotTests.cs +++ /dev/null @@ -1,284 +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 OpenTelemetry.Tests; - -using Xunit; - -namespace OpenTelemetry.Metrics.Tests -{ - public class MetricSnapshotTests - { - [Fact] - public void VerifySnapshot_Counter() - { - var exportedMetrics = new List(); - var exportedSnapshots = new List(); - - using var meter = new Meter(Utils.GetCurrentMethodName()); - var counter = meter.CreateCounter("meter"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedMetrics) - .AddInMemoryExporter(exportedSnapshots) - .Build(); - - // FIRST EXPORT - counter.Add(10); - meterProvider.ForceFlush(); - - // Verify Metric 1 - Assert.Single(exportedMetrics); - var metric1 = exportedMetrics[0]; - var metricPoints1Enumerator = metric1.GetMetricPoints().GetEnumerator(); - Assert.True(metricPoints1Enumerator.MoveNext()); - ref readonly var metricPoint1 = ref metricPoints1Enumerator.Current; - Assert.Equal(10, metricPoint1.GetSumLong()); - - // Verify Snapshot 1 - Assert.Single(exportedSnapshots); - var snapshot1 = exportedSnapshots[0]; - Assert.Single(snapshot1.MetricPoints); - Assert.Equal(10, snapshot1.MetricPoints[0].GetSumLong()); - - // Verify Metric == Snapshot - Assert.Equal(metric1.Name, snapshot1.Name); - Assert.Equal(metric1.Description, snapshot1.Description); - Assert.Equal(metric1.Unit, snapshot1.Unit); - Assert.Equal(metric1.MeterName, snapshot1.MeterName); - Assert.Equal(metric1.MetricType, snapshot1.MetricType); - Assert.Equal(metric1.MeterVersion, snapshot1.MeterVersion); - - // SECOND EXPORT - counter.Add(5); - meterProvider.ForceFlush(); - - // Verify Metric 1, after second export - // This value is expected to be updated. - Assert.Equal(15, metricPoint1.GetSumLong()); - - // Verify Metric 2 - Assert.Equal(2, exportedMetrics.Count); - var metric2 = exportedMetrics[1]; - var metricPoints2Enumerator = metric2.GetMetricPoints().GetEnumerator(); - Assert.True(metricPoints2Enumerator.MoveNext()); - ref readonly var metricPoint2 = ref metricPoints2Enumerator.Current; - Assert.Equal(15, metricPoint2.GetSumLong()); - - // Verify Snapshot 1, after second export - // This value is expected to be unchanged. - Assert.Equal(10, snapshot1.MetricPoints[0].GetSumLong()); - - // Verify Snapshot 2 - Assert.Equal(2, exportedSnapshots.Count); - var snapshot2 = exportedSnapshots[1]; - Assert.Single(snapshot2.MetricPoints); - Assert.Equal(15, snapshot2.MetricPoints[0].GetSumLong()); - } - - [Fact] - public void VerifySnapshot_Histogram() - { - var exportedMetrics = new List(); - var exportedSnapshots = new List(); - - using var meter = new Meter(Utils.GetCurrentMethodName()); - var histogram = meter.CreateHistogram("histogram"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedMetrics) - .AddInMemoryExporter(exportedSnapshots) - .Build(); - - // FIRST EXPORT - histogram.Record(10); - meterProvider.ForceFlush(); - - // Verify Metric 1 - Assert.Single(exportedMetrics); - var metric1 = exportedMetrics[0]; - var metricPoints1Enumerator = metric1.GetMetricPoints().GetEnumerator(); - Assert.True(metricPoints1Enumerator.MoveNext()); - ref readonly var metricPoint1 = ref metricPoints1Enumerator.Current; - Assert.Equal(1, metricPoint1.GetHistogramCount()); - Assert.Equal(10, metricPoint1.GetHistogramSum()); - metricPoint1.TryGetHistogramMinMaxValues(out var min, out var max); - Assert.Equal(10, min); - Assert.Equal(10, max); - - // Verify Snapshot 1 - Assert.Single(exportedSnapshots); - var snapshot1 = exportedSnapshots[0]; - Assert.Single(snapshot1.MetricPoints); - Assert.Equal(1, snapshot1.MetricPoints[0].GetHistogramCount()); - Assert.Equal(10, snapshot1.MetricPoints[0].GetHistogramSum()); - snapshot1.MetricPoints[0].TryGetHistogramMinMaxValues(out min, out max); - Assert.Equal(10, min); - Assert.Equal(10, max); - - // Verify Metric == Snapshot - Assert.Equal(metric1.Name, snapshot1.Name); - Assert.Equal(metric1.Description, snapshot1.Description); - Assert.Equal(metric1.Unit, snapshot1.Unit); - Assert.Equal(metric1.MeterName, snapshot1.MeterName); - Assert.Equal(metric1.MetricType, snapshot1.MetricType); - Assert.Equal(metric1.MeterVersion, snapshot1.MeterVersion); - - // SECOND EXPORT - histogram.Record(5); - meterProvider.ForceFlush(); - - // Verify Metric 1 after second export - // This value is expected to be updated. - Assert.Equal(2, metricPoint1.GetHistogramCount()); - Assert.Equal(15, metricPoint1.GetHistogramSum()); - metricPoint1.TryGetHistogramMinMaxValues(out min, out max); - Assert.Equal(5, min); - Assert.Equal(10, max); - - // Verify Metric 2 - Assert.Equal(2, exportedMetrics.Count); - var metric2 = exportedMetrics[1]; - var metricPoints2Enumerator = metric2.GetMetricPoints().GetEnumerator(); - Assert.True(metricPoints2Enumerator.MoveNext()); - ref readonly var metricPoint2 = ref metricPoints2Enumerator.Current; - Assert.Equal(2, metricPoint2.GetHistogramCount()); - Assert.Equal(15, metricPoint2.GetHistogramSum()); - metricPoint2.TryGetHistogramMinMaxValues(out min, out max); - Assert.Equal(5, min); - Assert.Equal(10, max); - - // Verify Snapshot 1 after second export - // This value is expected to be unchanged. - Assert.Equal(1, snapshot1.MetricPoints[0].GetHistogramCount()); - Assert.Equal(10, snapshot1.MetricPoints[0].GetHistogramSum()); - snapshot1.MetricPoints[0].TryGetHistogramMinMaxValues(out min, out max); - Assert.Equal(10, min); - Assert.Equal(10, max); - - // Verify Snapshot 2 - Assert.Equal(2, exportedSnapshots.Count); - var snapshot2 = exportedSnapshots[1]; - Assert.Single(snapshot2.MetricPoints); - Assert.Equal(2, snapshot2.MetricPoints[0].GetHistogramCount()); - Assert.Equal(15, snapshot2.MetricPoints[0].GetHistogramSum()); - snapshot2.MetricPoints[0].TryGetHistogramMinMaxValues(out min, out max); - Assert.Equal(5, min); - Assert.Equal(10, max); - } - - [Fact] - public void VerifySnapshot_ExponentialHistogram() - { - var expectedHistogram = new Base2ExponentialBucketHistogram(); - var exportedMetrics = new List(); - var exportedSnapshots = new List(); - - using var meter = new Meter(Utils.GetCurrentMethodName()); - var histogram = meter.CreateHistogram("histogram"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddView("histogram", new Base2ExponentialBucketHistogramConfiguration()) - .AddInMemoryExporter(exportedMetrics) - .AddInMemoryExporter(exportedSnapshots) - .Build(); - - // FIRST EXPORT - expectedHistogram.Record(10); - histogram.Record(10); - meterProvider.ForceFlush(); - - // Verify Metric 1 - Assert.Single(exportedMetrics); - var metric1 = exportedMetrics[0]; - var metricPoints1Enumerator = metric1.GetMetricPoints().GetEnumerator(); - Assert.True(metricPoints1Enumerator.MoveNext()); - ref readonly var metricPoint1 = ref metricPoints1Enumerator.Current; - Assert.Equal(1, metricPoint1.GetHistogramCount()); - Assert.Equal(10, metricPoint1.GetHistogramSum()); - metricPoint1.TryGetHistogramMinMaxValues(out var min, out var max); - Assert.Equal(10, min); - Assert.Equal(10, max); - AggregatorTest.AssertExponentialBucketsAreCorrect(expectedHistogram, metricPoint1.GetExponentialHistogramData()); - - // Verify Snapshot 1 - Assert.Single(exportedSnapshots); - var snapshot1 = exportedSnapshots[0]; - Assert.Single(snapshot1.MetricPoints); - Assert.Equal(1, snapshot1.MetricPoints[0].GetHistogramCount()); - Assert.Equal(10, snapshot1.MetricPoints[0].GetHistogramSum()); - snapshot1.MetricPoints[0].TryGetHistogramMinMaxValues(out min, out max); - Assert.Equal(10, min); - Assert.Equal(10, max); - AggregatorTest.AssertExponentialBucketsAreCorrect(expectedHistogram, snapshot1.MetricPoints[0].GetExponentialHistogramData()); - - // Verify Metric == Snapshot - Assert.Equal(metric1.Name, snapshot1.Name); - Assert.Equal(metric1.Description, snapshot1.Description); - Assert.Equal(metric1.Unit, snapshot1.Unit); - Assert.Equal(metric1.MeterName, snapshot1.MeterName); - Assert.Equal(metric1.MetricType, snapshot1.MetricType); - Assert.Equal(metric1.MeterVersion, snapshot1.MeterVersion); - - // SECOND EXPORT - expectedHistogram.Record(5); - histogram.Record(5); - meterProvider.ForceFlush(); - - // Verify Metric 1 after second export - // This value is expected to be updated. - Assert.Equal(2, metricPoint1.GetHistogramCount()); - Assert.Equal(15, metricPoint1.GetHistogramSum()); - metricPoint1.TryGetHistogramMinMaxValues(out min, out max); - Assert.Equal(5, min); - Assert.Equal(10, max); - - // Verify Metric 2 - Assert.Equal(2, exportedMetrics.Count); - var metric2 = exportedMetrics[1]; - var metricPoints2Enumerator = metric2.GetMetricPoints().GetEnumerator(); - Assert.True(metricPoints2Enumerator.MoveNext()); - ref readonly var metricPoint2 = ref metricPoints2Enumerator.Current; - Assert.Equal(2, metricPoint2.GetHistogramCount()); - Assert.Equal(15, metricPoint2.GetHistogramSum()); - metricPoint1.TryGetHistogramMinMaxValues(out min, out max); - Assert.Equal(5, min); - Assert.Equal(10, max); - AggregatorTest.AssertExponentialBucketsAreCorrect(expectedHistogram, metricPoint2.GetExponentialHistogramData()); - - // Verify Snapshot 1 after second export - // This value is expected to be unchanged. - Assert.Equal(1, snapshot1.MetricPoints[0].GetHistogramCount()); - Assert.Equal(10, snapshot1.MetricPoints[0].GetHistogramSum()); - snapshot1.MetricPoints[0].TryGetHistogramMinMaxValues(out min, out max); - Assert.Equal(10, min); - Assert.Equal(10, max); - - // Verify Snapshot 2 - Assert.Equal(2, exportedSnapshots.Count); - var snapshot2 = exportedSnapshots[1]; - Assert.Single(snapshot2.MetricPoints); - Assert.Equal(2, snapshot2.MetricPoints[0].GetHistogramCount()); - Assert.Equal(15, snapshot2.MetricPoints[0].GetHistogramSum()); - snapshot2.MetricPoints[0].TryGetHistogramMinMaxValues(out min, out max); - Assert.Equal(5, min); - Assert.Equal(10, max); - AggregatorTest.AssertExponentialBucketsAreCorrect(expectedHistogram, snapshot2.MetricPoints[0].GetExponentialHistogramData()); - } - } -} diff --git a/test/OpenTelemetry.Tests/Metrics/MetricSnapshotTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricSnapshotTestsBase.cs new file mode 100644 index 00000000000..08ce90ffe44 --- /dev/null +++ b/test/OpenTelemetry.Tests/Metrics/MetricSnapshotTestsBase.cs @@ -0,0 +1,328 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Tests; + +using Xunit; + +namespace OpenTelemetry.Metrics.Tests; + +#pragma warning disable SA1402 + +public abstract class MetricSnapshotTestsBase +{ + private readonly IConfiguration configuration; + + protected MetricSnapshotTestsBase(bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints) + { + this.configuration = MetricApiTestsBase.BuildConfiguration( + emitOverflowAttribute, + shouldReclaimUnusedMetricPoints); + } + + [Fact] + public void VerifySnapshot_Counter() + { + var exportedMetrics = new List(); + var exportedSnapshots = new List(); + + 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) + .Build(); + + // FIRST EXPORT + counter.Add(10); + meterProvider.ForceFlush(); + + // Verify Metric 1 + Assert.Single(exportedMetrics); + var metric1 = exportedMetrics[0]; + var metricPoints1Enumerator = metric1.GetMetricPoints().GetEnumerator(); + Assert.True(metricPoints1Enumerator.MoveNext()); + ref readonly var metricPoint1 = ref metricPoints1Enumerator.Current; + Assert.Equal(10, metricPoint1.GetSumLong()); + + // Verify Snapshot 1 + Assert.Single(exportedSnapshots); + var snapshot1 = exportedSnapshots[0]; + Assert.Single(snapshot1.MetricPoints); + Assert.Equal(10, snapshot1.MetricPoints[0].GetSumLong()); + + // Verify Metric == Snapshot + Assert.Equal(metric1.Name, snapshot1.Name); + Assert.Equal(metric1.Description, snapshot1.Description); + Assert.Equal(metric1.Unit, snapshot1.Unit); + Assert.Equal(metric1.MeterName, snapshot1.MeterName); + Assert.Equal(metric1.MetricType, snapshot1.MetricType); + Assert.Equal(metric1.MeterVersion, snapshot1.MeterVersion); + + // SECOND EXPORT + counter.Add(5); + meterProvider.ForceFlush(); + + // Verify Metric 1, after second export + // This value is expected to be updated. + Assert.Equal(15, metricPoint1.GetSumLong()); + + // Verify Metric 2 + Assert.Equal(2, exportedMetrics.Count); + var metric2 = exportedMetrics[1]; + var metricPoints2Enumerator = metric2.GetMetricPoints().GetEnumerator(); + Assert.True(metricPoints2Enumerator.MoveNext()); + ref readonly var metricPoint2 = ref metricPoints2Enumerator.Current; + Assert.Equal(15, metricPoint2.GetSumLong()); + + // Verify Snapshot 1, after second export + // This value is expected to be unchanged. + Assert.Equal(10, snapshot1.MetricPoints[0].GetSumLong()); + + // Verify Snapshot 2 + Assert.Equal(2, exportedSnapshots.Count); + var snapshot2 = exportedSnapshots[1]; + + Assert.Single(snapshot2.MetricPoints); + + Assert.Equal(15, snapshot2.MetricPoints[0].GetSumLong()); + } + + [Fact] + public void VerifySnapshot_Histogram() + { + var exportedMetrics = new List(); + var exportedSnapshots = new List(); + + 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) + .Build(); + + // FIRST EXPORT + histogram.Record(10); + meterProvider.ForceFlush(); + + // Verify Metric 1 + Assert.Single(exportedMetrics); + var metric1 = exportedMetrics[0]; + var metricPoints1Enumerator = metric1.GetMetricPoints().GetEnumerator(); + Assert.True(metricPoints1Enumerator.MoveNext()); + ref readonly var metricPoint1 = ref metricPoints1Enumerator.Current; + Assert.Equal(1, metricPoint1.GetHistogramCount()); + Assert.Equal(10, metricPoint1.GetHistogramSum()); + metricPoint1.TryGetHistogramMinMaxValues(out var min, out var max); + Assert.Equal(10, min); + Assert.Equal(10, max); + + // Verify Snapshot 1 + Assert.Single(exportedSnapshots); + var snapshot1 = exportedSnapshots[0]; + Assert.Single(snapshot1.MetricPoints); + Assert.Equal(1, snapshot1.MetricPoints[0].GetHistogramCount()); + Assert.Equal(10, snapshot1.MetricPoints[0].GetHistogramSum()); + snapshot1.MetricPoints[0].TryGetHistogramMinMaxValues(out min, out max); + Assert.Equal(10, min); + Assert.Equal(10, max); + + // Verify Metric == Snapshot + Assert.Equal(metric1.Name, snapshot1.Name); + Assert.Equal(metric1.Description, snapshot1.Description); + Assert.Equal(metric1.Unit, snapshot1.Unit); + Assert.Equal(metric1.MeterName, snapshot1.MeterName); + Assert.Equal(metric1.MetricType, snapshot1.MetricType); + Assert.Equal(metric1.MeterVersion, snapshot1.MeterVersion); + + // SECOND EXPORT + histogram.Record(5); + meterProvider.ForceFlush(); + + // Verify Metric 1 after second export + // This value is expected to be updated. + Assert.Equal(2, metricPoint1.GetHistogramCount()); + Assert.Equal(15, metricPoint1.GetHistogramSum()); + metricPoint1.TryGetHistogramMinMaxValues(out min, out max); + Assert.Equal(5, min); + Assert.Equal(10, max); + + // Verify Metric 2 + Assert.Equal(2, exportedMetrics.Count); + var metric2 = exportedMetrics[1]; + var metricPoints2Enumerator = metric2.GetMetricPoints().GetEnumerator(); + Assert.True(metricPoints2Enumerator.MoveNext()); + ref readonly var metricPoint2 = ref metricPoints2Enumerator.Current; + Assert.Equal(2, metricPoint2.GetHistogramCount()); + Assert.Equal(15, metricPoint2.GetHistogramSum()); + metricPoint2.TryGetHistogramMinMaxValues(out min, out max); + Assert.Equal(5, min); + Assert.Equal(10, max); + + // Verify Snapshot 1 after second export + // This value is expected to be unchanged. + Assert.Equal(1, snapshot1.MetricPoints[0].GetHistogramCount()); + Assert.Equal(10, snapshot1.MetricPoints[0].GetHistogramSum()); + snapshot1.MetricPoints[0].TryGetHistogramMinMaxValues(out min, out max); + Assert.Equal(10, min); + Assert.Equal(10, max); + + // Verify Snapshot 2 + Assert.Equal(2, exportedSnapshots.Count); + var snapshot2 = exportedSnapshots[1]; + Assert.Single(snapshot2.MetricPoints); + Assert.Equal(2, snapshot2.MetricPoints[0].GetHistogramCount()); + Assert.Equal(15, snapshot2.MetricPoints[0].GetHistogramSum()); + snapshot2.MetricPoints[0].TryGetHistogramMinMaxValues(out min, out max); + Assert.Equal(5, min); + Assert.Equal(10, max); + } + + [Fact] + public void VerifySnapshot_ExponentialHistogram() + { + var expectedHistogram = new Base2ExponentialBucketHistogram(); + var exportedMetrics = new List(); + var exportedSnapshots = new List(); + + 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) + .AddInMemoryExporter(exportedSnapshots) + .Build(); + + // FIRST EXPORT + expectedHistogram.Record(10); + histogram.Record(10); + meterProvider.ForceFlush(); + + // Verify Metric 1 + Assert.Single(exportedMetrics); + var metric1 = exportedMetrics[0]; + var metricPoints1Enumerator = metric1.GetMetricPoints().GetEnumerator(); + Assert.True(metricPoints1Enumerator.MoveNext()); + ref readonly var metricPoint1 = ref metricPoints1Enumerator.Current; + Assert.Equal(1, metricPoint1.GetHistogramCount()); + Assert.Equal(10, metricPoint1.GetHistogramSum()); + metricPoint1.TryGetHistogramMinMaxValues(out var min, out var max); + Assert.Equal(10, min); + Assert.Equal(10, max); + AggregatorTestsBase.AssertExponentialBucketsAreCorrect(expectedHistogram, metricPoint1.GetExponentialHistogramData()); + + // Verify Snapshot 1 + Assert.Single(exportedSnapshots); + var snapshot1 = exportedSnapshots[0]; + Assert.Single(snapshot1.MetricPoints); + Assert.Equal(1, snapshot1.MetricPoints[0].GetHistogramCount()); + Assert.Equal(10, snapshot1.MetricPoints[0].GetHistogramSum()); + snapshot1.MetricPoints[0].TryGetHistogramMinMaxValues(out min, out max); + Assert.Equal(10, min); + Assert.Equal(10, max); + AggregatorTestsBase.AssertExponentialBucketsAreCorrect(expectedHistogram, snapshot1.MetricPoints[0].GetExponentialHistogramData()); + + // Verify Metric == Snapshot + Assert.Equal(metric1.Name, snapshot1.Name); + Assert.Equal(metric1.Description, snapshot1.Description); + Assert.Equal(metric1.Unit, snapshot1.Unit); + Assert.Equal(metric1.MeterName, snapshot1.MeterName); + Assert.Equal(metric1.MetricType, snapshot1.MetricType); + Assert.Equal(metric1.MeterVersion, snapshot1.MeterVersion); + + // SECOND EXPORT + expectedHistogram.Record(5); + histogram.Record(5); + meterProvider.ForceFlush(); + + // Verify Metric 1 after second export + // This value is expected to be updated. + Assert.Equal(2, metricPoint1.GetHistogramCount()); + Assert.Equal(15, metricPoint1.GetHistogramSum()); + metricPoint1.TryGetHistogramMinMaxValues(out min, out max); + Assert.Equal(5, min); + Assert.Equal(10, max); + + // Verify Metric 2 + Assert.Equal(2, exportedMetrics.Count); + var metric2 = exportedMetrics[1]; + var metricPoints2Enumerator = metric2.GetMetricPoints().GetEnumerator(); + Assert.True(metricPoints2Enumerator.MoveNext()); + ref readonly var metricPoint2 = ref metricPoints2Enumerator.Current; + Assert.Equal(2, metricPoint2.GetHistogramCount()); + Assert.Equal(15, metricPoint2.GetHistogramSum()); + metricPoint1.TryGetHistogramMinMaxValues(out min, out max); + Assert.Equal(5, min); + Assert.Equal(10, max); + AggregatorTestsBase.AssertExponentialBucketsAreCorrect(expectedHistogram, metricPoint2.GetExponentialHistogramData()); + + // Verify Snapshot 1 after second export + // This value is expected to be unchanged. + Assert.Equal(1, snapshot1.MetricPoints[0].GetHistogramCount()); + Assert.Equal(10, snapshot1.MetricPoints[0].GetHistogramSum()); + snapshot1.MetricPoints[0].TryGetHistogramMinMaxValues(out min, out max); + Assert.Equal(10, min); + Assert.Equal(10, max); + + // Verify Snapshot 2 + Assert.Equal(2, exportedSnapshots.Count); + var snapshot2 = exportedSnapshots[1]; + Assert.Single(snapshot2.MetricPoints); + Assert.Equal(2, snapshot2.MetricPoints[0].GetHistogramCount()); + Assert.Equal(15, snapshot2.MetricPoints[0].GetHistogramSum()); + snapshot2.MetricPoints[0].TryGetHistogramMinMaxValues(out min, out max); + Assert.Equal(5, min); + Assert.Equal(10, max); + AggregatorTestsBase.AssertExponentialBucketsAreCorrect(expectedHistogram, snapshot2.MetricPoints[0].GetExponentialHistogramData()); + } +} + +public class MetricSnapshotTests : MetricSnapshotTestsBase +{ + public MetricSnapshotTests() + : base(emitOverflowAttribute: false, shouldReclaimUnusedMetricPoints: false) + { + } +} + +public class MetricSnapshotTestsWithOverflowAttribute : MetricSnapshotTestsBase +{ + public MetricSnapshotTestsWithOverflowAttribute() + : 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/MetricTestData.cs b/test/OpenTelemetry.Tests/Metrics/MetricTestData.cs index 4c08b1e5c21..669212a103a 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricTestData.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricTestData.cs @@ -1,72 +1,59 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.Metrics.Tests +namespace OpenTelemetry.Metrics.Tests; + +public class MetricTestData { - public class MetricTestData - { - public static IEnumerable InvalidInstrumentNames - => new List - { - new object[] { " " }, - new object[] { "-first-char-not-alphabetic" }, - new object[] { "1first-char-not-alphabetic" }, - new object[] { "invalid+separator" }, - new object[] { new string('m', 64) }, - new object[] { "a\xb5" }, // `\xb5` is the Micro character - }; + public static IEnumerable InvalidInstrumentNames + => new List + { + new object[] { " " }, + new object[] { "-first-char-not-alphabetic" }, + new object[] { "1first-char-not-alphabetic" }, + new object[] { "invalid+separator" }, + new object[] { new string('m', 256) }, + new object[] { "a\xb5" }, // `\xb5` is the Micro character + }; - public static IEnumerable ValidInstrumentNames - => new List - { - new object[] { "m" }, - new object[] { "first-char-alphabetic" }, - new object[] { "my-2-instrument" }, - new object[] { "my.metric" }, - new object[] { "my_metric2" }, - new object[] { new string('m', 63) }, - new object[] { "CaSe-InSeNsItIvE" }, - }; + public static IEnumerable ValidInstrumentNames + => new List + { + new object[] { "m" }, + new object[] { "first-char-alphabetic" }, + new object[] { "my-2-instrument" }, + new object[] { "my.metric" }, + new object[] { "my_metric2" }, + new object[] { new string('m', 255) }, + new object[] { "CaSe-InSeNsItIvE" }, + new object[] { "my_metric/environment/database" }, + }; - public static IEnumerable InvalidHistogramBoundaries - => new List - { - new object[] { new double[] { 0, 0 } }, - new object[] { new double[] { 1, 0 } }, - new object[] { new double[] { 0, 1, 1, 2 } }, - new object[] { new double[] { 0, 1, 2, -1 } }, - }; + public static IEnumerable InvalidHistogramBoundaries + => new List + { + new object[] { new double[] { 0, 0 } }, + new object[] { new double[] { 1, 0 } }, + new object[] { new double[] { 0, 1, 1, 2 } }, + new object[] { new double[] { 0, 1, 2, -1 } }, + }; - public static IEnumerable ValidHistogramMinMax - => new List - { - new object[] { new double[] { -10, 0, 1, 9, 10, 11, 19 }, new HistogramConfiguration(), -10, 19 }, - new object[] { new double[] { double.NegativeInfinity }, new HistogramConfiguration(), double.NegativeInfinity, double.NegativeInfinity }, - new object[] { new double[] { double.NegativeInfinity, 0, double.PositiveInfinity }, new HistogramConfiguration(), double.NegativeInfinity, double.PositiveInfinity }, - new object[] { new double[] { 1 }, new HistogramConfiguration(), 1, 1 }, - new object[] { new double[] { 5, 100, 4, 101, -2, 97 }, new ExplicitBucketHistogramConfiguration() { Boundaries = new double[] { 10, 20 } }, -2, 101 }, - new object[] { new double[] { 5, 100, 4, 101, -2, 97 }, new Base2ExponentialBucketHistogramConfiguration(), 4, 101 }, - }; + public static IEnumerable ValidHistogramMinMax + => new List + { + new object[] { new double[] { -10, 0, 1, 9, 10, 11, 19 }, new HistogramConfiguration(), -10, 19 }, + new object[] { new double[] { double.NegativeInfinity }, new HistogramConfiguration(), double.NegativeInfinity, double.NegativeInfinity }, + new object[] { new double[] { double.NegativeInfinity, 0, double.PositiveInfinity }, new HistogramConfiguration(), double.NegativeInfinity, double.PositiveInfinity }, + new object[] { new double[] { 1 }, new HistogramConfiguration(), 1, 1 }, + new object[] { new double[] { 5, 100, 4, 101, -2, 97 }, new ExplicitBucketHistogramConfiguration() { Boundaries = new double[] { 10, 20 } }, -2, 101 }, + new object[] { new double[] { 5, 100, 4, 101, -2, 97 }, new Base2ExponentialBucketHistogramConfiguration(), 4, 101 }, + }; - public static IEnumerable InvalidHistogramMinMax - => new List - { - new object[] { new double[] { 1 }, new HistogramConfiguration() { RecordMinMax = false } }, - new object[] { new double[] { 1 }, new ExplicitBucketHistogramConfiguration() { Boundaries = new double[] { 10, 20 }, RecordMinMax = false } }, - new object[] { new double[] { 1 }, new Base2ExponentialBucketHistogramConfiguration() { RecordMinMax = false } }, - }; - } + public static IEnumerable InvalidHistogramMinMax + => new List + { + new object[] { new double[] { 1 }, new HistogramConfiguration() { RecordMinMax = false } }, + new object[] { new double[] { 1 }, new ExplicitBucketHistogramConfiguration() { Boundaries = new double[] { 10, 20 }, RecordMinMax = false } }, + new object[] { new double[] { 1 }, new Base2ExponentialBucketHistogramConfiguration() { RecordMinMax = false } }, + }; } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs index 181027e37a2..7d72b773ea6 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs @@ -1,25 +1,95 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 +#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; 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. public static void ValidateMetricPointTags(List> expectedTags, ReadOnlyTagCollection actualTags) { int tagIndex = 0; @@ -89,7 +159,7 @@ public static int GetNumberOfMetricPoints(List metrics) return count; } - public static MetricPoint? GetFirstMetricPoint(List metrics) + public static MetricPoint? GetFirstMetricPoint(IEnumerable metrics) { foreach (var metric in metrics) { @@ -102,6 +172,8 @@ public static int GetNumberOfMetricPoints(List metrics) return null; } + // 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. // Provide tags input sorted by Key public static void CheckTagsForNthMetricPoint(List metrics, List> tags, int n) { @@ -123,8 +195,111 @@ 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 IReadOnlyList GetExemplars(MetricPoint mp) + { + if (mp.TryGetExemplars(out var exemplars)) + { + return exemplars.ToReadOnlyList(); + } + + return Array.Empty(); + } + +#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 { - return mp.GetExemplars().Where(exemplar => exemplar.Timestamp != default).ToArray(); + 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 16f2568abe8..70a26753ef0 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs @@ -1,1270 +1,1304 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Metrics; using OpenTelemetry.Internal; using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Metrics.Tests +namespace OpenTelemetry.Metrics.Tests; + +public class MetricViewTests : MetricTestsBase { - public class MetricViewTests : MetricTestsBase + private const int MaxTimeToAllowForFlush = 10000; + + [Fact] + public void ViewToRenameMetric() { - private const int MaxTimeToAllowForFlush = 10000; + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddView("name1", "renamed") + .AddInMemoryExporter(exportedItems)); + + // Expecting one metric stream. + var counterLong = meter.CreateCounter("name1"); + counterLong.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal("renamed", metric.Name); + } - [Fact] - public void ViewToRenameMetric() - { - using var meter = new Meter(Utils.GetCurrentMethodName()); - var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddView("name1", "renamed") - .AddInMemoryExporter(exportedItems) - .Build(); - - // Expecting one metric stream. - var counterLong = meter.CreateCounter("name1"); - counterLong.Add(10); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Single(exportedItems); - var metric = exportedItems[0]; - Assert.Equal("renamed", metric.Name); - } + [Theory] + [MemberData(nameof(MetricTestData.InvalidInstrumentNames), MemberType = typeof(MetricTestData))] + public void AddViewWithInvalidNameThrowsArgumentException(string viewNewName) + { + var exportedItems = new List(); - [Theory] - [MemberData(nameof(MetricTestData.InvalidInstrumentNames), MemberType = typeof(MetricTestData))] - public void AddViewWithInvalidNameThrowsArgumentException(string viewNewName) - { - var exportedItems = new List(); + using var meter1 = new Meter("AddViewWithInvalidNameThrowsArgumentException"); - using var meter1 = new Meter("AddViewWithInvalidNameThrowsArgumentException"); + var ex = Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter1.Name) + .AddView("name1", viewNewName) + .AddInMemoryExporter(exportedItems))); - var ex = Assert.Throws(() => Sdk.CreateMeterProviderBuilder() - .AddMeter(meter1.Name) - .AddView("name1", viewNewName) - .AddInMemoryExporter(exportedItems) - .Build()); + Assert.Contains($"Custom view name {viewNewName} is invalid.", ex.Message); - Assert.Contains($"Custom view name {viewNewName} is invalid.", ex.Message); + ex = Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter1.Name) + .AddView("name1", new MetricStreamConfiguration() { Name = viewNewName }) + .AddInMemoryExporter(exportedItems))); - ex = Assert.Throws(() => Sdk.CreateMeterProviderBuilder() - .AddMeter(meter1.Name) - .AddView("name1", new MetricStreamConfiguration() { Name = viewNewName }) - .AddInMemoryExporter(exportedItems) - .Build()); + Assert.Contains($"Custom view name {viewNewName} is invalid.", ex.Message); + } - Assert.Contains($"Custom view name {viewNewName} is invalid.", ex.Message); - } + [Fact] + public void AddViewWithNullMetricStreamConfigurationThrowsArgumentnullException() + { + var exportedItems = new List(); - [Fact] - public void AddViewWithNullMetricStreamConfigurationThrowsArgumentnullException() - { - var exportedItems = new List(); + using var meter1 = new Meter("AddViewWithInvalidNameThrowsArgumentException"); + + Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter1.Name) + .AddView("name1", (MetricStreamConfiguration)null) + .AddInMemoryExporter(exportedItems))); + } - using var meter1 = new Meter("AddViewWithInvalidNameThrowsArgumentException"); + [Fact] + public void AddViewWithNameThrowsInvalidArgumentExceptionWhenConflict() + { + var exportedItems = new List(); - Assert.Throws(() => Sdk.CreateMeterProviderBuilder() - .AddMeter(meter1.Name) - .AddView("name1", (MetricStreamConfiguration)null) - .AddInMemoryExporter(exportedItems) - .Build()); - } + using var meter1 = new Meter("AddViewWithGuaranteedConflictThrowsInvalidArgumentException"); - [Fact] - public void AddViewWithNameThrowsInvalidArgumentExceptionWhenConflict() - { - var exportedItems = new List(); + Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter1.Name) + .AddView("instrumenta.*", name: "newname") + .AddInMemoryExporter(exportedItems))); + } - using var meter1 = new Meter("AddViewWithGuaranteedConflictThrowsInvalidArgumentException"); + [Fact] + public void AddViewWithNameInMetricStreamConfigurationThrowsInvalidArgumentExceptionWhenConflict() + { + var exportedItems = new List(); - Assert.Throws(() => Sdk.CreateMeterProviderBuilder() - .AddMeter(meter1.Name) - .AddView("instrumenta.*", name: "newname") - .AddInMemoryExporter(exportedItems) - .Build()); - } + using var meter1 = new Meter("AddViewWithGuaranteedConflictThrowsInvalidArgumentException"); - [Fact] - public void AddViewWithNameInMetricStreamConfigurationThrowsInvalidArgumentExceptionWhenConflict() - { - var exportedItems = new List(); + Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter1.Name) + .AddView("instrumenta.*", new MetricStreamConfiguration() { Name = "newname" }) + .AddInMemoryExporter(exportedItems))); + } - using var meter1 = new Meter("AddViewWithGuaranteedConflictThrowsInvalidArgumentException"); + [Fact] + public void AddViewWithExceptionInUserCallbackAppliedDefault() + { + var exportedItems = new List(); - Assert.Throws(() => Sdk.CreateMeterProviderBuilder() - .AddMeter(meter1.Name) - .AddView("instrumenta.*", new MetricStreamConfiguration() { Name = "newname" }) - .AddInMemoryExporter(exportedItems) - .Build()); - } + using var meter1 = new Meter("AddViewWithExceptionInUserCallback"); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter1.Name) + .AddView((instrument) => { throw new Exception("bad"); }) + .AddInMemoryExporter(exportedItems)); - [Fact] - public void AddViewWithExceptionInUserCallbackAppliedDefault() + using (var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log)) { - var exportedItems = new List(); + var counter1 = meter1.CreateCounter("counter1"); + counter1.Add(1); + + var metricViewIgnoredEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 41); + Assert.Single(metricViewIgnoredEvents); + } - using var meter1 = new Meter("AddViewWithExceptionInUserCallback"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter1.Name) - .AddView((instrument) => { throw new Exception("bad"); }) - .AddInMemoryExporter(exportedItems) - .Build(); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); - using (var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log)) - { - var counter1 = meter1.CreateCounter("counter1"); - counter1.Add(1); - Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 41)); - } + // Counter is still reported with default config + // even if View is ignored due to View exception. + Assert.Single(exportedItems); + } - meterProvider.ForceFlush(MaxTimeToAllowForFlush); + [Fact] + public void AddViewWithExceptionInUserCallbackNoDefault() + { + var exportedItems = new List(); - // Counter is still reported with default config - // even if View is ignored due to View exception. - Assert.Single(exportedItems); - } + using var meter1 = new Meter("AddViewWithExceptionInUserCallback"); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter1.Name) + .AddView((instrument) => { throw new Exception("bad"); }) + .AddView("*", MetricStreamConfiguration.Drop) + .AddInMemoryExporter(exportedItems)); - [Fact] - public void AddViewWithExceptionInUserCallbackNoDefault() + using (var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log)) { - var exportedItems = new List(); + var counter1 = meter1.CreateCounter("counter1"); + counter1.Add(1); + Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 41)); + } - using var meter1 = new Meter("AddViewWithExceptionInUserCallback"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter1.Name) - .AddView((instrument) => { throw new Exception("bad"); }) - .AddView("*", MetricStreamConfiguration.Drop) - .AddInMemoryExporter(exportedItems) - .Build(); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); - using (var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log)) - { - var counter1 = meter1.CreateCounter("counter1"); - counter1.Add(1); - Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 41)); - } + // Counter is not reported. + // as the View is ignored due to View exception. + // and Default is suppressed with * -> Drop + Assert.Empty(exportedItems); + } - meterProvider.ForceFlush(MaxTimeToAllowForFlush); + [Fact] + public void AddViewsWithAndWithoutExceptionInUserCallback() + { + var exportedItems = new List(); - // Counter is not reported. - // as the View is ignored due to View exception. - // and Default is suppressed with * -> Drop - Assert.Empty(exportedItems); - } + using var meter1 = new Meter("AddViewWithExceptionInUserCallback"); + 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)); - [Fact] - public void AddViewsWithAndWithoutExceptionInUserCallback() + using (var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log)) { - var exportedItems = new List(); + var counter1 = meter1.CreateCounter("counter1"); + counter1.Add(1); - using var meter1 = new Meter("AddViewWithExceptionInUserCallback"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter1.Name) - .AddView((instrument) => { throw new Exception("bad"); }) - .AddView((instrument) => { return new MetricStreamConfiguration() { Name = "newname" }; }) - .AddInMemoryExporter(exportedItems) - .Build(); + var metricViewIgnoredEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 41); + Assert.Single(metricViewIgnoredEvents); + } - using (var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log)) - { - var counter1 = meter1.CreateCounter("counter1"); - counter1.Add(1); - Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 41)); - } + meterProvider.ForceFlush(MaxTimeToAllowForFlush); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); + // Counter is still reported with 2nd View + // even if 1st View is ignored due to View exception. + Assert.Single(exportedItems); + Assert.Equal("newname", exportedItems[0].Name); + } - // Counter is still reported with 2nd View - // even if 1st View is ignored due to View exception. - Assert.Single(exportedItems); - Assert.Equal("newname", exportedItems[0].Name); - } + [Theory] + [MemberData(nameof(MetricTestData.InvalidHistogramBoundaries), MemberType = typeof(MetricTestData))] + public void AddViewWithInvalidHistogramBoundsThrowsArgumentException(double[] boundaries) + { + var ex = Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder + .AddView("name1", new ExplicitBucketHistogramConfiguration { Boundaries = boundaries }))); - [Theory] - [MemberData(nameof(MetricTestData.InvalidHistogramBoundaries), MemberType = typeof(MetricTestData))] - public void AddViewWithInvalidHistogramBoundsThrowsArgumentException(double[] boundaries) - { - var ex = Assert.Throws(() => Sdk.CreateMeterProviderBuilder() - .AddView("name1", new ExplicitBucketHistogramConfiguration { Boundaries = boundaries })); + Assert.Contains("Histogram boundaries must be in ascending order with distinct values", ex.Message); + } - Assert.Contains("Histogram boundaries must be in ascending order with distinct values", ex.Message); - } + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(1)] + public void AddViewWithInvalidExponentialHistogramMaxSizeConfigThrowsArgumentException(int maxSize) + { + var ex = Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder + .AddView("name1", new Base2ExponentialBucketHistogramConfiguration { MaxSize = maxSize }))); - [Theory] - [InlineData(-1)] - [InlineData(0)] - [InlineData(1)] - public void AddViewWithInvalidExponentialHistogramMaxSizeConfigThrowsArgumentException(int maxSize) - { - var ex = Assert.Throws(() => Sdk.CreateMeterProviderBuilder() - .AddView("name1", new Base2ExponentialBucketHistogramConfiguration { MaxSize = maxSize })); + Assert.Contains("Histogram max size is invalid", ex.Message); + } - Assert.Contains("Histogram max size is invalid", ex.Message); - } + [Theory] + [InlineData(-12)] + [InlineData(21)] + public void AddViewWithInvalidExponentialHistogramMaxScaleConfigThrowsArgumentException(int maxScale) + { + var ex = Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder + .AddView("name1", new Base2ExponentialBucketHistogramConfiguration { MaxScale = maxScale }))); - [Theory] - [InlineData(-12)] - [InlineData(21)] - public void AddViewWithInvalidExponentialHistogramMaxScaleConfigThrowsArgumentException(int maxScale) - { - var ex = Assert.Throws(() => Sdk.CreateMeterProviderBuilder() - .AddView("name1", new Base2ExponentialBucketHistogramConfiguration { MaxScale = maxScale })); + Assert.Contains("Histogram max scale is invalid", ex.Message); + } - Assert.Contains("Histogram max scale is invalid", ex.Message); - } + [Theory] + [MemberData(nameof(MetricTestData.InvalidHistogramBoundaries), MemberType = typeof(MetricTestData))] + public void AddViewWithInvalidHistogramBoundsIgnored(double[] boundaries) + { + var exportedItems = new List(); + + using var meter1 = new Meter("AddViewWithInvalidHistogramBoundsIgnored"); - [Theory] - [MemberData(nameof(MetricTestData.InvalidHistogramBoundaries), MemberType = typeof(MetricTestData))] - public void AddViewWithInvalidHistogramBoundsIgnored(double[] boundaries) + var counter1 = meter1.CreateCounter("counter1"); + + using (var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter1.Name) + .AddView((instrument) => + { + return instrument.Name == counter1.Name + ? new ExplicitBucketHistogramConfiguration() { Boundaries = boundaries } + : null; + }) + .AddInMemoryExporter(exportedItems))) { - var exportedItems = new List(); + counter1.Add(1); + } - using var meter1 = new Meter("AddViewWithInvalidHistogramBoundsIgnored"); + // Counter is aggregated with default configuration + // as the View config is ignored due to invalid histogram bounds. + Assert.Single(exportedItems); + } - var counter1 = meter1.CreateCounter("counter1"); + [Theory] + [MemberData(nameof(MetricTestData.ValidInstrumentNames), MemberType = typeof(MetricTestData))] + public void ViewWithValidNameExported(string viewNewName) + { + var exportedItems = new List(); + + using var meter1 = new Meter("ViewWithInvalidNameIgnoredTest"); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter1.Name) + .AddView("name1", viewNewName) + .AddInMemoryExporter(exportedItems)); + + var counterLong = meter1.CreateCounter("name1"); + counterLong.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + // Expecting one metric stream. + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal(viewNewName, metric.Name); + } - using (var provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter1.Name) - .AddView((instrument) => - { - return instrument.Name == counter1.Name - ? new ExplicitBucketHistogramConfiguration() { Boundaries = boundaries } - : null; - }) - .AddInMemoryExporter(exportedItems) - .Build()) + [Fact] + public void ViewToRenameMetricConditionally() + { + using var meter1 = new Meter($"{Utils.GetCurrentMethodName()}.1"); + using var meter2 = new Meter($"{Utils.GetCurrentMethodName()}.2"); + + var exportedItems = new List(); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter1.Name) + .AddMeter(meter2.Name) + .AddView((instrument) => { - counter1.Add(1); - } + if (instrument.Meter.Name.Equals(meter2.Name, StringComparison.OrdinalIgnoreCase) + && instrument.Name.Equals("name1", StringComparison.OrdinalIgnoreCase)) + { + return new MetricStreamConfiguration() { Name = "name1_Renamed", Description = "new description" }; + } + else + { + return null; + } + }) + .AddInMemoryExporter(exportedItems)); + + // Without views only 1 stream would be + // exported (the 2nd one gets dropped due to + // name conflict). Due to renaming with Views, + // we expect 2 metric streams here. + var counter1 = meter1.CreateCounter("name1", "unit", "original_description"); + var counter2 = meter2.CreateCounter("name1", "unit", "original_description"); + counter1.Add(10); + counter2.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + Assert.Equal("name1", exportedItems[0].Name); + Assert.Equal("name1_Renamed", exportedItems[1].Name); + Assert.Equal("original_description", exportedItems[0].Description); + Assert.Equal("new description", exportedItems[1].Description); + } - // Counter is aggregated with default configuration - // as the View config is ignored due to invalid histogram bounds. - Assert.Single(exportedItems); - } + [Theory] + [MemberData(nameof(MetricTestData.InvalidInstrumentNames), MemberType = typeof(MetricTestData))] + public void ViewWithInvalidNameIgnoredConditionally(string viewNewName) + { + using var meter1 = new Meter("ViewToRenameMetricConditionallyTest"); + var exportedItems = new List(); + 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 + // so the view is allowed to be added, but upon instrument creation it's going to be ignored. + .AddView((instrument) => + { + if (instrument.Meter.Name.Equals(meter1.Name, StringComparison.OrdinalIgnoreCase) + && instrument.Name.Equals("name1", StringComparison.OrdinalIgnoreCase)) + { + // invalid instrument name as per the spec + return new MetricStreamConfiguration() { Name = viewNewName, Description = "new description" }; + } + else + { + return null; + } + }) + .AddInMemoryExporter(exportedItems)); - [Theory] - [MemberData(nameof(MetricTestData.ValidInstrumentNames), MemberType = typeof(MetricTestData))] - public void ViewWithValidNameExported(string viewNewName) - { - var exportedItems = new List(); - - using var meter1 = new Meter("ViewWithInvalidNameIgnoredTest"); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter1.Name) - .AddView("name1", viewNewName) - .AddInMemoryExporter(exportedItems) - .Build(); - - var counterLong = meter1.CreateCounter("name1"); - counterLong.Add(10); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - - // Expecting one metric stream. - Assert.Single(exportedItems); - var metric = exportedItems[0]; - Assert.Equal(viewNewName, metric.Name); - } + // Because the MetricStreamName passed is invalid, the view is ignored, + // and default aggregation is used. + var counter1 = meter1.CreateCounter("name1", "unit", "original_description"); + counter1.Add(10); - [Fact] - public void ViewToRenameMetricConditionally() - { - using var meter1 = new Meter($"{Utils.GetCurrentMethodName()}.1"); - using var meter2 = new Meter($"{Utils.GetCurrentMethodName()}.2"); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); - var exportedItems = new List(); + Assert.Single(exportedItems); + } - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter1.Name) - .AddMeter(meter2.Name) - .AddView((instrument) => + [Theory] + [MemberData(nameof(MetricTestData.ValidInstrumentNames), MemberType = typeof(MetricTestData))] + public void ViewWithValidNameConditionally(string viewNewName) + { + using var meter1 = new Meter("ViewToRenameMetricConditionallyTest"); + var exportedItems = new List(); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter1.Name) + .AddView((instrument) => + { + if (instrument.Meter.Name.Equals(meter1.Name, StringComparison.OrdinalIgnoreCase) + && instrument.Name.Equals("name1", StringComparison.OrdinalIgnoreCase)) { - if (instrument.Meter.Name.Equals(meter2.Name, StringComparison.OrdinalIgnoreCase) - && instrument.Name.Equals("name1", StringComparison.OrdinalIgnoreCase)) - { - return new MetricStreamConfiguration() { Name = "name1_Renamed", Description = "new description" }; - } - else - { - return null; - } - }) - .AddInMemoryExporter(exportedItems) - .Build(); - - // Without views only 1 stream would be - // exported (the 2nd one gets dropped due to - // name conflict). Due to renaming with Views, - // we expect 2 metric streams here. - var counter1 = meter1.CreateCounter("name1", "unit", "original_description"); - var counter2 = meter2.CreateCounter("name1", "unit", "original_description"); - counter1.Add(10); - counter2.Add(10); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Equal(2, exportedItems.Count); - Assert.Equal("name1", exportedItems[0].Name); - Assert.Equal("name1_Renamed", exportedItems[1].Name); - Assert.Equal("original_description", exportedItems[0].Description); - Assert.Equal("new description", exportedItems[1].Description); - } + // invalid instrument name as per the spec + return new MetricStreamConfiguration() { Name = viewNewName, Description = "new description" }; + } + else + { + return null; + } + }) + .AddInMemoryExporter(exportedItems)); - [Theory] - [MemberData(nameof(MetricTestData.InvalidInstrumentNames), MemberType = typeof(MetricTestData))] - public void ViewWithInvalidNameIgnoredConditionally(string viewNewName) - { - using var meter1 = new Meter("ViewToRenameMetricConditionallyTest"); - var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter1.Name) + // Expecting one metric stream. + var counter1 = meter1.CreateCounter("name1", "unit", "original_description"); + counter1.Add(10); - // since here it's a func, we can't validate the name right away - // so the view is allowed to be added, but upon instrument creation it's going to be ignored. - .AddView((instrument) => - { - if (instrument.Meter.Name.Equals(meter1.Name, StringComparison.OrdinalIgnoreCase) - && instrument.Name.Equals("name1", StringComparison.OrdinalIgnoreCase)) - { - // invalid instrument name as per the spec - return new MetricStreamConfiguration() { Name = viewNewName, Description = "new description" }; - } - else - { - return null; - } - }) - .AddInMemoryExporter(exportedItems) - .Build(); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); - // Because the MetricStreamName passed is invalid, the view is ignored, - // and default aggregation is used. - var counter1 = meter1.CreateCounter("name1", "unit", "original_description"); - counter1.Add(10); + // Expecting one metric stream. + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal(viewNewName, metric.Name); + } - meterProvider.ForceFlush(MaxTimeToAllowForFlush); + [Fact] + public void ViewWithNullCustomNameTakesInstrumentName() + { + var exportedItems = new List(); - Assert.Single(exportedItems); - } + using var meter = new Meter("ViewToRenameMetricConditionallyTest"); - [Theory] - [MemberData(nameof(MetricTestData.ValidInstrumentNames), MemberType = typeof(MetricTestData))] - public void ViewWithValidNameConditionally(string viewNewName) - { - using var meter1 = new Meter("ViewToRenameMetricConditionallyTest"); - var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter1.Name) - .AddView((instrument) => + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddView((instrument) => + { + if (instrument.Name.Equals("name1", StringComparison.OrdinalIgnoreCase)) { - if (instrument.Meter.Name.Equals(meter1.Name, StringComparison.OrdinalIgnoreCase) - && instrument.Name.Equals("name1", StringComparison.OrdinalIgnoreCase)) - { - // invalid instrument name as per the spec - return new MetricStreamConfiguration() { Name = viewNewName, Description = "new description" }; - } - else - { - return null; - } - }) - .AddInMemoryExporter(exportedItems) - .Build(); + // null View name + return new MetricStreamConfiguration() { }; + } + else + { + return null; + } + }) + .AddInMemoryExporter(exportedItems)); - // Expecting one metric stream. - var counter1 = meter1.CreateCounter("name1", "unit", "original_description"); - counter1.Add(10); + // Expecting one metric stream. + // Since the View name was null, the instrument name was used instead + var counter1 = meter.CreateCounter("name1", "unit", "original_description"); + counter1.Add(10); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); - // Expecting one metric stream. - Assert.Single(exportedItems); - var metric = exportedItems[0]; - Assert.Equal(viewNewName, metric.Name); - } + // Expecting one metric stream. + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal(counter1.Name, metric.Name); + } - [Fact] - public void ViewWithNullCustomNameTakesInstrumentName() - { - var exportedItems = new List(); + [Fact] + public void ViewToProduceMultipleStreamsFromInstrument() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddView("name1", "renamedStream1") + .AddView("name1", "renamedStream2") + .AddInMemoryExporter(exportedItems)); + + // Expecting two metric stream. + var counterLong = meter.CreateCounter("name1"); + counterLong.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + Assert.Equal("renamedStream1", exportedItems[0].Name); + Assert.Equal("renamedStream2", exportedItems[1].Name); + } + + [Fact] + public void ViewToProduceMultipleStreamsWithDuplicatesFromInstrument() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddView("name1", "renamedStream1") + .AddView("name1", "renamedStream2") + .AddView("name1", "renamedStream2") + .AddInMemoryExporter(exportedItems)); + + // Expecting three metric stream. + // the second .AddView("name1", "renamedStream2") + // produces a conflicting metric stream. + var counterLong = meter.CreateCounter("name1"); + counterLong.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(3, exportedItems.Count); + Assert.Equal("renamedStream1", exportedItems[0].Name); + Assert.Equal("renamedStream2", exportedItems[1].Name); + Assert.Equal("renamedStream2", exportedItems[2].Name); + } - using var meter = new Meter("ViewToRenameMetricConditionallyTest"); + [Fact] + public void ViewWithHistogramConfigurationIgnoredWhenAppliedToNonHistogram() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddView((instrument) => - { - if (instrument.Name.Equals("name1", StringComparison.OrdinalIgnoreCase)) - { - // null View name - return new MetricStreamConfiguration() { }; - } - else - { - return null; - } - }) - .AddInMemoryExporter(exportedItems) - .Build(); + 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)); - // Expecting one metric stream. - // Since the View name was null, the instrument name was used instead - var counter1 = meter.CreateCounter("name1", "unit", "original_description"); - counter1.Add(10); + var counter = meter.CreateCounter("NotAHistogram"); + counter.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + var metric = exportedItems[0]; - // Expecting one metric stream. - Assert.Single(exportedItems); - var metric = exportedItems[0]; - Assert.Equal(counter1.Name, metric.Name); - } + Assert.Equal("NotAHistogram", metric.Name); - [Fact] - public void ViewToProduceMultipleStreamsFromInstrument() + List metricPoints = new List(); + foreach (ref readonly var mp in metric.GetMetricPoints()) { - using var meter = new Meter(Utils.GetCurrentMethodName()); - var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddView("name1", "renamedStream1") - .AddView("name1", "renamedStream2") - .AddInMemoryExporter(exportedItems) - .Build(); - - // Expecting two metric stream. - var counterLong = meter.CreateCounter("name1"); - counterLong.Add(10); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Equal(2, exportedItems.Count); - Assert.Equal("renamedStream1", exportedItems[0].Name); - Assert.Equal("renamedStream2", exportedItems[1].Name); + metricPoints.Add(mp); } - [Fact] - public void ViewToProduceMultipleStreamsWithDuplicatesFromInstrument() + Assert.Single(metricPoints); + var metricPoint = metricPoints[0]; + Assert.Equal(10, metricPoint.GetSumLong()); + } + + [Fact] + public void ViewToProduceCustomHistogramBound() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + var boundaries = new double[] { 10, 20 }; + + 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)); + + var histogram = meter.CreateHistogram("MyHistogram"); + histogram.Record(-10); + histogram.Record(0); + histogram.Record(1); + histogram.Record(9); + histogram.Record(10); + histogram.Record(11); + histogram.Record(19); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + var metricDefault = exportedItems[0]; + var metricCustom = exportedItems[1]; + + Assert.Equal("MyHistogramDefaultBound", metricDefault.Name); + Assert.Equal("MyHistogram", metricCustom.Name); + + List metricPointsDefault = new List(); + foreach (ref readonly var mp in metricDefault.GetMetricPoints()) { - using var meter = new Meter(Utils.GetCurrentMethodName()); - var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddView("name1", "renamedStream1") - .AddView("name1", "renamedStream2") - .AddView("name1", "renamedStream2") - .AddInMemoryExporter(exportedItems) - .Build(); - - // Expecting three metric stream. - // the second .AddView("name1", "renamedStream2") - // produces a conflicting metric stream. - var counterLong = meter.CreateCounter("name1"); - counterLong.Add(10); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Equal(3, exportedItems.Count); - Assert.Equal("renamedStream1", exportedItems[0].Name); - Assert.Equal("renamedStream2", exportedItems[1].Name); - Assert.Equal("renamedStream2", exportedItems[2].Name); + metricPointsDefault.Add(mp); } - [Fact] - public void ViewWithHistogramConfigurationIgnoredWhenAppliedToNonHistogram() - { - using var meter = new Meter(Utils.GetCurrentMethodName()); - var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddView("NotAHistogram", new ExplicitBucketHistogramConfiguration() { Name = "ImAnExplicitBoundsHistogram" }) - .AddView("NotAHistogram", new Base2ExponentialBucketHistogramConfiguration() { Name = "ImAnExponentialHistogram" }) - .AddInMemoryExporter(exportedItems) - .Build(); + Assert.Single(metricPointsDefault); + var histogramPoint = metricPointsDefault[0]; - var counter = meter.CreateCounter("NotAHistogram"); - counter.Add(10); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); + var count = histogramPoint.GetHistogramCount(); + var sum = histogramPoint.GetHistogramSum(); - Assert.Single(exportedItems); - var metric = exportedItems[0]; + Assert.Equal(40, sum); + Assert.Equal(7, count); - Assert.Equal("NotAHistogram", metric.Name); + int index = 0; + int actualCount = 0; + var expectedBucketCounts = new long[] { 2, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + foreach (var histogramMeasurement in histogramPoint.GetHistogramBuckets()) + { + Assert.Equal(expectedBucketCounts[index], histogramMeasurement.BucketCount); + index++; + actualCount++; + } - List metricPoints = new List(); - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } + Assert.Equal(Metric.DefaultHistogramBounds.Length + 1, actualCount); - Assert.Single(metricPoints); - var metricPoint = metricPoints[0]; - Assert.Equal(10, metricPoint.GetSumLong()); + List metricPointsCustom = new List(); + foreach (ref readonly var mp in metricCustom.GetMetricPoints()) + { + metricPointsCustom.Add(mp); } - [Fact] - public void ViewToProduceCustomHistogramBound() + Assert.Single(metricPointsCustom); + histogramPoint = metricPointsCustom[0]; + + count = histogramPoint.GetHistogramCount(); + sum = histogramPoint.GetHistogramSum(); + + Assert.Equal(40, sum); + Assert.Equal(7, count); + + index = 0; + actualCount = 0; + expectedBucketCounts = new long[] { 5, 2, 0 }; + foreach (var histogramMeasurement in histogramPoint.GetHistogramBuckets()) { - using var meter = new Meter(Utils.GetCurrentMethodName()); - var exportedItems = new List(); - var boundaries = new double[] { 10, 20 }; - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddView("MyHistogram", new ExplicitBucketHistogramConfiguration() { Name = "MyHistogramDefaultBound" }) - .AddView("MyHistogram", new ExplicitBucketHistogramConfiguration() { Boundaries = boundaries }) - .AddInMemoryExporter(exportedItems) - .Build(); - - var histogram = meter.CreateHistogram("MyHistogram"); - histogram.Record(-10); - histogram.Record(0); - histogram.Record(1); - histogram.Record(9); - histogram.Record(10); - histogram.Record(11); - histogram.Record(19); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Equal(2, exportedItems.Count); - var metricDefault = exportedItems[0]; - var metricCustom = exportedItems[1]; - - Assert.Equal("MyHistogramDefaultBound", metricDefault.Name); - Assert.Equal("MyHistogram", metricCustom.Name); - - List metricPointsDefault = new List(); - foreach (ref readonly var mp in metricDefault.GetMetricPoints()) - { - metricPointsDefault.Add(mp); - } + Assert.Equal(expectedBucketCounts[index], histogramMeasurement.BucketCount); + index++; + actualCount++; + } - Assert.Single(metricPointsDefault); - var histogramPoint = metricPointsDefault[0]; + Assert.Equal(boundaries.Length + 1, actualCount); + } - var count = histogramPoint.GetHistogramCount(); - var sum = histogramPoint.GetHistogramSum(); + [Fact] + public void ViewToProduceExponentialHistogram() + { + var valuesToRecord = new[] { -10, 0, 1, 9, 10, 11, 19 }; - Assert.Equal(40, sum); - Assert.Equal(7, count); + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); - int index = 0; - int actualCount = 0; - var expectedBucketCounts = new long[] { 2, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; - foreach (var histogramMeasurement in histogramPoint.GetHistogramBuckets()) - { - Assert.Equal(expectedBucketCounts[index], histogramMeasurement.BucketCount); - index++; - actualCount++; - } + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddView("MyHistogram", new Base2ExponentialBucketHistogramConfiguration()) + .AddInMemoryExporter(exportedItems)); - Assert.Equal(Metric.DefaultHistogramBounds.Length + 1, actualCount); + var histogram = meter.CreateHistogram("MyHistogram"); + var expectedHistogram = new Base2ExponentialBucketHistogram(); + foreach (var value in valuesToRecord) + { + histogram.Record(value); - List metricPointsCustom = new List(); - foreach (ref readonly var mp in metricCustom.GetMetricPoints()) + if (value >= 0) { - metricPointsCustom.Add(mp); + expectedHistogram.Record(value); } + } - Assert.Single(metricPointsCustom); - histogramPoint = metricPointsCustom[0]; + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + var metric = exportedItems[0]; - count = histogramPoint.GetHistogramCount(); - sum = histogramPoint.GetHistogramSum(); + Assert.Equal("MyHistogram", metric.Name); - Assert.Equal(40, sum); - Assert.Equal(7, count); + var metricPoints = new List(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } - index = 0; - actualCount = 0; - expectedBucketCounts = new long[] { 5, 2, 0 }; - foreach (var histogramMeasurement in histogramPoint.GetHistogramBuckets()) - { - Assert.Equal(expectedBucketCounts[index], histogramMeasurement.BucketCount); - index++; - actualCount++; - } + Assert.Single(metricPoints); + var metricPoint = metricPoints[0]; - Assert.Equal(boundaries.Length + 1, actualCount); - } + var count = metricPoint.GetHistogramCount(); + var sum = metricPoint.GetHistogramSum(); - [Fact] - public void ViewToProduceExponentialHistogram() - { - var valuesToRecord = new[] { -10, 0, 1, 9, 10, 11, 19 }; + AggregatorTestsBase.AssertExponentialBucketsAreCorrect(expectedHistogram, metricPoint.GetExponentialHistogramData()); + Assert.Equal(50, sum); + Assert.Equal(6, count); + } - using var meter = new Meter(Utils.GetCurrentMethodName()); - var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddView("MyHistogram", new Base2ExponentialBucketHistogramConfiguration()) - .AddInMemoryExporter(exportedItems) - .Build(); + [Theory] + [MemberData(nameof(MetricTestData.ValidHistogramMinMax), MemberType = typeof(MetricTestData))] + public void HistogramMinMax(double[] values, HistogramConfiguration histogramConfiguration, double expectedMin, double expectedMax) + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var histogram = meter.CreateHistogram("MyHistogram"); + var exportedItems = new List(); - var histogram = meter.CreateHistogram("MyHistogram"); - var expectedHistogram = new Base2ExponentialBucketHistogram(); - foreach (var value in valuesToRecord) - { - histogram.Record(value); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddView(histogram.Name, histogramConfiguration) + .AddInMemoryExporter(exportedItems)); - if (value >= 0) - { - expectedHistogram.Record(value); - } - } + for (var i = 0; i < values.Length; i++) + { + histogram.Record(values[i]); + } - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Single(exportedItems); - var metric = exportedItems[0]; + meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Equal("MyHistogram", metric.Name); + var metricPoints = new List(); + foreach (ref readonly var mp in exportedItems[0].GetMetricPoints()) + { + metricPoints.Add(mp); + } - var metricPoints = new List(); - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } + var histogramPoint = metricPoints[0]; + if (histogramPoint.TryGetHistogramMinMaxValues(out double min, out double max)) + { + Assert.Equal(expectedMin, min); + Assert.Equal(expectedMax, max); + } + else + { + Assert.Fail("MinMax expected"); + } + } - Assert.Single(metricPoints); - var metricPoint = metricPoints[0]; + [Theory] + [MemberData(nameof(MetricTestData.InvalidHistogramMinMax), MemberType = typeof(MetricTestData))] + public void HistogramMinMaxNotPresent(double[] values, HistogramConfiguration histogramConfiguration) + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var histogram = meter.CreateHistogram("MyHistogram"); + var exportedItems = new List(); - var count = metricPoint.GetHistogramCount(); - var sum = metricPoint.GetHistogramSum(); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddView(histogram.Name, histogramConfiguration) + .AddInMemoryExporter(exportedItems)); - AggregatorTest.AssertExponentialBucketsAreCorrect(expectedHistogram, metricPoint.GetExponentialHistogramData()); - Assert.Equal(50, sum); - Assert.Equal(6, count); + for (var i = 0; i < values.Length; i++) + { + histogram.Record(values[i]); } - [Theory] - [MemberData(nameof(MetricTestData.ValidHistogramMinMax), MemberType = typeof(MetricTestData))] - public void HistogramMinMax(double[] values, HistogramConfiguration histogramConfiguration, double expectedMin, double expectedMax) + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + var metricPoints = new List(); + foreach (ref readonly var mp in exportedItems[0].GetMetricPoints()) { - using var meter = new Meter(Utils.GetCurrentMethodName()); - var histogram = meter.CreateHistogram("MyHistogram"); - var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddView(histogram.Name, histogramConfiguration) - .AddInMemoryExporter(exportedItems) - .Build(); + metricPoints.Add(mp); + } - for (var i = 0; i < values.Length; i++) - { - histogram.Record(values[i]); - } + var histogramPoint = metricPoints[0]; + Assert.False(histogramPoint.TryGetHistogramMinMaxValues(out double _, out double _)); + } - meterProvider.ForceFlush(MaxTimeToAllowForFlush); + [Fact] + public void ViewToSelectTagKeys() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); - var metricPoints = new List(); - foreach (ref readonly var mp in exportedItems[0].GetMetricPoints()) + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddView("FruitCounter", new MetricStreamConfiguration() { - metricPoints.Add(mp); - } - - var histogramPoint = metricPoints[0]; - if (histogramPoint.TryGetHistogramMinMaxValues(out double min, out double max)) + TagKeys = new string[] { "name" }, + Name = "NameOnly", + }) + .AddView("FruitCounter", new MetricStreamConfiguration() { - Assert.Equal(expectedMin, min); - Assert.Equal(expectedMax, max); - } - else + TagKeys = new string[] { "size" }, + Name = "SizeOnly", + }) + .AddView("FruitCounter", new MetricStreamConfiguration() { - Assert.Fail("MinMax expected"); - } + TagKeys = Array.Empty(), + Name = "NoTags", + }) + .AddInMemoryExporter(exportedItems)); + + var counter = meter.CreateCounter("FruitCounter"); + counter.Add(10, new("name", "apple"), new("color", "red"), new("size", "small")); + counter.Add(10, new("name", "apple"), new("color", "red"), new("size", "small")); + + counter.Add(10, new("name", "apple"), new("color", "red"), new("size", "medium")); + counter.Add(10, new("name", "apple"), new("color", "red"), new("size", "medium")); + + counter.Add(10, new("name", "apple"), new("color", "red"), new("size", "large")); + counter.Add(10, new("name", "apple"), new("color", "red"), new("size", "large")); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(3, exportedItems.Count); + var metric = exportedItems[0]; + Assert.Equal("NameOnly", metric.Name); + List metricPoints = new List(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); } - [Theory] - [MemberData(nameof(MetricTestData.InvalidHistogramMinMax), MemberType = typeof(MetricTestData))] - public void HistogramMinMaxNotPresent(double[] values, HistogramConfiguration histogramConfiguration) + // Only one point expected "apple" + Assert.Single(metricPoints); + + metric = exportedItems[1]; + Assert.Equal("SizeOnly", metric.Name); + metricPoints.Clear(); + foreach (ref readonly var mp in metric.GetMetricPoints()) { - using var meter = new Meter(Utils.GetCurrentMethodName()); - var histogram = meter.CreateHistogram("MyHistogram"); - var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddView(histogram.Name, histogramConfiguration) - .AddInMemoryExporter(exportedItems) - .Build(); + metricPoints.Add(mp); + } - for (var i = 0; i < values.Length; i++) - { - histogram.Record(values[i]); - } + // 3 points small,medium,large expected + Assert.Equal(3, metricPoints.Count); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); + metric = exportedItems[2]; + Assert.Equal("NoTags", metric.Name); + metricPoints.Clear(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } - var metricPoints = new List(); - foreach (ref readonly var mp in exportedItems[0].GetMetricPoints()) - { - metricPoints.Add(mp); - } + // Single point expected. + Assert.Single(metricPoints); + } - var histogramPoint = metricPoints[0]; - Assert.False(histogramPoint.TryGetHistogramMinMaxValues(out double _, out double _)); - } + [Fact] + public void ViewToDropSingleInstrument() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddView("counterNotInteresting", MetricStreamConfiguration.Drop) + .AddInMemoryExporter(exportedItems)); + + // Expecting one metric stream. + var counterInteresting = meter.CreateCounter("counterInteresting"); + var counterNotInteresting = meter.CreateCounter("counterNotInteresting"); + counterInteresting.Add(10); + counterNotInteresting.Add(10); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal("counterInteresting", metric.Name); + } - [Fact] - public void ViewToSelectTagKeys() - { - using var meter = new Meter(Utils.GetCurrentMethodName()); - var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddView("FruitCounter", new MetricStreamConfiguration() - { - TagKeys = new string[] { "name" }, - Name = "NameOnly", - }) - .AddView("FruitCounter", new MetricStreamConfiguration() - { - TagKeys = new string[] { "size" }, - Name = "SizeOnly", - }) - .AddView("FruitCounter", new MetricStreamConfiguration() - { - TagKeys = Array.Empty(), - Name = "NoTags", - }) - .AddInMemoryExporter(exportedItems) - .Build(); + [Fact] + public void ViewToDropSingleInstrumentObservableCounter() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddView("observableCounterNotInteresting", MetricStreamConfiguration.Drop) + .AddInMemoryExporter(exportedItems)); + + // Expecting one metric stream. + meter.CreateObservableCounter("observableCounterNotInteresting", () => { return 10; }, "ms"); + meter.CreateObservableCounter("observableCounterInteresting", () => { return 10; }, "ms"); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal("observableCounterInteresting", metric.Name); + } - var counter = meter.CreateCounter("FruitCounter"); - counter.Add(10, new("name", "apple"), new("color", "red"), new("size", "small")); - counter.Add(10, new("name", "apple"), new("color", "red"), new("size", "small")); + [Fact] + public void ViewToDropSingleInstrumentObservableGauge() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddView("observableGaugeNotInteresting", MetricStreamConfiguration.Drop) + .AddInMemoryExporter(exportedItems)); + + // Expecting one metric stream. + meter.CreateObservableGauge("observableGaugeNotInteresting", () => { return 10; }, "ms"); + meter.CreateObservableGauge("observableGaugeInteresting", () => { return 10; }, "ms"); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal("observableGaugeInteresting", metric.Name); + } - counter.Add(10, new("name", "apple"), new("color", "red"), new("size", "medium")); - counter.Add(10, new("name", "apple"), new("color", "red"), new("size", "medium")); + [Fact] + public void ViewToDropMultipleInstruments() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddView("server*", MetricStreamConfiguration.Drop) + .AddInMemoryExporter(exportedItems)); + + // Expecting two client metric streams as both server* are dropped. + var serverRequests = meter.CreateCounter("server.requests"); + var serverExceptions = meter.CreateCounter("server.exceptions"); + var clientRequests = meter.CreateCounter("client.requests"); + var clientExceptions = meter.CreateCounter("client.exceptions"); + serverRequests.Add(10); + serverExceptions.Add(10); + clientRequests.Add(10); + clientExceptions.Add(10); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + Assert.Equal("client.requests", exportedItems[0].Name); + Assert.Equal("client.exceptions", exportedItems[1].Name); + } - counter.Add(10, new("name", "apple"), new("color", "red"), new("size", "large")); - counter.Add(10, new("name", "apple"), new("color", "red"), new("size", "large")); + [Fact] + public void ViewToDropAndRetainInstrument() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + + 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)); + + // Expecting one metric stream even though a View is asking + // to drop the instrument, because another View is matching + // the instrument, which asks to aggregate with defaults + // and a use a new name for the resulting metric. + var serverRequests = meter.CreateCounter("server.requests"); + serverRequests.Add(10); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + Assert.Equal("server.request_renamed", exportedItems[0].Name); + } - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Equal(3, exportedItems.Count); - var metric = exportedItems[0]; - Assert.Equal("NameOnly", metric.Name); - List metricPoints = new List(); - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } + [Fact] + public void ViewConflict_OneInstrument_DifferentDescription() + { + var exportedItems = new List(); - // Only one point expected "apple" - Assert.Single(metricPoints); + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - metric = exportedItems[1]; - Assert.Equal("SizeOnly", metric.Name); - metricPoints.Clear(); - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } + 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)); - // 3 points small,medium,large expected - Assert.Equal(3, metricPoints.Count); + var instrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); - metric = exportedItems[2]; - Assert.Equal("NoTags", metric.Name); - metricPoints.Clear(); - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } + instrument.Add(10); - // Single point expected. - Assert.Single(metricPoints); - } + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); - [Fact] - public void ViewToDropSingleInstrument() - { - using var meter = new Meter(Utils.GetCurrentMethodName()); - var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddView("counterNotInteresting", MetricStreamConfiguration.Drop) - .AddInMemoryExporter(exportedItems) - .Build(); - - // Expecting one metric stream. - var counterInteresting = meter.CreateCounter("counterInteresting"); - var counterNotInteresting = meter.CreateCounter("counterNotInteresting"); - counterInteresting.Add(10); - counterNotInteresting.Add(10); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Single(exportedItems); - var metric = exportedItems[0]; - Assert.Equal("counterInteresting", metric.Name); - } + var metric1 = exportedItems[0]; + var metric2 = exportedItems[1]; + Assert.Equal("newDescription1", metric1.Description); + Assert.Equal("newDescription2", metric2.Description); - [Fact] - public void ViewToDropSingleInstrumentObservableCounter() + List metric1MetricPoints = new List(); + foreach (ref readonly var mp in metric1.GetMetricPoints()) { - using var meter = new Meter(Utils.GetCurrentMethodName()); - var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddView("observableCounterNotInteresting", MetricStreamConfiguration.Drop) - .AddInMemoryExporter(exportedItems) - .Build(); - - // Expecting one metric stream. - meter.CreateObservableCounter("observableCounterNotInteresting", () => { return 10; }, "ms"); - meter.CreateObservableCounter("observableCounterInteresting", () => { return 10; }, "ms"); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Single(exportedItems); - var metric = exportedItems[0]; - Assert.Equal("observableCounterInteresting", metric.Name); + metric1MetricPoints.Add(mp); } - [Fact] - public void ViewToDropSingleInstrumentObservableGauge() - { - using var meter = new Meter(Utils.GetCurrentMethodName()); - var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddView("observableGaugeNotInteresting", MetricStreamConfiguration.Drop) - .AddInMemoryExporter(exportedItems) - .Build(); - - // Expecting one metric stream. - meter.CreateObservableGauge("observableGaugeNotInteresting", () => { return 10; }, "ms"); - meter.CreateObservableGauge("observableGaugeInteresting", () => { return 10; }, "ms"); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Single(exportedItems); - var metric = exportedItems[0]; - Assert.Equal("observableGaugeInteresting", metric.Name); - } + Assert.Single(metric1MetricPoints); + var metricPoint1 = metric1MetricPoints[0]; + Assert.Equal(10, metricPoint1.GetSumLong()); - [Fact] - public void ViewToDropMultipleInstruments() + List metric2MetricPoints = new List(); + foreach (ref readonly var mp in metric2.GetMetricPoints()) { - using var meter = new Meter(Utils.GetCurrentMethodName()); - var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddView("server*", MetricStreamConfiguration.Drop) - .AddInMemoryExporter(exportedItems) - .Build(); - - // Expecting two client metric streams as both server* are dropped. - var serverRequests = meter.CreateCounter("server.requests"); - var serverExceptions = meter.CreateCounter("server.exceptions"); - var clientRequests = meter.CreateCounter("client.requests"); - var clientExceptions = meter.CreateCounter("client.exceptions"); - serverRequests.Add(10); - serverExceptions.Add(10); - clientRequests.Add(10); - clientExceptions.Add(10); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Equal(2, exportedItems.Count); - Assert.Equal("client.requests", exportedItems[0].Name); - Assert.Equal("client.exceptions", exportedItems[1].Name); + metric2MetricPoints.Add(mp); } - [Fact] - public void ViewToDropAndRetainInstrument() - { - using var meter = new Meter(Utils.GetCurrentMethodName()); - var exportedItems = new List(); - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddView("server.requests", MetricStreamConfiguration.Drop) - .AddView("server.requests", "server.request_renamed") - .AddInMemoryExporter(exportedItems) - .Build(); - - // Expecting one metric stream even though a View is asking - // to drop the instrument, because another View is matching - // the instrument, which asks to aggregate with defaults - // and a use a new name for the resulting metric. - var serverRequests = meter.CreateCounter("server.requests"); - serverRequests.Add(10); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Single(exportedItems); - Assert.Equal("server.request_renamed", exportedItems[0].Name); - } + Assert.Single(metric2MetricPoints); + var metricPoint2 = metric2MetricPoints[0]; + Assert.Equal(10, metricPoint2.GetSumLong()); + } - [Fact] - public void ViewConflict_OneInstrument_DifferentDescription() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CardinalityLimitofMatchingViewTakesPrecedenceOverMeterProvider(bool setDefault) + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => { - var exportedItems = new List(); + if (setDefault) + { +#pragma warning disable CS0618 // Type or member is obsolete + builder.SetMaxMetricPointsPerMetricStream(3); +#pragma warning restore CS0618 // Type or member is obsolete + } - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + builder .AddMeter(meter.Name) - .AddView("instrumentName", new MetricStreamConfiguration() { Description = "newDescription1" }) - .AddView("instrumentName", new MetricStreamConfiguration() { Description = "newDescription2" }) + .AddView((instrument) => + { + if (instrument.Name == "counter2") + { + return new MetricStreamConfiguration() { Name = "MetricStreamA", CardinalityLimit = 10000 }; + } + + return null; + }) .AddInMemoryExporter(exportedItems); + }); - using var meterProvider = meterProviderBuilder.Build(); + var counter1 = meter.CreateCounter("counter1"); + counter1.Add(100); - var instrument = meter.CreateCounter("instrumentName", "instrumentUnit", "instrumentDescription"); + var counter2 = meter.CreateCounter("counter2"); + counter2.Add(100); - instrument.Add(10); + var counter3 = meter.CreateCounter("counter3"); + counter3.Add(100); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Equal(2, exportedItems.Count); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); - var metric1 = exportedItems[0]; - var metric2 = exportedItems[1]; - Assert.Equal("newDescription1", metric1.Description); - Assert.Equal("newDescription2", metric2.Description); + Assert.Equal(3, exportedItems.Count); - List metric1MetricPoints = new List(); - foreach (ref readonly var mp in metric1.GetMetricPoints()) - { - metric1MetricPoints.Add(mp); - } + Assert.Equal(10000, exportedItems[1].AggregatorStore.CardinalityLimit); + if (setDefault) + { + Assert.Equal(3, exportedItems[0].AggregatorStore.CardinalityLimit); + Assert.Equal(3, exportedItems[2].AggregatorStore.CardinalityLimit); + } + else + { + Assert.Equal(2000, exportedItems[0].AggregatorStore.CardinalityLimit); + Assert.Equal(2000, exportedItems[2].AggregatorStore.CardinalityLimit); + } + } - Assert.Single(metric1MetricPoints); - var metricPoint1 = metric1MetricPoints[0]; - Assert.Equal(10, metricPoint1.GetSumLong()); + [Fact] + public void ViewConflict_TwoDistinctInstruments_ThreeStreams() + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - List metric2MetricPoints = new List(); - foreach (ref readonly var mp in metric2.GetMetricPoints()) + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddView((instrument) => { - metric2MetricPoints.Add(mp); - } + return new MetricStreamConfiguration() { Name = "MetricStreamA", Description = "description", CardinalityLimit = 256 }; + }) + .AddView((instrument) => + { + return instrument.Description == "description1" + ? new MetricStreamConfiguration() { Name = "MetricStreamB", CardinalityLimit = 3 } + : new MetricStreamConfiguration() { Name = "MetricStreamC", CardinalityLimit = 200000 }; + }) + .AddView((instrument) => + { + // This view is ignored as the passed in CardinalityLimit is out of range. + return new MetricStreamConfiguration() { Name = "MetricStreamD", CardinalityLimit = -1 }; + }) + .AddInMemoryExporter(exportedItems)); - Assert.Single(metric2MetricPoints); - var metricPoint2 = metric2MetricPoints[0]; - Assert.Equal(10, metricPoint2.GetSumLong()); - } + var instrument1 = meter.CreateCounter("name", "unit", "description1"); + var instrument2 = meter.CreateCounter("name", "unit", "description2"); - [Fact] - public void ViewConflict_TwoDistinctInstruments_ThreeStreams() - { - var exportedItems = new List(); + instrument1.Add(10); + instrument2.Add(10); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddView((instrument) => - { - return new MetricStreamConfiguration() { Name = "MetricStreamA", Description = "description" }; - }) - .AddView((instrument) => - { - return instrument.Description == "description1" - ? new MetricStreamConfiguration() { Name = "MetricStreamB" } - : new MetricStreamConfiguration() { Name = "MetricStreamC" }; - }) - .AddInMemoryExporter(exportedItems); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(3, exportedItems.Count); - using var meterProvider = meterProviderBuilder.Build(); + var metricA = exportedItems[0]; + var metricB = exportedItems[1]; + var metricC = exportedItems[2]; - var instrument1 = meter.CreateCounter("name", "unit", "description1"); - var instrument2 = meter.CreateCounter("name", "unit", "description2"); + Assert.Equal(256, metricA.AggregatorStore.CardinalityLimit); + Assert.Equal("MetricStreamA", metricA.Name); + Assert.Equal(20, GetAggregatedValue(metricA)); - instrument1.Add(10); - instrument2.Add(10); + Assert.Equal(3, metricB.AggregatorStore.CardinalityLimit); + Assert.Equal("MetricStreamB", metricB.Name); + Assert.Equal(10, GetAggregatedValue(metricB)); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Equal(3, exportedItems.Count); + Assert.Equal(200000, metricC.AggregatorStore.CardinalityLimit); + Assert.Equal("MetricStreamC", metricC.Name); + Assert.Equal(10, GetAggregatedValue(metricC)); - var metricA = exportedItems[0]; - var metricB = exportedItems[1]; - var metricC = exportedItems[2]; + long GetAggregatedValue(Metric metric) + { + var metricPoints = new List(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } - Assert.Equal("MetricStreamA", metricA.Name); - Assert.Equal(20, GetAggregatedValue(metricA)); + Assert.Single(metricPoints); + return metricPoints[0].GetSumLong(); + } + } - Assert.Equal("MetricStreamB", metricB.Name); - Assert.Equal(10, GetAggregatedValue(metricB)); + [Fact] + public void ViewConflict_TwoIdenticalInstruments_TwoViews_DifferentTags() + { + var exportedItems = new List(); - Assert.Equal("MetricStreamC", metricC.Name); - Assert.Equal(10, GetAggregatedValue(metricC)); + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - long GetAggregatedValue(Metric metric) + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddView((instrument) => { - var metricPoints = new List(); - foreach (ref readonly var mp in metric.GetMetricPoints()) - { - metricPoints.Add(mp); - } + return new MetricStreamConfiguration { TagKeys = new[] { "key1" } }; + }) + .AddView((instrument) => + { + return new MetricStreamConfiguration { TagKeys = new[] { "key2" } }; + }) + .AddInMemoryExporter(exportedItems)); - Assert.Single(metricPoints); - return metricPoints[0].GetSumLong(); - } - } + var instrument1 = meter.CreateCounter("name"); + var instrument2 = meter.CreateCounter("name"); - [Fact] - public void ViewConflict_TwoIdenticalInstruments_TwoViews_DifferentTags() + var tags = new KeyValuePair[] { - var exportedItems = new List(); - - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddView((instrument) => - { - return new MetricStreamConfiguration { TagKeys = new[] { "key1" } }; - }) - .AddView((instrument) => - { - return new MetricStreamConfiguration { TagKeys = new[] { "key2" } }; - }) - .AddInMemoryExporter(exportedItems); + new("key1", "value"), + new("key2", "value"), + }; + + instrument1.Add(10, tags); + instrument2.Add(10, tags); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + Assert.Equal(2, exportedItems.Count); + var metric1 = new List() { exportedItems[0] }; + var metric2 = new List() { exportedItems[1] }; + var tag1 = new List> { tags[0] }; + var tag2 = new List> { tags[1] }; + + Assert.Equal("name", exportedItems[0].Name); + Assert.Equal("name", exportedItems[1].Name); + Assert.Equal(20, GetLongSum(metric1)); + Assert.Equal(20, GetLongSum(metric2)); + CheckTagsForNthMetricPoint(metric1, tag1, 1); + CheckTagsForNthMetricPoint(metric2, tag2, 1); + } - using var meterProvider = meterProviderBuilder.Build(); + [Fact] + public void ViewConflict_TwoIdenticalInstruments_TwoViews_SameTags() + { + var exportedItems = new List(); - var instrument1 = meter.CreateCounter("name"); - var instrument2 = meter.CreateCounter("name"); + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var tags = new KeyValuePair[] + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddView((instrument) => { - new("key1", "value"), - new("key2", "value"), - }; - - instrument1.Add(10, tags); - instrument2.Add(10, tags); - - meterProvider.ForceFlush(MaxTimeToAllowForFlush); - - Assert.Equal(2, exportedItems.Count); - var metric1 = new List() { exportedItems[0] }; - var metric2 = new List() { exportedItems[1] }; - var tag1 = new List> { tags[0] }; - var tag2 = new List> { tags[1] }; - - Assert.Equal("name", exportedItems[0].Name); - Assert.Equal("name", exportedItems[1].Name); - Assert.Equal(20, GetLongSum(metric1)); - Assert.Equal(20, GetLongSum(metric2)); - CheckTagsForNthMetricPoint(metric1, tag1, 1); - CheckTagsForNthMetricPoint(metric2, tag2, 1); - } - - [Fact] - public void ViewConflict_TwoIdenticalInstruments_TwoViews_SameTags() - { - var exportedItems = new List(); + return new MetricStreamConfiguration { TagKeys = new[] { "key1" } }; + }) + .AddView((instrument) => + { + return new MetricStreamConfiguration { TagKeys = new[] { "key1" } }; + }) + .AddInMemoryExporter(exportedItems)); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddView((instrument) => - { - return new MetricStreamConfiguration { TagKeys = new[] { "key1" } }; - }) - .AddView((instrument) => - { - return new MetricStreamConfiguration { TagKeys = new[] { "key1" } }; - }) - .AddInMemoryExporter(exportedItems); + var instrument1 = meter.CreateCounter("name"); + var instrument2 = meter.CreateCounter("name"); - using var meterProvider = meterProviderBuilder.Build(); + var tags = new KeyValuePair[] + { + new("key1", "value"), + new("key2", "value"), + }; - var instrument1 = meter.CreateCounter("name"); - var instrument2 = meter.CreateCounter("name"); + instrument1.Add(10, tags); + instrument2.Add(10, tags); - var tags = new KeyValuePair[] - { - new("key1", "value"), - new("key2", "value"), - }; + meterProvider.ForceFlush(MaxTimeToAllowForFlush); - instrument1.Add(10, tags); - instrument2.Add(10, tags); + Assert.Equal(2, exportedItems.Count); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); + var metric1 = new List() { exportedItems[0] }; + var tag1 = new List> { tags[0] }; + Assert.Equal("name", exportedItems[0].Name); + Assert.Equal(20, GetLongSum(metric1)); + CheckTagsForNthMetricPoint(metric1, tag1, 1); - Assert.Equal(2, exportedItems.Count); + var metric2 = new List() { exportedItems[1] }; + var tag2 = new List> { tags[0] }; + Assert.Equal("name", exportedItems[1].Name); + Assert.Equal(20, GetLongSum(metric2)); + CheckTagsForNthMetricPoint(metric2, tag2, 1); + } - var metric1 = new List() { exportedItems[0] }; - var tag1 = new List> { tags[0] }; - Assert.Equal("name", exportedItems[0].Name); - Assert.Equal(20, GetLongSum(metric1)); - CheckTagsForNthMetricPoint(metric1, tag1, 1); + [Fact] + public void ViewConflict_TwoIdenticalInstruments_TwoViews_DifferentHistogramBounds() + { + var exportedItems = new List(); - var metric2 = new List() { exportedItems[1] }; - var tag2 = new List> { tags[0] }; - Assert.Equal("name", exportedItems[1].Name); - Assert.Equal(20, GetLongSum(metric2)); - CheckTagsForNthMetricPoint(metric2, tag2, 1); - } + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - [Fact] - public void ViewConflict_TwoIdenticalInstruments_TwoViews_DifferentHistogramBounds() - { - var exportedItems = new List(); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddView((instrument) => + { + return new ExplicitBucketHistogramConfiguration { Boundaries = new[] { 5.0, 10.0 } }; + }) + .AddView((instrument) => + { + return new ExplicitBucketHistogramConfiguration { Boundaries = new[] { 10.0, 20.0 } }; + }) + .AddInMemoryExporter(exportedItems)); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddView((instrument) => - { - return new ExplicitBucketHistogramConfiguration { Boundaries = new[] { 5.0, 10.0 } }; - }) - .AddView((instrument) => - { - return new ExplicitBucketHistogramConfiguration { Boundaries = new[] { 10.0, 20.0 } }; - }) - .AddInMemoryExporter(exportedItems); + var instrument1 = meter.CreateHistogram("name"); + var instrument2 = meter.CreateHistogram("name"); - using var meterProvider = meterProviderBuilder.Build(); + instrument1.Record(15); + instrument2.Record(15); - var instrument1 = meter.CreateHistogram("name"); - var instrument2 = meter.CreateHistogram("name"); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); - instrument1.Record(15); - instrument2.Record(15); + Assert.Equal(2, exportedItems.Count); + var metric1 = exportedItems[0]; + var metric2 = exportedItems[1]; - meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal("name", exportedItems[0].Name); + Assert.Equal("name", exportedItems[1].Name); - Assert.Equal(2, exportedItems.Count); - var metric1 = exportedItems[0]; - var metric2 = exportedItems[1]; + var metricPoints = new List(); + foreach (ref readonly var mp in metric1.GetMetricPoints()) + { + metricPoints.Add(mp); + } - Assert.Equal("name", exportedItems[0].Name); - Assert.Equal("name", exportedItems[1].Name); + Assert.Single(metricPoints); + var metricPoint = metricPoints[0]; + Assert.Equal(2, metricPoint.GetHistogramCount()); + Assert.Equal(30, metricPoint.GetHistogramSum()); - var metricPoints = new List(); - foreach (ref readonly var mp in metric1.GetMetricPoints()) - { - metricPoints.Add(mp); - } + var index = 0; + var actualCount = 0; + var expectedBucketCounts = new long[] { 0, 0, 2 }; + foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets()) + { + Assert.Equal(expectedBucketCounts[index], histogramMeasurement.BucketCount); + index++; + actualCount++; + } - Assert.Single(metricPoints); - var metricPoint = metricPoints[0]; - Assert.Equal(2, metricPoint.GetHistogramCount()); - Assert.Equal(30, metricPoint.GetHistogramSum()); - - var index = 0; - var actualCount = 0; - var expectedBucketCounts = new long[] { 0, 0, 2 }; - foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets()) - { - Assert.Equal(expectedBucketCounts[index], histogramMeasurement.BucketCount); - index++; - actualCount++; - } + metricPoints = new List(); + foreach (ref readonly var mp in metric2.GetMetricPoints()) + { + metricPoints.Add(mp); + } - metricPoints = new List(); - foreach (ref readonly var mp in metric2.GetMetricPoints()) - { - metricPoints.Add(mp); - } + Assert.Single(metricPoints); + metricPoint = metricPoints[0]; + Assert.Equal(2, metricPoint.GetHistogramCount()); + Assert.Equal(30, metricPoint.GetHistogramSum()); - Assert.Single(metricPoints); - metricPoint = metricPoints[0]; - Assert.Equal(2, metricPoint.GetHistogramCount()); - Assert.Equal(30, metricPoint.GetHistogramSum()); - - index = 0; - actualCount = 0; - expectedBucketCounts = new long[] { 0, 2, 0 }; - foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets()) - { - Assert.Equal(expectedBucketCounts[index], histogramMeasurement.BucketCount); - index++; - actualCount++; - } + index = 0; + actualCount = 0; + expectedBucketCounts = new long[] { 0, 2, 0 }; + foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets()) + { + Assert.Equal(expectedBucketCounts[index], histogramMeasurement.BucketCount); + index++; + actualCount++; } + } - [Fact] - public void ViewConflict_TwoInstruments_OneMatchesView() - { - var exportedItems = new List(); + [Fact] + public void ViewConflict_TwoInstruments_OneMatchesView() + { + var exportedItems = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddView((instrument) => + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddView((instrument) => + { + if (instrument.Name == "name") { - if (instrument.Name == "name") - { - return new MetricStreamConfiguration { Name = "othername", TagKeys = new[] { "key1" } }; - } - else - { - return null; - } - }) - .AddInMemoryExporter(exportedItems); + return new MetricStreamConfiguration { Name = "othername", TagKeys = new[] { "key1" } }; + } + else + { + return null; + } + }) + .AddInMemoryExporter(exportedItems)); - using var meterProvider = meterProviderBuilder.Build(); + var instrument1 = meter.CreateCounter("name"); + var instrument2 = meter.CreateCounter("othername"); - var instrument1 = meter.CreateCounter("name"); - var instrument2 = meter.CreateCounter("othername"); + var tags = new KeyValuePair[] + { + new("key1", "value"), + new("key2", "value"), + }; - var tags = new KeyValuePair[] - { - new("key1", "value"), - new("key2", "value"), - }; + instrument1.Add(10, tags); + instrument2.Add(10, tags); - instrument1.Add(10, tags); - instrument2.Add(10, tags); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + var metric1 = new List() { exportedItems[0] }; + var metric2 = new List() { exportedItems[1] }; - Assert.Equal(2, exportedItems.Count); - var metric1 = new List() { exportedItems[0] }; - var metric2 = new List() { exportedItems[1] }; + var tags1 = new List> { tags[0] }; + var tags2 = new List> { tags[0], tags[1] }; - var tags1 = new List> { tags[0] }; - var tags2 = new List> { tags[0], tags[1] }; + Assert.Equal("othername", exportedItems[0].Name); + Assert.Equal("othername", exportedItems[1].Name); - Assert.Equal("othername", exportedItems[0].Name); - Assert.Equal("othername", exportedItems[1].Name); + Assert.Equal(10, GetLongSum(metric1)); + Assert.Equal(10, GetLongSum(metric2)); - Assert.Equal(10, GetLongSum(metric1)); - Assert.Equal(10, GetLongSum(metric2)); + CheckTagsForNthMetricPoint(metric1, tags1, 1); + CheckTagsForNthMetricPoint(metric2, tags2, 1); + } - CheckTagsForNthMetricPoint(metric1, tags1, 1); - CheckTagsForNthMetricPoint(metric2, tags2, 1); - } + [Fact] + public void ViewConflict_TwoInstruments_ConflictAvoidedBecauseSecondInstrumentIsDropped() + { + var exportedItems = new List(); - [Fact] - public void ViewConflict_TwoInstruments_ConflictAvoidedBecauseSecondInstrumentIsDropped() - { - var exportedItems = new List(); + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddView((instrument) => + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddView((instrument) => + { + if (instrument.Name == "name") { - if (instrument.Name == "name") - { - return new MetricStreamConfiguration { Name = "othername" }; - } - else - { - return MetricStreamConfiguration.Drop; - } - }) - .AddInMemoryExporter(exportedItems); - - using var meterProvider = meterProviderBuilder.Build(); + return new MetricStreamConfiguration { Name = "othername" }; + } + else + { + return MetricStreamConfiguration.Drop; + } + }) + .AddInMemoryExporter(exportedItems)); - var instrument1 = meter.CreateCounter("name"); - var instrument2 = meter.CreateCounter("othername"); + var instrument1 = meter.CreateCounter("name"); + var instrument2 = meter.CreateCounter("othername"); - instrument1.Add(10); - instrument2.Add(20); + instrument1.Add(10); + instrument2.Add(20); - meterProvider.ForceFlush(MaxTimeToAllowForFlush); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.Single(exportedItems); - var metric1 = new List() { exportedItems[0] }; + Assert.Single(exportedItems); + var metric1 = new List() { exportedItems[0] }; - Assert.Equal("othername", exportedItems[0].Name); - Assert.Equal(10, GetLongSum(metric1)); - } + Assert.Equal("othername", exportedItems[0].Name); + Assert.Equal(10, GetLongSum(metric1)); } } diff --git a/test/OpenTelemetry.Tests/Metrics/MultipleReadersTests.cs b/test/OpenTelemetry.Tests/Metrics/MultipleReadersTests.cs index 9b02d905b1b..16309219654 100644 --- a/test/OpenTelemetry.Tests/Metrics/MultipleReadersTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MultipleReadersTests.cs @@ -1,234 +1,220 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Metrics; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Metrics.Tests +namespace OpenTelemetry.Metrics.Tests; + +public class MultipleReadersTests { - public class MultipleReadersTests + [Theory] + [InlineData(MetricReaderTemporalityPreference.Delta, false)] + [InlineData(MetricReaderTemporalityPreference.Delta, true)] + [InlineData(MetricReaderTemporalityPreference.Cumulative, false)] + [InlineData(MetricReaderTemporalityPreference.Cumulative, true)] + public void SdkSupportsMultipleReaders(MetricReaderTemporalityPreference aggregationTemporality, bool hasViews) { - [Theory] - [InlineData(MetricReaderTemporalityPreference.Delta, false)] - [InlineData(MetricReaderTemporalityPreference.Delta, true)] - [InlineData(MetricReaderTemporalityPreference.Cumulative, false)] - [InlineData(MetricReaderTemporalityPreference.Cumulative, true)] - public void SdkSupportsMultipleReaders(MetricReaderTemporalityPreference aggregationTemporality, bool hasViews) - { - var exportedItems1 = new List(); - var exportedItems2 = new List(); + var exportedItems1 = new List(); + var exportedItems2 = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{aggregationTemporality}.{hasViews}"); + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{aggregationTemporality}.{hasViews}"); - var counter = meter.CreateCounter("counter"); + var counter = meter.CreateCounter("counter"); - int index = 0; - var values = new long[] { 100, 200, 300, 400 }; - long GetValue() => values[index++]; - var gauge = meter.CreateObservableGauge("gauge", () => GetValue()); + int index = 0; + var values = new long[] { 100, 200, 300, 400 }; + long GetValue() => values[index++]; + var gauge = meter.CreateObservableGauge("gauge", () => GetValue()); - int indexSum = 0; - var valuesSum = new long[] { 1000, 1200, 1300, 1400 }; - long GetSum() => valuesSum[indexSum++]; - var observableCounter = meter.CreateObservableCounter("obs-counter", () => GetSum()); + int indexSum = 0; + var valuesSum = new long[] { 1000, 1200, 1300, 1400 }; + long GetSum() => valuesSum[indexSum++]; + var observableCounter = meter.CreateObservableCounter("obs-counter", () => GetSum()); - bool defaultNamedOptionsConfigureCalled = false; - bool namedOptionsConfigureCalled = false; + bool defaultNamedOptionsConfigureCalled = false; + bool namedOptionsConfigureCalled = false; - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.Configure(o => - { - defaultNamedOptionsConfigureCalled = true; - }); - services.Configure("Exporter2", o => - { - namedOptionsConfigureCalled = true; - }); - }) - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems1, metricReaderOptions => + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.Configure(o => { - metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta; - }) - .AddInMemoryExporter("Exporter2", exportedItems2, metricReaderOptions => + defaultNamedOptionsConfigureCalled = true; + }); + services.Configure("Exporter2", o => { - metricReaderOptions.TemporalityPreference = aggregationTemporality; + namedOptionsConfigureCalled = true; }); - - if (hasViews) + }) + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems1, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta; + }) + .AddInMemoryExporter("Exporter2", exportedItems2, metricReaderOptions => { - meterProviderBuilder.AddView("counter", "renamedCounter"); - meterProviderBuilder.AddView("gauge", "renamedGauge"); - meterProviderBuilder.AddView("obs-counter", "renamedObservableCounter"); - } + metricReaderOptions.TemporalityPreference = aggregationTemporality; + }); - using var meterProvider = meterProviderBuilder.Build(); + if (hasViews) + { + meterProviderBuilder.AddView("counter", "renamedCounter"); + meterProviderBuilder.AddView("gauge", "renamedGauge"); + meterProviderBuilder.AddView("obs-counter", "renamedObservableCounter"); + } - Assert.True(defaultNamedOptionsConfigureCalled); - Assert.True(namedOptionsConfigureCalled); + using var meterProvider = meterProviderBuilder.Build(); - counter.Add(10, new KeyValuePair("key", "value")); + Assert.True(defaultNamedOptionsConfigureCalled); + Assert.True(namedOptionsConfigureCalled); - meterProvider.ForceFlush(); + counter.Add(10, new KeyValuePair("key", "value")); - Assert.Equal(3, exportedItems1.Count); - Assert.Equal(3, exportedItems2.Count); + meterProvider.ForceFlush(); - if (hasViews) - { - Assert.Equal("renamedCounter", exportedItems1[0].Name); - Assert.Equal("renamedCounter", exportedItems2[0].Name); + Assert.Equal(3, exportedItems1.Count); + Assert.Equal(3, exportedItems2.Count); - Assert.Equal("renamedGauge", exportedItems1[1].Name); - Assert.Equal("renamedGauge", exportedItems2[1].Name); + if (hasViews) + { + Assert.Equal("renamedCounter", exportedItems1[0].Name); + Assert.Equal("renamedCounter", exportedItems2[0].Name); - Assert.Equal("renamedObservableCounter", exportedItems1[2].Name); - Assert.Equal("renamedObservableCounter", exportedItems2[2].Name); - } - else - { - Assert.Equal("counter", exportedItems1[0].Name); - Assert.Equal("counter", exportedItems2[0].Name); + Assert.Equal("renamedGauge", exportedItems1[1].Name); + Assert.Equal("renamedGauge", exportedItems2[1].Name); - Assert.Equal("gauge", exportedItems1[1].Name); - Assert.Equal("gauge", exportedItems2[1].Name); + Assert.Equal("renamedObservableCounter", exportedItems1[2].Name); + Assert.Equal("renamedObservableCounter", exportedItems2[2].Name); + } + else + { + Assert.Equal("counter", exportedItems1[0].Name); + Assert.Equal("counter", exportedItems2[0].Name); - Assert.Equal("obs-counter", exportedItems1[2].Name); - Assert.Equal("obs-counter", exportedItems2[2].Name); - } + Assert.Equal("gauge", exportedItems1[1].Name); + Assert.Equal("gauge", exportedItems2[1].Name); - // Check value exported for Counter - AssertLongSumValueForMetric(exportedItems1[0], 10); - AssertLongSumValueForMetric(exportedItems2[0], 10); + Assert.Equal("obs-counter", exportedItems1[2].Name); + Assert.Equal("obs-counter", exportedItems2[2].Name); + } - // Check value exported for Gauge - AssertLongSumValueForMetric(exportedItems1[1], 100); - AssertLongSumValueForMetric(exportedItems2[1], 200); + // Check value exported for Counter + AssertLongSumValueForMetric(exportedItems1[0], 10); + AssertLongSumValueForMetric(exportedItems2[0], 10); - // Check value exported for ObservableCounter - AssertLongSumValueForMetric(exportedItems1[2], 1000); - if (aggregationTemporality == MetricReaderTemporalityPreference.Delta) - { - AssertLongSumValueForMetric(exportedItems2[2], 1200); - } - else - { - AssertLongSumValueForMetric(exportedItems2[2], 1200); - } + // Check value exported for Gauge + AssertLongSumValueForMetric(exportedItems1[1], 100); + AssertLongSumValueForMetric(exportedItems2[1], 200); - exportedItems1.Clear(); - exportedItems2.Clear(); + // Check value exported for ObservableCounter + AssertLongSumValueForMetric(exportedItems1[2], 1000); + if (aggregationTemporality == MetricReaderTemporalityPreference.Delta) + { + AssertLongSumValueForMetric(exportedItems2[2], 1200); + } + else + { + AssertLongSumValueForMetric(exportedItems2[2], 1200); + } - counter.Add(15, new KeyValuePair("key", "value")); + exportedItems1.Clear(); + exportedItems2.Clear(); - meterProvider.ForceFlush(); + counter.Add(15, new KeyValuePair("key", "value")); - Assert.Equal(3, exportedItems1.Count); - Assert.Equal(3, exportedItems2.Count); + meterProvider.ForceFlush(); - // Check value exported for Counter - AssertLongSumValueForMetric(exportedItems1[0], 15); - if (aggregationTemporality == MetricReaderTemporalityPreference.Delta) - { - AssertLongSumValueForMetric(exportedItems2[0], 15); - } - else - { - AssertLongSumValueForMetric(exportedItems2[0], 25); - } + Assert.Equal(3, exportedItems1.Count); + Assert.Equal(3, exportedItems2.Count); + + // Check value exported for Counter + AssertLongSumValueForMetric(exportedItems1[0], 15); + if (aggregationTemporality == MetricReaderTemporalityPreference.Delta) + { + AssertLongSumValueForMetric(exportedItems2[0], 15); + } + else + { + AssertLongSumValueForMetric(exportedItems2[0], 25); + } - // Check value exported for Gauge - AssertLongSumValueForMetric(exportedItems1[1], 300); - AssertLongSumValueForMetric(exportedItems2[1], 400); + // Check value exported for Gauge + AssertLongSumValueForMetric(exportedItems1[1], 300); + AssertLongSumValueForMetric(exportedItems2[1], 400); - // Check value exported for ObservableCounter - AssertLongSumValueForMetric(exportedItems1[2], 300); - if (aggregationTemporality == MetricReaderTemporalityPreference.Delta) - { - AssertLongSumValueForMetric(exportedItems2[2], 200); - } - else - { - AssertLongSumValueForMetric(exportedItems2[2], 1400); - } + // Check value exported for ObservableCounter + AssertLongSumValueForMetric(exportedItems1[2], 300); + if (aggregationTemporality == MetricReaderTemporalityPreference.Delta) + { + AssertLongSumValueForMetric(exportedItems2[2], 200); } + else + { + AssertLongSumValueForMetric(exportedItems2[2], 1400); + } + } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void ObservableInstrumentCallbacksInvokedForEachReaders(bool hasViews) + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ObservableInstrumentCallbacksInvokedForEachReaders(bool hasViews) + { + var exportedItems1 = new List(); + var exportedItems2 = new List(); + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{hasViews}"); + int callbackInvocationCount = 0; + var gauge = meter.CreateObservableGauge("gauge", () => { - var exportedItems1 = new List(); - var exportedItems2 = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{hasViews}"); - int callbackInvocationCount = 0; - var gauge = meter.CreateObservableGauge("gauge", () => - { - callbackInvocationCount++; - return 10 * callbackInvocationCount; - }); + callbackInvocationCount++; + return 10 * callbackInvocationCount; + }); - var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddInMemoryExporter(exportedItems1) - .AddInMemoryExporter(exportedItems2); + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems1) + .AddInMemoryExporter(exportedItems2); - if (hasViews) - { - meterProviderBuilder.AddView("gauge", "renamedGauge"); - } + if (hasViews) + { + meterProviderBuilder.AddView("gauge", "renamedGauge"); + } - using var meterProvider = meterProviderBuilder.Build(); - meterProvider.ForceFlush(); + using var meterProvider = meterProviderBuilder.Build(); + meterProvider.ForceFlush(); - // VALIDATE - Assert.Equal(2, callbackInvocationCount); - Assert.Single(exportedItems1); - Assert.Single(exportedItems2); + // VALIDATE + Assert.Equal(2, callbackInvocationCount); + Assert.Single(exportedItems1); + Assert.Single(exportedItems2); - if (hasViews) - { - Assert.Equal("renamedGauge", exportedItems1[0].Name); - Assert.Equal("renamedGauge", exportedItems2[0].Name); - } - else - { - Assert.Equal("gauge", exportedItems1[0].Name); - Assert.Equal("gauge", exportedItems2[0].Name); - } + if (hasViews) + { + Assert.Equal("renamedGauge", exportedItems1[0].Name); + Assert.Equal("renamedGauge", exportedItems2[0].Name); + } + else + { + Assert.Equal("gauge", exportedItems1[0].Name); + Assert.Equal("gauge", exportedItems2[0].Name); } + } - private static void AssertLongSumValueForMetric(Metric metric, long value) + private static void AssertLongSumValueForMetric(Metric metric, long value) + { + var metricPoints = metric.GetMetricPoints(); + var metricPointsEnumerator = metricPoints.GetEnumerator(); + Assert.True(metricPointsEnumerator.MoveNext()); // One MetricPoint is emitted for the Metric + ref readonly var metricPointForFirstExport = ref metricPointsEnumerator.Current; + if (metric.MetricType.IsSum()) { - var metricPoints = metric.GetMetricPoints(); - var metricPointsEnumerator = metricPoints.GetEnumerator(); - Assert.True(metricPointsEnumerator.MoveNext()); // One MetricPoint is emitted for the Metric - ref readonly var metricPointForFirstExport = ref metricPointsEnumerator.Current; - if (metric.MetricType.IsSum()) - { - Assert.Equal(value, metricPointForFirstExport.GetSumLong()); - } - else - { - Assert.Equal(value, metricPointForFirstExport.GetGaugeLastValueLong()); - } + Assert.Equal(value, metricPointForFirstExport.GetSumLong()); + } + else + { + Assert.Equal(value, metricPointForFirstExport.GetGaugeLastValueLong()); } } } diff --git a/test/OpenTelemetry.Tests/OpenTelemetry.Tests.csproj b/test/OpenTelemetry.Tests/OpenTelemetry.Tests.csproj index 4c67f8f2fe3..86fe7e36844 100644 --- a/test/OpenTelemetry.Tests/OpenTelemetry.Tests.csproj +++ b/test/OpenTelemetry.Tests/OpenTelemetry.Tests.csproj @@ -1,9 +1,7 @@ Unit test project for OpenTelemetry - - net7.0;net6.0 - $(TargetFrameworks);net462 + $(TargetFrameworksForTests) $(NoWarn),CS0618 @@ -15,24 +13,27 @@ - - + + + - + + + + - - - all + runtime; build; native; contentfiles; analyzers + diff --git a/test/OpenTelemetry.Tests/Resources/OtelEnvResourceDetectorTest.cs b/test/OpenTelemetry.Tests/Resources/OtelEnvResourceDetectorTest.cs index 0dfaf60aae6..a6180b7a4cd 100644 --- a/test/OpenTelemetry.Tests/Resources/OtelEnvResourceDetectorTest.cs +++ b/test/OpenTelemetry.Tests/Resources/OtelEnvResourceDetectorTest.cs @@ -1,103 +1,89 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.Configuration; using Xunit; -namespace OpenTelemetry.Resources.Tests +namespace OpenTelemetry.Resources.Tests; + +public class OtelEnvResourceDetectorTest : IDisposable { - public class OtelEnvResourceDetectorTest : IDisposable + public OtelEnvResourceDetectorTest() { - public OtelEnvResourceDetectorTest() - { - Environment.SetEnvironmentVariable(OtelEnvResourceDetector.EnvVarKey, null); - } + Environment.SetEnvironmentVariable(OtelEnvResourceDetector.EnvVarKey, null); + } - public void Dispose() - { - Environment.SetEnvironmentVariable(OtelEnvResourceDetector.EnvVarKey, null); - GC.SuppressFinalize(this); - } + public void Dispose() + { + Environment.SetEnvironmentVariable(OtelEnvResourceDetector.EnvVarKey, null); + GC.SuppressFinalize(this); + } - [Fact] - public void OtelEnvResource_EnvVarKey() - { - Assert.Equal("OTEL_RESOURCE_ATTRIBUTES", OtelEnvResourceDetector.EnvVarKey); - } + [Fact] + public void OtelEnvResource_EnvVarKey() + { + Assert.Equal("OTEL_RESOURCE_ATTRIBUTES", OtelEnvResourceDetector.EnvVarKey); + } - [Fact] - public void OtelEnvResource_NullEnvVar() - { - // Arrange - var resource = new OtelEnvResourceDetector( - new ConfigurationBuilder().AddEnvironmentVariables().Build()) - .Detect(); + [Fact] + public void OtelEnvResource_NullEnvVar() + { + // Arrange + var resource = new OtelEnvResourceDetector( + new ConfigurationBuilder().AddEnvironmentVariables().Build()) + .Detect(); - // Assert - Assert.Equal(Resource.Empty, resource); - } + // Assert + Assert.Equal(Resource.Empty, resource); + } - [Fact] - public void OtelEnvResource_WithEnvVar_1() - { - // Arrange - var envVarValue = "Key1=Val1,Key2=Val2"; - Environment.SetEnvironmentVariable(OtelEnvResourceDetector.EnvVarKey, envVarValue); - var resource = new OtelEnvResourceDetector( - new ConfigurationBuilder().AddEnvironmentVariables().Build()) - .Detect(); + [Fact] + public void OtelEnvResource_WithEnvVar_1() + { + // Arrange + var envVarValue = "Key1=Val1,Key2=Val2"; + Environment.SetEnvironmentVariable(OtelEnvResourceDetector.EnvVarKey, envVarValue); + var resource = new OtelEnvResourceDetector( + new ConfigurationBuilder().AddEnvironmentVariables().Build()) + .Detect(); - // Assert - Assert.NotEqual(Resource.Empty, resource); - Assert.Contains(new KeyValuePair("Key1", "Val1"), resource.Attributes); - } + // Assert + Assert.NotEqual(Resource.Empty, resource); + Assert.Contains(new KeyValuePair("Key1", "Val1"), resource.Attributes); + } - [Fact] - public void OtelEnvResource_WithEnvVar_2() - { - // Arrange - var envVarValue = "Key1,Key2=Val2"; - Environment.SetEnvironmentVariable(OtelEnvResourceDetector.EnvVarKey, envVarValue); - var resource = new OtelEnvResourceDetector( - new ConfigurationBuilder().AddEnvironmentVariables().Build()) - .Detect(); + [Fact] + public void OtelEnvResource_WithEnvVar_2() + { + // Arrange + var envVarValue = "Key1,Key2=Val2"; + Environment.SetEnvironmentVariable(OtelEnvResourceDetector.EnvVarKey, envVarValue); + var resource = new OtelEnvResourceDetector( + new ConfigurationBuilder().AddEnvironmentVariables().Build()) + .Detect(); - // Assert - Assert.NotEqual(Resource.Empty, resource); - Assert.Single(resource.Attributes); - Assert.Contains(new KeyValuePair("Key2", "Val2"), resource.Attributes); - } + // Assert + Assert.NotEqual(Resource.Empty, resource); + Assert.Single(resource.Attributes); + Assert.Contains(new KeyValuePair("Key2", "Val2"), resource.Attributes); + } - [Fact] - public void OtelEnvResource_UsingIConfiguration() + [Fact] + public void OtelEnvResource_UsingIConfiguration() + { + var values = new Dictionary() { - var values = new Dictionary() - { - [OtelEnvResourceDetector.EnvVarKey] = "Key1=Val1,Key2=Val2", - }; + [OtelEnvResourceDetector.EnvVarKey] = "Key1=Val1,Key2=Val2", + }; - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(values) - .Build(); + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(values) + .Build(); - var resource = new OtelEnvResourceDetector(configuration).Detect(); + var resource = new OtelEnvResourceDetector(configuration).Detect(); - Assert.NotEqual(Resource.Empty, resource); - Assert.Contains(new KeyValuePair("Key1", "Val1"), resource.Attributes); - Assert.Contains(new KeyValuePair("Key2", "Val2"), resource.Attributes); - } + Assert.NotEqual(Resource.Empty, resource); + Assert.Contains(new KeyValuePair("Key1", "Val1"), resource.Attributes); + Assert.Contains(new KeyValuePair("Key2", "Val2"), resource.Attributes); } } diff --git a/test/OpenTelemetry.Tests/Resources/OtelServiceNameEnvVarDetectorTests.cs b/test/OpenTelemetry.Tests/Resources/OtelServiceNameEnvVarDetectorTests.cs index 15416e40b52..9b3bf437cc8 100644 --- a/test/OpenTelemetry.Tests/Resources/OtelServiceNameEnvVarDetectorTests.cs +++ b/test/OpenTelemetry.Tests/Resources/OtelServiceNameEnvVarDetectorTests.cs @@ -1,88 +1,74 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.Configuration; using Xunit; -namespace OpenTelemetry.Resources.Tests +namespace OpenTelemetry.Resources.Tests; + +public class OtelServiceNameEnvVarDetectorTests : IDisposable { - public class OtelServiceNameEnvVarDetectorTests : IDisposable + public OtelServiceNameEnvVarDetectorTests() { - public OtelServiceNameEnvVarDetectorTests() - { - Environment.SetEnvironmentVariable(OtelServiceNameEnvVarDetector.EnvVarKey, null); - } + Environment.SetEnvironmentVariable(OtelServiceNameEnvVarDetector.EnvVarKey, null); + } - public void Dispose() - { - Environment.SetEnvironmentVariable(OtelServiceNameEnvVarDetector.EnvVarKey, null); - GC.SuppressFinalize(this); - } + public void Dispose() + { + Environment.SetEnvironmentVariable(OtelServiceNameEnvVarDetector.EnvVarKey, null); + GC.SuppressFinalize(this); + } - [Fact] - public void OtelServiceNameEnvVar_EnvVarKey() - { - Assert.Equal("OTEL_SERVICE_NAME", OtelServiceNameEnvVarDetector.EnvVarKey); - } + [Fact] + public void OtelServiceNameEnvVar_EnvVarKey() + { + Assert.Equal("OTEL_SERVICE_NAME", OtelServiceNameEnvVarDetector.EnvVarKey); + } - [Fact] - public void OtelServiceNameEnvVar_Null() - { - // Act - var resource = new OtelServiceNameEnvVarDetector( - new ConfigurationBuilder().AddEnvironmentVariables().Build()) - .Detect(); + [Fact] + public void OtelServiceNameEnvVar_Null() + { + // Act + var resource = new OtelServiceNameEnvVarDetector( + new ConfigurationBuilder().AddEnvironmentVariables().Build()) + .Detect(); - // Assert - Assert.Equal(Resource.Empty, resource); - } + // Assert + Assert.Equal(Resource.Empty, resource); + } - [Fact] - public void OtelServiceNameEnvVar_WithValue() - { - // Arrange - var envVarValue = "my-service"; - Environment.SetEnvironmentVariable(OtelServiceNameEnvVarDetector.EnvVarKey, envVarValue); + [Fact] + public void OtelServiceNameEnvVar_WithValue() + { + // Arrange + var envVarValue = "my-service"; + Environment.SetEnvironmentVariable(OtelServiceNameEnvVarDetector.EnvVarKey, envVarValue); - // Act - var resource = new OtelServiceNameEnvVarDetector( - new ConfigurationBuilder().AddEnvironmentVariables().Build()) - .Detect(); + // Act + var resource = new OtelServiceNameEnvVarDetector( + new ConfigurationBuilder().AddEnvironmentVariables().Build()) + .Detect(); - // Assert - Assert.NotEqual(Resource.Empty, resource); - Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceName, envVarValue), resource.Attributes); - } + // Assert + Assert.NotEqual(Resource.Empty, resource); + Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceName, envVarValue), resource.Attributes); + } - [Fact] - public void OtelServiceNameEnvVar_UsingIConfiguration() + [Fact] + public void OtelServiceNameEnvVar_UsingIConfiguration() + { + var values = new Dictionary() { - var values = new Dictionary() - { - [OtelServiceNameEnvVarDetector.EnvVarKey] = "my-service", - }; + [OtelServiceNameEnvVarDetector.EnvVarKey] = "my-service", + }; - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(values) - .Build(); + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(values) + .Build(); - var resource = new OtelServiceNameEnvVarDetector(configuration).Detect(); + var resource = new OtelServiceNameEnvVarDetector(configuration).Detect(); - Assert.NotEqual(Resource.Empty, resource); - Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceName, "my-service"), resource.Attributes); - } + Assert.NotEqual(Resource.Empty, resource); + Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceName, "my-service"), resource.Attributes); } } diff --git a/test/OpenTelemetry.Tests/Resources/ResourceBuilderTests.cs b/test/OpenTelemetry.Tests/Resources/ResourceBuilderTests.cs index 820e8a59198..15ecdeedc88 100644 --- a/test/OpenTelemetry.Tests/Resources/ResourceBuilderTests.cs +++ b/test/OpenTelemetry.Tests/Resources/ResourceBuilderTests.cs @@ -1,83 +1,87 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Xunit; -namespace OpenTelemetry.Resources.Tests +namespace OpenTelemetry.Resources.Tests; + +public class ResourceBuilderTests { - public class ResourceBuilderTests + [Fact] + public void ServiceResource_ServiceName() + { + var resource = ResourceBuilder.CreateEmpty().AddService("my-service").Build(); + Assert.Equal(2, resource.Attributes.Count()); + Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceName, "my-service"), resource.Attributes); + Assert.Single(resource.Attributes.Where(kvp => kvp.Key == ResourceSemanticConventions.AttributeServiceName)); + Assert.True(Guid.TryParse((string)resource.Attributes.Single(kvp => kvp.Key == ResourceSemanticConventions.AttributeServiceInstance).Value, out _)); + } + + [Fact] + public void ServiceResource_ServiceNameAndInstance() + { + var resource = ResourceBuilder.CreateEmpty().AddService("my-service", serviceInstanceId: "123").Build(); + Assert.Equal(2, resource.Attributes.Count()); + Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceName, "my-service"), resource.Attributes); + Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceInstance, "123"), resource.Attributes); + } + + [Fact] + public void ServiceResource_ServiceNameAndInstanceAndNamespace() + { + var resource = ResourceBuilder.CreateEmpty().AddService("my-service", "my-namespace", serviceInstanceId: "123").Build(); + Assert.Equal(3, resource.Attributes.Count()); + Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceName, "my-service"), resource.Attributes); + Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceInstance, "123"), resource.Attributes); + Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceNamespace, "my-namespace"), resource.Attributes); + } + + [Fact] + public void ServiceResource_ServiceNameAndInstanceAndNamespaceAndVersion() { - [Fact] - public void ServiceResource_ServiceName() - { - var resource = ResourceBuilder.CreateEmpty().AddService("my-service").Build(); - Assert.Equal(2, resource.Attributes.Count()); - Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceName, "my-service"), resource.Attributes); - Assert.Single(resource.Attributes.Where(kvp => kvp.Key == ResourceSemanticConventions.AttributeServiceName)); - Assert.True(Guid.TryParse((string)resource.Attributes.Single(kvp => kvp.Key == ResourceSemanticConventions.AttributeServiceInstance).Value, out _)); - } + var resource = ResourceBuilder.CreateEmpty().AddService("my-service", "my-namespace", "1.2.3", serviceInstanceId: "123").Build(); + Assert.Equal(4, resource.Attributes.Count()); + Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceName, "my-service"), resource.Attributes); + Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceInstance, "123"), resource.Attributes); + Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceNamespace, "my-namespace"), resource.Attributes); + Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceVersion, "1.2.3"), resource.Attributes); + } + + [Fact] + public void ServiceResource_AutoGenerateServiceInstanceIdOff() + { + var resource = ResourceBuilder.CreateEmpty().AddService("my-service", autoGenerateServiceInstanceId: false).Build(); + Assert.Single(resource.Attributes); + Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceName, "my-service"), resource.Attributes); + } + + [Fact] + public void ServiceResourceGeneratesConsistentInstanceId() + { + var firstResource = ResourceBuilder.CreateEmpty().AddService("my-service").Build(); + + var firstInstanceIdAttribute = firstResource.Attributes.FirstOrDefault(kvp => kvp.Key == ResourceSemanticConventions.AttributeServiceInstance); - [Fact] - public void ServiceResource_ServiceNameAndInstance() - { - var resource = ResourceBuilder.CreateEmpty().AddService("my-service", serviceInstanceId: "123").Build(); - Assert.Equal(2, resource.Attributes.Count()); - Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceName, "my-service"), resource.Attributes); - Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceInstance, "123"), resource.Attributes); - } + Assert.NotNull(firstInstanceIdAttribute.Value); - [Fact] - public void ServiceResource_ServiceNameAndInstanceAndNamespace() - { - var resource = ResourceBuilder.CreateEmpty().AddService("my-service", "my-namespace", serviceInstanceId: "123").Build(); - Assert.Equal(3, resource.Attributes.Count()); - Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceName, "my-service"), resource.Attributes); - Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceInstance, "123"), resource.Attributes); - Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceNamespace, "my-namespace"), resource.Attributes); - } + var secondResource = ResourceBuilder.CreateEmpty().AddService("other-service").Build(); - [Fact] - public void ServiceResource_ServiceNameAndInstanceAndNamespaceAndVersion() - { - var resource = ResourceBuilder.CreateEmpty().AddService("my-service", "my-namespace", "1.2.3", serviceInstanceId: "123").Build(); - Assert.Equal(4, resource.Attributes.Count()); - Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceName, "my-service"), resource.Attributes); - Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceInstance, "123"), resource.Attributes); - Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceNamespace, "my-namespace"), resource.Attributes); - Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceVersion, "1.2.3"), resource.Attributes); - } + var secondInstanceIdAttribute = secondResource.Attributes.FirstOrDefault(kvp => kvp.Key == ResourceSemanticConventions.AttributeServiceInstance); - [Fact] - public void ServiceResource_AutoGenerateServiceInstanceIdOff() - { - var resource = ResourceBuilder.CreateEmpty().AddService("my-service", autoGenerateServiceInstanceId: false).Build(); - Assert.Single(resource.Attributes); - Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceName, "my-service"), resource.Attributes); - } + Assert.NotNull(secondInstanceIdAttribute.Value); - [Fact] - public void ClearTest() - { - var resource = ResourceBuilder.CreateEmpty() - .AddTelemetrySdk() - .Clear() - .AddService("my-service", autoGenerateServiceInstanceId: false) - .Build(); - Assert.Single(resource.Attributes); - Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceName, "my-service"), resource.Attributes); - } + Assert.Equal(firstInstanceIdAttribute.Value, secondInstanceIdAttribute.Value); + } + + [Fact] + public void ClearTest() + { + var resource = ResourceBuilder.CreateEmpty() + .AddTelemetrySdk() + .Clear() + .AddService("my-service", autoGenerateServiceInstanceId: false) + .Build(); + Assert.Single(resource.Attributes); + Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceName, "my-service"), resource.Attributes); } } diff --git a/test/OpenTelemetry.Tests/Resources/ResourceTest.cs b/test/OpenTelemetry.Tests/Resources/ResourceTest.cs index 31d0fbdf4f3..337870a8a11 100644 --- a/test/OpenTelemetry.Tests/Resources/ResourceTest.cs +++ b/test/OpenTelemetry.Tests/Resources/ResourceTest.cs @@ -1,632 +1,618 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace OpenTelemetry.Resources.Tests +namespace OpenTelemetry.Resources.Tests; + +public class ResourceTest : IDisposable { - public class ResourceTest : IDisposable + private const string KeyName = "key"; + private const string ValueName = "value"; + + public ResourceTest() { - private const string KeyName = "key"; - private const string ValueName = "value"; + ClearEnvVars(); + } - public ResourceTest() - { - ClearEnvVars(); - } + public void Dispose() + { + ClearEnvVars(); + GC.SuppressFinalize(this); + } - public void Dispose() - { - ClearEnvVars(); - GC.SuppressFinalize(this); - } + [Fact] + public void CreateResource_NullAttributeCollection() + { + // Act and Assert + var resource = new Resource(null); + Assert.Empty(resource.Attributes); + } - [Fact] - public void CreateResource_NullAttributeCollection() - { - // Act and Assert - var resource = new Resource(null); - Assert.Empty(resource.Attributes); - } + [Fact] + public void CreateResource_NullAttributeValue() + { + // Arrange + var attributes = new Dictionary { { "NullValue", null } }; - [Fact] - public void CreateResource_NullAttributeValue() - { - // Arrange - var attributes = new Dictionary { { "NullValue", null } }; + // Act and Assert + Assert.Throws(() => new Resource(attributes)); + } - // Act and Assert - Assert.Throws(() => new Resource(attributes)); - } + [Fact] + public void CreateResource_EmptyAttributeKey() + { + // Arrange + var attributes = new Dictionary { { string.Empty, "value" } }; - [Fact] - public void CreateResource_EmptyAttributeKey() - { - // Arrange - var attributes = new Dictionary { { string.Empty, "value" } }; + // Act + var resource = new Resource(attributes); - // Act - var resource = new Resource(attributes); + // Assert + Assert.Single(resource.Attributes); - // Assert - Assert.Single(resource.Attributes); + var attribute = resource.Attributes.Single(); + Assert.Empty(attribute.Key); + Assert.Equal("value", attribute.Value); + } - var attribute = resource.Attributes.Single(); - Assert.Empty(attribute.Key); - Assert.Equal("value", attribute.Value); - } + [Fact] + public void CreateResource_EmptyAttributeValue() + { + // Arrange + var attributes = new Dictionary { { "EmptyValue", string.Empty } }; - [Fact] - public void CreateResource_EmptyAttributeValue() - { - // Arrange - var attributes = new Dictionary { { "EmptyValue", string.Empty } }; + // does not throw + var resource = new Resource(attributes); - // does not throw - var resource = new Resource(attributes); + // Assert + Assert.Single(resource.Attributes); + Assert.Contains(new KeyValuePair("EmptyValue", string.Empty), resource.Attributes); + } - // Assert - Assert.Single(resource.Attributes); - Assert.Contains(new KeyValuePair("EmptyValue", string.Empty), resource.Attributes); - } + [Fact] + public void CreateResource_EmptyArray() + { + // Arrange + var attributes = new Dictionary { { "EmptyArray", Array.Empty() } }; - [Fact] - public void CreateResource_EmptyArray() - { - // Arrange - var attributes = new Dictionary { { "EmptyArray", Array.Empty() } }; + // does not throw + var resource = new Resource(attributes); - // does not throw - var resource = new Resource(attributes); + // Assert + Assert.Single(resource.Attributes); + Assert.Equal(Array.Empty(), resource.Attributes.Where(x => x.Key == "EmptyArray").FirstOrDefault().Value); + } - // Assert - Assert.Single(resource.Attributes); - Assert.Equal(Array.Empty(), resource.Attributes.Where(x => x.Key == "EmptyArray").FirstOrDefault().Value); - } + [Fact] + public void CreateResource_EmptyAttribute() + { + // Arrange + var attributeCount = 0; + var attributes = CreateAttributes(attributeCount); - [Fact] - public void CreateResource_EmptyAttribute() - { - // Arrange - var attributeCount = 0; - var attributes = CreateAttributes(attributeCount); + // Act + var resource = new Resource(attributes); - // Act - var resource = new Resource(attributes); + // Assert + ValidateResource(resource, attributeCount); + } - // Assert - ValidateResource(resource, attributeCount); - } + [Fact] + public void CreateResource_SingleAttribute() + { + // Arrange + var attributeCount = 1; + var attributes = CreateAttributes(attributeCount); - [Fact] - public void CreateResource_SingleAttribute() - { - // Arrange - var attributeCount = 1; - var attributes = CreateAttributes(attributeCount); + // Act + var resource = new Resource(attributes); - // Act - var resource = new Resource(attributes); + // Assert + ValidateResource(resource, attributeCount); + } - // Assert - ValidateResource(resource, attributeCount); - } + [Fact] + public void CreateResource_MultipleAttribute() + { + // Arrange + var attributeCount = 5; + var attributes = CreateAttributes(attributeCount); - [Fact] - public void CreateResource_MultipleAttribute() - { - // Arrange - var attributeCount = 5; - var attributes = CreateAttributes(attributeCount); + // Act + var resource = new Resource(attributes); - // Act - var resource = new Resource(attributes); + // Assert + ValidateResource(resource, attributeCount); + } - // Assert - ValidateResource(resource, attributeCount); - } + [Fact] + public void CreateResource_SupportedAttributeTypes() + { + // Arrange + var attributes = new Dictionary + { + { "string", "stringValue" }, + { "bool", true }, + { "double", 0.1d }, + { "long", 1L }, + + // int and float supported by conversion to long and double + { "int", 1 }, + { "short", (short)1 }, + { "float", 0.1f }, + }; + + // Act + var resource = new Resource(attributes); + + // Assert + Assert.Equal(7, resource.Attributes.Count()); + Assert.Contains(new KeyValuePair("string", "stringValue"), resource.Attributes); + Assert.Contains(new KeyValuePair("bool", true), resource.Attributes); + Assert.Contains(new KeyValuePair("double", 0.1d), resource.Attributes); + Assert.Contains(new KeyValuePair("long", 1L), resource.Attributes); + + Assert.Contains(new KeyValuePair("int", 1L), resource.Attributes); + Assert.Contains(new KeyValuePair("short", 1L), resource.Attributes); + + double convertedFloat = Convert.ToDouble(0.1f, System.Globalization.CultureInfo.InvariantCulture); + Assert.Contains(new KeyValuePair("float", convertedFloat), resource.Attributes); + } - [Fact] - public void CreateResource_SupportedAttributeTypes() - { - // Arrange - var attributes = new Dictionary - { - { "string", "stringValue" }, - { "bool", true }, - { "double", 0.1d }, - { "long", 1L }, - - // int and float supported by conversion to long and double - { "int", 1 }, - { "short", (short)1 }, - { "float", 0.1f }, - }; - - // Act - var resource = new Resource(attributes); - - // Assert - Assert.Equal(7, resource.Attributes.Count()); - Assert.Contains(new KeyValuePair("string", "stringValue"), resource.Attributes); - Assert.Contains(new KeyValuePair("bool", true), resource.Attributes); - Assert.Contains(new KeyValuePair("double", 0.1d), resource.Attributes); - Assert.Contains(new KeyValuePair("long", 1L), resource.Attributes); - - Assert.Contains(new KeyValuePair("int", 1L), resource.Attributes); - Assert.Contains(new KeyValuePair("short", 1L), resource.Attributes); - - double convertedFloat = Convert.ToDouble(0.1f, System.Globalization.CultureInfo.InvariantCulture); - Assert.Contains(new KeyValuePair("float", convertedFloat), resource.Attributes); - } + [Fact] + public void CreateResource_SupportedAttributeArrayTypes() + { + // Arrange + var attributes = new Dictionary + { + // natively supported array types + { "string arr", new string[] { "stringValue" } }, + { "bool arr", new bool[] { true } }, + { "double arr", new double[] { 0.1d } }, + { "long arr", new long[] { 1L } }, + + // have to convert to other primitive array types + { "int arr", new int[] { 1 } }, + { "short arr", new short[] { (short)1 } }, + { "float arr", new float[] { 0.1f } }, + }; + + // Act + var resource = new Resource(attributes); + + // Assert + Assert.Equal(7, resource.Attributes.Count()); + Assert.Equal(new string[] { "stringValue" }, resource.Attributes.Where(x => x.Key == "string arr").FirstOrDefault().Value); + Assert.Equal(new bool[] { true }, resource.Attributes.Where(x => x.Key == "bool arr").FirstOrDefault().Value); + Assert.Equal(new double[] { 0.1d }, resource.Attributes.Where(x => x.Key == "double arr").FirstOrDefault().Value); + Assert.Equal(new long[] { 1L }, resource.Attributes.Where(x => x.Key == "long arr").FirstOrDefault().Value); + + var longArr = new long[] { 1 }; + var doubleArr = new double[] { Convert.ToDouble(0.1f, System.Globalization.CultureInfo.InvariantCulture) }; + Assert.Equal(longArr, resource.Attributes.Where(x => x.Key == "int arr").FirstOrDefault().Value); + Assert.Equal(longArr, resource.Attributes.Where(x => x.Key == "short arr").FirstOrDefault().Value); + Assert.Equal(doubleArr, resource.Attributes.Where(x => x.Key == "float arr").FirstOrDefault().Value); + } - [Fact] - public void CreateResource_SupportedAttributeArrayTypes() + [Fact] + public void CreateResource_NotSupportedAttributeTypes() + { + var attributes = new Dictionary { - // Arrange - var attributes = new Dictionary - { - // natively supported array types - { "string arr", new string[] { "stringValue" } }, - { "bool arr", new bool[] { true } }, - { "double arr", new double[] { 0.1d } }, - { "long arr", new long[] { 1L } }, - - // have to convert to other primitive array types - { "int arr", new int[] { 1 } }, - { "short arr", new short[] { (short)1 } }, - { "float arr", new float[] { 0.1f } }, - }; - - // Act - var resource = new Resource(attributes); - - // Assert - Assert.Equal(7, resource.Attributes.Count()); - Assert.Equal(new string[] { "stringValue" }, resource.Attributes.Where(x => x.Key == "string arr").FirstOrDefault().Value); - Assert.Equal(new bool[] { true }, resource.Attributes.Where(x => x.Key == "bool arr").FirstOrDefault().Value); - Assert.Equal(new double[] { 0.1d }, resource.Attributes.Where(x => x.Key == "double arr").FirstOrDefault().Value); - Assert.Equal(new long[] { 1L }, resource.Attributes.Where(x => x.Key == "long arr").FirstOrDefault().Value); - - var longArr = new long[] { 1 }; - var doubleArr = new double[] { Convert.ToDouble(0.1f, System.Globalization.CultureInfo.InvariantCulture) }; - Assert.Equal(longArr, resource.Attributes.Where(x => x.Key == "int arr").FirstOrDefault().Value); - Assert.Equal(longArr, resource.Attributes.Where(x => x.Key == "short arr").FirstOrDefault().Value); - Assert.Equal(doubleArr, resource.Attributes.Where(x => x.Key == "float arr").FirstOrDefault().Value); - } + { "dynamic", new { } }, + { "array", new int[1] }, + { "complex", this }, + }; - [Fact] - public void CreateResource_NotSupportedAttributeTypes() - { - var attributes = new Dictionary - { - { "dynamic", new { } }, - { "array", new int[1] }, - { "complex", this }, - }; - - Assert.Throws(() => new Resource(attributes)); - } + Assert.Throws(() => new Resource(attributes)); + } - [Fact] - public void MergeResource_EmptyAttributeSource_MultiAttributeTarget() - { - // Arrange - var sourceAttributeCount = 0; - var sourceAttributes = CreateAttributes(sourceAttributeCount); - var sourceResource = new Resource(sourceAttributes); + [Fact] + public void MergeResource_EmptyAttributeSource_MultiAttributeTarget() + { + // Arrange + var sourceAttributeCount = 0; + var sourceAttributes = CreateAttributes(sourceAttributeCount); + var sourceResource = new Resource(sourceAttributes); - var otherAttributeCount = 3; - var otherAttributes = CreateAttributes(otherAttributeCount); - var otherResource = new Resource(otherAttributes); + var otherAttributeCount = 3; + var otherAttributes = CreateAttributes(otherAttributeCount); + var otherResource = new Resource(otherAttributes); - // Act - var newResource = sourceResource.Merge(otherResource); + // Act + var newResource = sourceResource.Merge(otherResource); - // Assert - Assert.NotSame(otherResource, newResource); - Assert.NotSame(sourceResource, newResource); + // Assert + Assert.NotSame(otherResource, newResource); + Assert.NotSame(sourceResource, newResource); - ValidateResource(newResource, sourceAttributeCount + otherAttributeCount); - } + ValidateResource(newResource, sourceAttributeCount + otherAttributeCount); + } - [Fact] - public void MergeResource_MultiAttributeSource_EmptyAttributeTarget() - { - // Arrange - var sourceAttributeCount = 3; - var sourceAttributes = CreateAttributes(sourceAttributeCount); - var sourceResource = new Resource(sourceAttributes); - - var otherAttributeCount = 0; - var otherAttributes = CreateAttributes(otherAttributeCount); - var otherResource = new Resource(otherAttributes); - - // Act - var newResource = sourceResource.Merge(otherResource); - - // Assert - Assert.NotSame(otherResource, newResource); - Assert.NotSame(sourceResource, newResource); - ValidateResource(newResource, sourceAttributeCount + otherAttributeCount); - } + [Fact] + public void MergeResource_MultiAttributeSource_EmptyAttributeTarget() + { + // Arrange + var sourceAttributeCount = 3; + var sourceAttributes = CreateAttributes(sourceAttributeCount); + var sourceResource = new Resource(sourceAttributes); + + var otherAttributeCount = 0; + var otherAttributes = CreateAttributes(otherAttributeCount); + var otherResource = new Resource(otherAttributes); + + // Act + var newResource = sourceResource.Merge(otherResource); + + // Assert + Assert.NotSame(otherResource, newResource); + Assert.NotSame(sourceResource, newResource); + ValidateResource(newResource, sourceAttributeCount + otherAttributeCount); + } - [Fact] - public void MergeResource_MultiAttributeSource_MultiAttributeTarget_NoOverlap() - { - // Arrange - var sourceAttributeCount = 3; - var sourceAttributes = CreateAttributes(sourceAttributeCount); - var sourceResource = new Resource(sourceAttributes); - - var otherAttributeCount = 3; - var otherAttributes = CreateAttributes(otherAttributeCount, sourceAttributeCount); - var otherResource = new Resource(otherAttributes); - - // Act - var newResource = sourceResource.Merge(otherResource); - - // Assert - Assert.NotSame(otherResource, newResource); - Assert.NotSame(sourceResource, newResource); - ValidateResource(newResource, sourceAttributeCount + otherAttributeCount); - } + [Fact] + public void MergeResource_MultiAttributeSource_MultiAttributeTarget_NoOverlap() + { + // Arrange + var sourceAttributeCount = 3; + var sourceAttributes = CreateAttributes(sourceAttributeCount); + var sourceResource = new Resource(sourceAttributes); + + var otherAttributeCount = 3; + var otherAttributes = CreateAttributes(otherAttributeCount, sourceAttributeCount); + var otherResource = new Resource(otherAttributes); + + // Act + var newResource = sourceResource.Merge(otherResource); + + // Assert + Assert.NotSame(otherResource, newResource); + Assert.NotSame(sourceResource, newResource); + ValidateResource(newResource, sourceAttributeCount + otherAttributeCount); + } - [Fact] - public void MergeResource_MultiAttributeSource_MultiAttributeTarget_SingleOverlap() - { - // Arrange - var sourceAttributeCount = 3; - var sourceAttributes = CreateAttributes(sourceAttributeCount); - var sourceResource = new Resource(sourceAttributes); - - var otherAttributeCount = 3; - var otherAttributes = CreateAttributes(otherAttributeCount, sourceAttributeCount - 1); - var otherResource = new Resource(otherAttributes); - - // Act - var newResource = sourceResource.Merge(otherResource); - - // Assert - Assert.NotSame(otherResource, newResource); - Assert.NotSame(sourceResource, newResource); - ValidateResource(newResource, sourceAttributeCount + otherAttributeCount - 1); - - // Also verify target attributes were not overwritten - foreach (var otherAttribute in otherAttributes) - { - Assert.Contains(otherAttribute, otherResource.Attributes); - } - } + [Fact] + public void MergeResource_MultiAttributeSource_MultiAttributeTarget_SingleOverlap() + { + // Arrange + var sourceAttributeCount = 3; + var sourceAttributes = CreateAttributes(sourceAttributeCount); + var sourceResource = new Resource(sourceAttributes); - [Fact] - public void MergeResource_MultiAttributeSource_MultiAttributeTarget_FullOverlap() - { - // Arrange - var sourceAttributeCount = 3; - var sourceAttributes = CreateAttributes(sourceAttributeCount); - var sourceResource = new Resource(sourceAttributes); - - var otherAttributeCount = 3; - var otherAttributes = CreateAttributes(otherAttributeCount); - var otherResource = new Resource(otherAttributes); - - // Act - var newResource = sourceResource.Merge(otherResource); - - // Assert - Assert.NotSame(otherResource, newResource); - Assert.NotSame(sourceResource, newResource); - ValidateResource(newResource, otherAttributeCount); - - // Also verify target attributes were not overwritten - foreach (var otherAttribute in otherAttributes) - { - Assert.Contains(otherAttribute, otherResource.Attributes); - } - } + var otherAttributeCount = 3; + var otherAttributes = CreateAttributes(otherAttributeCount, sourceAttributeCount - 1); + var otherResource = new Resource(otherAttributes); + + // Act + var newResource = sourceResource.Merge(otherResource); + + // Assert + Assert.NotSame(otherResource, newResource); + Assert.NotSame(sourceResource, newResource); + ValidateResource(newResource, sourceAttributeCount + otherAttributeCount - 1); - [Fact] - public void MergeResource_MultiAttributeSource_DuplicatedKeysInPrimary() + // Also verify target attributes were not overwritten + foreach (var otherAttribute in otherAttributes) { - // Arrange - var sourceAttributes = new List> - { - new KeyValuePair("key1", "value1"), - new KeyValuePair("key1", "value1.1"), - }; - var sourceResource = new Resource(sourceAttributes); - - var otherAttributes = new List> - { - new KeyValuePair("key2", "value2"), - }; - - var otherResource = new Resource(otherAttributes); - - // Act - var newResource = sourceResource.Merge(otherResource); - - // Assert - Assert.NotSame(otherResource, newResource); - Assert.NotSame(sourceResource, newResource); - - Assert.Equal(2, newResource.Attributes.Count()); - Assert.Contains(new KeyValuePair("key1", "value1"), newResource.Attributes); - Assert.Contains(new KeyValuePair("key2", "value2"), newResource.Attributes); + Assert.Contains(otherAttribute, otherResource.Attributes); } + } - [Fact] - public void MergeResource_UpdatingResourceOverridesCurrentResource() - { - // Arrange - var currentAttributes = new Dictionary { { "value", "currentValue" } }; - var updatingAttributes = new Dictionary { { "value", "updatedValue" } }; - var currentResource = new Resource(currentAttributes); - var updatingResource = new Resource(updatingAttributes); + [Fact] + public void MergeResource_MultiAttributeSource_MultiAttributeTarget_FullOverlap() + { + // Arrange + var sourceAttributeCount = 3; + var sourceAttributes = CreateAttributes(sourceAttributeCount); + var sourceResource = new Resource(sourceAttributes); - var newResource = currentResource.Merge(updatingResource); + var otherAttributeCount = 3; + var otherAttributes = CreateAttributes(otherAttributeCount); + var otherResource = new Resource(otherAttributes); - // Assert - Assert.Single(newResource.Attributes); - Assert.Contains(new KeyValuePair("value", "updatedValue"), newResource.Attributes); - } + // Act + var newResource = sourceResource.Merge(otherResource); - [Fact] - public void GetResourceWithTelemetrySDKAttributes() - { - // Arrange - var resource = ResourceBuilder.CreateDefault().AddTelemetrySdk().Build(); - - // Assert - var attributes = resource.Attributes; - Assert.Equal(4, attributes.Count()); - ValidateDefaultAttributes(attributes); - ValidateTelemetrySdkAttributes(attributes); - } + // Assert + Assert.NotSame(otherResource, newResource); + Assert.NotSame(sourceResource, newResource); + ValidateResource(newResource, otherAttributeCount); - [Fact] - public void GetResourceWithDefaultAttributes_EmptyResource() + // Also verify target attributes were not overwritten + foreach (var otherAttribute in otherAttributes) { - // Arrange - var resource = ResourceBuilder.CreateDefault().Build(); - - // Assert - var attributes = resource.Attributes; - Assert.Equal(4, attributes.Count()); - ValidateDefaultAttributes(attributes); - ValidateTelemetrySdkAttributes(attributes); + Assert.Contains(otherAttribute, otherResource.Attributes); } + } - [Fact] - public void GetResourceWithDefaultAttributes_ResourceWithAttrs() + [Fact] + public void MergeResource_MultiAttributeSource_DuplicatedKeysInPrimary() + { + // Arrange + var sourceAttributes = new List> { - // Arrange - var resource = ResourceBuilder.CreateDefault().AddAttributes(CreateAttributes(2)).Build(); - - // Assert - var attributes = resource.Attributes; - Assert.Equal(6, attributes.Count()); - ValidateAttributes(attributes, 0, 1); - ValidateDefaultAttributes(attributes); - ValidateTelemetrySdkAttributes(attributes); - } + new KeyValuePair("key1", "value1"), + new KeyValuePair("key1", "value1.1"), + }; + var sourceResource = new Resource(sourceAttributes); - [Fact] - public void GetResourceWithDefaultAttributes_WithResourceEnvVar() + var otherAttributes = new List> { - // Arrange - Environment.SetEnvironmentVariable(OtelEnvResourceDetector.EnvVarKey, "EVKey1=EVVal1,EVKey2=EVVal2"); - var resource = ResourceBuilder.CreateDefault().AddAttributes(CreateAttributes(2)).Build(); - - // Assert - var attributes = resource.Attributes; - Assert.Equal(8, attributes.Count()); - ValidateAttributes(attributes, 0, 1); - ValidateDefaultAttributes(attributes); - Assert.Contains(new KeyValuePair("EVKey1", "EVVal1"), attributes); - Assert.Contains(new KeyValuePair("EVKey2", "EVVal2"), attributes); - ValidateTelemetrySdkAttributes(attributes); - } + new KeyValuePair("key2", "value2"), + }; - [Fact] - public void EnvironmentVariableDetectors_DoNotDuplicateAttributes() - { - // Arrange - Environment.SetEnvironmentVariable(OtelEnvResourceDetector.EnvVarKey, "EVKey1=EVVal1,EVKey2=EVVal2"); - var resource = ResourceBuilder.CreateDefault().AddEnvironmentVariableDetector().AddEnvironmentVariableDetector().Build(); - - // Assert - var attributes = resource.Attributes; - Assert.Equal(6, attributes.Count()); - Assert.Contains(new KeyValuePair("EVKey1", "EVVal1"), attributes); - Assert.Contains(new KeyValuePair("EVKey2", "EVVal2"), attributes); - ValidateTelemetrySdkAttributes(attributes); - } + var otherResource = new Resource(otherAttributes); - [Fact] - public void GetResource_WithServiceEnvVar() - { - // Arrange - Environment.SetEnvironmentVariable(OtelServiceNameEnvVarDetector.EnvVarKey, "some-service"); - var resource = ResourceBuilder.CreateDefault().AddAttributes(CreateAttributes(2)).Build(); - - // Assert - var attributes = resource.Attributes; - Assert.Equal(6, attributes.Count()); - ValidateAttributes(attributes, 0, 1); - Assert.Contains(new KeyValuePair("service.name", "some-service"), attributes); - ValidateTelemetrySdkAttributes(attributes); - } + // Act + var newResource = sourceResource.Merge(otherResource); - [Fact] - public void GetResource_WithServiceNameSetWithTwoEnvVars() - { - // Arrange - Environment.SetEnvironmentVariable(OtelEnvResourceDetector.EnvVarKey, "service.name=from-resource-attr"); - Environment.SetEnvironmentVariable(OtelServiceNameEnvVarDetector.EnvVarKey, "from-service-name"); - var resource = ResourceBuilder.CreateDefault().AddAttributes(CreateAttributes(2)).Build(); - - // Assert - var attributes = resource.Attributes; - Assert.Equal(6, attributes.Count()); - ValidateAttributes(attributes, 0, 1); - Assert.Contains(new KeyValuePair("service.name", "from-service-name"), attributes); - ValidateTelemetrySdkAttributes(attributes); - } + // Assert + Assert.NotSame(otherResource, newResource); + Assert.NotSame(sourceResource, newResource); - [Fact] - public void GetResource_WithServiceNameSetWithTwoEnvVarsAndCode() - { - // Arrange - Environment.SetEnvironmentVariable(OtelEnvResourceDetector.EnvVarKey, "service.name=from-resource-attr"); - Environment.SetEnvironmentVariable(OtelServiceNameEnvVarDetector.EnvVarKey, "from-service-name"); - var resource = ResourceBuilder.CreateDefault().AddService("from-code").AddAttributes(CreateAttributes(2)).Build(); - - // Assert - var attributes = resource.Attributes; - Assert.Equal(7, attributes.Count()); - ValidateAttributes(attributes, 0, 1); - Assert.Contains(new KeyValuePair("service.name", "from-code"), attributes); - ValidateTelemetrySdkAttributes(attributes); - } + Assert.Equal(2, newResource.Attributes.Count()); + Assert.Contains(new KeyValuePair("key1", "value1"), newResource.Attributes); + Assert.Contains(new KeyValuePair("key2", "value2"), newResource.Attributes); + } - [Fact] - public void ResourceBuilder_AddDetector_Test() - { - bool factoryExecuted = false; + [Fact] + public void MergeResource_UpdatingResourceOverridesCurrentResource() + { + // Arrange + var currentAttributes = new Dictionary { { "value", "currentValue" } }; + var updatingAttributes = new Dictionary { { "value", "updatedValue" } }; + var currentResource = new Resource(currentAttributes); + var updatingResource = new Resource(updatingAttributes); - var builder = ResourceBuilder.CreateDefault(); + var newResource = currentResource.Merge(updatingResource); - builder.AddDetector(sp => - { - factoryExecuted = true; - return new NoopResourceDetector(); - }); + // Assert + Assert.Single(newResource.Attributes); + Assert.Contains(new KeyValuePair("value", "updatedValue"), newResource.Attributes); + } - Assert.Throws(() => builder.Build()); - Assert.False(factoryExecuted); + [Fact] + public void GetResourceWithTelemetrySDKAttributes() + { + // Arrange + var resource = ResourceBuilder.CreateDefault().AddTelemetrySdk().Build(); + + // Assert + var attributes = resource.Attributes; + Assert.Equal(4, attributes.Count()); + ValidateDefaultAttributes(attributes); + ValidateTelemetrySdkAttributes(attributes); + } - var serviceCollection = new ServiceCollection(); - using var serviceProvider = serviceCollection.BuildServiceProvider(); + [Fact] + public void GetResourceWithDefaultAttributes_EmptyResource() + { + // Arrange + var resource = ResourceBuilder.CreateDefault().Build(); + + // Assert + var attributes = resource.Attributes; + Assert.Equal(4, attributes.Count()); + ValidateDefaultAttributes(attributes); + ValidateTelemetrySdkAttributes(attributes); + } - builder.ServiceProvider = serviceProvider; + [Fact] + public void GetResourceWithDefaultAttributes_ResourceWithAttrs() + { + // Arrange + var resource = ResourceBuilder.CreateDefault().AddAttributes(CreateAttributes(2)).Build(); + + // Assert + var attributes = resource.Attributes; + Assert.Equal(6, attributes.Count()); + ValidateAttributes(attributes, 0, 1); + ValidateDefaultAttributes(attributes); + ValidateTelemetrySdkAttributes(attributes); + } - var resource = builder.Build(); + [Fact] + public void GetResourceWithDefaultAttributes_WithResourceEnvVar() + { + // Arrange + Environment.SetEnvironmentVariable(OtelEnvResourceDetector.EnvVarKey, "EVKey1=EVVal1,EVKey2=EVVal2"); + var resource = ResourceBuilder.CreateDefault().AddAttributes(CreateAttributes(2)).Build(); + + // Assert + var attributes = resource.Attributes; + Assert.Equal(8, attributes.Count()); + ValidateAttributes(attributes, 0, 1); + ValidateDefaultAttributes(attributes); + Assert.Contains(new KeyValuePair("EVKey1", "EVVal1"), attributes); + Assert.Contains(new KeyValuePair("EVKey2", "EVVal2"), attributes); + ValidateTelemetrySdkAttributes(attributes); + } - Assert.True(factoryExecuted); - } + [Fact] + public void EnvironmentVariableDetectors_DoNotDuplicateAttributes() + { + // Arrange + Environment.SetEnvironmentVariable(OtelEnvResourceDetector.EnvVarKey, "EVKey1=EVVal1,EVKey2=EVVal2"); + var resource = ResourceBuilder.CreateDefault().AddEnvironmentVariableDetector().AddEnvironmentVariableDetector().Build(); + + // Assert + var attributes = resource.Attributes; + Assert.Equal(6, attributes.Count()); + Assert.Contains(new KeyValuePair("EVKey1", "EVVal1"), attributes); + Assert.Contains(new KeyValuePair("EVKey2", "EVVal2"), attributes); + ValidateTelemetrySdkAttributes(attributes); + } - [Fact] - public void ResourceBuilder_AddDetectorInternal_Test() - { - var builder = ResourceBuilder.CreateDefault(); + [Fact] + public void GetResource_WithServiceEnvVar() + { + // Arrange + Environment.SetEnvironmentVariable(OtelServiceNameEnvVarDetector.EnvVarKey, "some-service"); + var resource = ResourceBuilder.CreateDefault().AddAttributes(CreateAttributes(2)).Build(); + + // Assert + var attributes = resource.Attributes; + Assert.Equal(6, attributes.Count()); + ValidateAttributes(attributes, 0, 1); + Assert.Contains(new KeyValuePair("service.name", "some-service"), attributes); + ValidateTelemetrySdkAttributes(attributes); + } - bool nullTestRun = false; + [Fact] + public void GetResource_WithServiceNameSetWithTwoEnvVars() + { + // Arrange + Environment.SetEnvironmentVariable(OtelEnvResourceDetector.EnvVarKey, "service.name=from-resource-attr"); + Environment.SetEnvironmentVariable(OtelServiceNameEnvVarDetector.EnvVarKey, "from-service-name"); + var resource = ResourceBuilder.CreateDefault().AddAttributes(CreateAttributes(2)).Build(); + + // Assert + var attributes = resource.Attributes; + Assert.Equal(6, attributes.Count()); + ValidateAttributes(attributes, 0, 1); + Assert.Contains(new KeyValuePair("service.name", "from-service-name"), attributes); + ValidateTelemetrySdkAttributes(attributes); + } - builder.AddDetectorInternal(sp => - { - nullTestRun = true; - Assert.Null(sp); - return new NoopResourceDetector(); - }); + [Fact] + public void GetResource_WithServiceNameSetWithTwoEnvVarsAndCode() + { + // Arrange + Environment.SetEnvironmentVariable(OtelEnvResourceDetector.EnvVarKey, "service.name=from-resource-attr"); + Environment.SetEnvironmentVariable(OtelServiceNameEnvVarDetector.EnvVarKey, "from-service-name"); + var resource = ResourceBuilder.CreateDefault().AddService("from-code").AddAttributes(CreateAttributes(2)).Build(); + + // Assert + var attributes = resource.Attributes; + Assert.Equal(7, attributes.Count()); + ValidateAttributes(attributes, 0, 1); + Assert.Contains(new KeyValuePair("service.name", "from-code"), attributes); + ValidateTelemetrySdkAttributes(attributes); + } - builder.Build(); + [Fact] + public void ResourceBuilder_AddDetector_Test() + { + bool factoryExecuted = false; - Assert.True(nullTestRun); + var builder = ResourceBuilder.CreateDefault(); - builder = ResourceBuilder.CreateDefault(); + builder.AddDetector(sp => + { + factoryExecuted = true; + return new NoopResourceDetector(); + }); - bool validTestRun = false; + Assert.Throws(() => builder.Build()); + Assert.False(factoryExecuted); - var serviceCollection = new ServiceCollection(); - using var serviceProvider = serviceCollection.BuildServiceProvider(); + var serviceCollection = new ServiceCollection(); + using var serviceProvider = serviceCollection.BuildServiceProvider(); - builder.ServiceProvider = serviceProvider; + builder.ServiceProvider = serviceProvider; - builder.AddDetectorInternal(sp => - { - validTestRun = true; - Assert.NotNull(sp); - return new NoopResourceDetector(); - }); + var resource = builder.Build(); - builder.Build(); + Assert.True(factoryExecuted); + } - Assert.True(validTestRun); - } + [Fact] + public void ResourceBuilder_AddDetectorInternal_Test() + { + var builder = ResourceBuilder.CreateDefault(); - internal static void ValidateTelemetrySdkAttributes(IEnumerable> attributes) - { - Assert.Contains(new KeyValuePair("telemetry.sdk.name", "opentelemetry"), attributes); - Assert.Contains(new KeyValuePair("telemetry.sdk.language", "dotnet"), attributes); - var versionAttribute = attributes.Where(pair => pair.Key.Equals("telemetry.sdk.version")); - Assert.Single(versionAttribute); - } + bool nullTestRun = false; - internal static void ValidateDefaultAttributes(IEnumerable> attributes) + builder.AddDetectorInternal(sp => { - var serviceName = attributes.Where(pair => pair.Key.Equals("service.name")); - Assert.Single(serviceName); - Assert.Contains("unknown_service", serviceName.FirstOrDefault().Value as string); - } + nullTestRun = true; + Assert.Null(sp); + return new NoopResourceDetector(); + }); - private static void ClearEnvVars() - { - Environment.SetEnvironmentVariable(OtelEnvResourceDetector.EnvVarKey, null); - Environment.SetEnvironmentVariable(OtelServiceNameEnvVarDetector.EnvVarKey, null); - } + builder.Build(); - private static void AddAttributes(Dictionary attributes, int attributeCount, int startIndex = 0) - { - for (var i = startIndex; i < attributeCount + startIndex; ++i) - { - attributes.Add($"{KeyName}{i}", $"{ValueName}{i}"); - } - } + Assert.True(nullTestRun); - private static void ValidateAttributes(IEnumerable> attributes, int startIndex = 0, int endIndex = 0) - { - var keyValuePairs = attributes as KeyValuePair[] ?? attributes.ToArray(); - var endInd = endIndex == 0 ? keyValuePairs.Length - 1 : endIndex; - for (var i = startIndex; i <= endInd; ++i) - { - Assert.Contains(new KeyValuePair($"{KeyName}{i}", $"{ValueName}{i}"), keyValuePairs); - } - } + builder = ResourceBuilder.CreateDefault(); - private static void ValidateResource(Resource resource, int attributeCount) + bool validTestRun = false; + + var serviceCollection = new ServiceCollection(); + using var serviceProvider = serviceCollection.BuildServiceProvider(); + + builder.ServiceProvider = serviceProvider; + + builder.AddDetectorInternal(sp => { - Assert.NotNull(resource); - Assert.NotNull(resource.Attributes); - Assert.Equal(attributeCount, resource.Attributes.Count()); - ValidateAttributes(resource.Attributes); - } + validTestRun = true; + Assert.NotNull(sp); + return new NoopResourceDetector(); + }); + + builder.Build(); + + Assert.True(validTestRun); + } + + internal static void ValidateTelemetrySdkAttributes(IEnumerable> attributes) + { + Assert.Contains(new KeyValuePair("telemetry.sdk.name", "opentelemetry"), attributes); + Assert.Contains(new KeyValuePair("telemetry.sdk.language", "dotnet"), attributes); + var versionAttribute = attributes.Where(pair => pair.Key.Equals("telemetry.sdk.version")); + Assert.Single(versionAttribute); + } + + internal static void ValidateDefaultAttributes(IEnumerable> attributes) + { + var serviceName = attributes.Where(pair => pair.Key.Equals("service.name")); + Assert.Single(serviceName); + Assert.Contains("unknown_service", serviceName.FirstOrDefault().Value as string); + } + + private static void ClearEnvVars() + { + Environment.SetEnvironmentVariable(OtelEnvResourceDetector.EnvVarKey, null); + Environment.SetEnvironmentVariable(OtelServiceNameEnvVarDetector.EnvVarKey, null); + } - private static Dictionary CreateAttributes(int attributeCount, int startIndex = 0) + private static void AddAttributes(Dictionary attributes, int attributeCount, int startIndex = 0) + { + for (var i = startIndex; i < attributeCount + startIndex; ++i) { - var attributes = new Dictionary(); - AddAttributes(attributes, attributeCount, startIndex); - return attributes; + attributes.Add($"{KeyName}{i}", $"{ValueName}{i}"); } + } - private sealed class NoopResourceDetector : IResourceDetector + private static void ValidateAttributes(IEnumerable> attributes, int startIndex = 0, int endIndex = 0) + { + var keyValuePairs = attributes as KeyValuePair[] ?? attributes.ToArray(); + var endInd = endIndex == 0 ? keyValuePairs.Length - 1 : endIndex; + for (var i = startIndex; i <= endInd; ++i) { - public Resource Detect() => Resource.Empty; + Assert.Contains(new KeyValuePair($"{KeyName}{i}", $"{ValueName}{i}"), keyValuePairs); } } + + private static void ValidateResource(Resource resource, int attributeCount) + { + Assert.NotNull(resource); + Assert.NotNull(resource.Attributes); + Assert.Equal(attributeCount, resource.Attributes.Count()); + ValidateAttributes(resource.Attributes); + } + + private static Dictionary CreateAttributes(int attributeCount, int startIndex = 0) + { + var attributes = new Dictionary(); + AddAttributes(attributes, attributeCount, startIndex); + return attributes; + } + + private sealed class NoopResourceDetector : IResourceDetector + { + public Resource Detect() => Resource.Empty; + } } diff --git a/test/OpenTelemetry.Tests/SdkTests.cs b/test/OpenTelemetry.Tests/SdkTests.cs new file mode 100644 index 00000000000..f4d4908fc4b --- /dev/null +++ b/test/OpenTelemetry.Tests/SdkTests.cs @@ -0,0 +1,28 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using Xunit; + +namespace OpenTelemetry.Tests; + +public class SdkTests +{ + [Theory] + [InlineData(null, "1.0.0")] + [InlineData("1.5.0", "1.5.0")] + [InlineData("1.0.0.0", "1.0.0.0")] + [InlineData("1.0-beta.1", "1.0-beta.1")] + [InlineData("1.5.0-alpha.1.40+807f703e1b4d9874a92bd86d9f2d4ebe5b5d52e4", "1.5.0-alpha.1.40")] + [InlineData("1.5.0-rc.1+807f703e1b4d9874a92bd86d9f2d4ebe5b5d52e4", "1.5.0-rc.1")] + [InlineData("8.0", "8.0")] + [InlineData("8", "8")] + [InlineData("8.0.1.18-alpha1", "8.0.1.18-alpha1")] + public void ParseAssemblyInformationalVersionTests(string? informationalVersion, string expectedVersion) + { + var actualVersion = Sdk.ParseAssemblyInformationalVersion(informationalVersion); + + Assert.Equal(expectedVersion, actualVersion); + } +} diff --git a/test/OpenTelemetry.Tests/Shared/CustomTextMapPropagator.cs b/test/OpenTelemetry.Tests/Shared/CustomTextMapPropagator.cs new file mode 100644 index 00000000000..a3524f17cbf --- /dev/null +++ b/test/OpenTelemetry.Tests/Shared/CustomTextMapPropagator.cs @@ -0,0 +1,51 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using OpenTelemetry.Context.Propagation; + +namespace OpenTelemetry.Tests; + +internal sealed class CustomTextMapPropagator : TextMapPropagator +{ + private static readonly PropagationContext DefaultPropagationContext = default; + + public ActivityTraceId TraceId { get; set; } + + public ActivitySpanId SpanId { get; set; } + + public Action Injected { get; set; } + + public override ISet Fields => null; + +#pragma warning disable SA1201 // Elements should appear in the correct order +#pragma warning disable SA1010 // Opening square brackets should be spaced correctly + public Dictionary> InjectValues = []; +#pragma warning restore SA1010 // Opening square brackets should be spaced correctly +#pragma warning restore SA1201 // Elements should appear in the correct order + + public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) + { + if (this.TraceId != default && this.SpanId != default) + { + return new PropagationContext( + new ActivityContext( + this.TraceId, + this.SpanId, + ActivityTraceFlags.Recorded), + default); + } + + return DefaultPropagationContext; + } + + public override void Inject(PropagationContext context, T carrier, Action setter) + { + foreach (var kv in this.InjectValues) + { + setter(carrier, kv.Key, kv.Value.Invoke(context)); + } + + this.Injected?.Invoke(context); + } +} diff --git a/test/OpenTelemetry.Tests/Shared/DelegatingExporter.cs b/test/OpenTelemetry.Tests/Shared/DelegatingExporter.cs index 238d83cf037..c4615cb7fc5 100644 --- a/test/OpenTelemetry.Tests/Shared/DelegatingExporter.cs +++ b/test/OpenTelemetry.Tests/Shared/DelegatingExporter.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 namespace OpenTelemetry.Tests; diff --git a/test/OpenTelemetry.Tests/Shared/DelegatingProcessor.cs b/test/OpenTelemetry.Tests/Shared/DelegatingProcessor.cs index e5baf02b644..db5148a5ab6 100644 --- a/test/OpenTelemetry.Tests/Shared/DelegatingProcessor.cs +++ b/test/OpenTelemetry.Tests/Shared/DelegatingProcessor.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 namespace OpenTelemetry.Tests; diff --git a/test/OpenTelemetry.Tests/Shared/EnabledOnDockerPlatformTheoryAttribute.cs b/test/OpenTelemetry.Tests/Shared/EnabledOnDockerPlatformTheoryAttribute.cs index fd99e0c4499..4fda3e1a9bc 100644 --- a/test/OpenTelemetry.Tests/Shared/EnabledOnDockerPlatformTheoryAttribute.cs +++ b/test/OpenTelemetry.Tests/Shared/EnabledOnDockerPlatformTheoryAttribute.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Text; diff --git a/test/OpenTelemetry.Tests/Shared/EventSourceTestHelper.cs b/test/OpenTelemetry.Tests/Shared/EventSourceTestHelper.cs index 4478e6cee22..c0f5055d490 100644 --- a/test/OpenTelemetry.Tests/Shared/EventSourceTestHelper.cs +++ b/test/OpenTelemetry.Tests/Shared/EventSourceTestHelper.cs @@ -1,146 +1,132 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Tracing; using System.Globalization; using System.Reflection; -namespace OpenTelemetry.Tests +namespace OpenTelemetry.Tests; + +internal static class EventSourceTestHelper { - internal static class EventSourceTestHelper + public static void MethodsAreImplementedConsistentlyWithTheirAttributes(EventSource eventSource) { - public static void MethodsAreImplementedConsistentlyWithTheirAttributes(EventSource eventSource) + foreach (MethodInfo publicMethod in GetEventMethods(eventSource)) { - foreach (MethodInfo publicMethod in GetEventMethods(eventSource)) - { - VerifyMethodImplementation(eventSource, publicMethod); - } + VerifyMethodImplementation(eventSource, publicMethod); } + } - private static void VerifyMethodImplementation(EventSource eventSource, MethodInfo eventMethod) + private static void VerifyMethodImplementation(EventSource eventSource, MethodInfo eventMethod) + { + using var listener = new TestEventListener(); + listener.EnableEvents(eventSource, EventLevel.Verbose, EventKeywords.All); + try { - using var listener = new TestEventListener(); - listener.EnableEvents(eventSource, EventLevel.Verbose, EventKeywords.All); - try - { - object[] eventArguments = GenerateEventArguments(eventMethod); - eventMethod.Invoke(eventSource, eventArguments); + object[] eventArguments = GenerateEventArguments(eventMethod); + eventMethod.Invoke(eventSource, eventArguments); - EventWrittenEventArgs actualEvent = listener.Messages.FirstOrDefault(x => x.EventName == eventMethod.Name); + EventWrittenEventArgs actualEvent = listener.Messages.FirstOrDefault(x => x.EventName == eventMethod.Name); - if (actualEvent == null) + if (actualEvent == null) + { + // check for errors + actualEvent = listener.Messages.FirstOrDefault(x => x.EventId == 0); + if (actualEvent != null) { - // check for errors - actualEvent = listener.Messages.FirstOrDefault(x => x.EventId == 0); - if (actualEvent != null) - { - throw new Exception(actualEvent.Message); - } - - // give up - throw new Exception("Listener failed to collect event."); + throw new Exception(actualEvent.Message); } - VerifyEventId(eventMethod, actualEvent); - VerifyEventLevel(eventMethod, actualEvent); - VerifyEventMessage(eventMethod, actualEvent, eventArguments); + // give up + throw new Exception("Listener failed to collect event."); } - catch (Exception e) - { - var name = eventMethod.DeclaringType.Name + "." + eventMethod.Name; - throw new Exception("Method '" + name + "' is implemented incorrectly.", e); - } - finally - { - listener.ClearMessages(); - } + VerifyEventId(eventMethod, actualEvent); + VerifyEventLevel(eventMethod, actualEvent); + VerifyEventMessage(eventMethod, actualEvent, eventArguments); } - - private static object[] GenerateEventArguments(MethodInfo eventMethod) + catch (Exception e) { - ParameterInfo[] parameters = eventMethod.GetParameters(); - var arguments = new object[parameters.Length]; - for (int i = 0; i < parameters.Length; i++) - { - arguments[i] = GenerateArgument(parameters[i]); - } + var name = eventMethod.DeclaringType.Name + "." + eventMethod.Name; - return arguments; + throw new Exception("Method '" + name + "' is implemented incorrectly.", e); } - - private static object GenerateArgument(ParameterInfo parameter) + finally { - if (parameter.ParameterType == typeof(string)) - { - return "Test String"; - } - - if (parameter.ParameterType.IsValueType) - { - return Activator.CreateInstance(parameter.ParameterType); - } - - throw new NotSupportedException("Complex types are not supported"); + listener.ClearMessages(); } + } - private static void VerifyEventId(MethodInfo eventMethod, EventWrittenEventArgs actualEvent) + private static object[] GenerateEventArguments(MethodInfo eventMethod) + { + ParameterInfo[] parameters = eventMethod.GetParameters(); + var arguments = new object[parameters.Length]; + for (int i = 0; i < parameters.Length; i++) { - int expectedEventId = GetEventAttribute(eventMethod).EventId; - AssertEqual(nameof(VerifyEventId), expectedEventId, actualEvent.EventId); + arguments[i] = GenerateArgument(parameters[i]); } - private static void VerifyEventLevel(MethodInfo eventMethod, EventWrittenEventArgs actualEvent) - { - EventLevel expectedLevel = GetEventAttribute(eventMethod).Level; - AssertEqual(nameof(VerifyEventLevel), expectedLevel, actualEvent.Level); - } + return arguments; + } - private static void VerifyEventMessage(MethodInfo eventMethod, EventWrittenEventArgs actualEvent, object[] eventArguments) + private static object GenerateArgument(ParameterInfo parameter) + { + if (parameter.ParameterType == typeof(string)) { - string expectedMessage = eventArguments.Length == 0 - ? GetEventAttribute(eventMethod).Message - : string.Format(CultureInfo.InvariantCulture, GetEventAttribute(eventMethod).Message, eventArguments); - string actualMessage = string.Format(CultureInfo.InvariantCulture, actualEvent.Message, actualEvent.Payload.ToArray()); - AssertEqual(nameof(VerifyEventMessage), expectedMessage, actualMessage); + return "Test String"; } - private static void AssertEqual(string methodName, T expected, T actual) + if (parameter.ParameterType.IsValueType) { - if (!expected.Equals(actual)) - { - var errorMessage = string.Format( - CultureInfo.InvariantCulture, - "{0} Failed: expected: '{1}' actual: '{2}'", - methodName, - expected, - actual); - throw new Exception(errorMessage); - } + return Activator.CreateInstance(parameter.ParameterType); } - private static EventAttribute GetEventAttribute(MethodInfo eventMethod) - { - return (EventAttribute)eventMethod.GetCustomAttributes(typeof(EventAttribute), false).Single(); - } + throw new NotSupportedException("Complex types are not supported"); + } + + private static void VerifyEventId(MethodInfo eventMethod, EventWrittenEventArgs actualEvent) + { + int expectedEventId = GetEventAttribute(eventMethod).EventId; + AssertEqual(nameof(VerifyEventId), expectedEventId, actualEvent.EventId); + } + + private static void VerifyEventLevel(MethodInfo eventMethod, EventWrittenEventArgs actualEvent) + { + EventLevel expectedLevel = GetEventAttribute(eventMethod).Level; + AssertEqual(nameof(VerifyEventLevel), expectedLevel, actualEvent.Level); + } + + private static void VerifyEventMessage(MethodInfo eventMethod, EventWrittenEventArgs actualEvent, object[] eventArguments) + { + string expectedMessage = eventArguments.Length == 0 + ? GetEventAttribute(eventMethod).Message + : string.Format(CultureInfo.InvariantCulture, GetEventAttribute(eventMethod).Message, eventArguments); + string actualMessage = string.Format(CultureInfo.InvariantCulture, actualEvent.Message, actualEvent.Payload.ToArray()); + AssertEqual(nameof(VerifyEventMessage), expectedMessage, actualMessage); + } - private static IEnumerable GetEventMethods(EventSource eventSource) + private static void AssertEqual(string methodName, T expected, T actual) + { + if (!expected.Equals(actual)) { - MethodInfo[] methods = eventSource.GetType().GetMethods(); - return methods.Where(m => m.GetCustomAttributes(typeof(EventAttribute), false).Any()); + var errorMessage = string.Format( + CultureInfo.InvariantCulture, + "{0} Failed: expected: '{1}' actual: '{2}'", + methodName, + expected, + actual); + throw new Exception(errorMessage); } } + + private static EventAttribute GetEventAttribute(MethodInfo eventMethod) + { + return (EventAttribute)eventMethod.GetCustomAttributes(typeof(EventAttribute), false).Single(); + } + + private static IEnumerable GetEventMethods(EventSource eventSource) + { + MethodInfo[] methods = eventSource.GetType().GetMethods(); + return methods.Where(m => m.GetCustomAttributes(typeof(EventAttribute), false).Any()); + } } diff --git a/test/OpenTelemetry.Tests/Shared/IEEE754Double.cs b/test/OpenTelemetry.Tests/Shared/IEEE754Double.cs index 679830d334d..e897b09b29d 100644 --- a/test/OpenTelemetry.Tests/Shared/IEEE754Double.cs +++ b/test/OpenTelemetry.Tests/Shared/IEEE754Double.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Runtime.InteropServices; @@ -69,7 +56,7 @@ public static IEEE754Double FromULong(ulong value) public static IEEE754Double FromString(string value) { - return IEEE754Double.FromLong(Convert.ToInt64(value.Replace(" ", string.Empty), 2)); + return FromLong(Convert.ToInt64(value.Replace(" ", string.Empty), 2)); } public override string ToString() diff --git a/test/OpenTelemetry.Tests/Shared/InMemoryEventListener.cs b/test/OpenTelemetry.Tests/Shared/InMemoryEventListener.cs index dfaf067fe63..9768d619643 100644 --- a/test/OpenTelemetry.Tests/Shared/InMemoryEventListener.cs +++ b/test/OpenTelemetry.Tests/Shared/InMemoryEventListener.cs @@ -1,36 +1,22 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Collections.Concurrent; using System.Diagnostics.Tracing; -namespace OpenTelemetry.Tests +namespace OpenTelemetry.Tests; + +internal class InMemoryEventListener : EventListener { - internal class InMemoryEventListener : EventListener - { - public ConcurrentQueue Events = new(); + public ConcurrentQueue Events = new(); - public InMemoryEventListener(EventSource eventSource, EventLevel minLevel = EventLevel.Verbose) - { - this.EnableEvents(eventSource, minLevel); - } + public InMemoryEventListener(EventSource eventSource, EventLevel minLevel = EventLevel.Verbose) + { + this.EnableEvents(eventSource, minLevel); + } - protected override void OnEventWritten(EventWrittenEventArgs eventData) - { - this.Events.Enqueue(eventData); - } + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + this.Events.Enqueue(eventData); } } diff --git a/test/OpenTelemetry.Tests/Shared/MathHelper.cs b/test/OpenTelemetry.Tests/Shared/MathHelper.cs new file mode 100644 index 00000000000..11a7573700e --- /dev/null +++ b/test/OpenTelemetry.Tests/Shared/MathHelper.cs @@ -0,0 +1,71 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma warning disable SA1119 // Statement should not use unnecessary parenthesis + +namespace OpenTelemetry.Tests; + +internal static class MathHelper +{ + // Math.BitIncrement was introduced in .NET Core 3.0. + // This is the implementation from: + // https://github.com/dotnet/runtime/blob/v7.0.0/src/libraries/System.Private.CoreLib/src/System/Math.cs#L259 + public static double BitIncrement(double x) + { +#if NET6_0_OR_GREATER + return Math.BitIncrement(x); +#else + long bits = BitConverter.DoubleToInt64Bits(x); + + if (((bits >> 32) & 0x7FF00000) >= 0x7FF00000) + { + // NaN returns NaN + // -Infinity returns double.MinValue + // +Infinity returns +Infinity + + return (bits == unchecked((long)(0xFFF00000_00000000))) ? double.MinValue : x; + } + + if (bits == unchecked((long)(0x80000000_00000000))) + { + // -0.0 returns double.Epsilon + return double.Epsilon; + } + + // Negative values need to be decremented + // Positive values need to be incremented + + bits += ((bits < 0) ? -1 : +1); + return BitConverter.Int64BitsToDouble(bits); +#endif + } + + public static double BitDecrement(double x) + { +#if NET6_0_OR_GREATER + return Math.BitDecrement(x); +#else + long bits = BitConverter.DoubleToInt64Bits(x); + + if (((bits >> 32) & 0x7FF00000) >= 0x7FF00000) + { + // NaN returns NaN + // -Infinity returns -Infinity + // +Infinity returns double.MaxValue + return (bits == 0x7FF00000_00000000) ? double.MaxValue : x; + } + + if (bits == 0x00000000_00000000) + { + // +0.0 returns -double.Epsilon + return -double.Epsilon; + } + + // Negative values need to be incremented + // Positive values need to be decremented + + bits += ((bits < 0) ? +1 : -1); + return BitConverter.Int64BitsToDouble(bits); +#endif + } +} diff --git a/test/OpenTelemetry.Tests/Shared/RecordOnlySampler.cs b/test/OpenTelemetry.Tests/Shared/RecordOnlySampler.cs index 357e678c6fd..eb03f764e62 100644 --- a/test/OpenTelemetry.Tests/Shared/RecordOnlySampler.cs +++ b/test/OpenTelemetry.Tests/Shared/RecordOnlySampler.cs @@ -1,28 +1,14 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Trace; -namespace OpenTelemetry.Tests +namespace OpenTelemetry.Tests; + +internal class RecordOnlySampler : TestSampler { - internal class RecordOnlySampler : TestSampler + public override SamplingResult ShouldSample(in SamplingParameters param) { - public override SamplingResult ShouldSample(in SamplingParameters param) - { - return new SamplingResult(SamplingDecision.RecordOnly); - } + return new SamplingResult(SamplingDecision.RecordOnly); } } diff --git a/test/OpenTelemetry.Tests/Shared/SkipUnlessEnvVarFoundFactAttribute.cs b/test/OpenTelemetry.Tests/Shared/SkipUnlessEnvVarFoundFactAttribute.cs index 346c91a3df9..adfebcaf184 100644 --- a/test/OpenTelemetry.Tests/Shared/SkipUnlessEnvVarFoundFactAttribute.cs +++ b/test/OpenTelemetry.Tests/Shared/SkipUnlessEnvVarFoundFactAttribute.cs @@ -1,43 +1,29 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Xunit; -namespace OpenTelemetry.Tests +namespace OpenTelemetry.Tests; + +internal class SkipUnlessEnvVarFoundFactAttribute : FactAttribute { - internal class SkipUnlessEnvVarFoundFactAttribute : FactAttribute + public SkipUnlessEnvVarFoundFactAttribute(string environmentVariable) { - public SkipUnlessEnvVarFoundFactAttribute(string environmentVariable) + if (string.IsNullOrEmpty(GetEnvironmentVariable(environmentVariable))) { - if (string.IsNullOrEmpty(GetEnvironmentVariable(environmentVariable))) - { - this.Skip = $"Skipped because {environmentVariable} environment variable was not configured."; - } + this.Skip = $"Skipped because {environmentVariable} environment variable was not configured."; } + } - public static string GetEnvironmentVariable(string environmentVariableName) - { - string environmentVariableValue = Environment.GetEnvironmentVariable(environmentVariableName, EnvironmentVariableTarget.Process); - - if (string.IsNullOrEmpty(environmentVariableValue)) - { - environmentVariableValue = Environment.GetEnvironmentVariable(environmentVariableName, EnvironmentVariableTarget.Machine); - } + public static string GetEnvironmentVariable(string environmentVariableName) + { + string environmentVariableValue = Environment.GetEnvironmentVariable(environmentVariableName, EnvironmentVariableTarget.Process); - return environmentVariableValue; + if (string.IsNullOrEmpty(environmentVariableValue)) + { + environmentVariableValue = Environment.GetEnvironmentVariable(environmentVariableName, EnvironmentVariableTarget.Machine); } + + return environmentVariableValue; } } diff --git a/test/OpenTelemetry.Tests/Shared/SkipUnlessEnvVarFoundTheoryAttribute.cs b/test/OpenTelemetry.Tests/Shared/SkipUnlessEnvVarFoundTheoryAttribute.cs index 927610da19a..dc5ce2ca66f 100644 --- a/test/OpenTelemetry.Tests/Shared/SkipUnlessEnvVarFoundTheoryAttribute.cs +++ b/test/OpenTelemetry.Tests/Shared/SkipUnlessEnvVarFoundTheoryAttribute.cs @@ -1,42 +1,29 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + using Xunit; -namespace OpenTelemetry.Tests +namespace OpenTelemetry.Tests; + +internal class SkipUnlessEnvVarFoundTheoryAttribute : TheoryAttribute { - internal class SkipUnlessEnvVarFoundTheoryAttribute : TheoryAttribute + public SkipUnlessEnvVarFoundTheoryAttribute(string environmentVariable) { - public SkipUnlessEnvVarFoundTheoryAttribute(string environmentVariable) + if (string.IsNullOrEmpty(GetEnvironmentVariable(environmentVariable))) { - if (string.IsNullOrEmpty(GetEnvironmentVariable(environmentVariable))) - { - this.Skip = $"Skipped because {environmentVariable} environment variable was not configured."; - } + this.Skip = $"Skipped because {environmentVariable} environment variable was not configured."; } + } - public static string GetEnvironmentVariable(string environmentVariableName) - { - string environmentVariableValue = Environment.GetEnvironmentVariable(environmentVariableName, EnvironmentVariableTarget.Process); - - if (string.IsNullOrEmpty(environmentVariableValue)) - { - environmentVariableValue = Environment.GetEnvironmentVariable(environmentVariableName, EnvironmentVariableTarget.Machine); - } + public static string GetEnvironmentVariable(string environmentVariableName) + { + string environmentVariableValue = Environment.GetEnvironmentVariable(environmentVariableName, EnvironmentVariableTarget.Process); - return environmentVariableValue; + if (string.IsNullOrEmpty(environmentVariableValue)) + { + environmentVariableValue = Environment.GetEnvironmentVariable(environmentVariableName, EnvironmentVariableTarget.Machine); } + + return environmentVariableValue; } } diff --git a/test/OpenTelemetry.Tests/Shared/SkipUnlessTrueTheoryAttribute.cs b/test/OpenTelemetry.Tests/Shared/SkipUnlessTrueTheoryAttribute.cs new file mode 100644 index 00000000000..087bff4366f --- /dev/null +++ b/test/OpenTelemetry.Tests/Shared/SkipUnlessTrueTheoryAttribute.cs @@ -0,0 +1,33 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Reflection; +using OpenTelemetry.Internal; +using Xunit; + +namespace OpenTelemetry.Tests; + +internal sealed class SkipUnlessTrueTheoryAttribute : TheoryAttribute +{ + public SkipUnlessTrueTheoryAttribute(Type typeContainingTest, string testFieldName, string skipMessage) + { + Guard.ThrowIfNull(typeContainingTest); + Guard.ThrowIfNullOrEmpty(testFieldName); + Guard.ThrowIfNullOrEmpty(skipMessage); + + var field = typeContainingTest.GetField(testFieldName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) + ?? throw new InvalidOperationException($"Static field '{testFieldName}' could not be found on '{typeContainingTest}' type."); + + if (field.FieldType != typeof(Func)) + { + throw new InvalidOperationException($"Field '{testFieldName}' on '{typeContainingTest}' type should be defined as '{typeof(Func)}'."); + } + + var testFunc = (Func)field.GetValue(null); + + if (!testFunc()) + { + this.Skip = skipMessage; + } + } +} diff --git a/test/OpenTelemetry.Tests/Shared/TagTransformerJsonHelperTest.cs b/test/OpenTelemetry.Tests/Shared/TagTransformerJsonHelperTest.cs new file mode 100644 index 00000000000..14a310d2248 --- /dev/null +++ b/test/OpenTelemetry.Tests/Shared/TagTransformerJsonHelperTest.cs @@ -0,0 +1,140 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Internal; +using Xunit; + +namespace OpenTelemetry.Tests.Shared; + +public class TagTransformerJsonHelperTest +{ + [Theory] + [InlineData(new object[] { new char[] { } })] + [InlineData(new object[] { new char[] { 'a' } })] + [InlineData(new object[] { new char[] { '1', '2', '3' } })] + public void CharArray(char[] data) + { + VerifySerialization(data); + } + + [Theory] + [InlineData(new object[] { new string[] { } })] + [InlineData(new object[] { new string[] { "one" } })] + [InlineData(new object[] { new string[] { "" } })] + [InlineData(new object[] { new string[] { "a", "b", "c", "d" } })] + [InlineData(new object[] { new string[] { "\r\n", "\t", "\"" } })] + [InlineData(new object[] { new string[] { "longlonglonglonglonglonglonglonglong" } })] + public void StringArray(string[] data) + { + VerifySerialization(data); + } + + [Theory] + [InlineData(new object[] { new bool[] { } })] + [InlineData(new object[] { new bool[] { true } })] + [InlineData(new object[] { new bool[] { true, false, false, true } })] + public void BooleanArray(bool[] data) + { + VerifySerialization(data); + } + + [Theory] + [InlineData(new object[] { new byte[] { } })] + [InlineData(new object[] { new byte[] { 0 } })] + [InlineData(new object[] { new byte[] { byte.MaxValue, byte.MinValue, 4, 13 } })] + public void ByteArray(byte[] data) + { + VerifySerialization(data); + } + + [Theory] + [InlineData(new object[] { new sbyte[] { } })] + [InlineData(new object[] { new sbyte[] { 0 } })] + [InlineData(new object[] { new sbyte[] { sbyte.MaxValue, sbyte.MinValue, 4, 13 } })] + public void SByteArray(sbyte[] data) + { + VerifySerialization(data); + } + + [Theory] + [InlineData(new object[] { new short[] { } })] + [InlineData(new object[] { new short[] { 0 } })] + [InlineData(new object[] { new short[] { short.MaxValue, short.MinValue, 4, 13 } })] + public void ShortArray(short[] data) + { + VerifySerialization(data); + } + + [Theory] + [InlineData(new object[] { new ushort[] { } })] + [InlineData(new object[] { new ushort[] { 0 } })] + [InlineData(new object[] { new ushort[] { ushort.MaxValue, ushort.MinValue, 4, 13 } })] + public void UShortArray(ushort[] data) + { + VerifySerialization(data); + } + + [Theory] + [InlineData(new object[] { new int[] { } })] + [InlineData(new object[] { new int[] { 0 } })] + [InlineData(new object[] { new int[] { int.MaxValue, int.MinValue, 4, 13 } })] + public void IntArray(int[] data) + { + VerifySerialization(data); + } + + [Theory] + [InlineData(new object[] { new uint[] { } })] + [InlineData(new object[] { new uint[] { 0 } })] + [InlineData(new object[] { new uint[] { uint.MaxValue, uint.MinValue, 4, 13 } })] + public void UIntArray(uint[] data) + { + VerifySerialization(data); + } + + [Theory] + [InlineData(new object[] { new long[] { } })] + [InlineData(new object[] { new long[] { 0 } })] + [InlineData(new object[] { new long[] { long.MaxValue, long.MinValue, 4, 13 } })] + public void LongArray(long[] data) + { + VerifySerialization(data); + } + + [Theory] + [InlineData(new object[] { new ulong[] { } })] + [InlineData(new object[] { new ulong[] { 0 } })] + [InlineData(new object[] { new ulong[] { ulong.MaxValue, ulong.MinValue, 4, 13 } })] + public void ULongArray(ulong[] data) + { + VerifySerialization(data); + } + + [Theory] + [InlineData(new object[] { new float[] { } })] + [InlineData(new object[] { new float[] { 0 } })] + [InlineData(new object[] { new float[] { float.MaxValue, float.MinValue, 4, 13 } })] + [InlineData(new object[] { new float[] { float.Epsilon } })] + public void FloatArray(float[] data) + { + VerifySerialization(data); + } + + [Theory] + [InlineData(new object[] { new double[] { } })] + [InlineData(new object[] { new double[] { 0 } })] + [InlineData(new object[] { new double[] { double.MaxValue, double.MinValue, 4, 13 } })] + [InlineData(new object[] { new double[] { double.Epsilon } })] + public void DoubleArray(double[] data) + { + VerifySerialization(data); + } + + private static void VerifySerialization(Array data) + { + var reflectionBasedResult = System.Text.Json.JsonSerializer.Serialize(data); + var rawResult = TagTransformerJsonHelper.JsonSerializeArrayTag(data); + + Assert.Equal(reflectionBasedResult, rawResult); + } +} diff --git a/test/OpenTelemetry.Tests/Shared/TestActivityExportProcessor.cs b/test/OpenTelemetry.Tests/Shared/TestActivityExportProcessor.cs index ac5e00c23f2..092ce859c67 100644 --- a/test/OpenTelemetry.Tests/Shared/TestActivityExportProcessor.cs +++ b/test/OpenTelemetry.Tests/Shared/TestActivityExportProcessor.cs @@ -1,35 +1,21 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -namespace OpenTelemetry.Tests +namespace OpenTelemetry.Tests; + +internal class TestActivityExportProcessor : SimpleActivityExportProcessor { - internal class TestActivityExportProcessor : SimpleActivityExportProcessor - { - public List ExportedItems = new(); + public List ExportedItems = new(); - public TestActivityExportProcessor(BaseExporter exporter) - : base(exporter) - { - } + public TestActivityExportProcessor(BaseExporter exporter) + : base(exporter) + { + } - protected override void OnExport(Activity data) - { - this.ExportedItems.Add(data); - } + protected override void OnExport(Activity data) + { + this.ExportedItems.Add(data); } } diff --git a/test/OpenTelemetry.Tests/Shared/TestActivityProcessor.cs b/test/OpenTelemetry.Tests/Shared/TestActivityProcessor.cs index d9be8568804..36d47b7b8fa 100644 --- a/test/OpenTelemetry.Tests/Shared/TestActivityProcessor.cs +++ b/test/OpenTelemetry.Tests/Shared/TestActivityProcessor.cs @@ -1,69 +1,55 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -namespace OpenTelemetry.Tests +namespace OpenTelemetry.Tests; + +internal class TestActivityProcessor : BaseProcessor { - internal class TestActivityProcessor : BaseProcessor - { - public Action StartAction; - public Action EndAction; + public Action StartAction; + public Action EndAction; - public TestActivityProcessor() - { - } + public TestActivityProcessor() + { + } - public TestActivityProcessor(Action onStart, Action onEnd) - { - this.StartAction = onStart; - this.EndAction = onEnd; - } + public TestActivityProcessor(Action onStart, Action onEnd) + { + this.StartAction = onStart; + this.EndAction = onEnd; + } - public bool ShutdownCalled { get; private set; } = false; + public bool ShutdownCalled { get; private set; } = false; - public bool ForceFlushCalled { get; private set; } = false; + public bool ForceFlushCalled { get; private set; } = false; - public bool DisposedCalled { get; private set; } = false; + public bool DisposedCalled { get; private set; } = false; - public override void OnStart(Activity span) - { - this.StartAction?.Invoke(span); - } + public override void OnStart(Activity span) + { + this.StartAction?.Invoke(span); + } - public override void OnEnd(Activity span) - { - this.EndAction?.Invoke(span); - } + public override void OnEnd(Activity span) + { + this.EndAction?.Invoke(span); + } - protected override bool OnForceFlush(int timeoutMilliseconds) - { - this.ForceFlushCalled = true; - return true; - } + protected override bool OnForceFlush(int timeoutMilliseconds) + { + this.ForceFlushCalled = true; + return true; + } - protected override bool OnShutdown(int timeoutMilliseconds) - { - this.ShutdownCalled = true; - return true; - } + protected override bool OnShutdown(int timeoutMilliseconds) + { + this.ShutdownCalled = true; + return true; + } - protected override void Dispose(bool disposing) - { - this.DisposedCalled = true; - } + protected override void Dispose(bool disposing) + { + this.DisposedCalled = true; } } diff --git a/test/OpenTelemetry.Tests/Shared/TestEventListener.cs b/test/OpenTelemetry.Tests/Shared/TestEventListener.cs index 95f4cccf9ab..b0cf1d0b120 100644 --- a/test/OpenTelemetry.Tests/Shared/TestEventListener.cs +++ b/test/OpenTelemetry.Tests/Shared/TestEventListener.cs @@ -1,101 +1,87 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Tracing; -namespace OpenTelemetry.Tests +namespace OpenTelemetry.Tests; + +/// +/// Event listener for testing event sources. +/// +internal class TestEventListener : EventListener { + /// Unique Id used to identify events from the test thread. + private readonly Guid activityId; + + /// A queue of events that have been logged. + private readonly List events; + /// - /// Event listener for testing event sources. + /// Lock for event writing tracking. /// - internal class TestEventListener : EventListener - { - /// Unique Id used to identify events from the test thread. - private readonly Guid activityId; + private readonly AutoResetEvent eventWritten; - /// A queue of events that have been logged. - private readonly List events; - - /// - /// Lock for event writing tracking. - /// - private readonly AutoResetEvent eventWritten; + /// + /// Initializes a new instance of the class. + /// + public TestEventListener() + { + this.activityId = Guid.NewGuid(); + EventSource.SetCurrentThreadActivityId(this.activityId); - /// - /// Initializes a new instance of the class. - /// - public TestEventListener() + this.events = new List(); + this.eventWritten = new AutoResetEvent(false); + this.OnOnEventWritten = e => { - this.activityId = Guid.NewGuid(); - EventSource.SetCurrentThreadActivityId(this.activityId); - - this.events = new List(); - this.eventWritten = new AutoResetEvent(false); - this.OnOnEventWritten = e => - { - this.events.Add(e); - this.eventWritten.Set(); - }; - } + this.events.Add(e); + this.eventWritten.Set(); + }; + } - /// Gets or sets the handler for event source creation. - public Action OnOnEventSourceCreated { get; set; } + /// Gets or sets the handler for event source creation. + public Action OnOnEventSourceCreated { get; set; } - /// Gets or sets the handler for event source writes. - public Action OnOnEventWritten { get; set; } + /// Gets or sets the handler for event source writes. + public Action OnOnEventWritten { get; set; } - /// Gets the events that have been written. - public IList Messages + /// Gets the events that have been written. + public IList Messages + { + get { - get + if (this.events.Count == 0) { - if (this.events.Count == 0) - { - this.eventWritten.WaitOne(TimeSpan.FromSeconds(5)); - } - - return this.events; + this.eventWritten.WaitOne(TimeSpan.FromSeconds(5)); } - } - /// - /// Clears all event messages so that testing can assert expected counts. - /// - public void ClearMessages() - { - this.events.Clear(); + return this.events; } + } - /// Handler for event source writes. - /// The event data that was written. - protected override void OnEventWritten(EventWrittenEventArgs eventData) - { - if (eventData.ActivityId == this.activityId) - { - this.OnOnEventWritten(eventData); - } - } + /// + /// Clears all event messages so that testing can assert expected counts. + /// + public void ClearMessages() + { + this.events.Clear(); + } - /// Handler for event source creation. - /// The event source that was created. - protected override void OnEventSourceCreated(EventSource eventSource) + /// Handler for event source writes. + /// The event data that was written. + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + if (eventData.ActivityId == this.activityId) { - // Check for null because this method is called by the base class constructor before we can initialize it - Action callback = this.OnOnEventSourceCreated; - callback?.Invoke(eventSource); + this.OnOnEventWritten(eventData); } } + + /// Handler for event source creation. + /// The event source that was created. + protected override void OnEventSourceCreated(EventSource eventSource) + { + // Check for null because this method is called by the base class constructor before we can initialize it + Action callback = this.OnOnEventSourceCreated; + callback?.Invoke(eventSource); + } } diff --git a/test/OpenTelemetry.Tests/Shared/TestHttpServer.cs b/test/OpenTelemetry.Tests/Shared/TestHttpServer.cs index 4daceee7b6e..5965e5fecf4 100644 --- a/test/OpenTelemetry.Tests/Shared/TestHttpServer.cs +++ b/test/OpenTelemetry.Tests/Shared/TestHttpServer.cs @@ -1,109 +1,103 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + using System.Net; -namespace OpenTelemetry.Tests +namespace OpenTelemetry.Tests; + +internal class TestHttpServer { - internal class TestHttpServer + private static readonly Random GlobalRandom = new(); + + public static IDisposable RunServer(Action action, out string host, out int port) { - private static readonly Random GlobalRandom = new(); + host = "localhost"; + port = 0; + RunningServer server = null; - public static IDisposable RunServer(Action action, out string host, out int port) + var retryCount = 5; + while (retryCount > 0) { - host = "localhost"; - port = 0; - RunningServer server = null; - - var retryCount = 5; - while (retryCount > 0) + try { - try - { - port = GlobalRandom.Next(2000, 5000); - server = new RunningServer(action, host, port); - server.Start(); - break; - } - catch (HttpListenerException) - { - retryCount--; - } + port = GlobalRandom.Next(2000, 5000); + server = new RunningServer(action, host, port); + server.Start(); + break; + } + catch (HttpListenerException) + { + server?.Dispose(); + server = null; + retryCount--; } - - return server; } - private class RunningServer : IDisposable + if (server == null) { - private readonly Task httpListenerTask; - private readonly HttpListener listener; - private readonly AutoResetEvent initialized = new(false); + throw new InvalidOperationException("Server could not be started."); + } - public RunningServer(Action action, string host, int port) - { - this.listener = new HttpListener(); + return server; + } + + private class RunningServer : IDisposable + { + private readonly Task httpListenerTask; + private readonly HttpListener listener; + private readonly AutoResetEvent initialized = new(false); + + public RunningServer(Action action, string host, int port) + { + this.listener = new HttpListener(); - this.listener.Prefixes.Add($"http://{host}:{port}/"); - this.listener.Start(); + this.listener.Prefixes.Add($"http://{host}:{port}/"); + this.listener.Start(); - this.httpListenerTask = new Task(async () => + this.httpListenerTask = new Task(async () => + { + while (true) { - while (true) + try { - try - { - var ctxTask = this.listener.GetContextAsync(); + var ctxTask = this.listener.GetContextAsync(); - this.initialized.Set(); + this.initialized.Set(); - action(await ctxTask.ConfigureAwait(false)); - } - catch (Exception ex) + action(await ctxTask); + } + catch (Exception ex) + { + if (ex is ObjectDisposedException + || (ex is HttpListenerException httpEx && httpEx.ErrorCode == 995)) { - if (ex is ObjectDisposedException - || (ex is HttpListenerException httpEx && httpEx.ErrorCode == 995)) - { - // Listener was closed before we got into GetContextAsync or - // Listener was closed while we were in GetContextAsync. - break; - } - - throw; + // Listener was closed before we got into GetContextAsync or + // Listener was closed while we were in GetContextAsync. + break; } + + throw; } - }); - } + } + }); + } - public void Start() + public void Start() + { + this.httpListenerTask.Start(); + this.initialized.WaitOne(); + } + + public void Dispose() + { + try { - this.httpListenerTask.Start(); - this.initialized.WaitOne(); + this.listener.Close(); + this.httpListenerTask?.Wait(); } - - public void Dispose() + catch (ObjectDisposedException) { - try - { - this.listener.Close(); - this.httpListenerTask?.Wait(); - } - catch (ObjectDisposedException) - { - // swallow this exception just in case - } + // swallow this exception just in case } } } diff --git a/test/OpenTelemetry.Tests/Shared/TestSampler.cs b/test/OpenTelemetry.Tests/Shared/TestSampler.cs index 63da229489d..7bc39ba454c 100644 --- a/test/OpenTelemetry.Tests/Shared/TestSampler.cs +++ b/test/OpenTelemetry.Tests/Shared/TestSampler.cs @@ -1,33 +1,19 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Trace; -namespace OpenTelemetry.Tests +namespace OpenTelemetry.Tests; + +internal class TestSampler : Sampler { - internal class TestSampler : Sampler - { - public Func SamplingAction { get; set; } + public Func SamplingAction { get; set; } - public SamplingParameters LatestSamplingParameters { get; private set; } + public SamplingParameters LatestSamplingParameters { get; private set; } - public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) - { - this.LatestSamplingParameters = samplingParameters; - return this.SamplingAction?.Invoke(samplingParameters) ?? new SamplingResult(SamplingDecision.RecordAndSample); - } + public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) + { + this.LatestSamplingParameters = samplingParameters; + return this.SamplingAction?.Invoke(samplingParameters) ?? new SamplingResult(SamplingDecision.RecordAndSample); } } diff --git a/test/OpenTelemetry.Tests/Shared/Utils.cs b/test/OpenTelemetry.Tests/Shared/Utils.cs index eb9697dcf0f..d85c8c74e47 100644 --- a/test/OpenTelemetry.Tests/Shared/Utils.cs +++ b/test/OpenTelemetry.Tests/Shared/Utils.cs @@ -1,35 +1,21 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Runtime.CompilerServices; -namespace OpenTelemetry.Tests +namespace OpenTelemetry.Tests; + +internal class Utils { - internal class Utils + [MethodImpl(MethodImplOptions.NoInlining)] + public static string GetCurrentMethodName() { - [MethodImpl(MethodImplOptions.NoInlining)] - public static string GetCurrentMethodName() - { - var method = new StackFrame(1).GetMethod(); + var method = new StackFrame(1).GetMethod(); - Debug.Assert(method != null, "Failed to get Method from the executing stack."); - Debug.Assert(method!.DeclaringType != null, "DeclaringType is not expected to be null."); + Debug.Assert(method != null, "Failed to get Method from the executing stack."); + Debug.Assert(method!.DeclaringType != null, "DeclaringType is not expected to be null."); - return $"{method.DeclaringType!.FullName}.{method.Name}"; - } + return $"{method.DeclaringType!.FullName}.{method.Name}"; } } diff --git a/test/OpenTelemetry.Tests/SimpleExportProcessorTest.cs b/test/OpenTelemetry.Tests/SimpleExportProcessorTest.cs index 70b2ab311bf..3e5b92c5b52 100644 --- a/test/OpenTelemetry.Tests/SimpleExportProcessorTest.cs +++ b/test/OpenTelemetry.Tests/SimpleExportProcessorTest.cs @@ -1,58 +1,44 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Xunit; -namespace OpenTelemetry.Tests +namespace OpenTelemetry.Tests; + +public class SimpleExportProcessorTest { - public class SimpleExportProcessorTest + [Fact] + public void Verify_SimpleExportProcessor_HandlesException() { - [Fact] - public void Verify_SimpleExportProcessor_HandlesException() - { - int counter = 0; + int counter = 0; - // here our exporter will throw an exception. - var testExporter = new DelegatingExporter + // here our exporter will throw an exception. + var testExporter = new DelegatingExporter + { + OnExportFunc = (batch) => { - OnExportFunc = (batch) => - { - counter++; - throw new Exception("test exception"); - }, - }; + counter++; + throw new Exception("test exception"); + }, + }; - var testSimpleExportProcessor = new TestSimpleExportProcessor(testExporter); + var testSimpleExportProcessor = new TestSimpleExportProcessor(testExporter); - // Verify that the Processor catches and suppresses the exception. - testSimpleExportProcessor.OnEnd(new object()); + // Verify that the Processor catches and suppresses the exception. + testSimpleExportProcessor.OnEnd(new object()); - // verify Exporter OnExport wall called. - Assert.Equal(1, counter); - } + // verify Exporter OnExport wall called. + Assert.Equal(1, counter); + } - /// - /// Testable class for abstract . - /// - public class TestSimpleExportProcessor : SimpleExportProcessor + /// + /// Testable class for abstract . + /// + public class TestSimpleExportProcessor : SimpleExportProcessor + { + public TestSimpleExportProcessor(BaseExporter exporter) + : base(exporter) { - public TestSimpleExportProcessor(BaseExporter exporter) - : base(exporter) - { - } } } } diff --git a/test/OpenTelemetry.Tests/SuppressInstrumentationTest.cs b/test/OpenTelemetry.Tests/SuppressInstrumentationTest.cs index 7e53508dc62..d562699fec8 100644 --- a/test/OpenTelemetry.Tests/SuppressInstrumentationTest.cs +++ b/test/OpenTelemetry.Tests/SuppressInstrumentationTest.cs @@ -1,115 +1,101 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Xunit; -namespace OpenTelemetry.Tests +namespace OpenTelemetry.Tests; + +public class SuppressInstrumentationTest { - public class SuppressInstrumentationTest + [Fact] + public static void UsingSuppressInstrumentation() { - [Fact] - public static void UsingSuppressInstrumentation() + Assert.False(Sdk.SuppressInstrumentation); + + using (var scope = SuppressInstrumentationScope.Begin()) { - Assert.False(Sdk.SuppressInstrumentation); + Assert.True(Sdk.SuppressInstrumentation); - using (var scope = SuppressInstrumentationScope.Begin()) + using (var innerScope = SuppressInstrumentationScope.Begin()) { - Assert.True(Sdk.SuppressInstrumentation); - - using (var innerScope = SuppressInstrumentationScope.Begin()) - { - innerScope.Dispose(); + innerScope.Dispose(); - Assert.True(Sdk.SuppressInstrumentation); - - scope.Dispose(); - } + Assert.True(Sdk.SuppressInstrumentation); - Assert.False(Sdk.SuppressInstrumentation); + scope.Dispose(); } Assert.False(Sdk.SuppressInstrumentation); } - [Theory] - [InlineData(null)] - [InlineData(false)] - [InlineData(true)] - public void SuppressInstrumentationBeginTest(bool? shouldBegin) - { - Assert.False(Sdk.SuppressInstrumentation); + Assert.False(Sdk.SuppressInstrumentation); + } - using var scope = shouldBegin.HasValue ? SuppressInstrumentationScope.Begin(shouldBegin.Value) : SuppressInstrumentationScope.Begin(); - if (shouldBegin.HasValue) - { - Assert.Equal(shouldBegin.Value, Sdk.SuppressInstrumentation); - } - else - { - Assert.True(Sdk.SuppressInstrumentation); // Default behavior is to pass true and suppress the instrumentation - } - } + [Theory] + [InlineData(null)] + [InlineData(false)] + [InlineData(true)] + public void SuppressInstrumentationBeginTest(bool? shouldBegin) + { + Assert.False(Sdk.SuppressInstrumentation); - [Fact] - public async void SuppressInstrumentationScopeEnterIsLocalToAsyncFlow() + using var scope = shouldBegin.HasValue ? SuppressInstrumentationScope.Begin(shouldBegin.Value) : SuppressInstrumentationScope.Begin(); + if (shouldBegin.HasValue) { - Assert.False(Sdk.SuppressInstrumentation); - - // SuppressInstrumentationScope.Enter called inside the task is only applicable to this async flow - 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 + Assert.Equal(shouldBegin.Value, Sdk.SuppressInstrumentation); } - - [Fact] - public void DecrementIfTriggeredOnlyWorksInReferenceCountingMode() + else { - // Instrumentation is not suppressed, DecrementIfTriggered is a no op - Assert.False(Sdk.SuppressInstrumentation); - Assert.Equal(0, SuppressInstrumentationScope.DecrementIfTriggered()); - Assert.False(Sdk.SuppressInstrumentation); - - // Instrumentation is suppressed in reference counting mode, DecrementIfTriggered should work - Assert.Equal(1, SuppressInstrumentationScope.Enter()); - Assert.True(Sdk.SuppressInstrumentation); - Assert.Equal(0, SuppressInstrumentationScope.DecrementIfTriggered()); - Assert.False(Sdk.SuppressInstrumentation); // Instrumentation is not suppressed anymore + Assert.True(Sdk.SuppressInstrumentation); // Default behavior is to pass true and suppress the instrumentation } + } + + [Fact] + public async void SuppressInstrumentationScopeEnterIsLocalToAsyncFlow() + { + Assert.False(Sdk.SuppressInstrumentation); - [Fact] - public void IncrementIfTriggeredOnlyWorksInReferenceCountingMode() + // SuppressInstrumentationScope.Enter called inside the task is only applicable to this async flow + await Task.Factory.StartNew(() => { - // Instrumentation is not suppressed, IncrementIfTriggered is a no op Assert.False(Sdk.SuppressInstrumentation); - Assert.Equal(0, SuppressInstrumentationScope.IncrementIfTriggered()); - Assert.False(Sdk.SuppressInstrumentation); - - // Instrumentation is suppressed in reference counting mode, IncrementIfTriggered should work Assert.Equal(1, SuppressInstrumentationScope.Enter()); - Assert.Equal(2, SuppressInstrumentationScope.IncrementIfTriggered()); Assert.True(Sdk.SuppressInstrumentation); - Assert.Equal(1, SuppressInstrumentationScope.DecrementIfTriggered()); - Assert.True(Sdk.SuppressInstrumentation); // Instrumentation is still suppressed as IncrementIfTriggered incremented the slot count after Enter, need to decrement the slot count again to enable instrumentation - Assert.Equal(0, SuppressInstrumentationScope.DecrementIfTriggered()); - Assert.False(Sdk.SuppressInstrumentation); // Instrumentation is not suppressed anymore - } + }); + + 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 + } + + [Fact] + public void DecrementIfTriggeredOnlyWorksInReferenceCountingMode() + { + // Instrumentation is not suppressed, DecrementIfTriggered is a no op + Assert.False(Sdk.SuppressInstrumentation); + Assert.Equal(0, SuppressInstrumentationScope.DecrementIfTriggered()); + Assert.False(Sdk.SuppressInstrumentation); + + // Instrumentation is suppressed in reference counting mode, DecrementIfTriggered should work + Assert.Equal(1, SuppressInstrumentationScope.Enter()); + Assert.True(Sdk.SuppressInstrumentation); + Assert.Equal(0, SuppressInstrumentationScope.DecrementIfTriggered()); + Assert.False(Sdk.SuppressInstrumentation); // Instrumentation is not suppressed anymore + } + + [Fact] + public void IncrementIfTriggeredOnlyWorksInReferenceCountingMode() + { + // Instrumentation is not suppressed, IncrementIfTriggered is a no op + Assert.False(Sdk.SuppressInstrumentation); + Assert.Equal(0, SuppressInstrumentationScope.IncrementIfTriggered()); + Assert.False(Sdk.SuppressInstrumentation); + + // Instrumentation is suppressed in reference counting mode, IncrementIfTriggered should work + Assert.Equal(1, SuppressInstrumentationScope.Enter()); + Assert.Equal(2, SuppressInstrumentationScope.IncrementIfTriggered()); + Assert.True(Sdk.SuppressInstrumentation); + Assert.Equal(1, SuppressInstrumentationScope.DecrementIfTriggered()); + Assert.True(Sdk.SuppressInstrumentation); // Instrumentation is still suppressed as IncrementIfTriggered incremented the slot count after Enter, need to decrement the slot count again to enable instrumentation + Assert.Equal(0, SuppressInstrumentationScope.DecrementIfTriggered()); + Assert.False(Sdk.SuppressInstrumentation); // Instrumentation is not suppressed anymore } } diff --git a/test/OpenTelemetry.Tests/TestSelfDiagnosticsConfigRefresher.cs b/test/OpenTelemetry.Tests/TestSelfDiagnosticsConfigRefresher.cs new file mode 100644 index 00000000000..faee6e8f287 --- /dev/null +++ b/test/OpenTelemetry.Tests/TestSelfDiagnosticsConfigRefresher.cs @@ -0,0 +1,22 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics.CodeAnalysis; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Tests; + +internal class TestSelfDiagnosticsConfigRefresher(Stream stream = null) : SelfDiagnosticsConfigRefresher +{ + private readonly Stream stream = stream; + + public bool TryGetLogStreamCalled { get; private set; } + + public override bool TryGetLogStream(int byteCount, [NotNullWhen(true)] out Stream stream, out int availableByteCount) + { + this.TryGetLogStreamCalled = true; + stream = this.stream; + availableByteCount = 0; + return true; + } +} diff --git a/test/OpenTelemetry.Tests/Trace/AttributesExtensions.cs b/test/OpenTelemetry.Tests/Trace/AttributesExtensions.cs index 81caf36442f..deb5e383b5b 100644 --- a/test/OpenTelemetry.Tests/Trace/AttributesExtensions.cs +++ b/test/OpenTelemetry.Tests/Trace/AttributesExtensions.cs @@ -1,42 +1,28 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Xunit; -namespace OpenTelemetry.Trace.Tests +namespace OpenTelemetry.Trace.Tests; + +internal static class AttributesExtensions { - internal static class AttributesExtensions + public static object GetValue(this IEnumerable> attributes, string key) { - public static object GetValue(this IEnumerable> attributes, string key) - { - return attributes.FirstOrDefault(kvp => kvp.Key == key).Value; - } + return attributes.FirstOrDefault(kvp => kvp.Key == key).Value; + } - public static void AssertAreSame( - this IEnumerable> attributes, - IEnumerable> expectedAttributes) - { - var expectedKeyValuePairs = expectedAttributes as KeyValuePair[] ?? expectedAttributes.ToArray(); - var actualKeyValuePairs = attributes as KeyValuePair[] ?? attributes.ToArray(); - Assert.Equal(actualKeyValuePairs.Length, expectedKeyValuePairs.Length); + public static void AssertAreSame( + this IEnumerable> attributes, + IEnumerable> expectedAttributes) + { + var expectedKeyValuePairs = expectedAttributes as KeyValuePair[] ?? expectedAttributes.ToArray(); + var actualKeyValuePairs = attributes as KeyValuePair[] ?? attributes.ToArray(); + Assert.Equal(actualKeyValuePairs.Length, expectedKeyValuePairs.Length); - foreach (var attr in actualKeyValuePairs) - { - Assert.Contains(attr, expectedKeyValuePairs); - } + foreach (var attr in actualKeyValuePairs) + { + Assert.Contains(attr, expectedKeyValuePairs); } } } diff --git a/test/OpenTelemetry.Tests/Trace/BatchExportActivityProcessorOptionsTest.cs b/test/OpenTelemetry.Tests/Trace/BatchExportActivityProcessorOptionsTest.cs index cca1feb6e94..27f1e3de92f 100644 --- a/test/OpenTelemetry.Tests/Trace/BatchExportActivityProcessorOptionsTest.cs +++ b/test/OpenTelemetry.Tests/Trace/BatchExportActivityProcessorOptionsTest.cs @@ -1,131 +1,117 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.Configuration; using Xunit; -namespace OpenTelemetry.Trace.Tests +namespace OpenTelemetry.Trace.Tests; + +public class BatchExportActivityProcessorOptionsTest : IDisposable { - public class BatchExportActivityProcessorOptionsTest : IDisposable + public BatchExportActivityProcessorOptionsTest() { - public BatchExportActivityProcessorOptionsTest() - { - ClearEnvVars(); - } + ClearEnvVars(); + } - public void Dispose() - { - ClearEnvVars(); - GC.SuppressFinalize(this); - } + public void Dispose() + { + ClearEnvVars(); + GC.SuppressFinalize(this); + } - [Fact] - public void BatchExportProcessorOptions_Defaults() - { - var options = new BatchExportActivityProcessorOptions(); + [Fact] + public void BatchExportProcessorOptions_Defaults() + { + var options = new BatchExportActivityProcessorOptions(); - Assert.Equal(30000, options.ExporterTimeoutMilliseconds); - Assert.Equal(512, options.MaxExportBatchSize); - Assert.Equal(2048, options.MaxQueueSize); - Assert.Equal(5000, options.ScheduledDelayMilliseconds); - } + Assert.Equal(30000, options.ExporterTimeoutMilliseconds); + Assert.Equal(512, options.MaxExportBatchSize); + Assert.Equal(2048, options.MaxQueueSize); + Assert.Equal(5000, options.ScheduledDelayMilliseconds); + } - [Fact] - public void BatchExportProcessorOptions_EnvironmentVariableOverride() - { - Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.ExporterTimeoutEnvVarKey, "1"); - Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.MaxExportBatchSizeEnvVarKey, "2"); - Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.MaxQueueSizeEnvVarKey, "3"); - Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.ScheduledDelayEnvVarKey, "4"); + [Fact] + public void BatchExportProcessorOptions_EnvironmentVariableOverride() + { + Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.ExporterTimeoutEnvVarKey, "1"); + Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.MaxExportBatchSizeEnvVarKey, "2"); + Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.MaxQueueSizeEnvVarKey, "3"); + Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.ScheduledDelayEnvVarKey, "4"); - var options = new BatchExportActivityProcessorOptions(); + var options = new BatchExportActivityProcessorOptions(); - Assert.Equal(1, options.ExporterTimeoutMilliseconds); - Assert.Equal(2, options.MaxExportBatchSize); - Assert.Equal(3, options.MaxQueueSize); - Assert.Equal(4, options.ScheduledDelayMilliseconds); - } + Assert.Equal(1, options.ExporterTimeoutMilliseconds); + Assert.Equal(2, options.MaxExportBatchSize); + Assert.Equal(3, options.MaxQueueSize); + Assert.Equal(4, options.ScheduledDelayMilliseconds); + } - [Fact] - public void BatchExportProcessorOptions_UsingIConfiguration() - { - var values = new Dictionary() - { - [BatchExportActivityProcessorOptions.MaxQueueSizeEnvVarKey] = "1", - [BatchExportActivityProcessorOptions.MaxExportBatchSizeEnvVarKey] = "2", - [BatchExportActivityProcessorOptions.ExporterTimeoutEnvVarKey] = "3", - [BatchExportActivityProcessorOptions.ScheduledDelayEnvVarKey] = "4", - }; - - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(values) - .Build(); - - var options = new BatchExportActivityProcessorOptions(configuration); - - Assert.Equal(1, options.MaxQueueSize); - Assert.Equal(2, options.MaxExportBatchSize); - Assert.Equal(3, options.ExporterTimeoutMilliseconds); - Assert.Equal(4, options.ScheduledDelayMilliseconds); - } - - [Fact] - public void BatchExportProcessorOptions_InvalidEnvironmentVariableOverride() + [Fact] + public void BatchExportProcessorOptions_UsingIConfiguration() + { + var values = new Dictionary() { - Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.ExporterTimeoutEnvVarKey, "invalid"); - Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.MaxExportBatchSizeEnvVarKey, "invalid"); - Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.MaxQueueSizeEnvVarKey, "invalid"); - Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.ScheduledDelayEnvVarKey, "invalid"); - - var options = new BatchExportActivityProcessorOptions(); + [BatchExportActivityProcessorOptions.MaxQueueSizeEnvVarKey] = "1", + [BatchExportActivityProcessorOptions.MaxExportBatchSizeEnvVarKey] = "2", + [BatchExportActivityProcessorOptions.ExporterTimeoutEnvVarKey] = "3", + [BatchExportActivityProcessorOptions.ScheduledDelayEnvVarKey] = "4", + }; + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(values) + .Build(); + + var options = new BatchExportActivityProcessorOptions(configuration); + + Assert.Equal(1, options.MaxQueueSize); + Assert.Equal(2, options.MaxExportBatchSize); + Assert.Equal(3, options.ExporterTimeoutMilliseconds); + Assert.Equal(4, options.ScheduledDelayMilliseconds); + } - Assert.Equal(30000, options.ExporterTimeoutMilliseconds); - Assert.Equal(512, options.MaxExportBatchSize); - Assert.Equal(2048, options.MaxQueueSize); - Assert.Equal(5000, options.ScheduledDelayMilliseconds); - } + [Fact] + public void BatchExportProcessorOptions_InvalidEnvironmentVariableOverride() + { + Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.ExporterTimeoutEnvVarKey, "invalid"); + Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.MaxExportBatchSizeEnvVarKey, "invalid"); + Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.MaxQueueSizeEnvVarKey, "invalid"); + Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.ScheduledDelayEnvVarKey, "invalid"); - [Fact] - public void BatchExportProcessorOptions_SetterOverridesEnvironmentVariable() - { - Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.ExporterTimeoutEnvVarKey, "123"); + var options = new BatchExportActivityProcessorOptions(); - var options = new BatchExportActivityProcessorOptions - { - ExporterTimeoutMilliseconds = 89000, - }; + Assert.Equal(30000, options.ExporterTimeoutMilliseconds); + Assert.Equal(512, options.MaxExportBatchSize); + Assert.Equal(2048, options.MaxQueueSize); + Assert.Equal(5000, options.ScheduledDelayMilliseconds); + } - Assert.Equal(89000, options.ExporterTimeoutMilliseconds); - } + [Fact] + public void BatchExportProcessorOptions_SetterOverridesEnvironmentVariable() + { + Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.ExporterTimeoutEnvVarKey, "123"); - [Fact] - public void BatchExportProcessorOptions_EnvironmentVariableNames() + var options = new BatchExportActivityProcessorOptions { - Assert.Equal("OTEL_BSP_EXPORT_TIMEOUT", BatchExportActivityProcessorOptions.ExporterTimeoutEnvVarKey); - Assert.Equal("OTEL_BSP_MAX_EXPORT_BATCH_SIZE", BatchExportActivityProcessorOptions.MaxExportBatchSizeEnvVarKey); - Assert.Equal("OTEL_BSP_MAX_QUEUE_SIZE", BatchExportActivityProcessorOptions.MaxQueueSizeEnvVarKey); - Assert.Equal("OTEL_BSP_SCHEDULE_DELAY", BatchExportActivityProcessorOptions.ScheduledDelayEnvVarKey); - } + ExporterTimeoutMilliseconds = 89000, + }; - private static void ClearEnvVars() - { - Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.ExporterTimeoutEnvVarKey, null); - Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.MaxExportBatchSizeEnvVarKey, null); - Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.MaxQueueSizeEnvVarKey, null); - Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.ScheduledDelayEnvVarKey, null); - } + Assert.Equal(89000, options.ExporterTimeoutMilliseconds); + } + + [Fact] + public void BatchExportProcessorOptions_EnvironmentVariableNames() + { + Assert.Equal("OTEL_BSP_EXPORT_TIMEOUT", BatchExportActivityProcessorOptions.ExporterTimeoutEnvVarKey); + Assert.Equal("OTEL_BSP_MAX_EXPORT_BATCH_SIZE", BatchExportActivityProcessorOptions.MaxExportBatchSizeEnvVarKey); + Assert.Equal("OTEL_BSP_MAX_QUEUE_SIZE", BatchExportActivityProcessorOptions.MaxQueueSizeEnvVarKey); + Assert.Equal("OTEL_BSP_SCHEDULE_DELAY", BatchExportActivityProcessorOptions.ScheduledDelayEnvVarKey); + } + + private static void ClearEnvVars() + { + Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.ExporterTimeoutEnvVarKey, null); + Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.MaxExportBatchSizeEnvVarKey, null); + Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.MaxQueueSizeEnvVarKey, null); + Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.ScheduledDelayEnvVarKey, null); } } diff --git a/test/OpenTelemetry.Tests/Trace/BatchExportActivityProcessorTest.cs b/test/OpenTelemetry.Tests/Trace/BatchExportActivityProcessorTest.cs index 7a78acacce7..14a97e39c91 100644 --- a/test/OpenTelemetry.Tests/Trace/BatchExportActivityProcessorTest.cs +++ b/test/OpenTelemetry.Tests/Trace/BatchExportActivityProcessorTest.cs @@ -1,214 +1,200 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Exporter; using Xunit; -namespace OpenTelemetry.Trace.Tests +namespace OpenTelemetry.Trace.Tests; + +public class BatchExportActivityProcessorTest { - public class BatchExportActivityProcessorTest + [Fact] + public void CheckNullExporter() { - [Fact] - public void CheckNullExporter() - { - Assert.Throws(() => new BatchActivityExportProcessor(null)); - } + Assert.Throws(() => new BatchActivityExportProcessor(null)); + } + + [Fact] + public void CheckConstructorWithInvalidValues() + { + var exportedItems = new List(); + Assert.Throws(() => new BatchActivityExportProcessor(new InMemoryExporter(exportedItems), maxQueueSize: 0)); + Assert.Throws(() => new BatchActivityExportProcessor(new InMemoryExporter(exportedItems), maxExportBatchSize: 0)); + Assert.Throws(() => new BatchActivityExportProcessor(new InMemoryExporter(exportedItems), maxQueueSize: 1, maxExportBatchSize: 2049)); + Assert.Throws(() => new BatchActivityExportProcessor(new InMemoryExporter(exportedItems), scheduledDelayMilliseconds: 0)); + Assert.Throws(() => new BatchActivityExportProcessor(new InMemoryExporter(exportedItems), exporterTimeoutMilliseconds: -1)); + } - [Fact] - public void CheckConstructorWithInvalidValues() + [Fact] + public void CheckIfBatchIsExportingOnQueueLimit() + { + var exportedItems = new List(); + using var exporter = new InMemoryExporter(exportedItems); + using var processor = new BatchActivityExportProcessor( + exporter, + maxQueueSize: 1, + maxExportBatchSize: 1, + scheduledDelayMilliseconds: 100_000); + + using var activity = new Activity("start") { - var exportedItems = new List(); - Assert.Throws(() => new BatchActivityExportProcessor(new InMemoryExporter(exportedItems), maxQueueSize: 0)); - Assert.Throws(() => new BatchActivityExportProcessor(new InMemoryExporter(exportedItems), maxExportBatchSize: 0)); - Assert.Throws(() => new BatchActivityExportProcessor(new InMemoryExporter(exportedItems), maxQueueSize: 1, maxExportBatchSize: 2049)); - Assert.Throws(() => new BatchActivityExportProcessor(new InMemoryExporter(exportedItems), scheduledDelayMilliseconds: 0)); - Assert.Throws(() => new BatchActivityExportProcessor(new InMemoryExporter(exportedItems), exporterTimeoutMilliseconds: -1)); - } + ActivityTraceFlags = ActivityTraceFlags.Recorded, + }; - [Fact] - public void CheckIfBatchIsExportingOnQueueLimit() + processor.OnEnd(activity); + + for (int i = 0; i < 10 && exportedItems.Count == 0; i++) { - var exportedItems = new List(); - using var exporter = new InMemoryExporter(exportedItems); - using var processor = new BatchActivityExportProcessor( - exporter, - maxQueueSize: 1, - maxExportBatchSize: 1, - scheduledDelayMilliseconds: 100_000); - - using var activity = new Activity("start") - { - ActivityTraceFlags = ActivityTraceFlags.Recorded, - }; - - processor.OnEnd(activity); - - for (int i = 0; i < 10 && exportedItems.Count == 0; i++) - { - Thread.Sleep(500); - } - - Assert.Single(exportedItems); - - Assert.Equal(1, processor.ProcessedCount); - Assert.Equal(1, processor.ReceivedCount); - Assert.Equal(0, processor.DroppedCount); + Thread.Sleep(500); } - [Fact] - public void CheckForceFlushWithInvalidTimeout() + Assert.Single(exportedItems); + + Assert.Equal(1, processor.ProcessedCount); + Assert.Equal(1, processor.ReceivedCount); + Assert.Equal(0, processor.DroppedCount); + } + + [Fact] + public void CheckForceFlushWithInvalidTimeout() + { + var exportedItems = new List(); + using var exporter = new InMemoryExporter(exportedItems); + using var processor = new BatchActivityExportProcessor(exporter, maxQueueSize: 2, maxExportBatchSize: 1); + Assert.Throws(() => processor.ForceFlush(-2)); + } + + [Theory] + [InlineData(Timeout.Infinite)] + [InlineData(0)] + [InlineData(1)] + public void CheckForceFlushExport(int timeout) + { + var exportedItems = new List(); + using var exporter = new InMemoryExporter(exportedItems); + using var processor = new BatchActivityExportProcessor( + exporter, + maxQueueSize: 3, + maxExportBatchSize: 3, + exporterTimeoutMilliseconds: 30000); + + using var activity1 = new Activity("start1") { - var exportedItems = new List(); - using var exporter = new InMemoryExporter(exportedItems); - using var processor = new BatchActivityExportProcessor(exporter, maxQueueSize: 2, maxExportBatchSize: 1); - Assert.Throws(() => processor.ForceFlush(-2)); - } + ActivityTraceFlags = ActivityTraceFlags.Recorded, + }; - [Theory] - [InlineData(Timeout.Infinite)] - [InlineData(0)] - [InlineData(1)] - public void CheckForceFlushExport(int timeout) + using var activity2 = new Activity("start2") { - var exportedItems = new List(); - using var exporter = new InMemoryExporter(exportedItems); - using var processor = new BatchActivityExportProcessor( - exporter, - maxQueueSize: 3, - maxExportBatchSize: 3, - exporterTimeoutMilliseconds: 30000); - - using var activity1 = new Activity("start1") - { - ActivityTraceFlags = ActivityTraceFlags.Recorded, - }; - - using var activity2 = new Activity("start2") - { - ActivityTraceFlags = ActivityTraceFlags.Recorded, - }; - - processor.OnEnd(activity1); - processor.OnEnd(activity2); - - Assert.Equal(0, processor.ProcessedCount); - - // waiting to see if time is triggering the exporter - Thread.Sleep(1_000); - Assert.Empty(exportedItems); + ActivityTraceFlags = ActivityTraceFlags.Recorded, + }; - // forcing flush - processor.ForceFlush(timeout); + processor.OnEnd(activity1); + processor.OnEnd(activity2); - if (timeout == 0) - { - // ForceFlush(0) will trigger flush and return immediately, so let's sleep for a while - Thread.Sleep(1_000); - } + Assert.Equal(0, processor.ProcessedCount); - Assert.Equal(2, exportedItems.Count); + // waiting to see if time is triggering the exporter + Thread.Sleep(1_000); + Assert.Empty(exportedItems); - Assert.Equal(2, processor.ProcessedCount); - Assert.Equal(2, processor.ReceivedCount); - Assert.Equal(0, processor.DroppedCount); - } + // forcing flush + processor.ForceFlush(timeout); - [Theory] - [InlineData(Timeout.Infinite)] - [InlineData(0)] - [InlineData(1)] - public void CheckShutdownExport(int timeout) + if (timeout == 0) { - var exportedItems = new List(); - using var exporter = new InMemoryExporter(exportedItems); - using var processor = new BatchActivityExportProcessor( - exporter, - maxQueueSize: 3, - maxExportBatchSize: 3, - exporterTimeoutMilliseconds: 30000); - - using var activity = new Activity("start") - { - ActivityTraceFlags = ActivityTraceFlags.Recorded, - }; - - processor.OnEnd(activity); - processor.Shutdown(timeout); - - if (timeout == 0) - { - // Shutdown(0) will trigger flush and return immediately, so let's sleep for a while - Thread.Sleep(1_000); - } - - Assert.Single(exportedItems); - - Assert.Equal(1, processor.ProcessedCount); - Assert.Equal(1, processor.ReceivedCount); - Assert.Equal(0, processor.DroppedCount); + // ForceFlush(0) will trigger flush and return immediately, so let's sleep for a while + Thread.Sleep(1_000); } - [Fact] - public void CheckExportForRecordingButNotSampledActivity() + Assert.Equal(2, exportedItems.Count); + + Assert.Equal(2, processor.ProcessedCount); + Assert.Equal(2, processor.ReceivedCount); + Assert.Equal(0, processor.DroppedCount); + } + + [Theory] + [InlineData(Timeout.Infinite)] + [InlineData(0)] + [InlineData(1)] + public void CheckShutdownExport(int timeout) + { + var exportedItems = new List(); + using var exporter = new InMemoryExporter(exportedItems); + using var processor = new BatchActivityExportProcessor( + exporter, + maxQueueSize: 3, + maxExportBatchSize: 3, + exporterTimeoutMilliseconds: 30000); + + using var activity = new Activity("start") { - var exportedItems = new List(); - using var exporter = new InMemoryExporter(exportedItems); - using var processor = new BatchActivityExportProcessor( - exporter, - maxQueueSize: 1, - maxExportBatchSize: 1); - - using var activity = new Activity("start") - { - ActivityTraceFlags = ActivityTraceFlags.None, - }; - - processor.OnEnd(activity); - processor.Shutdown(); - - Assert.Empty(exportedItems); - Assert.Equal(0, processor.ProcessedCount); - } + ActivityTraceFlags = ActivityTraceFlags.Recorded, + }; - [Fact] - public void CheckExportDrainsBatchOnFailure() + processor.OnEnd(activity); + processor.Shutdown(timeout); + + if (timeout == 0) { - using var processor = new BatchActivityExportProcessor( - exporter: new FailureExporter(), - maxQueueSize: 3, - maxExportBatchSize: 3); - - using var activity = new Activity("start") - { - ActivityTraceFlags = ActivityTraceFlags.Recorded, - }; - - processor.OnEnd(activity); - processor.OnEnd(activity); - processor.OnEnd(activity); - processor.Shutdown(); - - Assert.Equal(3, processor.ProcessedCount); // Verify batch was drained even though nothing was exported. + // Shutdown(0) will trigger flush and return immediately, so let's sleep for a while + Thread.Sleep(1_000); } - private class FailureExporter : BaseExporter - where T : class + Assert.Single(exportedItems); + + Assert.Equal(1, processor.ProcessedCount); + Assert.Equal(1, processor.ReceivedCount); + Assert.Equal(0, processor.DroppedCount); + } + + [Fact] + public void CheckExportForRecordingButNotSampledActivity() + { + var exportedItems = new List(); + using var exporter = new InMemoryExporter(exportedItems); + using var processor = new BatchActivityExportProcessor( + exporter, + maxQueueSize: 1, + maxExportBatchSize: 1); + + using var activity = new Activity("start") { - public override ExportResult Export(in Batch batch) => ExportResult.Failure; - } + ActivityTraceFlags = ActivityTraceFlags.None, + }; + + processor.OnEnd(activity); + processor.Shutdown(); + + Assert.Empty(exportedItems); + Assert.Equal(0, processor.ProcessedCount); + } + + [Fact] + public void CheckExportDrainsBatchOnFailure() + { + using var processor = new BatchActivityExportProcessor( + exporter: new FailureExporter(), + maxQueueSize: 3, + maxExportBatchSize: 3); + + using var activity = new Activity("start") + { + ActivityTraceFlags = ActivityTraceFlags.Recorded, + }; + + processor.OnEnd(activity); + processor.OnEnd(activity); + processor.OnEnd(activity); + processor.Shutdown(); + + Assert.Equal(3, processor.ProcessedCount); // Verify batch was drained even though nothing was exported. + } + + private class FailureExporter : BaseExporter + where T : class + { + public override ExportResult Export(in Batch batch) => ExportResult.Failure; } } diff --git a/test/OpenTelemetry.Tests/Trace/BatchTest.cs b/test/OpenTelemetry.Tests/Trace/BatchTest.cs index f40a0c19f79..b1d5b170d52 100644 --- a/test/OpenTelemetry.Tests/Trace/BatchTest.cs +++ b/test/OpenTelemetry.Tests/Trace/BatchTest.cs @@ -1,175 +1,161 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Internal; using Xunit; -namespace OpenTelemetry.Trace.Tests +namespace OpenTelemetry.Trace.Tests; + +public class BatchTest { - public class BatchTest + [Fact] + public void CheckConstructorExceptions() + { + Assert.Throws(() => new Batch((string[])null, 0)); + Assert.Throws(() => new Batch(Array.Empty(), -1)); + Assert.Throws(() => new Batch(Array.Empty(), 1)); + } + + [Fact] + public void CheckValidConstructors() { - [Fact] - public void CheckConstructorExceptions() + var value = "a"; + var batch = new Batch(value); + foreach (var item in batch) { - Assert.Throws(() => new Batch((string[])null, 0)); - Assert.Throws(() => new Batch(Array.Empty(), -1)); - Assert.Throws(() => new Batch(Array.Empty(), 1)); + Assert.Equal(value, item); } - [Fact] - public void CheckValidConstructors() + var circularBuffer = new CircularBuffer(1); + circularBuffer.Add(value); + batch = new Batch(circularBuffer, 1); + foreach (var item in batch) { - var value = "a"; - var batch = new Batch(value); - foreach (var item in batch) - { - Assert.Equal(value, item); - } - - var circularBuffer = new CircularBuffer(1); - circularBuffer.Add(value); - batch = new Batch(circularBuffer, 1); - foreach (var item in batch) - { - Assert.Equal(value, item); - } + Assert.Equal(value, item); } + } + + [Fact] + public void CheckDispose() + { + var value = "a"; + var batch = new Batch(value); + batch.Dispose(); // A test to make sure it doesn't bomb on a null CircularBuffer. + + var circularBuffer = new CircularBuffer(10); + circularBuffer.Add(value); + circularBuffer.Add(value); + circularBuffer.Add(value); + batch = new Batch(circularBuffer, 10); // Max size = 10 + batch.GetEnumerator().MoveNext(); + Assert.Equal(3, circularBuffer.AddedCount); + Assert.Equal(1, circularBuffer.RemovedCount); + batch.Dispose(); // Test anything remaining in the batch is drained when disposed. + Assert.Equal(3, circularBuffer.AddedCount); + Assert.Equal(3, circularBuffer.RemovedCount); + batch.Dispose(); // Verify we don't go into an infinite loop or thrown when empty. + + circularBuffer = new CircularBuffer(10); + circularBuffer.Add(value); + circularBuffer.Add(value); + circularBuffer.Add(value); + batch = new Batch(circularBuffer, 2); // Max size = 2 + Assert.Equal(3, circularBuffer.AddedCount); + Assert.Equal(0, circularBuffer.RemovedCount); + batch.Dispose(); // Test the batch is drained up to max size. + Assert.Equal(3, circularBuffer.AddedCount); + Assert.Equal(2, circularBuffer.RemovedCount); + } + + [Fact] + public void CheckEnumerator() + { + var value = "a"; + var batch = new Batch(value); + var enumerator = batch.GetEnumerator(); + ValidateEnumerator(enumerator, value); + + var circularBuffer = new CircularBuffer(1); + circularBuffer.Add(value); + batch = new Batch(circularBuffer, 1); + enumerator = batch.GetEnumerator(); + ValidateEnumerator(enumerator, value); + } - [Fact] - public void CheckDispose() + [Fact] + public void CheckMultipleEnumerator() + { + var value = "a"; + var circularBuffer = new CircularBuffer(10); + circularBuffer.Add(value); + circularBuffer.Add(value); + circularBuffer.Add(value); + var batch = new Batch(circularBuffer, 10); + + int itemsProcessed = 0; + foreach (var item in batch) { - var value = "a"; - var batch = new Batch(value); - batch.Dispose(); // A test to make sure it doesn't bomb on a null CircularBuffer. - - var circularBuffer = new CircularBuffer(10); - circularBuffer.Add(value); - circularBuffer.Add(value); - circularBuffer.Add(value); - batch = new Batch(circularBuffer, 10); // Max size = 10 - batch.GetEnumerator().MoveNext(); - Assert.Equal(3, circularBuffer.AddedCount); - Assert.Equal(1, circularBuffer.RemovedCount); - batch.Dispose(); // Test anything remaining in the batch is drained when disposed. - Assert.Equal(3, circularBuffer.AddedCount); - Assert.Equal(3, circularBuffer.RemovedCount); - batch.Dispose(); // Verify we don't go into an infinite loop or thrown when empty. - - circularBuffer = new CircularBuffer(10); - circularBuffer.Add(value); - circularBuffer.Add(value); - circularBuffer.Add(value); - batch = new Batch(circularBuffer, 2); // Max size = 2 - Assert.Equal(3, circularBuffer.AddedCount); - Assert.Equal(0, circularBuffer.RemovedCount); - batch.Dispose(); // Test the batch is drained up to max size. - Assert.Equal(3, circularBuffer.AddedCount); - Assert.Equal(2, circularBuffer.RemovedCount); + itemsProcessed++; } - [Fact] - public void CheckEnumerator() + Assert.Equal(3, itemsProcessed); + + itemsProcessed = 0; + foreach (var item in batch) { - var value = "a"; - var batch = new Batch(value); - var enumerator = batch.GetEnumerator(); - ValidateEnumerator(enumerator, value); - - var circularBuffer = new CircularBuffer(1); - circularBuffer.Add(value); - batch = new Batch(circularBuffer, 1); - enumerator = batch.GetEnumerator(); - ValidateEnumerator(enumerator, value); + itemsProcessed++; } - [Fact] - public void CheckMultipleEnumerator() - { - var value = "a"; - var circularBuffer = new CircularBuffer(10); - circularBuffer.Add(value); - circularBuffer.Add(value); - circularBuffer.Add(value); - var batch = new Batch(circularBuffer, 10); - - int itemsProcessed = 0; - foreach (var item in batch) - { - itemsProcessed++; - } + Assert.Equal(0, itemsProcessed); + } - Assert.Equal(3, itemsProcessed); + [Fact] + public void CheckEnumeratorResetException() + { + var value = "a"; + var batch = new Batch(value); + var enumerator = batch.GetEnumerator(); + Assert.Throws(() => enumerator.Reset()); + } - itemsProcessed = 0; - foreach (var item in batch) - { - itemsProcessed++; - } + [Fact] + public void DrainIntoNewBatchTest() + { + var circularBuffer = new CircularBuffer(100); + circularBuffer.Add("a"); + circularBuffer.Add("b"); - Assert.Equal(0, itemsProcessed); - } + Batch batch = new Batch(circularBuffer, 10); - [Fact] - public void CheckEnumeratorResetException() - { - var value = "a"; - var batch = new Batch(value); - var enumerator = batch.GetEnumerator(); - Assert.Throws(() => enumerator.Reset()); - } + Assert.Equal(2, batch.Count); - [Fact] - public void DrainIntoNewBatchTest() + string[] storage = new string[10]; + int selectedItemCount = 0; + foreach (string item in batch) { - var circularBuffer = new CircularBuffer(100); - circularBuffer.Add("a"); - circularBuffer.Add("b"); - - Batch batch = new Batch(circularBuffer, 10); - - Assert.Equal(2, batch.Count); - - string[] storage = new string[10]; - int selectedItemCount = 0; - foreach (string item in batch) + if (item == "b") { - if (item == "b") - { - storage[selectedItemCount++] = item; - } + storage[selectedItemCount++] = item; } + } - batch = new Batch(storage, selectedItemCount); + batch = new Batch(storage, selectedItemCount); - Assert.Equal(1, batch.Count); + Assert.Equal(1, batch.Count); - ValidateEnumerator(batch.GetEnumerator(), "b"); - } + ValidateEnumerator(batch.GetEnumerator(), "b"); + } - private static void ValidateEnumerator(Batch.Enumerator enumerator, string expected) + private static void ValidateEnumerator(Batch.Enumerator enumerator, string expected) + { + if (enumerator.Current != null) { - if (enumerator.Current != null) - { - Assert.Equal(expected, enumerator.Current); - } + Assert.Equal(expected, enumerator.Current); + } - if (enumerator.MoveNext()) - { - Assert.Equal(expected, enumerator.Current); - } + if (enumerator.MoveNext()) + { + Assert.Equal(expected, enumerator.Current); } } } diff --git a/test/OpenTelemetry.Tests/Trace/CompositeActivityProcessorTests.cs b/test/OpenTelemetry.Tests/Trace/CompositeActivityProcessorTests.cs index 4b7874e2384..8f8bccb8e29 100644 --- a/test/OpenTelemetry.Tests/Trace/CompositeActivityProcessorTests.cs +++ b/test/OpenTelemetry.Tests/Trace/CompositeActivityProcessorTests.cs @@ -1,126 +1,112 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Trace.Tests +namespace OpenTelemetry.Trace.Tests; + +public class CompositeActivityProcessorTests { - public class CompositeActivityProcessorTests + [Fact] + public void CompositeActivityProcessor_BadArgs() { - [Fact] - public void CompositeActivityProcessor_BadArgs() - { - Assert.Throws(() => new CompositeProcessor(null)); - Assert.Throws(() => new CompositeProcessor(Array.Empty>())); - - using var p1 = new TestActivityProcessor(null, null); - using var processor = new CompositeProcessor(new[] { p1 }); - Assert.Throws(() => processor.AddProcessor(null)); - } + Assert.Throws(() => new CompositeProcessor(null)); + Assert.Throws(() => new CompositeProcessor(Array.Empty>())); - [Fact] - public void CompositeActivityProcessor_CallsAllProcessorSequentially() - { - var result = string.Empty; + using var p1 = new TestActivityProcessor(null, null); + using var processor = new CompositeProcessor(new[] { p1 }); + Assert.Throws(() => processor.AddProcessor(null)); + } - using var p1 = new TestActivityProcessor( - activity => { result += "1"; }, - activity => { result += "3"; }); - using var p2 = new TestActivityProcessor( - activity => { result += "2"; }, - activity => { result += "4"; }); + [Fact] + public void CompositeActivityProcessor_CallsAllProcessorSequentially() + { + var result = string.Empty; - using var activity = new Activity("test"); + using var p1 = new TestActivityProcessor( + activity => { result += "1"; }, + activity => { result += "3"; }); + using var p2 = new TestActivityProcessor( + activity => { result += "2"; }, + activity => { result += "4"; }); - using (var processor = new CompositeProcessor(new[] { p1, p2 })) - { - processor.OnStart(activity); - processor.OnEnd(activity); - } + using var activity = new Activity("test"); - Assert.Equal("1234", result); + using (var processor = new CompositeProcessor(new[] { p1, p2 })) + { + processor.OnStart(activity); + processor.OnEnd(activity); } - [Fact] - public void CompositeActivityProcessor_ProcessorThrows() - { - using var p1 = new TestActivityProcessor( - activity => { throw new Exception("Start exception"); }, - activity => { throw new Exception("End exception"); }); + Assert.Equal("1234", result); + } - using var activity = new Activity("test"); + [Fact] + public void CompositeActivityProcessor_ProcessorThrows() + { + using var p1 = new TestActivityProcessor( + activity => { throw new Exception("Start exception"); }, + activity => { throw new Exception("End exception"); }); - using var processor = new CompositeProcessor(new[] { p1 }); - Assert.Throws(() => { processor.OnStart(activity); }); - Assert.Throws(() => { processor.OnEnd(activity); }); - } + using var activity = new Activity("test"); - [Fact] - public void CompositeActivityProcessor_ShutsDownAll() - { - using var p1 = new TestActivityProcessor(null, null); - using var p2 = new TestActivityProcessor(null, null); + using var processor = new CompositeProcessor(new[] { p1 }); + Assert.Throws(() => { processor.OnStart(activity); }); + Assert.Throws(() => { processor.OnEnd(activity); }); + } - using var processor = new CompositeProcessor(new[] { p1, p2 }); - processor.Shutdown(); - Assert.True(p1.ShutdownCalled); - Assert.True(p2.ShutdownCalled); - } + [Fact] + public void CompositeActivityProcessor_ShutsDownAll() + { + using var p1 = new TestActivityProcessor(null, null); + using var p2 = new TestActivityProcessor(null, null); - [Theory] - [InlineData(Timeout.Infinite)] - [InlineData(0)] - [InlineData(1)] - public void CompositeActivityProcessor_ForceFlush(int timeout) - { - using var p1 = new TestActivityProcessor(null, null); - using var p2 = new TestActivityProcessor(null, null); + using var processor = new CompositeProcessor(new[] { p1, p2 }); + processor.Shutdown(); + Assert.True(p1.ShutdownCalled); + Assert.True(p2.ShutdownCalled); + } - using var processor = new CompositeProcessor(new[] { p1, p2 }); - processor.ForceFlush(timeout); + [Theory] + [InlineData(Timeout.Infinite)] + [InlineData(0)] + [InlineData(1)] + public void CompositeActivityProcessor_ForceFlush(int timeout) + { + using var p1 = new TestActivityProcessor(null, null); + using var p2 = new TestActivityProcessor(null, null); - Assert.True(p1.ForceFlushCalled); - Assert.True(p2.ForceFlushCalled); - } + using var processor = new CompositeProcessor(new[] { p1, p2 }); + processor.ForceFlush(timeout); - [Fact] - public void CompositeActivityProcessor_ForwardsParentProvider() - { - using TracerProvider provider = new TestProvider(); + Assert.True(p1.ForceFlushCalled); + Assert.True(p2.ForceFlushCalled); + } - using var p1 = new TestActivityProcessor(null, null); - using var p2 = new TestActivityProcessor(null, null); + [Fact] + public void CompositeActivityProcessor_ForwardsParentProvider() + { + using TracerProvider provider = new TestProvider(); - using var processor = new CompositeProcessor(new[] { p1, p2 }); + using var p1 = new TestActivityProcessor(null, null); + using var p2 = new TestActivityProcessor(null, null); - Assert.Null(processor.ParentProvider); - Assert.Null(p1.ParentProvider); - Assert.Null(p2.ParentProvider); + using var processor = new CompositeProcessor(new[] { p1, p2 }); - processor.SetParentProvider(provider); + Assert.Null(processor.ParentProvider); + Assert.Null(p1.ParentProvider); + Assert.Null(p2.ParentProvider); - Assert.Equal(provider, processor.ParentProvider); - Assert.Equal(provider, p1.ParentProvider); - Assert.Equal(provider, p2.ParentProvider); - } + processor.SetParentProvider(provider); - private sealed class TestProvider : TracerProvider - { - } + Assert.Equal(provider, processor.ParentProvider); + Assert.Equal(provider, p1.ParentProvider); + Assert.Equal(provider, p2.ParentProvider); + } + + private sealed class TestProvider : TracerProvider + { } } diff --git a/test/OpenTelemetry.Tests/Trace/CurrentSpanTests.cs b/test/OpenTelemetry.Tests/Trace/CurrentSpanTests.cs index d9348373530..e9b76794625 100644 --- a/test/OpenTelemetry.Tests/Trace/CurrentSpanTests.cs +++ b/test/OpenTelemetry.Tests/Trace/CurrentSpanTests.cs @@ -1,53 +1,39 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using Xunit; -namespace OpenTelemetry.Trace.Tests +namespace OpenTelemetry.Trace.Tests; + +public class CurrentSpanTests : IDisposable { - public class CurrentSpanTests : IDisposable + private readonly Tracer tracer; + + public CurrentSpanTests() + { + Activity.DefaultIdFormat = ActivityIdFormat.W3C; + Activity.ForceDefaultIdFormat = true; + + this.tracer = TracerProvider.Default.GetTracer(null); + } + + [Fact] + public void CurrentSpan_WhenNoContext() + { + Assert.False(Tracer.CurrentSpan.Context.IsValid); + } + + [Fact] + public void CurrentSpan_WhenActivityExists() + { + using var activity = new Activity("foo").Start(); + Assert.True(Tracer.CurrentSpan.Context.IsValid); + } + + public void Dispose() { - private readonly Tracer tracer; - - public CurrentSpanTests() - { - Activity.DefaultIdFormat = ActivityIdFormat.W3C; - Activity.ForceDefaultIdFormat = true; - - this.tracer = TracerProvider.Default.GetTracer(null); - } - - [Fact] - public void CurrentSpan_WhenNoContext() - { - Assert.False(Tracer.CurrentSpan.Context.IsValid); - } - - [Fact] - public void CurrentSpan_WhenActivityExists() - { - using var activity = new Activity("foo").Start(); - Assert.True(Tracer.CurrentSpan.Context.IsValid); - } - - public void Dispose() - { - Activity.Current = null; - GC.SuppressFinalize(this); - } + Activity.Current = null; + GC.SuppressFinalize(this); } } diff --git a/test/OpenTelemetry.Tests/Trace/ExceptionProcessorTest.cs b/test/OpenTelemetry.Tests/Trace/ExceptionProcessorTest.cs index c51802a000f..98d8d01a034 100644 --- a/test/OpenTelemetry.Tests/Trace/ExceptionProcessorTest.cs +++ b/test/OpenTelemetry.Tests/Trace/ExceptionProcessorTest.cs @@ -1,151 +1,137 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Trace.Tests +namespace OpenTelemetry.Trace.Tests; + +public class ExceptionProcessorTest { - public class ExceptionProcessorTest + [Fact] + public void ActivityStatusSetToErrorWhenExceptionProcessorEnabled() { - [Fact] - public void ActivityStatusSetToErrorWhenExceptionProcessorEnabled() + var activitySourceName = Utils.GetCurrentMethodName(); + using var activitySource = new ActivitySource(activitySourceName); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .SetSampler(new AlwaysOnSampler()) + .AddProcessor(new ExceptionProcessor()) + .Build(); + + Activity activity1 = null; + Activity activity2 = null; + Activity activity3 = null; + Activity activity4 = null; + Activity activity5 = null; + + try { - var activitySourceName = Utils.GetCurrentMethodName(); - using var activitySource = new ActivitySource(activitySourceName); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .SetSampler(new AlwaysOnSampler()) - .AddProcessor(new ExceptionProcessor()) - .Build(); - - Activity activity1 = null; - Activity activity2 = null; - Activity activity3 = null; - Activity activity4 = null; - Activity activity5 = null; - - try + using (activity1 = activitySource.StartActivity("Activity1")) { - using (activity1 = activitySource.StartActivity("Activity1")) + using (activity2 = activitySource.StartActivity("Activity2")) { - using (activity2 = activitySource.StartActivity("Activity2")) - { - throw new Exception("Oops!"); - } + throw new Exception("Oops!"); } } - catch (Exception) + } + catch (Exception) + { + using (activity3 = activitySource.StartActivity("Activity3")) { - using (activity3 = activitySource.StartActivity("Activity3")) - { - } } - finally + } + finally + { + using (activity4 = activitySource.StartActivity("Activity4")) { - using (activity4 = activitySource.StartActivity("Activity4")) - { - } } + } - try - { - throw new Exception("Oops!"); - } - catch (Exception) - { - /* - Note: Behavior here is different depending on the processor - architecture. + try + { + throw new Exception("Oops!"); + } + catch (Exception) + { + /* + Note: Behavior here is different depending on the processor + architecture. - x86: Exception is cleared BEFORE the catch runs. - Marshal.GetExceptionPointers returns zero. + x86: Exception is cleared BEFORE the catch runs. + Marshal.GetExceptionPointers returns zero. - non-x86: Exception is cleared AFTER the catch runs. - Marshal.GetExceptionPointers returns non-zero. - */ - activity5 = activitySource.StartActivity("Activity5"); - } - finally - { - activity5.Dispose(); - } + non-x86: Exception is cleared AFTER the catch runs. + Marshal.GetExceptionPointers returns non-zero. + */ + activity5 = activitySource.StartActivity("Activity5"); + } + finally + { + activity5.Dispose(); + } - Assert.Equal(StatusCode.Error, activity1.GetStatus().StatusCode); - Assert.Null(GetTagValue(activity1, "otel.exception_pointers")); - Assert.Equal(StatusCode.Error, activity2.GetStatus().StatusCode); - Assert.Null(GetTagValue(activity2, "otel.exception_pointers")); - Assert.Equal(StatusCode.Unset, activity3.GetStatus().StatusCode); - Assert.Null(GetTagValue(activity3, "otel.exception_pointers")); - Assert.Equal(StatusCode.Unset, activity4.GetStatus().StatusCode); - Assert.Null(GetTagValue(activity4, "otel.exception_pointers")); - Assert.Equal(StatusCode.Unset, activity5.GetStatus().StatusCode); + Assert.Equal(StatusCode.Error, activity1.GetStatus().StatusCode); + Assert.Null(GetTagValue(activity1, "otel.exception_pointers")); + Assert.Equal(StatusCode.Error, activity2.GetStatus().StatusCode); + Assert.Null(GetTagValue(activity2, "otel.exception_pointers")); + Assert.Equal(StatusCode.Unset, activity3.GetStatus().StatusCode); + Assert.Null(GetTagValue(activity3, "otel.exception_pointers")); + Assert.Equal(StatusCode.Unset, activity4.GetStatus().StatusCode); + Assert.Null(GetTagValue(activity4, "otel.exception_pointers")); + Assert.Equal(StatusCode.Unset, activity5.GetStatus().StatusCode); #if !NETFRAMEWORK - if (Environment.Is64BitProcess) - { - // In this rare case, the following Activity tag will not get cleaned up due to perf consideration. - Assert.NotNull(GetTagValue(activity5, "otel.exception_pointers")); - } - else - { - Assert.Null(GetTagValue(activity5, "otel.exception_pointers")); - } -#endif + if (Environment.Is64BitProcess) + { + // In this rare case, the following Activity tag will not get cleaned up due to perf consideration. + Assert.NotNull(GetTagValue(activity5, "otel.exception_pointers")); } - - [Fact] - public void ActivityStatusNotSetWhenExceptionProcessorNotEnabled() + else { - var activitySourceName = Utils.GetCurrentMethodName(); - using var activitySource = new ActivitySource(activitySourceName); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .SetSampler(new AlwaysOnSampler()) - .Build(); + Assert.Null(GetTagValue(activity5, "otel.exception_pointers")); + } +#endif + } - Activity activity = null; + [Fact] + public void ActivityStatusNotSetWhenExceptionProcessorNotEnabled() + { + var activitySourceName = Utils.GetCurrentMethodName(); + using var activitySource = new ActivitySource(activitySourceName); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .SetSampler(new AlwaysOnSampler()) + .Build(); - try - { - using (activity = activitySource.StartActivity("Activity")) - { - throw new Exception("Oops!"); - } - } - catch (Exception) + Activity activity = null; + + try + { + using (activity = activitySource.StartActivity("Activity")) { + throw new Exception("Oops!"); } - - Assert.Equal(StatusCode.Unset, activity.GetStatus().StatusCode); } - - private static object GetTagValue(Activity activity, string tagName) + catch (Exception) { - Debug.Assert(activity != null, "Activity should not be null"); + } + + Assert.Equal(StatusCode.Unset, activity.GetStatus().StatusCode); + } - foreach (ref readonly var tag in activity.EnumerateTagObjects()) + private static object GetTagValue(Activity activity, string tagName) + { + Debug.Assert(activity != null, "Activity should not be null"); + + foreach (ref readonly var tag in activity.EnumerateTagObjects()) + { + if (tag.Key == tagName) { - if (tag.Key == tagName) - { - return tag.Value; - } + return tag.Value; } - - return null; } + + return null; } } diff --git a/test/OpenTelemetry.Tests/Trace/ExportProcessorTest.cs b/test/OpenTelemetry.Tests/Trace/ExportProcessorTest.cs index 1158c0b33f0..9c4768bc251 100644 --- a/test/OpenTelemetry.Tests/Trace/ExportProcessorTest.cs +++ b/test/OpenTelemetry.Tests/Trace/ExportProcessorTest.cs @@ -1,95 +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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Exporter; using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Trace.Tests +namespace OpenTelemetry.Trace.Tests; + +public class ExportProcessorTest { - public class ExportProcessorTest + [Fact] + public void ExportProcessorIgnoresActivityWhenDropped() { - [Fact] - public void ExportProcessorIgnoresActivityWhenDropped() - { - var activitySourceName = Utils.GetCurrentMethodName(); - var sampler = new AlwaysOffSampler(); - var exportedItems = new List(); - using var processor = new TestActivityExportProcessor(new InMemoryExporter(exportedItems)); - using var activitySource = new ActivitySource(activitySourceName); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .SetSampler(sampler) - .AddProcessor(processor) - .Build(); + var activitySourceName = Utils.GetCurrentMethodName(); + var sampler = new AlwaysOffSampler(); + var exportedItems = new List(); + using var processor = new TestActivityExportProcessor(new InMemoryExporter(exportedItems)); + using var activitySource = new ActivitySource(activitySourceName); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .SetSampler(sampler) + .AddProcessor(processor) + .Build(); - using (var activity = activitySource.StartActivity("Activity")) - { - Assert.False(activity.IsAllDataRequested); - Assert.Equal(ActivityTraceFlags.None, activity.ActivityTraceFlags); - } - - Assert.Empty(processor.ExportedItems); + using (var activity = activitySource.StartActivity("Activity")) + { + Assert.False(activity.IsAllDataRequested); + Assert.Equal(ActivityTraceFlags.None, activity.ActivityTraceFlags); } - [Fact] - public void ExportProcessorIgnoresActivityMarkedAsRecordOnly() - { - var activitySourceName = Utils.GetCurrentMethodName(); - var sampler = new RecordOnlySampler(); - var exportedItems = new List(); - using var processor = new TestActivityExportProcessor(new InMemoryExporter(exportedItems)); - using var activitySource = new ActivitySource(activitySourceName); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .SetSampler(sampler) - .AddProcessor(processor) - .Build(); + Assert.Empty(processor.ExportedItems); + } - using (var activity = activitySource.StartActivity("Activity")) - { - Assert.True(activity.IsAllDataRequested); - Assert.Equal(ActivityTraceFlags.None, activity.ActivityTraceFlags); - } + [Fact] + public void ExportProcessorIgnoresActivityMarkedAsRecordOnly() + { + var activitySourceName = Utils.GetCurrentMethodName(); + var sampler = new RecordOnlySampler(); + var exportedItems = new List(); + using var processor = new TestActivityExportProcessor(new InMemoryExporter(exportedItems)); + using var activitySource = new ActivitySource(activitySourceName); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .SetSampler(sampler) + .AddProcessor(processor) + .Build(); - Assert.Empty(processor.ExportedItems); + using (var activity = activitySource.StartActivity("Activity")) + { + Assert.True(activity.IsAllDataRequested); + Assert.Equal(ActivityTraceFlags.None, activity.ActivityTraceFlags); } - [Fact] - public void ExportProcessorExportsActivityMarkedAsRecordAndSample() - { - var activitySourceName = Utils.GetCurrentMethodName(); - var sampler = new AlwaysOnSampler(); - var exportedItems = new List(); - using var processor = new TestActivityExportProcessor(new InMemoryExporter(exportedItems)); - using var activitySource = new ActivitySource(activitySourceName); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .SetSampler(sampler) - .AddProcessor(processor) - .Build(); + Assert.Empty(processor.ExportedItems); + } - using (var activity = activitySource.StartActivity("Activity")) - { - Assert.True(activity.IsAllDataRequested); - Assert.Equal(ActivityTraceFlags.Recorded, activity.ActivityTraceFlags); - } + [Fact] + public void ExportProcessorExportsActivityMarkedAsRecordAndSample() + { + var activitySourceName = Utils.GetCurrentMethodName(); + var sampler = new AlwaysOnSampler(); + var exportedItems = new List(); + using var processor = new TestActivityExportProcessor(new InMemoryExporter(exportedItems)); + using var activitySource = new ActivitySource(activitySourceName); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .SetSampler(sampler) + .AddProcessor(processor) + .Build(); - Assert.Single(processor.ExportedItems); + using (var activity = activitySource.StartActivity("Activity")) + { + Assert.True(activity.IsAllDataRequested); + Assert.Equal(ActivityTraceFlags.Recorded, activity.ActivityTraceFlags); } + + Assert.Single(processor.ExportedItems); } } diff --git a/test/OpenTelemetry.Tests/Trace/LinkTest.cs b/test/OpenTelemetry.Tests/Trace/LinkTest.cs index 6fa7e218989..fbfd543da1c 100644 --- a/test/OpenTelemetry.Tests/Trace/LinkTest.cs +++ b/test/OpenTelemetry.Tests/Trace/LinkTest.cs @@ -1,130 +1,117 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + using System.Diagnostics; using Xunit; -namespace OpenTelemetry.Trace.Tests +namespace OpenTelemetry.Trace.Tests; + +public class LinkTest : IDisposable { - public class LinkTest : IDisposable + private readonly IDictionary attributesMap = new Dictionary(); + private readonly SpanContext spanContext; + private readonly SpanAttributes tags; + + public LinkTest() { - private readonly IDictionary attributesMap = new Dictionary(); - private readonly SpanContext spanContext; - private readonly SpanAttributes tags; + this.spanContext = new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None); + + this.attributesMap.Add("MyAttributeKey0", "MyStringAttribute"); + this.attributesMap.Add("MyAttributeKey1", 10L); + this.attributesMap.Add("MyAttributeKey2", true); + this.attributesMap.Add("MyAttributeKey3", 0.005); + this.attributesMap.Add("MyAttributeKey4", new long[] { 1, 2 }); + this.attributesMap.Add("MyAttributeKey5", new string[] { "a", "b" }); + this.attributesMap.Add("MyAttributeKey6", new bool[] { true, false }); + this.attributesMap.Add("MyAttributeKey7", new double[] { 0.1, -0.1 }); + this.tags = new SpanAttributes(); + this.tags.Add("MyAttributeKey0", "MyStringAttribute"); + this.tags.Add("MyAttributeKey1", 10L); + this.tags.Add("MyAttributeKey2", true); + this.tags.Add("MyAttributeKey3", 0.005); + this.tags.Add("MyAttributeKey4", new long[] { 1, 2 }); + this.tags.Add("MyAttributeKey5", new string[] { "a", "b" }); + this.tags.Add("MyAttributeKey6", new bool[] { true, false }); + this.tags.Add("MyAttributeKey7", new double[] { 0.1, -0.1 }); + } - public LinkTest() - { - this.spanContext = new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None); - - this.attributesMap.Add("MyAttributeKey0", "MyStringAttribute"); - this.attributesMap.Add("MyAttributeKey1", 10L); - this.attributesMap.Add("MyAttributeKey2", true); - this.attributesMap.Add("MyAttributeKey3", 0.005); - this.attributesMap.Add("MyAttributeKey4", new long[] { 1, 2 }); - this.attributesMap.Add("MyAttributeKey5", new string[] { "a", "b" }); - this.attributesMap.Add("MyAttributeKey6", new bool[] { true, false }); - this.attributesMap.Add("MyAttributeKey7", new double[] { 0.1, -0.1 }); - this.tags = new SpanAttributes(); - this.tags.Add("MyAttributeKey0", "MyStringAttribute"); - this.tags.Add("MyAttributeKey1", 10L); - this.tags.Add("MyAttributeKey2", true); - this.tags.Add("MyAttributeKey3", 0.005); - this.tags.Add("MyAttributeKey4", new long[] { 1, 2 }); - this.tags.Add("MyAttributeKey5", new string[] { "a", "b" }); - this.tags.Add("MyAttributeKey6", new bool[] { true, false }); - this.tags.Add("MyAttributeKey7", new double[] { 0.1, -0.1 }); - } + [Fact] + public void FromSpanContext() + { + var link = new Link(this.spanContext); + Assert.Equal(this.spanContext.TraceId, link.Context.TraceId); + Assert.Equal(this.spanContext.SpanId, link.Context.SpanId); + } - [Fact] - public void FromSpanContext() - { - var link = new Link(this.spanContext); - Assert.Equal(this.spanContext.TraceId, link.Context.TraceId); - Assert.Equal(this.spanContext.SpanId, link.Context.SpanId); - } + [Fact] + public void FromSpanContext_WithAttributes() + { + var link = new Link(this.spanContext, this.tags); + Assert.Equal(this.spanContext.TraceId, link.Context.TraceId); + Assert.Equal(this.spanContext.SpanId, link.Context.SpanId); - [Fact] - public void FromSpanContext_WithAttributes() + foreach (var attributemap in this.attributesMap) { - var link = new Link(this.spanContext, this.tags); - Assert.Equal(this.spanContext.TraceId, link.Context.TraceId); - Assert.Equal(this.spanContext.SpanId, link.Context.SpanId); - - foreach (var attributemap in this.attributesMap) - { - Assert.Equal(attributemap.Value, link.Attributes.FirstOrDefault(a => a.Key == attributemap.Key).Value); - } + Assert.Equal(attributemap.Value, link.Attributes.FirstOrDefault(a => a.Key == attributemap.Key).Value); } + } - [Fact] - public void Equality() - { - var link1 = new Link(this.spanContext); - var link2 = new Link(this.spanContext); - object link3 = new Link(this.spanContext); + [Fact] + public void Equality() + { + var link1 = new Link(this.spanContext); + var link2 = new Link(this.spanContext); + object link3 = new Link(this.spanContext); - Assert.Equal(link1, link2); - Assert.True(link1 == link2); - Assert.True(link1.Equals(link3)); - } + Assert.Equal(link1, link2); + Assert.True(link1 == link2); + Assert.True(link1.Equals(link3)); + } - [Fact(Skip = "ActivityLink.Equals is broken in DS7 preview: https://github.com/dotnet/runtime/issues/74026")] - public void Equality_WithAttributes() - { - var link1 = new Link(this.spanContext, this.tags); - var link2 = new Link(this.spanContext, this.tags); - object link3 = new Link(this.spanContext, this.tags); + [Fact(Skip = "ActivityLink.Equals is broken in DS7 preview: https://github.com/dotnet/runtime/issues/74026")] + public void Equality_WithAttributes() + { + var link1 = new Link(this.spanContext, this.tags); + var link2 = new Link(this.spanContext, this.tags); + object link3 = new Link(this.spanContext, this.tags); - Assert.Equal(link1, link2); - Assert.True(link1 == link2); - Assert.True(link1.Equals(link3)); - } + Assert.Equal(link1, link2); + Assert.True(link1 == link2); + Assert.True(link1.Equals(link3)); + } - [Fact] - public void NotEquality() - { - var link1 = new Link(new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None)); - var link2 = new Link(new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None)); + [Fact] + public void NotEquality() + { + var link1 = new Link(new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None)); + var link2 = new Link(new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None)); - Assert.NotEqual(link1, link2); - Assert.True(link1 != link2); - } + Assert.NotEqual(link1, link2); + Assert.True(link1 != link2); + } - [Fact] - public void NotEquality_WithAttributes() - { - var tag1 = new SpanAttributes(); - var tag2 = this.tags; - var link1 = new Link(this.spanContext, tag1); - var link2 = new Link(this.spanContext, tag2); + [Fact] + public void NotEquality_WithAttributes() + { + var tag1 = new SpanAttributes(); + var tag2 = this.tags; + var link1 = new Link(this.spanContext, tag1); + var link2 = new Link(this.spanContext, tag2); - Assert.NotEqual(link1, link2); - Assert.True(link1 != link2); - } + Assert.NotEqual(link1, link2); + Assert.True(link1 != link2); + } - [Fact] - public void TestGetHashCode() - { - var link1 = new Link(this.spanContext, this.tags); - Assert.NotEqual(0, link1.GetHashCode()); - } + [Fact] + public void TestGetHashCode() + { + var link1 = new Link(this.spanContext, this.tags); + Assert.NotEqual(0, link1.GetHashCode()); + } - public void Dispose() - { - Activity.Current = null; - GC.SuppressFinalize(this); - } + public void Dispose() + { + Activity.Current = null; + GC.SuppressFinalize(this); } } diff --git a/test/OpenTelemetry.Tests/Trace/ParentBasedSamplerTests.cs b/test/OpenTelemetry.Tests/Trace/ParentBasedSamplerTests.cs index fa5596314f3..0861a224ca9 100644 --- a/test/OpenTelemetry.Tests/Trace/ParentBasedSamplerTests.cs +++ b/test/OpenTelemetry.Tests/Trace/ParentBasedSamplerTests.cs @@ -1,171 +1,138 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -using Moq; +using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Trace.Tests +namespace OpenTelemetry.Trace.Tests; + +public class ParentBasedSamplerTests { - public class ParentBasedSamplerTests - { - private readonly ParentBasedSampler parentBasedOnSampler = new(new AlwaysOnSampler()); - private readonly ParentBasedSampler parentBasedOffSampler = new(new AlwaysOffSampler()); + private readonly ParentBasedSampler parentBasedOnSampler = new(new AlwaysOnSampler()); + private readonly ParentBasedSampler parentBasedOffSampler = new(new AlwaysOffSampler()); - [Fact] - public void SampledParent() - { - // No parent, use delegate sampler. - Assert.Equal( - new SamplingResult(SamplingDecision.RecordAndSample), - this.parentBasedOnSampler.ShouldSample(default)); - - // No parent, use delegate sampler. - Assert.Equal( - new SamplingResult(SamplingDecision.Drop), - this.parentBasedOffSampler.ShouldSample(default)); - - // Not sampled parent, don't sample. - Assert.Equal( - new SamplingResult(SamplingDecision.Drop), - this.parentBasedOnSampler.ShouldSample( - new SamplingParameters( - parentContext: new ActivityContext( - ActivityTraceId.CreateRandom(), - ActivitySpanId.CreateRandom(), - ActivityTraceFlags.None), - traceId: default, - name: "Span", - kind: ActivityKind.Client))); - - // Sampled parent, sample. - Assert.Equal( - new SamplingResult(SamplingDecision.RecordAndSample), - this.parentBasedOffSampler.ShouldSample( - new SamplingParameters( - parentContext: new ActivityContext( - ActivityTraceId.CreateRandom(), - ActivitySpanId.CreateRandom(), - ActivityTraceFlags.Recorded), - traceId: default, - name: "Span", - kind: ActivityKind.Client))); - } - - /// - /// Checks fix for https://github.com/open-telemetry/opentelemetry-dotnet/issues/1846. - /// - [Fact] - public void DoNotExamineLinks() - { - var sampledLink = new ActivityLink[] - { - new ActivityLink( - new ActivityContext( + [Fact] + public void SampledParent() + { + // No parent, use delegate sampler. + Assert.Equal( + new SamplingResult(SamplingDecision.RecordAndSample), + this.parentBasedOnSampler.ShouldSample(default)); + + // No parent, use delegate sampler. + Assert.Equal( + new SamplingResult(SamplingDecision.Drop), + this.parentBasedOffSampler.ShouldSample(default)); + + // Not sampled parent, don't sample. + Assert.Equal( + new SamplingResult(SamplingDecision.Drop), + this.parentBasedOnSampler.ShouldSample( + new SamplingParameters( + parentContext: new ActivityContext( ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), - ActivityTraceFlags.Recorded)), - }; - - var notSampledParent = new ActivityContext( - ActivityTraceId.CreateRandom(), - ActivitySpanId.CreateRandom(), - ActivityTraceFlags.None); - - // Parent is not sampled - default behavior should be to DROP, - // even if a sampled linked activity exists. - Assert.Equal( - new SamplingResult(SamplingDecision.Drop), - this.parentBasedOffSampler.ShouldSample( - new SamplingParameters( - parentContext: notSampledParent, - traceId: default, - name: "Span", - kind: ActivityKind.Client, - links: sampledLink))); - } - - [Theory] - [InlineData(true, true)] - [InlineData(true, false)] - [InlineData(false, true)] - [InlineData(false, false)] - public void CustomSamplers(bool parentIsRemote, bool parentIsSampled) - { - var mockRepository = new MockRepository(MockBehavior.Strict); - var remoteParentSampled = mockRepository.Create(); - var remoteParentNotSampled = mockRepository.Create(); - var localParentSampled = mockRepository.Create(); - var localParentNotSampled = mockRepository.Create(); - - var samplerUnderTest = new ParentBasedSampler( - new AlwaysOnSampler(), // root - remoteParentSampled.Object, - remoteParentNotSampled.Object, - localParentSampled.Object, - localParentNotSampled.Object); - - var samplingParams = MakeTestParameters(parentIsRemote, parentIsSampled); - - Mock invokedSampler; - if (parentIsRemote && parentIsSampled) - { - invokedSampler = remoteParentSampled; - } - else if (parentIsRemote && !parentIsSampled) - { - invokedSampler = remoteParentNotSampled; - } - else if (!parentIsRemote && parentIsSampled) - { - invokedSampler = localParentSampled; - } - else - { - invokedSampler = localParentNotSampled; - } - - var expectedResult = new SamplingResult(SamplingDecision.RecordAndSample); - invokedSampler.Setup(sampler => sampler.ShouldSample(samplingParams)).Returns(expectedResult); - - var actualResult = samplerUnderTest.ShouldSample(samplingParams); - - mockRepository.VerifyAll(); - Assert.Equal(expectedResult, actualResult); - mockRepository.VerifyNoOtherCalls(); - } - - [Fact] - public void DisallowNullRootSampler() - { - Assert.Throws(() => new ParentBasedSampler(null)); - } + ActivityTraceFlags.None), + traceId: default, + name: "Span", + kind: ActivityKind.Client))); + + // Sampled parent, sample. + Assert.Equal( + new SamplingResult(SamplingDecision.RecordAndSample), + this.parentBasedOffSampler.ShouldSample( + new SamplingParameters( + parentContext: new ActivityContext( + ActivityTraceId.CreateRandom(), + ActivitySpanId.CreateRandom(), + ActivityTraceFlags.Recorded), + traceId: default, + name: "Span", + kind: ActivityKind.Client))); + } - private static SamplingParameters MakeTestParameters(bool parentIsRemote, bool parentIsSampled) + /// + /// Checks fix for https://github.com/open-telemetry/opentelemetry-dotnet/issues/1846. + /// + [Fact] + public void DoNotExamineLinks() + { + var sampledLink = new ActivityLink[] { - return new SamplingParameters( - parentContext: new ActivityContext( + new ActivityLink( + new ActivityContext( ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), - parentIsSampled ? ActivityTraceFlags.Recorded : ActivityTraceFlags.None, - null, - parentIsRemote), - traceId: default, - name: "Span", - kind: ActivityKind.Client); - } + ActivityTraceFlags.Recorded)), + }; + + var notSampledParent = new ActivityContext( + ActivityTraceId.CreateRandom(), + ActivitySpanId.CreateRandom(), + ActivityTraceFlags.None); + + // Parent is not sampled - default behavior should be to DROP, + // even if a sampled linked activity exists. + Assert.Equal( + new SamplingResult(SamplingDecision.Drop), + this.parentBasedOffSampler.ShouldSample( + new SamplingParameters( + parentContext: notSampledParent, + traceId: default, + name: "Span", + kind: ActivityKind.Client, + links: sampledLink))); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void CustomSamplers(bool parentIsRemote, bool parentIsSampled) + { + var remoteParentSampled = new TestSampler(); + var remoteParentNotSampled = new TestSampler(); + var localParentSampled = new TestSampler(); + var localParentNotSampled = new TestSampler(); + + var samplerUnderTest = new ParentBasedSampler( + new AlwaysOnSampler(), // root + remoteParentSampled, + remoteParentNotSampled, + localParentSampled, + localParentNotSampled); + + var samplingParams = MakeTestParameters(parentIsRemote, parentIsSampled); + var expectedResult = new SamplingResult(SamplingDecision.RecordAndSample); + var actualResult = samplerUnderTest.ShouldSample(samplingParams); + + Assert.Equal(parentIsRemote && parentIsSampled, remoteParentSampled.LatestSamplingParameters.Equals(samplingParams)); + Assert.Equal(parentIsRemote && !parentIsSampled, remoteParentNotSampled.LatestSamplingParameters.Equals(samplingParams)); + Assert.Equal(!parentIsRemote && parentIsSampled, localParentSampled.LatestSamplingParameters.Equals(samplingParams)); + Assert.Equal(!parentIsRemote && !parentIsSampled, localParentNotSampled.LatestSamplingParameters.Equals(samplingParams)); + + Assert.Equal(expectedResult, actualResult); + } + + [Fact] + public void DisallowNullRootSampler() + { + Assert.Throws(() => new ParentBasedSampler(null)); + } + + private static SamplingParameters MakeTestParameters(bool parentIsRemote, bool parentIsSampled) + { + return new SamplingParameters( + parentContext: new ActivityContext( + ActivityTraceId.CreateRandom(), + ActivitySpanId.CreateRandom(), + parentIsSampled ? ActivityTraceFlags.Recorded : ActivityTraceFlags.None, + null, + parentIsRemote), + traceId: default, + name: "Span", + kind: ActivityKind.Client); } } diff --git a/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTest.cs b/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTest.cs index 248a0bdee8f..728d3884e45 100644 --- a/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTest.cs +++ b/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTest.cs @@ -1,329 +1,320 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + using System.Diagnostics; using Xunit; -namespace OpenTelemetry.Context.Propagation.Tests +namespace OpenTelemetry.Context.Propagation.Tests; + +public class TraceContextPropagatorTest { - public class TraceContextPropagatorTest - { - private const string TraceParent = "traceparent"; - private const string TraceState = "tracestate"; - private const string TraceId = "0af7651916cd43dd8448eb211c80319c"; - private const string SpanId = "b9c7c989f97918e1"; + private const string TraceParent = "traceparent"; + private const string TraceState = "tracestate"; + private const string TraceId = "0af7651916cd43dd8448eb211c80319c"; + private const string SpanId = "b9c7c989f97918e1"; - private static readonly string[] Empty = Array.Empty(); - private static readonly Func, string, IEnumerable> Getter = (headers, name) => + private static readonly string[] Empty = Array.Empty(); + private static readonly Func, string, IEnumerable> Getter = (headers, name) => + { + if (headers.TryGetValue(name, out var value)) { - if (headers.TryGetValue(name, out var value)) - { - return new[] { value }; - } + return new[] { value }; + } - return Empty; - }; + return Empty; + }; - private static readonly Func, string, IEnumerable> ArrayGetter = (headers, name) => + private static readonly Func, string, IEnumerable> ArrayGetter = (headers, name) => + { + if (headers.TryGetValue(name, out var value)) { - if (headers.TryGetValue(name, out var value)) - { - return value; - } + return value; + } - return Array.Empty(); - }; + return Array.Empty(); + }; - private static readonly Action, string, string> Setter = (carrier, name, value) => - { - carrier[name] = value; - }; + private static readonly Action, string, string> Setter = (carrier, name, value) => + { + carrier[name] = value; + }; - [Fact] - public void CanParseExampleFromSpec() + [Fact] + public void CanParseExampleFromSpec() + { + var headers = new Dictionary { - var headers = new Dictionary - { - { TraceParent, $"00-{TraceId}-{SpanId}-01" }, - { TraceState, $"congo=lZWRzIHRoNhcm5hbCBwbGVhc3VyZS4,rojo=00-{TraceId}-00f067aa0ba902b7-01" }, - }; + { TraceParent, $"00-{TraceId}-{SpanId}-01" }, + { TraceState, $"congo=lZWRzIHRoNhcm5hbCBwbGVhc3VyZS4,rojo=00-{TraceId}-00f067aa0ba902b7-01" }, + }; - var f = new TraceContextPropagator(); - var ctx = f.Extract(default, headers, Getter); + var f = new TraceContextPropagator(); + var ctx = f.Extract(default, headers, Getter); - Assert.Equal(ActivityTraceId.CreateFromString(TraceId.AsSpan()), ctx.ActivityContext.TraceId); - Assert.Equal(ActivitySpanId.CreateFromString(SpanId.AsSpan()), ctx.ActivityContext.SpanId); + Assert.Equal(ActivityTraceId.CreateFromString(TraceId.AsSpan()), ctx.ActivityContext.TraceId); + Assert.Equal(ActivitySpanId.CreateFromString(SpanId.AsSpan()), ctx.ActivityContext.SpanId); - Assert.True(ctx.ActivityContext.IsRemote); - Assert.True(ctx.ActivityContext.IsValid()); - Assert.True((ctx.ActivityContext.TraceFlags & ActivityTraceFlags.Recorded) != 0); + Assert.True(ctx.ActivityContext.IsRemote); + Assert.True(ctx.ActivityContext.IsValid()); + Assert.True((ctx.ActivityContext.TraceFlags & ActivityTraceFlags.Recorded) != 0); - Assert.Equal($"congo=lZWRzIHRoNhcm5hbCBwbGVhc3VyZS4,rojo=00-{TraceId}-00f067aa0ba902b7-01", ctx.ActivityContext.TraceState); - } + Assert.Equal($"congo=lZWRzIHRoNhcm5hbCBwbGVhc3VyZS4,rojo=00-{TraceId}-00f067aa0ba902b7-01", ctx.ActivityContext.TraceState); + } - [Fact] - public void NotSampled() + [Fact] + public void NotSampled() + { + var headers = new Dictionary { - var headers = new Dictionary - { - { TraceParent, $"00-{TraceId}-{SpanId}-00" }, - }; + { TraceParent, $"00-{TraceId}-{SpanId}-00" }, + }; - var f = new TraceContextPropagator(); - var ctx = f.Extract(default, headers, Getter); + var f = new TraceContextPropagator(); + var ctx = f.Extract(default, headers, Getter); - Assert.Equal(ActivityTraceId.CreateFromString(TraceId.AsSpan()), ctx.ActivityContext.TraceId); - Assert.Equal(ActivitySpanId.CreateFromString(SpanId.AsSpan()), ctx.ActivityContext.SpanId); - Assert.True((ctx.ActivityContext.TraceFlags & ActivityTraceFlags.Recorded) == 0); + Assert.Equal(ActivityTraceId.CreateFromString(TraceId.AsSpan()), ctx.ActivityContext.TraceId); + Assert.Equal(ActivitySpanId.CreateFromString(SpanId.AsSpan()), ctx.ActivityContext.SpanId); + Assert.True((ctx.ActivityContext.TraceFlags & ActivityTraceFlags.Recorded) == 0); - Assert.True(ctx.ActivityContext.IsRemote); - Assert.True(ctx.ActivityContext.IsValid()); - } + Assert.True(ctx.ActivityContext.IsRemote); + Assert.True(ctx.ActivityContext.IsValid()); + } - [Fact] - public void IsBlankIfNoHeader() - { - var headers = new Dictionary(); + [Fact] + public void IsBlankIfNoHeader() + { + var headers = new Dictionary(); - var f = new TraceContextPropagator(); - var ctx = f.Extract(default, headers, Getter); + var f = new TraceContextPropagator(); + var ctx = f.Extract(default, headers, Getter); - Assert.False(ctx.ActivityContext.IsValid()); - } + Assert.False(ctx.ActivityContext.IsValid()); + } - [Fact] - public void IsBlankIfInvalid() + [Theory] + [InlineData($"00-xyz7651916cd43dd8448eb211c80319c-{SpanId}-01")] + [InlineData($"00-{TraceId}-xyz7c989f97918e1-01")] + [InlineData($"00-{TraceId}-{SpanId}-x1")] + [InlineData($"00-{TraceId}-{SpanId}-1x")] + public void IsBlankIfInvalid(string invalidTraceParent) + { + var headers = new Dictionary { - var headers = new Dictionary - { - { TraceParent, $"00-xyz7651916cd43dd8448eb211c80319c-{SpanId}-01" }, - }; + { TraceParent, invalidTraceParent }, + }; - var f = new TraceContextPropagator(); - var ctx = f.Extract(default, headers, Getter); + var f = new TraceContextPropagator(); + var ctx = f.Extract(default, headers, Getter); - Assert.False(ctx.ActivityContext.IsValid()); - } + Assert.False(ctx.ActivityContext.IsValid()); + } - [Fact] - public void TracestateToStringEmpty() + [Fact] + public void TracestateToStringEmpty() + { + var headers = new Dictionary { - var headers = new Dictionary - { - { TraceParent, $"00-{TraceId}-{SpanId}-01" }, - }; + { TraceParent, $"00-{TraceId}-{SpanId}-01" }, + }; - var f = new TraceContextPropagator(); - var ctx = f.Extract(default, headers, Getter); + var f = new TraceContextPropagator(); + var ctx = f.Extract(default, headers, Getter); - Assert.Null(ctx.ActivityContext.TraceState); - } + Assert.Null(ctx.ActivityContext.TraceState); + } - [Fact] - public void TracestateToString() + [Fact] + public void TracestateToString() + { + var headers = new Dictionary { - var headers = new Dictionary - { - { TraceParent, $"00-{TraceId}-{SpanId}-01" }, - { TraceState, "k1=v1,k2=v2,k3=v3" }, - }; + { TraceParent, $"00-{TraceId}-{SpanId}-01" }, + { TraceState, "k1=v1,k2=v2,k3=v3" }, + }; - var f = new TraceContextPropagator(); - var ctx = f.Extract(default, headers, Getter); + var f = new TraceContextPropagator(); + var ctx = f.Extract(default, headers, Getter); - Assert.Equal("k1=v1,k2=v2,k3=v3", ctx.ActivityContext.TraceState); - } + Assert.Equal("k1=v1,k2=v2,k3=v3", ctx.ActivityContext.TraceState); + } - [Fact] - public void Inject_NoTracestate() + [Fact] + public void Inject_NoTracestate() + { + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + var expectedHeaders = new Dictionary { - var traceId = ActivityTraceId.CreateRandom(); - var spanId = ActivitySpanId.CreateRandom(); - var expectedHeaders = new Dictionary - { - { TraceParent, $"00-{traceId}-{spanId}-01" }, - }; - - var activityContext = new ActivityContext(traceId, spanId, ActivityTraceFlags.Recorded, traceState: null); - PropagationContext propagationContext = new PropagationContext(activityContext, default); - var carrier = new Dictionary(); - var f = new TraceContextPropagator(); - f.Inject(propagationContext, carrier, Setter); - - Assert.Equal(expectedHeaders, carrier); - } + { TraceParent, $"00-{traceId}-{spanId}-01" }, + }; - [Fact] - public void Inject_WithTracestate() - { - var traceId = ActivityTraceId.CreateRandom(); - var spanId = ActivitySpanId.CreateRandom(); - var expectedHeaders = new Dictionary - { - { TraceParent, $"00-{traceId}-{spanId}-01" }, - { TraceState, $"congo=lZWRzIHRoNhcm5hbCBwbGVhc3VyZS4,rojo=00-{traceId}-00f067aa0ba902b7-01" }, - }; - - var activityContext = new ActivityContext(traceId, spanId, ActivityTraceFlags.Recorded, expectedHeaders[TraceState]); - PropagationContext propagationContext = new PropagationContext(activityContext, default); - var carrier = new Dictionary(); - var f = new TraceContextPropagator(); - f.Inject(propagationContext, carrier, Setter); - - Assert.Equal(expectedHeaders, carrier); - } + var activityContext = new ActivityContext(traceId, spanId, ActivityTraceFlags.Recorded, traceState: null); + PropagationContext propagationContext = new PropagationContext(activityContext, default); + var carrier = new Dictionary(); + var f = new TraceContextPropagator(); + f.Inject(propagationContext, carrier, Setter); - [Fact] - public void DuplicateKeys() - { - // test_tracestate_duplicated_keys - Assert.Empty(CallTraceContextPropagator("foo=1,foo=1")); - Assert.Empty(CallTraceContextPropagator("foo=1,foo=2")); - Assert.Empty(CallTraceContextPropagator(new string[] { "foo=1", "foo=1" })); - Assert.Empty(CallTraceContextPropagator(new string[] { "foo=1", "foo=2" })); - } + Assert.Equal(expectedHeaders, carrier); + } - [Fact] - public void Key_IllegalCharacters() + [Fact] + public void Inject_WithTracestate() + { + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + var expectedHeaders = new Dictionary { - // test_tracestate_key_illegal_characters - Assert.Empty(CallTraceContextPropagator("foo =1")); - Assert.Empty(CallTraceContextPropagator("FOO =1")); - Assert.Empty(CallTraceContextPropagator("foo.bar=1")); - } + { TraceParent, $"00-{traceId}-{spanId}-01" }, + { TraceState, $"congo=lZWRzIHRoNhcm5hbCBwbGVhc3VyZS4,rojo=00-{traceId}-00f067aa0ba902b7-01" }, + }; - [Fact] - public void Key_IllegalVendorFormat() - { - // test_tracestate_key_illegal_vendor_format - Assert.Empty(CallTraceContextPropagator("foo@=1,bar=2")); - Assert.Empty(CallTraceContextPropagator("@foo=1,bar=2")); - Assert.Empty(CallTraceContextPropagator("foo@@bar=1,bar=2")); - Assert.Empty(CallTraceContextPropagator("foo@bar@baz=1,bar=2")); - } + var activityContext = new ActivityContext(traceId, spanId, ActivityTraceFlags.Recorded, expectedHeaders[TraceState]); + PropagationContext propagationContext = new PropagationContext(activityContext, default); + var carrier = new Dictionary(); + var f = new TraceContextPropagator(); + f.Inject(propagationContext, carrier, Setter); - [Fact] - public void MemberCountLimit() - { - // test_tracestate_member_count_limit - var output1 = CallTraceContextPropagator(new string[] - { - "bar01=01,bar02=02,bar03=03,bar04=04,bar05=05,bar06=06,bar07=07,bar08=08,bar09=09,bar10=10", - "bar11=11,bar12=12,bar13=13,bar14=14,bar15=15,bar16=16,bar17=17,bar18=18,bar19=19,bar20=20", - "bar21=21,bar22=22,bar23=23,bar24=24,bar25=25,bar26=26,bar27=27,bar28=28,bar29=29,bar30=30", - "bar31=31,bar32=32", - }); - var expected = - "bar01=01,bar02=02,bar03=03,bar04=04,bar05=05,bar06=06,bar07=07,bar08=08,bar09=09,bar10=10" + "," + - "bar11=11,bar12=12,bar13=13,bar14=14,bar15=15,bar16=16,bar17=17,bar18=18,bar19=19,bar20=20" + "," + - "bar21=21,bar22=22,bar23=23,bar24=24,bar25=25,bar26=26,bar27=27,bar28=28,bar29=29,bar30=30" + "," + - "bar31=31,bar32=32"; - Assert.Equal(expected, output1); - - var output2 = CallTraceContextPropagator(new string[] - { - "bar01=01,bar02=02,bar03=03,bar04=04,bar05=05,bar06=06,bar07=07,bar08=08,bar09=09,bar10=10", - "bar11=11,bar12=12,bar13=13,bar14=14,bar15=15,bar16=16,bar17=17,bar18=18,bar19=19,bar20=20", - "bar21=21,bar22=22,bar23=23,bar24=24,bar25=25,bar26=26,bar27=27,bar28=28,bar29=29,bar30=30", - "bar31=31,bar32=32,bar33=33", - }); - Assert.Empty(output2); - } + Assert.Equal(expectedHeaders, carrier); + } - [Fact] - public void Key_KeyLengthLimit() - { - // test_tracestate_key_length_limit - string input1 = new string('z', 256) + "=1"; - Assert.Equal(input1, CallTraceContextPropagator(input1)); - Assert.Empty(CallTraceContextPropagator(new string('z', 257) + "=1")); - string input2 = new string('t', 241) + "@" + new string('v', 14) + "=1"; - Assert.Equal(input2, CallTraceContextPropagator(input2)); - Assert.Empty(CallTraceContextPropagator(new string('t', 242) + "@v=1")); - Assert.Empty(CallTraceContextPropagator("t@" + new string('v', 15) + "=1")); - } + [Fact] + public void DuplicateKeys() + { + // test_tracestate_duplicated_keys + Assert.Empty(CallTraceContextPropagator("foo=1,foo=1")); + Assert.Empty(CallTraceContextPropagator("foo=1,foo=2")); + Assert.Empty(CallTraceContextPropagator(new[] { "foo=1", "foo=1" })); + Assert.Empty(CallTraceContextPropagator(new[] { "foo=1", "foo=2" })); + } - [Fact] - public void Value_IllegalCharacters() - { - // test_tracestate_value_illegal_characters - Assert.Empty(CallTraceContextPropagator("foo=bar=baz")); - Assert.Empty(CallTraceContextPropagator("foo=,bar=3")); - } + [Fact] + public void Key_IllegalCharacters() + { + // test_tracestate_key_illegal_characters + Assert.Empty(CallTraceContextPropagator("foo =1")); + Assert.Empty(CallTraceContextPropagator("FOO =1")); + Assert.Empty(CallTraceContextPropagator("foo.bar=1")); + } - [Fact] - public void Traceparent_Version() + [Fact] + public void Key_IllegalVendorFormat() + { + // test_tracestate_key_illegal_vendor_format + Assert.Empty(CallTraceContextPropagator("foo@=1,bar=2")); + Assert.Empty(CallTraceContextPropagator("@foo=1,bar=2")); + Assert.Empty(CallTraceContextPropagator("foo@@bar=1,bar=2")); + Assert.Empty(CallTraceContextPropagator("foo@bar@baz=1,bar=2")); + } + + [Fact] + public void MemberCountLimit() + { + // test_tracestate_member_count_limit + var output1 = CallTraceContextPropagator(new string[] { - // test_traceparent_version_0x00 - Assert.NotEqual( - "12345678901234567890123456789012", - CallTraceContextPropagatorWithTraceParent("00-12345678901234567890123456789012-1234567890123456-01.")); - Assert.NotEqual( - "12345678901234567890123456789012", - CallTraceContextPropagatorWithTraceParent("00-12345678901234567890123456789012-1234567890123456-01-what-the-future-will-be-like")); - - // test_traceparent_version_0xcc - Assert.Equal( - "12345678901234567890123456789012", - CallTraceContextPropagatorWithTraceParent("cc-12345678901234567890123456789012-1234567890123456-01")); - Assert.Equal( - "12345678901234567890123456789012", - CallTraceContextPropagatorWithTraceParent("cc-12345678901234567890123456789012-1234567890123456-01-what-the-future-will-be-like")); - Assert.NotEqual( - "12345678901234567890123456789012", - CallTraceContextPropagatorWithTraceParent("cc-12345678901234567890123456789012-1234567890123456-01.what-the-future-will-be-like")); - - // test_traceparent_version_0xff - Assert.NotEqual( - "12345678901234567890123456789012", - CallTraceContextPropagatorWithTraceParent("ff-12345678901234567890123456789012-1234567890123456-01")); - } + "bar01=01,bar02=02,bar03=03,bar04=04,bar05=05,bar06=06,bar07=07,bar08=08,bar09=09,bar10=10", + "bar11=11,bar12=12,bar13=13,bar14=14,bar15=15,bar16=16,bar17=17,bar18=18,bar19=19,bar20=20", + "bar21=21,bar22=22,bar23=23,bar24=24,bar25=25,bar26=26,bar27=27,bar28=28,bar29=29,bar30=30", + "bar31=31,bar32=32", + }); + var expected = + "bar01=01,bar02=02,bar03=03,bar04=04,bar05=05,bar06=06,bar07=07,bar08=08,bar09=09,bar10=10" + "," + + "bar11=11,bar12=12,bar13=13,bar14=14,bar15=15,bar16=16,bar17=17,bar18=18,bar19=19,bar20=20" + "," + + "bar21=21,bar22=22,bar23=23,bar24=24,bar25=25,bar26=26,bar27=27,bar28=28,bar29=29,bar30=30" + "," + + "bar31=31,bar32=32"; + Assert.Equal(expected, output1); + + var output2 = CallTraceContextPropagator(new string[] + { + "bar01=01,bar02=02,bar03=03,bar04=04,bar05=05,bar06=06,bar07=07,bar08=08,bar09=09,bar10=10", + "bar11=11,bar12=12,bar13=13,bar14=14,bar15=15,bar16=16,bar17=17,bar18=18,bar19=19,bar20=20", + "bar21=21,bar22=22,bar23=23,bar24=24,bar25=25,bar26=26,bar27=27,bar28=28,bar29=29,bar30=30", + "bar31=31,bar32=32,bar33=33", + }); + Assert.Empty(output2); + } + + [Fact] + public void Key_KeyLengthLimit() + { + // test_tracestate_key_length_limit + string input1 = new string('z', 256) + "=1"; + Assert.Equal(input1, CallTraceContextPropagator(input1)); + Assert.Empty(CallTraceContextPropagator(new string('z', 257) + "=1")); + string input2 = new string('t', 241) + "@" + new string('v', 14) + "=1"; + Assert.Equal(input2, CallTraceContextPropagator(input2)); + Assert.Empty(CallTraceContextPropagator(new string('t', 242) + "@v=1")); + Assert.Empty(CallTraceContextPropagator("t@" + new string('v', 15) + "=1")); + } - private static string CallTraceContextPropagatorWithTraceParent(string traceparent) + [Fact] + public void Value_IllegalCharacters() + { + // test_tracestate_value_illegal_characters + Assert.Empty(CallTraceContextPropagator("foo=bar=baz")); + Assert.Empty(CallTraceContextPropagator("foo=,bar=3")); + } + + [Fact] + public void Traceparent_Version() + { + // test_traceparent_version_0x00 + Assert.NotEqual( + "12345678901234567890123456789012", + CallTraceContextPropagatorWithTraceParent("00-12345678901234567890123456789012-1234567890123456-01.")); + Assert.NotEqual( + "12345678901234567890123456789012", + CallTraceContextPropagatorWithTraceParent("00-12345678901234567890123456789012-1234567890123456-01-what-the-future-will-be-like")); + + // test_traceparent_version_0xcc + Assert.Equal( + "12345678901234567890123456789012", + CallTraceContextPropagatorWithTraceParent("cc-12345678901234567890123456789012-1234567890123456-01")); + Assert.Equal( + "12345678901234567890123456789012", + CallTraceContextPropagatorWithTraceParent("cc-12345678901234567890123456789012-1234567890123456-01-what-the-future-will-be-like")); + Assert.NotEqual( + "12345678901234567890123456789012", + CallTraceContextPropagatorWithTraceParent("cc-12345678901234567890123456789012-1234567890123456-01.what-the-future-will-be-like")); + + // test_traceparent_version_0xff + Assert.NotEqual( + "12345678901234567890123456789012", + CallTraceContextPropagatorWithTraceParent("ff-12345678901234567890123456789012-1234567890123456-01")); + } + + private static string CallTraceContextPropagatorWithTraceParent(string traceparent) + { + var headers = new Dictionary { - var headers = new Dictionary - { - { TraceParent, traceparent }, - }; - var f = new TraceContextPropagator(); - var ctx = f.Extract(default, headers, Getter); - return ctx.ActivityContext.TraceId.ToString(); - } + { TraceParent, traceparent }, + }; + var f = new TraceContextPropagator(); + var ctx = f.Extract(default, headers, Getter); + return ctx.ActivityContext.TraceId.ToString(); + } - private static string CallTraceContextPropagator(string tracestate) + private static string CallTraceContextPropagator(string tracestate) + { + var headers = new Dictionary { - var headers = new Dictionary - { - { TraceParent, $"00-{TraceId}-{SpanId}-01" }, - { TraceState, tracestate }, - }; - var f = new TraceContextPropagator(); - var ctx = f.Extract(default, headers, Getter); - return ctx.ActivityContext.TraceState; - } + { TraceParent, $"00-{TraceId}-{SpanId}-01" }, + { TraceState, tracestate }, + }; + var f = new TraceContextPropagator(); + var ctx = f.Extract(default, headers, Getter); + return ctx.ActivityContext.TraceState; + } - private static string CallTraceContextPropagator(string[] tracestate) + private static string CallTraceContextPropagator(string[] tracestate) + { + var headers = new Dictionary { - var headers = new Dictionary - { - { TraceParent, new string[] { $"00-{TraceId}-{SpanId}-01" } }, - { TraceState, tracestate }, - }; - var f = new TraceContextPropagator(); - var ctx = f.Extract(default, headers, ArrayGetter); - return ctx.ActivityContext.TraceState; - } + { TraceParent, new[] { $"00-{TraceId}-{SpanId}-01" } }, + { TraceState, tracestate }, + }; + var f = new TraceContextPropagator(); + var ctx = f.Extract(default, headers, ArrayGetter); + return ctx.ActivityContext.TraceState; } } diff --git a/test/OpenTelemetry.Tests/Trace/SamplersTest.cs b/test/OpenTelemetry.Tests/Trace/SamplersTest.cs index 6977860371f..94f158e910c 100644 --- a/test/OpenTelemetry.Tests/Trace/SamplersTest.cs +++ b/test/OpenTelemetry.Tests/Trace/SamplersTest.cs @@ -1,281 +1,268 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + using System.Diagnostics; using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Trace.Tests +namespace OpenTelemetry.Trace.Tests; + +public class SamplersTest { - public class SamplersTest - { - private static readonly ActivityKind ActivityKindServer = ActivityKind.Server; - private readonly ActivityTraceId traceId; - private readonly ActivitySpanId spanId; - private readonly ActivitySpanId parentSpanId; + private static readonly ActivityKind ActivityKindServer = ActivityKind.Server; + private readonly ActivityTraceId traceId; + private readonly ActivitySpanId spanId; + private readonly ActivitySpanId parentSpanId; - public SamplersTest() - { - this.traceId = ActivityTraceId.CreateRandom(); - this.spanId = ActivitySpanId.CreateRandom(); - this.parentSpanId = ActivitySpanId.CreateRandom(); - } + public SamplersTest() + { + this.traceId = ActivityTraceId.CreateRandom(); + this.spanId = ActivitySpanId.CreateRandom(); + this.parentSpanId = ActivitySpanId.CreateRandom(); + } - [Theory] - [InlineData(ActivityTraceFlags.Recorded)] - [InlineData(ActivityTraceFlags.None)] - public void AlwaysOnSampler_AlwaysReturnTrue(ActivityTraceFlags flags) - { - var parentContext = new ActivityContext(this.traceId, this.parentSpanId, flags); - var link = new ActivityLink(parentContext); + [Theory] + [InlineData(ActivityTraceFlags.Recorded)] + [InlineData(ActivityTraceFlags.None)] + public void AlwaysOnSampler_AlwaysReturnTrue(ActivityTraceFlags flags) + { + var parentContext = new ActivityContext(this.traceId, this.parentSpanId, flags); + var link = new ActivityLink(parentContext); - Assert.Equal( - SamplingDecision.RecordAndSample, - new AlwaysOnSampler().ShouldSample(new SamplingParameters(parentContext, this.traceId, "Another name", ActivityKindServer, null, new List { link })).Decision); - } + Assert.Equal( + SamplingDecision.RecordAndSample, + new AlwaysOnSampler().ShouldSample(new SamplingParameters(parentContext, this.traceId, "Another name", ActivityKindServer, null, new List { link })).Decision); + } - [Fact] - public void AlwaysOnSampler_GetDescription() - { - Assert.Equal("AlwaysOnSampler", new AlwaysOnSampler().Description); - } + [Fact] + public void AlwaysOnSampler_GetDescription() + { + Assert.Equal("AlwaysOnSampler", new AlwaysOnSampler().Description); + } - [Theory] - [InlineData(ActivityTraceFlags.Recorded)] - [InlineData(ActivityTraceFlags.None)] - public void AlwaysOffSampler_AlwaysReturnFalse(ActivityTraceFlags flags) - { - var parentContext = new ActivityContext(this.traceId, this.parentSpanId, flags); - var link = new ActivityLink(parentContext); + [Theory] + [InlineData(ActivityTraceFlags.Recorded)] + [InlineData(ActivityTraceFlags.None)] + public void AlwaysOffSampler_AlwaysReturnFalse(ActivityTraceFlags flags) + { + var parentContext = new ActivityContext(this.traceId, this.parentSpanId, flags); + var link = new ActivityLink(parentContext); - Assert.Equal( - SamplingDecision.Drop, - new AlwaysOffSampler().ShouldSample(new SamplingParameters(parentContext, this.traceId, "Another name", ActivityKindServer, null, new List { link })).Decision); - } + Assert.Equal( + SamplingDecision.Drop, + new AlwaysOffSampler().ShouldSample(new SamplingParameters(parentContext, this.traceId, "Another name", ActivityKindServer, null, new List { link })).Decision); + } - [Fact] - public void AlwaysOffSampler_GetDescription() - { - Assert.Equal("AlwaysOffSampler", new AlwaysOffSampler().Description); - } + [Fact] + public void AlwaysOffSampler_GetDescription() + { + Assert.Equal("AlwaysOffSampler", new AlwaysOffSampler().Description); + } - [Theory] - [InlineData(SamplingDecision.Drop)] - [InlineData(SamplingDecision.RecordOnly)] - [InlineData(SamplingDecision.RecordAndSample)] - public void TracerProviderSdkSamplerAttributesAreAppliedToLegacyActivity(SamplingDecision samplingDecision) + [Theory] + [InlineData(SamplingDecision.Drop)] + [InlineData(SamplingDecision.RecordOnly)] + [InlineData(SamplingDecision.RecordAndSample)] + public void TracerProviderSdkSamplerAttributesAreAppliedToLegacyActivity(SamplingDecision samplingDecision) + { + var testSampler = new TestSampler { - var testSampler = new TestSampler + SamplingAction = (samplingParams) => { - SamplingAction = (samplingParams) => + var attributes = new Dictionary { - var attributes = new Dictionary - { - { "tagkeybysampler", "tagvalueaddedbysampler" }, - }; - return new SamplingResult(samplingDecision, attributes); - }, - }; - - var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .SetSampler(testSampler) - .AddLegacySource(operationNameForLegacyActivity) - .Build(); - - using Activity activity = new Activity(operationNameForLegacyActivity); - activity.Start(); - Assert.NotNull(activity); - if (samplingDecision != SamplingDecision.Drop) - { - Assert.Contains(new KeyValuePair("tagkeybysampler", "tagvalueaddedbysampler"), activity.TagObjects); - } - - activity.Stop(); + { "tagkeybysampler", "tagvalueaddedbysampler" }, + }; + return new SamplingResult(samplingDecision, attributes); + }, + }; + + var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(testSampler) + .AddLegacySource(operationNameForLegacyActivity) + .Build(); + + using Activity activity = new Activity(operationNameForLegacyActivity); + activity.Start(); + Assert.NotNull(activity); + if (samplingDecision != SamplingDecision.Drop) + { + Assert.Contains(new KeyValuePair("tagkeybysampler", "tagvalueaddedbysampler"), activity.TagObjects); } - [Theory] - [InlineData(SamplingDecision.Drop)] - [InlineData(SamplingDecision.RecordOnly)] - [InlineData(SamplingDecision.RecordAndSample)] - public void SamplersCanModifyTraceStateOnLegacyActivity(SamplingDecision samplingDecision) + activity.Stop(); + } + + [Theory] + [InlineData(SamplingDecision.Drop)] + [InlineData(SamplingDecision.RecordOnly)] + [InlineData(SamplingDecision.RecordAndSample)] + public void SamplersCanModifyTraceStateOnLegacyActivity(SamplingDecision samplingDecision) + { + var existingTraceState = "a=1,b=2"; + var newTraceState = "a=1,b=2,c=3,d=4"; + var testSampler = new TestSampler { - var existingTraceState = "a=1,b=2"; - var newTraceState = "a=1,b=2,c=3,d=4"; - var testSampler = new TestSampler + SamplingAction = (samplingParams) => { - SamplingAction = (samplingParams) => - { - Assert.Equal(existingTraceState, samplingParams.ParentContext.TraceState); - return new SamplingResult(samplingDecision, newTraceState); - }, - }; - - var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .SetSampler(testSampler) - .AddLegacySource(operationNameForLegacyActivity) - .Build(); - - using var parentActivity = new Activity("Foo"); - parentActivity.TraceStateString = existingTraceState; - parentActivity.Start(); - - using var activity = new Activity(operationNameForLegacyActivity); - activity.Start(); - Assert.NotNull(activity); - if (samplingDecision != SamplingDecision.Drop) - { - Assert.Equal(newTraceState, activity.TraceStateString); - } - - activity.Stop(); - parentActivity.Stop(); + Assert.Equal(existingTraceState, samplingParams.ParentContext.TraceState); + return new SamplingResult(samplingDecision, newTraceState); + }, + }; + + var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(testSampler) + .AddLegacySource(operationNameForLegacyActivity) + .Build(); + + using var parentActivity = new Activity("Foo"); + parentActivity.TraceStateString = existingTraceState; + parentActivity.Start(); + + using var activity = new Activity(operationNameForLegacyActivity); + activity.Start(); + Assert.NotNull(activity); + if (samplingDecision != SamplingDecision.Drop) + { + Assert.Equal(newTraceState, activity.TraceStateString); } - [Theory] - [InlineData(SamplingDecision.Drop)] - [InlineData(SamplingDecision.RecordOnly)] - [InlineData(SamplingDecision.RecordAndSample)] - public void SamplersDoesNotImpactTraceStateWhenUsingNullLegacyActivity(SamplingDecision samplingDecision) + activity.Stop(); + parentActivity.Stop(); + } + + [Theory] + [InlineData(SamplingDecision.Drop)] + [InlineData(SamplingDecision.RecordOnly)] + [InlineData(SamplingDecision.RecordAndSample)] + public void SamplersDoesNotImpactTraceStateWhenUsingNullLegacyActivity(SamplingDecision samplingDecision) + { + var existingTraceState = "a=1,b=2"; + var testSampler = new TestSampler { - var existingTraceState = "a=1,b=2"; - var testSampler = new TestSampler + SamplingAction = (samplingParams) => { - SamplingAction = (samplingParams) => - { - Assert.Equal(existingTraceState, samplingParams.ParentContext.TraceState); - return new SamplingResult(samplingDecision); - }, - }; - - var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .SetSampler(testSampler) - .AddLegacySource(operationNameForLegacyActivity) - .Build(); - - using var parentActivity = new Activity("Foo"); - parentActivity.TraceStateString = existingTraceState; - parentActivity.Start(); - - using var activity = new Activity(operationNameForLegacyActivity); - activity.Start(); - Assert.NotNull(activity); - if (samplingDecision != SamplingDecision.Drop) - { - Assert.Equal(existingTraceState, activity.TraceStateString); - } - - activity.Stop(); - parentActivity.Stop(); + Assert.Equal(existingTraceState, samplingParams.ParentContext.TraceState); + return new SamplingResult(samplingDecision); + }, + }; + + var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(testSampler) + .AddLegacySource(operationNameForLegacyActivity) + .Build(); + + using var parentActivity = new Activity("Foo"); + parentActivity.TraceStateString = existingTraceState; + parentActivity.Start(); + + using var activity = new Activity(operationNameForLegacyActivity); + activity.Start(); + Assert.NotNull(activity); + if (samplingDecision != SamplingDecision.Drop) + { + Assert.Equal(existingTraceState, activity.TraceStateString); } - [Theory] - [InlineData(SamplingDecision.Drop)] - [InlineData(SamplingDecision.RecordOnly)] - [InlineData(SamplingDecision.RecordAndSample)] - public void SamplersCanModifyTraceState(SamplingDecision sampling) + activity.Stop(); + parentActivity.Stop(); + } + + [Theory] + [InlineData(SamplingDecision.Drop)] + [InlineData(SamplingDecision.RecordOnly)] + [InlineData(SamplingDecision.RecordAndSample)] + public void SamplersCanModifyTraceState(SamplingDecision sampling) + { + var parentTraceState = "a=1,b=2"; + var newTraceState = "a=1,b=2,c=3,d=4"; + var testSampler = new TestSampler { - var parentTraceState = "a=1,b=2"; - var newTraceState = "a=1,b=2,c=3,d=4"; - var testSampler = new TestSampler + SamplingAction = (samplingParams) => { - SamplingAction = (samplingParams) => - { - Assert.Equal(parentTraceState, samplingParams.ParentContext.TraceState); - return new SamplingResult(sampling, newTraceState); - }, - }; - - var activitySourceName = Utils.GetCurrentMethodName(); - using var activitySource = new ActivitySource(activitySourceName); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .SetSampler(testSampler) - .Build(); - - var parentContext = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded, parentTraceState, true); - - using var activity = activitySource.StartActivity("root", ActivityKind.Server, parentContext); - if (sampling != SamplingDecision.Drop) - { - Assert.Equal(newTraceState, activity.TraceStateString); - } + Assert.Equal(parentTraceState, samplingParams.ParentContext.TraceState); + return new SamplingResult(sampling, newTraceState); + }, + }; + + var activitySourceName = Utils.GetCurrentMethodName(); + using var activitySource = new ActivitySource(activitySourceName); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .SetSampler(testSampler) + .Build(); + + var parentContext = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded, parentTraceState, true); + + using var activity = activitySource.StartActivity("root", ActivityKind.Server, parentContext); + if (sampling != SamplingDecision.Drop) + { + Assert.Equal(newTraceState, activity.TraceStateString); } + } - [Theory] - [InlineData(SamplingDecision.Drop)] - [InlineData(SamplingDecision.RecordOnly)] - [InlineData(SamplingDecision.RecordAndSample)] - public void SamplersDoesNotImpactTraceStateWhenUsingNull(SamplingDecision sampling) + [Theory] + [InlineData(SamplingDecision.Drop)] + [InlineData(SamplingDecision.RecordOnly)] + [InlineData(SamplingDecision.RecordAndSample)] + public void SamplersDoesNotImpactTraceStateWhenUsingNull(SamplingDecision sampling) + { + var parentTraceState = "a=1,b=2"; + var testSampler = new TestSampler { - var parentTraceState = "a=1,b=2"; - var testSampler = new TestSampler + SamplingAction = (samplingParams) => { - SamplingAction = (samplingParams) => - { - Assert.Equal(parentTraceState, samplingParams.ParentContext.TraceState); - - // Not explicitly setting tracestate, leaving it null. - // backward compat test that existing - // samplers will not inadvertently - // reset Tracestate - return new SamplingResult(sampling); - }, - }; - - var activitySourceName = Utils.GetCurrentMethodName(); - using var activitySource = new ActivitySource(activitySourceName); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .SetSampler(testSampler) - .Build(); - - var parentContext = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded, parentTraceState, true); - - using var activity = activitySource.StartActivity("root", ActivityKind.Server, parentContext); - if (sampling != SamplingDecision.Drop) - { - Assert.Equal(parentTraceState, activity.TraceStateString); - } - } - - [Fact] - public void SamplerExceptionBubblesUpTest() + Assert.Equal(parentTraceState, samplingParams.ParentContext.TraceState); + + // Not explicitly setting tracestate, leaving it null. + // backward compat test that existing + // samplers will not inadvertently + // reset Tracestate + return new SamplingResult(sampling); + }, + }; + + var activitySourceName = Utils.GetCurrentMethodName(); + using var activitySource = new ActivitySource(activitySourceName); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .SetSampler(testSampler) + .Build(); + + var parentContext = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded, parentTraceState, true); + + using var activity = activitySource.StartActivity("root", ActivityKind.Server, parentContext); + if (sampling != SamplingDecision.Drop) { - // Note: This test verifies there is NO try/catch around sampling - // and it will throw. For the discussion behind this see: - // https://github.com/open-telemetry/opentelemetry-dotnet/pull/4072 - - var activitySourceName = Utils.GetCurrentMethodName(); - using var activitySource = new ActivitySource(activitySourceName); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .SetSampler(new ThrowingSampler()) - .Build(); - - Assert.Throws(() => activitySource.StartActivity("ThrowingSampler")); + Assert.Equal(parentTraceState, activity.TraceStateString); } + } - private sealed class ThrowingSampler : Sampler + [Fact] + public void SamplerExceptionBubblesUpTest() + { + // Note: This test verifies there is NO try/catch around sampling + // and it will throw. For the discussion behind this see: + // https://github.com/open-telemetry/opentelemetry-dotnet/pull/4072 + + var activitySourceName = Utils.GetCurrentMethodName(); + using var activitySource = new ActivitySource(activitySourceName); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .SetSampler(new ThrowingSampler()) + .Build(); + + Assert.Throws(() => activitySource.StartActivity("ThrowingSampler")); + } + + private sealed class ThrowingSampler : Sampler + { + public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) { - public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) - { - throw new InvalidOperationException("ThrowingSampler"); - } + throw new InvalidOperationException("ThrowingSampler"); } } } diff --git a/test/OpenTelemetry.Tests/Trace/SamplingResultTest.cs b/test/OpenTelemetry.Tests/Trace/SamplingResultTest.cs index 112475d2442..16b52b97ec2 100644 --- a/test/OpenTelemetry.Tests/Trace/SamplingResultTest.cs +++ b/test/OpenTelemetry.Tests/Trace/SamplingResultTest.cs @@ -1,131 +1,118 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + using Xunit; -namespace OpenTelemetry.Trace.Tests +namespace OpenTelemetry.Trace.Tests; + +public class SamplingResultTest { - public class SamplingResultTest + [Theory] + [InlineData(SamplingDecision.Drop)] + [InlineData(SamplingDecision.RecordAndSample)] + [InlineData(SamplingDecision.RecordOnly)] + public void VerifyCtor_SamplingDecision(SamplingDecision decision) { - [Theory] - [InlineData(SamplingDecision.Drop)] - [InlineData(SamplingDecision.RecordAndSample)] - [InlineData(SamplingDecision.RecordOnly)] - public void VerifyCtor_SamplingDecision(SamplingDecision decision) - { - var samplingResult = new SamplingResult(decision); - Assert.Equal(decision, samplingResult.Decision); - Assert.Empty(samplingResult.Attributes); - } - - [Theory] - [InlineData(false, SamplingDecision.Drop)] - [InlineData(true, SamplingDecision.RecordAndSample)] - public void VerifyCtor_Bool(bool isSampled, SamplingDecision expectedSamplingDecision) - { - var samplingResult = new SamplingResult(isSampled); - Assert.Equal(expectedSamplingDecision, samplingResult.Decision); - Assert.Empty(samplingResult.Attributes); - } - - [Theory] - [InlineData(SamplingDecision.Drop)] - [InlineData(SamplingDecision.RecordAndSample)] - [InlineData(SamplingDecision.RecordOnly)] - public void VerifyCtor_SamplingDecisionAndAttributes(SamplingDecision decision) + var samplingResult = new SamplingResult(decision); + Assert.Equal(decision, samplingResult.Decision); + Assert.Empty(samplingResult.Attributes); + } + + [Theory] + [InlineData(false, SamplingDecision.Drop)] + [InlineData(true, SamplingDecision.RecordAndSample)] + public void VerifyCtor_Bool(bool isSampled, SamplingDecision expectedSamplingDecision) + { + var samplingResult = new SamplingResult(isSampled); + Assert.Equal(expectedSamplingDecision, samplingResult.Decision); + Assert.Empty(samplingResult.Attributes); + } + + [Theory] + [InlineData(SamplingDecision.Drop)] + [InlineData(SamplingDecision.RecordAndSample)] + [InlineData(SamplingDecision.RecordOnly)] + public void VerifyCtor_SamplingDecisionAndAttributes(SamplingDecision decision) + { + var attributes = new Dictionary { - var attributes = new Dictionary + { "A", 1 }, + { "B", 2 }, + { "C", 3 }, + }; + + var samplingResult = new SamplingResult(decision, attributes); + Assert.Equal(decision, samplingResult.Decision); + Assert.True(attributes.SequenceEqual(samplingResult.Attributes)); + } + + [Theory] + [InlineData(SamplingDecision.Drop, true)] + [InlineData(SamplingDecision.RecordAndSample, false)] + [InlineData(SamplingDecision.RecordOnly, false)] + public void VerifyOperator_Equals(SamplingDecision decision, bool expected) + { + var samplingResult1 = new SamplingResult(SamplingDecision.Drop); + + var samplingResult2 = new SamplingResult(decision); + Assert.Equal(expected, samplingResult1 == samplingResult2); + } + + [Theory] + [InlineData(SamplingDecision.Drop, false)] + [InlineData(SamplingDecision.RecordAndSample, true)] + [InlineData(SamplingDecision.RecordOnly, true)] + public void VerifyOperator_NotEquals(SamplingDecision decision, bool expected) + { + var samplingResult1 = new SamplingResult(SamplingDecision.Drop); + + var samplingResult2 = new SamplingResult(decision); + Assert.Equal(expected, samplingResult1 != samplingResult2); + } + + [Fact] + public void Verify_Equals() + { + var samplingResult1 = new SamplingResult(SamplingDecision.Drop); + Assert.True(samplingResult1.Equals(samplingResult1)); + Assert.True(samplingResult1.Equals((object)samplingResult1)); + + var samplingResult2 = new SamplingResult(SamplingDecision.RecordAndSample); + Assert.False(samplingResult1.Equals(samplingResult2)); + Assert.True(samplingResult2.Equals(samplingResult2)); + Assert.False(samplingResult1.Equals((object)samplingResult2)); + Assert.True(samplingResult2.Equals((object)samplingResult2)); + + var samplingResult3 = new SamplingResult( + SamplingDecision.RecordOnly, + new Dictionary { { "A", 1 }, { "B", 2 }, { "C", 3 }, - }; - - var samplingResult = new SamplingResult(decision, attributes); - Assert.Equal(decision, samplingResult.Decision); - Assert.True(attributes.SequenceEqual(samplingResult.Attributes)); - } - - [Theory] - [InlineData(SamplingDecision.Drop, true)] - [InlineData(SamplingDecision.RecordAndSample, false)] - [InlineData(SamplingDecision.RecordOnly, false)] - public void VerifyOperator_Equals(SamplingDecision decision, bool expected) - { - var samplingResult1 = new SamplingResult(SamplingDecision.Drop); + }); + Assert.False(samplingResult1.Equals(samplingResult3)); + Assert.True(samplingResult3.Equals(samplingResult3)); + Assert.False(samplingResult1.Equals((object)samplingResult3)); + Assert.True(samplingResult3.Equals((object)samplingResult3)); - var samplingResult2 = new SamplingResult(decision); - Assert.Equal(expected, samplingResult1 == samplingResult2); - } - - [Theory] - [InlineData(SamplingDecision.Drop, false)] - [InlineData(SamplingDecision.RecordAndSample, true)] - [InlineData(SamplingDecision.RecordOnly, true)] - public void VerifyOperator_NotEquals(SamplingDecision decision, bool expected) - { - var samplingResult1 = new SamplingResult(SamplingDecision.Drop); + Assert.False(samplingResult1.Equals(Guid.Empty)); + } - var samplingResult2 = new SamplingResult(decision); - Assert.Equal(expected, samplingResult1 != samplingResult2); - } + [Theory] + [InlineData(SamplingDecision.Drop)] + [InlineData(SamplingDecision.RecordAndSample)] + [InlineData(SamplingDecision.RecordOnly)] + public void Verify_GetHashCode(SamplingDecision decision) + { + var samplingResult1 = new SamplingResult(decision); + var samplingResult2 = new SamplingResult(decision, new Dictionary + { + { "A", 1 }, + { "B", 2 }, + { "C", 3 }, + }); - [Fact] - public void Verify_Equals() - { - var samplingResult1 = new SamplingResult(SamplingDecision.Drop); - Assert.True(samplingResult1.Equals(samplingResult1)); - Assert.True(samplingResult1.Equals((object)samplingResult1)); - - var samplingResult2 = new SamplingResult(SamplingDecision.RecordAndSample); - Assert.False(samplingResult1.Equals(samplingResult2)); - Assert.True(samplingResult2.Equals(samplingResult2)); - Assert.False(samplingResult1.Equals((object)samplingResult2)); - Assert.True(samplingResult2.Equals((object)samplingResult2)); - - var samplingResult3 = new SamplingResult( - SamplingDecision.RecordOnly, - new Dictionary - { - { "A", 1 }, - { "B", 2 }, - { "C", 3 }, - }); - Assert.False(samplingResult1.Equals(samplingResult3)); - Assert.True(samplingResult3.Equals(samplingResult3)); - Assert.False(samplingResult1.Equals((object)samplingResult3)); - Assert.True(samplingResult3.Equals((object)samplingResult3)); - - Assert.False(samplingResult1.Equals(Guid.Empty)); - } - - [Theory] - [InlineData(SamplingDecision.Drop)] - [InlineData(SamplingDecision.RecordAndSample)] - [InlineData(SamplingDecision.RecordOnly)] - public void Verify_GetHashCode(SamplingDecision decision) - { - var samplingResult1 = new SamplingResult(decision); - var samplingResult2 = new SamplingResult(decision, new Dictionary - { - { "A", 1 }, - { "B", 2 }, - { "C", 3 }, - }); - - Assert.NotEqual(samplingResult1.GetHashCode(), samplingResult2.GetHashCode()); - } + Assert.NotEqual(samplingResult1.GetHashCode(), samplingResult2.GetHashCode()); } } diff --git a/test/OpenTelemetry.Tests/Trace/SimpleExportActivityProcessorTest.cs b/test/OpenTelemetry.Tests/Trace/SimpleExportActivityProcessorTest.cs index b53e585c14f..5cc52c6eee8 100644 --- a/test/OpenTelemetry.Tests/Trace/SimpleExportActivityProcessorTest.cs +++ b/test/OpenTelemetry.Tests/Trace/SimpleExportActivityProcessorTest.cs @@ -1,126 +1,112 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Exporter; using Xunit; -namespace OpenTelemetry.Trace.Tests +namespace OpenTelemetry.Trace.Tests; + +public class SimpleExportActivityProcessorTest { - public class SimpleExportActivityProcessorTest + [Fact] + public void CheckNullExporter() + { + Assert.Throws(() => new SimpleActivityExportProcessor(null)); + } + + [Fact] + public void CheckExportedOnEnd() { - [Fact] - public void CheckNullExporter() + var exportedItems = new List(); + using var exporter = new InMemoryExporter(exportedItems); + using var processor = new SimpleActivityExportProcessor(exporter); + + using var activity1 = new Activity("start1") { - Assert.Throws(() => new SimpleActivityExportProcessor(null)); - } + ActivityTraceFlags = ActivityTraceFlags.Recorded, + }; + + processor.OnEnd(activity1); + Assert.Single(exportedItems); - [Fact] - public void CheckExportedOnEnd() + using var activity2 = new Activity("start2") { - var exportedItems = new List(); - using var exporter = new InMemoryExporter(exportedItems); - using var processor = new SimpleActivityExportProcessor(exporter); - - using var activity1 = new Activity("start1") - { - ActivityTraceFlags = ActivityTraceFlags.Recorded, - }; - - processor.OnEnd(activity1); - Assert.Single(exportedItems); - - using var activity2 = new Activity("start2") - { - ActivityTraceFlags = ActivityTraceFlags.Recorded, - }; - - processor.OnEnd(activity2); - Assert.Equal(2, exportedItems.Count); - } - - [Theory] - [InlineData(Timeout.Infinite)] - [InlineData(0)] - [InlineData(1)] - public void CheckForceFlushExport(int timeout) + ActivityTraceFlags = ActivityTraceFlags.Recorded, + }; + + processor.OnEnd(activity2); + Assert.Equal(2, exportedItems.Count); + } + + [Theory] + [InlineData(Timeout.Infinite)] + [InlineData(0)] + [InlineData(1)] + public void CheckForceFlushExport(int timeout) + { + var exportedItems = new List(); + using var exporter = new InMemoryExporter(exportedItems); + using var processor = new SimpleActivityExportProcessor(exporter); + + using var activity1 = new Activity("start1") { - var exportedItems = new List(); - using var exporter = new InMemoryExporter(exportedItems); - using var processor = new SimpleActivityExportProcessor(exporter); - - using var activity1 = new Activity("start1") - { - ActivityTraceFlags = ActivityTraceFlags.Recorded, - }; - - using var activity2 = new Activity("start2") - { - ActivityTraceFlags = ActivityTraceFlags.Recorded, - }; - - processor.OnEnd(activity1); - processor.OnEnd(activity2); - - // checking before force flush - Assert.Equal(2, exportedItems.Count); - - // forcing flush - processor.ForceFlush(timeout); - Assert.Equal(2, exportedItems.Count); - } - - [Theory] - [InlineData(Timeout.Infinite)] - [InlineData(0)] - [InlineData(1)] - public void CheckShutdownExport(int timeout) + ActivityTraceFlags = ActivityTraceFlags.Recorded, + }; + + using var activity2 = new Activity("start2") { - var exportedItems = new List(); - using var exporter = new InMemoryExporter(exportedItems); - using var processor = new SimpleActivityExportProcessor(exporter); + ActivityTraceFlags = ActivityTraceFlags.Recorded, + }; - using var activity = new Activity("start") - { - ActivityTraceFlags = ActivityTraceFlags.Recorded, - }; + processor.OnEnd(activity1); + processor.OnEnd(activity2); - processor.OnEnd(activity); + // checking before force flush + Assert.Equal(2, exportedItems.Count); - // checking before shutdown - Assert.Single(exportedItems); + // forcing flush + processor.ForceFlush(timeout); + Assert.Equal(2, exportedItems.Count); + } - processor.Shutdown(timeout); - Assert.Single(exportedItems); - } + [Theory] + [InlineData(Timeout.Infinite)] + [InlineData(0)] + [InlineData(1)] + public void CheckShutdownExport(int timeout) + { + var exportedItems = new List(); + using var exporter = new InMemoryExporter(exportedItems); + using var processor = new SimpleActivityExportProcessor(exporter); - [Fact] - public void CheckExportForRecordingButNotSampledActivity() + using var activity = new Activity("start") { - var exportedItems = new List(); - using var exporter = new InMemoryExporter(exportedItems); - using var processor = new SimpleActivityExportProcessor(exporter); - - using var activity = new Activity("start") - { - ActivityTraceFlags = ActivityTraceFlags.None, - }; - - processor.OnEnd(activity); - Assert.Empty(exportedItems); - } + ActivityTraceFlags = ActivityTraceFlags.Recorded, + }; + + processor.OnEnd(activity); + + // checking before shutdown + Assert.Single(exportedItems); + + processor.Shutdown(timeout); + Assert.Single(exportedItems); + } + + [Fact] + public void CheckExportForRecordingButNotSampledActivity() + { + var exportedItems = new List(); + using var exporter = new InMemoryExporter(exportedItems); + using var processor = new SimpleActivityExportProcessor(exporter); + + using var activity = new Activity("start") + { + ActivityTraceFlags = ActivityTraceFlags.None, + }; + + processor.OnEnd(activity); + Assert.Empty(exportedItems); } } diff --git a/test/OpenTelemetry.Tests/Trace/SpanContextTest.cs b/test/OpenTelemetry.Tests/Trace/SpanContextTest.cs index b06c3e8c7cf..18b4bd8c73a 100644 --- a/test/OpenTelemetry.Tests/Trace/SpanContextTest.cs +++ b/test/OpenTelemetry.Tests/Trace/SpanContextTest.cs @@ -1,209 +1,195 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using Xunit; -namespace OpenTelemetry.Trace.Tests +namespace OpenTelemetry.Trace.Tests; + +public class SpanContextTest { - public class SpanContextTest + private static readonly byte[] FirstTraceIdBytes = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (byte)'a' }; + private static readonly byte[] SecondTraceIdBytes = { 0, 0, 0, 0, 0, 0, 0, (byte)'0', 0, 0, 0, 0, 0, 0, 0, 0 }; + private static readonly byte[] FirstSpanIdBytes = { 0, 0, 0, 0, 0, 0, 0, (byte)'a' }; + private static readonly byte[] SecondSpanIdBytes = { (byte)'0', 0, 0, 0, 0, 0, 0, 0 }; + + private static readonly SpanContext First = + new( + ActivityTraceId.CreateFromBytes(FirstTraceIdBytes), + ActivitySpanId.CreateFromBytes(FirstSpanIdBytes), + ActivityTraceFlags.None); + + private static readonly SpanContext Second = + new( + ActivityTraceId.CreateFromBytes(SecondTraceIdBytes), + ActivitySpanId.CreateFromBytes(SecondSpanIdBytes), + ActivityTraceFlags.Recorded); + + [Fact] + public void InvalidSpanContext() + { + Assert.Equal(default, default(SpanContext).TraceId); + Assert.Equal(default, default(SpanContext).SpanId); + Assert.Equal(ActivityTraceFlags.None, default(SpanContext).TraceFlags); + } + + [Fact] + public void IsValid() + { + Assert.False(default(SpanContext).IsValid); + Assert.False( + new SpanContext( + ActivityTraceId.CreateFromBytes(FirstTraceIdBytes), default, ActivityTraceFlags.None) + .IsValid); + Assert.False( + new SpanContext( + default, ActivitySpanId.CreateFromBytes(FirstSpanIdBytes), ActivityTraceFlags.None) + .IsValid); + Assert.True(First.IsValid); + Assert.True(Second.IsValid); + } + + [Fact] + public void GetTraceId() + { + Assert.Equal(ActivityTraceId.CreateFromBytes(FirstTraceIdBytes), First.TraceId); + Assert.Equal(ActivityTraceId.CreateFromBytes(SecondTraceIdBytes), Second.TraceId); + } + + [Fact] + public void GetSpanId() + { + Assert.Equal(ActivitySpanId.CreateFromBytes(FirstSpanIdBytes), First.SpanId); + Assert.Equal(ActivitySpanId.CreateFromBytes(SecondSpanIdBytes), Second.SpanId); + } + + [Fact] + public void GetTraceOptions() + { + Assert.Equal(ActivityTraceFlags.None, First.TraceFlags); + Assert.Equal(ActivityTraceFlags.Recorded, Second.TraceFlags); + } + + [Fact] + public void Equality1() + { + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + var context1 = new SpanContext(traceId, spanId, ActivityTraceFlags.Recorded); + var context2 = new SpanContext(traceId, spanId, ActivityTraceFlags.Recorded); + + Assert.Equal(context1, context2); + Assert.True(context1 == context2); + } + + [Fact] + public void Equality2() + { + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + var context1 = new SpanContext(traceId, spanId, ActivityTraceFlags.None, true); + var context2 = new SpanContext(traceId, spanId, ActivityTraceFlags.None, true); + + Assert.Equal(context1, context2); + Assert.True(context1 == context2); + } + + [Fact] + public void Equality3() { - private static readonly byte[] FirstTraceIdBytes = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (byte)'a' }; - private static readonly byte[] SecondTraceIdBytes = { 0, 0, 0, 0, 0, 0, 0, (byte)'0', 0, 0, 0, 0, 0, 0, 0, 0 }; - private static readonly byte[] FirstSpanIdBytes = { 0, 0, 0, 0, 0, 0, 0, (byte)'a' }; - private static readonly byte[] SecondSpanIdBytes = { (byte)'0', 0, 0, 0, 0, 0, 0, 0 }; - - private static readonly SpanContext First = - new( - ActivityTraceId.CreateFromBytes(FirstTraceIdBytes), - ActivitySpanId.CreateFromBytes(FirstSpanIdBytes), - ActivityTraceFlags.None); - - private static readonly SpanContext Second = - new( - ActivityTraceId.CreateFromBytes(SecondTraceIdBytes), - ActivitySpanId.CreateFromBytes(SecondSpanIdBytes), - ActivityTraceFlags.Recorded); - - [Fact] - public void InvalidSpanContext() - { - Assert.Equal(default, default(SpanContext).TraceId); - Assert.Equal(default, default(SpanContext).SpanId); - Assert.Equal(ActivityTraceFlags.None, default(SpanContext).TraceFlags); - } - - [Fact] - public void IsValid() - { - Assert.False(default(SpanContext).IsValid); - Assert.False( - new SpanContext( - ActivityTraceId.CreateFromBytes(FirstTraceIdBytes), default, ActivityTraceFlags.None) - .IsValid); - Assert.False( - new SpanContext( - default, ActivitySpanId.CreateFromBytes(FirstSpanIdBytes), ActivityTraceFlags.None) - .IsValid); - Assert.True(First.IsValid); - Assert.True(Second.IsValid); - } - - [Fact] - public void GetTraceId() - { - Assert.Equal(ActivityTraceId.CreateFromBytes(FirstTraceIdBytes), First.TraceId); - Assert.Equal(ActivityTraceId.CreateFromBytes(SecondTraceIdBytes), Second.TraceId); - } - - [Fact] - public void GetSpanId() - { - Assert.Equal(ActivitySpanId.CreateFromBytes(FirstSpanIdBytes), First.SpanId); - Assert.Equal(ActivitySpanId.CreateFromBytes(SecondSpanIdBytes), Second.SpanId); - } - - [Fact] - public void GetTraceOptions() - { - Assert.Equal(ActivityTraceFlags.None, First.TraceFlags); - Assert.Equal(ActivityTraceFlags.Recorded, Second.TraceFlags); - } - - [Fact] - public void Equality1() - { - var traceId = ActivityTraceId.CreateRandom(); - var spanId = ActivitySpanId.CreateRandom(); - var context1 = new SpanContext(traceId, spanId, ActivityTraceFlags.Recorded); - var context2 = new SpanContext(traceId, spanId, ActivityTraceFlags.Recorded); - - Assert.Equal(context1, context2); - Assert.True(context1 == context2); - } - - [Fact] - public void Equality2() - { - var traceId = ActivityTraceId.CreateRandom(); - var spanId = ActivitySpanId.CreateRandom(); - var context1 = new SpanContext(traceId, spanId, ActivityTraceFlags.None, true); - var context2 = new SpanContext(traceId, spanId, ActivityTraceFlags.None, true); - - Assert.Equal(context1, context2); - Assert.True(context1 == context2); - } - - [Fact] - public void Equality3() - { - var traceId = ActivityTraceId.CreateRandom(); - var spanId = ActivitySpanId.CreateRandom(); - IEnumerable> tracestate = new List>(); - var context1 = new SpanContext(traceId, spanId, ActivityTraceFlags.None, false, tracestate); - var context2 = new SpanContext(traceId, spanId, ActivityTraceFlags.None, false, tracestate); - - Assert.Equal(context1, context2); - Assert.True(context1 == context2); - } - - [Fact] - public void Equality4() - { - var traceId = ActivityTraceId.CreateRandom(); - var spanId = ActivitySpanId.CreateRandom(); - IEnumerable> tracestate = new List>(); - var context1 = new SpanContext(traceId, spanId, ActivityTraceFlags.None, false, tracestate); - object context2 = new SpanContext(traceId, spanId, ActivityTraceFlags.None, false, tracestate); - - Assert.Equal(context1, context2); - Assert.True(context1.Equals(context2)); - } - - [Fact] - public void Not_Equality_DifferentTraceId() - { - var spanId = ActivitySpanId.CreateRandom(); - var context1 = new SpanContext(ActivityTraceId.CreateRandom(), spanId, ActivityTraceFlags.Recorded); - var context2 = new SpanContext(ActivityTraceId.CreateRandom(), spanId, ActivityTraceFlags.Recorded); - - Assert.NotEqual(context1, context2); - Assert.True(context1 != context2); - } - - [Fact] - public void Not_Equality_DifferentSpanId() - { - var traceId = ActivityTraceId.CreateRandom(); - var context1 = new SpanContext(traceId, ActivitySpanId.CreateRandom(), ActivityTraceFlags.None, true); - var context2 = new SpanContext(traceId, ActivitySpanId.CreateRandom(), ActivityTraceFlags.None, true); - - Assert.NotEqual(context1, context2); - Assert.True(context1 != context2); - } - - [Fact] - public void Not_Equality_DifferentTraceFlags() - { - var traceId = ActivityTraceId.CreateRandom(); - var spanId = ActivitySpanId.CreateRandom(); - IEnumerable> tracestate = new List>(); - var context1 = new SpanContext(traceId, spanId, ActivityTraceFlags.Recorded, false, tracestate); - var context2 = new SpanContext(traceId, spanId, ActivityTraceFlags.None, false, tracestate); - - Assert.NotEqual(context1, context2); - Assert.True(context1 != context2); - } - - [Fact] - public void Not_Equality_DifferentIsRemote() - { - var traceId = ActivityTraceId.CreateRandom(); - var spanId = ActivitySpanId.CreateRandom(); - IEnumerable> tracestate = new List>(); - var context1 = new SpanContext(traceId, spanId, ActivityTraceFlags.Recorded, true, tracestate); - var context2 = new SpanContext(traceId, spanId, ActivityTraceFlags.Recorded, false, tracestate); - - Assert.NotEqual(context1, context2); - Assert.True(context1 != context2); - } - - [Fact] - public void Not_Equality_DifferentTraceState() - { - var traceId = ActivityTraceId.CreateRandom(); - var spanId = ActivitySpanId.CreateRandom(); - IEnumerable> tracestate1 = new List>() { new KeyValuePair("k", "v1") }; - IEnumerable> tracestate2 = new List>() { new KeyValuePair("k", "v2") }; - var context1 = new SpanContext(traceId, spanId, ActivityTraceFlags.Recorded, true, tracestate1); - var context2 = new SpanContext(traceId, spanId, ActivityTraceFlags.Recorded, true, tracestate2); - - Assert.NotEqual(context1, context2); - Assert.True(context1 != context2); - } - - [Fact] - public void TestGetHashCode() - { - var traceId = ActivityTraceId.CreateRandom(); - var spanId = ActivitySpanId.CreateRandom(); - IEnumerable> tracestate = new List>(); - var context1 = new SpanContext(traceId, spanId, ActivityTraceFlags.Recorded, true, tracestate); - - Assert.NotEqual(0, context1.GetHashCode()); - } + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + IEnumerable> tracestate = new List>(); + var context1 = new SpanContext(traceId, spanId, ActivityTraceFlags.None, false, tracestate); + var context2 = new SpanContext(traceId, spanId, ActivityTraceFlags.None, false, tracestate); + + Assert.Equal(context1, context2); + Assert.True(context1 == context2); + } + + [Fact] + public void Equality4() + { + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + IEnumerable> tracestate = new List>(); + var context1 = new SpanContext(traceId, spanId, ActivityTraceFlags.None, false, tracestate); + object context2 = new SpanContext(traceId, spanId, ActivityTraceFlags.None, false, tracestate); + + Assert.Equal(context1, context2); + Assert.True(context1.Equals(context2)); + } + + [Fact] + public void Not_Equality_DifferentTraceId() + { + var spanId = ActivitySpanId.CreateRandom(); + var context1 = new SpanContext(ActivityTraceId.CreateRandom(), spanId, ActivityTraceFlags.Recorded); + var context2 = new SpanContext(ActivityTraceId.CreateRandom(), spanId, ActivityTraceFlags.Recorded); + + Assert.NotEqual(context1, context2); + Assert.True(context1 != context2); + } + + [Fact] + public void Not_Equality_DifferentSpanId() + { + var traceId = ActivityTraceId.CreateRandom(); + var context1 = new SpanContext(traceId, ActivitySpanId.CreateRandom(), ActivityTraceFlags.None, true); + var context2 = new SpanContext(traceId, ActivitySpanId.CreateRandom(), ActivityTraceFlags.None, true); + + Assert.NotEqual(context1, context2); + Assert.True(context1 != context2); + } + + [Fact] + public void Not_Equality_DifferentTraceFlags() + { + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + IEnumerable> tracestate = new List>(); + var context1 = new SpanContext(traceId, spanId, ActivityTraceFlags.Recorded, false, tracestate); + var context2 = new SpanContext(traceId, spanId, ActivityTraceFlags.None, false, tracestate); + + Assert.NotEqual(context1, context2); + Assert.True(context1 != context2); + } + + [Fact] + public void Not_Equality_DifferentIsRemote() + { + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + IEnumerable> tracestate = new List>(); + var context1 = new SpanContext(traceId, spanId, ActivityTraceFlags.Recorded, true, tracestate); + var context2 = new SpanContext(traceId, spanId, ActivityTraceFlags.Recorded, false, tracestate); + + Assert.NotEqual(context1, context2); + Assert.True(context1 != context2); + } + + [Fact] + public void Not_Equality_DifferentTraceState() + { + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + IEnumerable> tracestate1 = new List>() { new KeyValuePair("k", "v1") }; + IEnumerable> tracestate2 = new List>() { new KeyValuePair("k", "v2") }; + var context1 = new SpanContext(traceId, spanId, ActivityTraceFlags.Recorded, true, tracestate1); + var context2 = new SpanContext(traceId, spanId, ActivityTraceFlags.Recorded, true, tracestate2); + + Assert.NotEqual(context1, context2); + Assert.True(context1 != context2); + } + + [Fact] + public void TestGetHashCode() + { + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + IEnumerable> tracestate = new List>(); + var context1 = new SpanContext(traceId, spanId, ActivityTraceFlags.Recorded, true, tracestate); + + Assert.NotEqual(0, context1.GetHashCode()); } } diff --git a/test/OpenTelemetry.Tests/Trace/TraceIdRatioBasedSamplerTest.cs b/test/OpenTelemetry.Tests/Trace/TraceIdRatioBasedSamplerTest.cs index c18e45744e6..bfcdb21a3b9 100644 --- a/test/OpenTelemetry.Tests/Trace/TraceIdRatioBasedSamplerTest.cs +++ b/test/OpenTelemetry.Tests/Trace/TraceIdRatioBasedSamplerTest.cs @@ -1,105 +1,92 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + using System.Diagnostics; using Xunit; -namespace OpenTelemetry.Trace.Tests +namespace OpenTelemetry.Trace.Tests; + +public class TraceIdRatioBasedSamplerTest { - public class TraceIdRatioBasedSamplerTest - { - private const string ActivityDisplayName = "MyActivityName"; - private static readonly ActivityKind ActivityKindServer = ActivityKind.Server; + private const string ActivityDisplayName = "MyActivityName"; + private static readonly ActivityKind ActivityKindServer = ActivityKind.Server; - [Fact] - public void OutOfRangeHighProbability() - { - Assert.Throws(() => new TraceIdRatioBasedSampler(1.01)); - } + [Fact] + public void OutOfRangeHighProbability() + { + Assert.Throws(() => new TraceIdRatioBasedSampler(1.01)); + } - [Fact] - public void OutOfRangeLowProbability() - { - Assert.Throws(() => new TraceIdRatioBasedSampler(-0.00001)); - } + [Fact] + public void OutOfRangeLowProbability() + { + Assert.Throws(() => new TraceIdRatioBasedSampler(-0.00001)); + } - [Fact] - public void SampleBasedOnTraceId() - { - Sampler defaultProbability = new TraceIdRatioBasedSampler(0.0001); + [Fact] + public void SampleBasedOnTraceId() + { + Sampler defaultProbability = new TraceIdRatioBasedSampler(0.0001); - // This traceId will not be sampled by the TraceIdRatioBasedSampler because the first 8 bytes as long - // is not less than probability * Long.MAX_VALUE; - var notSampledtraceId = - ActivityTraceId.CreateFromBytes( - new byte[] - { - 0x8F, - 0xFF, - 0xFF, - 0xFF, - 0xFF, - 0xFF, - 0xFF, - 0xFF, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - }); - Assert.Equal( - SamplingDecision.Drop, - defaultProbability.ShouldSample(new SamplingParameters(default, notSampledtraceId, ActivityDisplayName, ActivityKindServer, null, null)).Decision); + // This traceId will not be sampled by the TraceIdRatioBasedSampler because the first 8 bytes as long + // is not less than probability * Long.MAX_VALUE; + var notSampledtraceId = + ActivityTraceId.CreateFromBytes( + new byte[] + { + 0x8F, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + }); + Assert.Equal( + SamplingDecision.Drop, + defaultProbability.ShouldSample(new SamplingParameters(default, notSampledtraceId, ActivityDisplayName, ActivityKindServer, null, null)).Decision); - // This traceId will be sampled by the TraceIdRatioBasedSampler because the first 8 bytes as long - // is less than probability * Long.MAX_VALUE; - var sampledtraceId = - ActivityTraceId.CreateFromBytes( - new byte[] - { - 0x00, - 0x00, - 0xFF, - 0xFF, - 0xFF, - 0xFF, - 0xFF, - 0xFF, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - }); - Assert.Equal( - SamplingDecision.RecordAndSample, - defaultProbability.ShouldSample(new SamplingParameters(default, sampledtraceId, ActivityDisplayName, ActivityKindServer, null, null)).Decision); - } + // This traceId will be sampled by the TraceIdRatioBasedSampler because the first 8 bytes as long + // is less than probability * Long.MAX_VALUE; + var sampledtraceId = + ActivityTraceId.CreateFromBytes( + new byte[] + { + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + }); + Assert.Equal( + SamplingDecision.RecordAndSample, + defaultProbability.ShouldSample(new SamplingParameters(default, sampledtraceId, ActivityDisplayName, ActivityKindServer, null, null)).Decision); + } - [Fact] - public void GetDescription() - { - var expectedDescription = "TraceIdRatioBasedSampler{0.500000}"; - Assert.Equal(expectedDescription, new TraceIdRatioBasedSampler(0.5).Description); - } + [Fact] + public void GetDescription() + { + var expectedDescription = "TraceIdRatioBasedSampler{0.500000}"; + Assert.Equal(expectedDescription, new TraceIdRatioBasedSampler(0.5).Description); } } diff --git a/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderBaseTests.cs b/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderBaseTests.cs new file mode 100644 index 00000000000..6c0da445af6 --- /dev/null +++ b/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderBaseTests.cs @@ -0,0 +1,70 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using Xunit; + +namespace OpenTelemetry.Trace.Tests; + +public class TracerProviderBuilderBaseTests +{ + [Fact] + public void AddInstrumentationInvokesFactoryTest() + { + bool factoryInvoked = false; + + var instrumentation = new TestTracerProviderBuilder(); + instrumentation.AddInstrumentationViaProtectedMethod(() => + { + factoryInvoked = true; + + return null; + }); + + using var provider = instrumentation.Build(); + + Assert.True(factoryInvoked); + } + + [Fact] + public void AddInstrumentationValidatesInputTest() + { + Assert.Throws(() => + { + new TestTracerProviderBuilder().AddInstrumentationViaProtectedMethod( + name: null, + version: "1.0.0", + factory: () => null); + }); + + Assert.Throws(() => + { + new TestTracerProviderBuilder().AddInstrumentationViaProtectedMethod( + name: "name", + version: null, + factory: () => null); + }); + + Assert.Throws(() => + { + new TestTracerProviderBuilder().AddInstrumentationViaProtectedMethod( + name: "name", + version: "1.0.0", + factory: null); + }); + } + + private sealed class TestTracerProviderBuilder : TracerProviderBuilderBase + { + public void AddInstrumentationViaProtectedMethod(Func factory) + { + this.AddInstrumentation("MyName", "MyVersion", factory); + } + + public void AddInstrumentationViaProtectedMethod(string? name, string? version, Func? factory) + { + this.AddInstrumentation(name!, version!, factory!); + } + } +} diff --git a/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderExtensionsTest.cs b/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderExtensionsTest.cs index 3075bdf1c9c..2d582402f62 100644 --- a/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderExtensionsTest.cs +++ b/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderExtensionsTest.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using Microsoft.Extensions.Configuration; @@ -22,493 +9,519 @@ using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Trace.Tests +namespace OpenTelemetry.Trace.Tests; + +public class TracerProviderBuilderExtensionsTest { - public class TracerProviderBuilderExtensionsTest + [Fact] + public void SetErrorStatusOnExceptionEnabled() { - [Fact] - public void SetErrorStatusOnExceptionEnabled() + var activitySourceName = Utils.GetCurrentMethodName(); + using var activitySource = new ActivitySource(activitySourceName); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .SetSampler(new AlwaysOnSampler()) + .SetErrorStatusOnException(false) + .SetErrorStatusOnException(false) + .SetErrorStatusOnException(true) + .SetErrorStatusOnException(true) + .SetErrorStatusOnException(false) + .SetErrorStatusOnException() + .Build(); + + Activity activity = null; + + try { - var activitySourceName = Utils.GetCurrentMethodName(); - using var activitySource = new ActivitySource(activitySourceName); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .SetSampler(new AlwaysOnSampler()) - .SetErrorStatusOnException(false) - .SetErrorStatusOnException(false) - .SetErrorStatusOnException(true) - .SetErrorStatusOnException(true) - .SetErrorStatusOnException(false) - .SetErrorStatusOnException() - .Build(); - - Activity activity = null; - - try - { - using (activity = activitySource.StartActivity("Activity")) - { - throw new Exception("Oops!"); - } - } - catch (Exception) + using (activity = activitySource.StartActivity("Activity")) { + throw new Exception("Oops!"); } - - Assert.Equal(StatusCode.Error, activity.GetStatus().StatusCode); - Assert.Equal(ActivityStatusCode.Error, activity.Status); } + catch (Exception) + { + } + + Assert.Equal(StatusCode.Error, activity.GetStatus().StatusCode); + Assert.Equal(ActivityStatusCode.Error, activity.Status); + } - [Fact] - public void SetErrorStatusOnExceptionDisabled() + [Fact] + public void SetErrorStatusOnExceptionDisabled() + { + var activitySourceName = Utils.GetCurrentMethodName(); + using var activitySource = new ActivitySource(activitySourceName); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .SetSampler(new AlwaysOnSampler()) + .SetErrorStatusOnException() + .SetErrorStatusOnException(false) + .Build(); + + Activity activity = null; + + try { - var activitySourceName = Utils.GetCurrentMethodName(); - using var activitySource = new ActivitySource(activitySourceName); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .SetSampler(new AlwaysOnSampler()) - .SetErrorStatusOnException() - .SetErrorStatusOnException(false) - .Build(); - - Activity activity = null; - - try - { - using (activity = activitySource.StartActivity("Activity")) - { - throw new Exception("Oops!"); - } - } - catch (Exception) + using (activity = activitySource.StartActivity("Activity")) { + throw new Exception("Oops!"); } - - Assert.Equal(StatusCode.Unset, activity.GetStatus().StatusCode); - Assert.Equal(ActivityStatusCode.Unset, activity.Status); } - - [Fact] - public void SetErrorStatusOnExceptionDefault() + catch (Exception) { - var activitySourceName = Utils.GetCurrentMethodName(); - using var activitySource = new ActivitySource(activitySourceName); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .SetSampler(new AlwaysOnSampler()) - .Build(); + } - Activity activity = null; + Assert.Equal(StatusCode.Unset, activity.GetStatus().StatusCode); + Assert.Equal(ActivityStatusCode.Unset, activity.Status); + } - try - { - using (activity = activitySource.StartActivity("Activity")) - { - throw new Exception("Oops!"); - } - } - catch (Exception) + [Fact] + public void SetErrorStatusOnExceptionDefault() + { + var activitySourceName = Utils.GetCurrentMethodName(); + using var activitySource = new ActivitySource(activitySourceName); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .SetSampler(new AlwaysOnSampler()) + .Build(); + + Activity activity = null; + + try + { + using (activity = activitySource.StartActivity("Activity")) { + throw new Exception("Oops!"); } - - Assert.Equal(StatusCode.Unset, activity.GetStatus().StatusCode); } - - [Fact] - public void ServiceLifecycleAvailableToSDKBuilderTest() + catch (Exception) { - var builder = Sdk.CreateTracerProviderBuilder(); + } - MyInstrumentation myInstrumentation = null; + Assert.Equal(StatusCode.Unset, activity.GetStatus().StatusCode); + } - RunBuilderServiceLifecycleTest( - builder, - () => - { - var provider = builder.Build() as TracerProviderSdk; + [Fact] + public void ServiceLifecycleAvailableToSDKBuilderTest() + { + var builder = Sdk.CreateTracerProviderBuilder(); - // Note: Build can only be called once - Assert.Throws(() => builder.Build()); + MyInstrumentation myInstrumentation = null; - Assert.NotNull(provider); - Assert.NotNull(provider.OwnedServiceProvider); + RunBuilderServiceLifecycleTest( + builder, + () => + { + var provider = builder.Build() as TracerProviderSdk; - myInstrumentation = ((IServiceProvider)provider.OwnedServiceProvider).GetRequiredService(); + // Note: Build can only be called once + Assert.Throws(() => builder.Build()); - return provider; - }, - provider => - { - provider.Dispose(); - }); + Assert.NotNull(provider); + Assert.NotNull(provider.OwnedServiceProvider); - Assert.NotNull(myInstrumentation); - Assert.True(myInstrumentation.Disposed); - } + myInstrumentation = ((IServiceProvider)provider.OwnedServiceProvider).GetRequiredService(); - [Fact] - public void AddProcessorUsingDependencyInjectionTest() - { - var builder = Sdk.CreateTracerProviderBuilder(); + return provider; + }, + provider => + { + provider.Dispose(); + }); - builder.AddProcessor(); - builder.AddProcessor(); + Assert.NotNull(myInstrumentation); + Assert.True(myInstrumentation.Disposed); + } - using var provider = builder.Build() as TracerProviderSdk; + [Fact] + public void AddProcessorUsingDependencyInjectionTest() + { + var builder = Sdk.CreateTracerProviderBuilder(); - Assert.NotNull(provider); + builder.AddProcessor(); + builder.AddProcessor(); - var processors = ((IServiceProvider)provider.OwnedServiceProvider).GetServices(); + using var provider = builder.Build() as TracerProviderSdk; - // Note: Two "Add" calls but it is a singleton so only a single registration is produced - Assert.Single(processors); + Assert.NotNull(provider); - var processor = provider.Processor as CompositeProcessor; + var processors = ((IServiceProvider)provider.OwnedServiceProvider).GetServices(); - Assert.NotNull(processor); + // Note: Two "Add" calls but it is a singleton so only a single registration is produced + Assert.Single(processors); - // Note: Two "Add" calls due yield two processors added to provider, even though they are the same - Assert.True(processor.Head.Value is MyProcessor); - Assert.True(processor.Head.Next?.Value is MyProcessor); - } + var processor = provider.Processor as CompositeProcessor; - [Fact] - public void AddInstrumentationTest() - { - List instrumentation = null; + Assert.NotNull(processor); - using (var provider = Sdk.CreateTracerProviderBuilder() - .AddInstrumentation() - .AddInstrumentation((sp, provider) => new MyInstrumentation() { Provider = provider }) - .AddInstrumentation(new MyInstrumentation()) - .Build() as TracerProviderSdk) - { - Assert.NotNull(provider); + // Note: Two "Add" calls due yield two processors added to provider, even though they are the same + Assert.True(processor.Head.Value is MyProcessor); + Assert.True(processor.Head.Next?.Value is MyProcessor); + } - Assert.Equal(3, provider.Instrumentations.Count); + [Fact] + public void AddInstrumentationTest() + { + List instrumentation = null; + + using (var provider = Sdk.CreateTracerProviderBuilder() + .AddInstrumentation() + .AddInstrumentation((sp, provider) => new MyInstrumentation() { Provider = provider }) + .AddInstrumentation(new MyInstrumentation()) + .AddInstrumentation(() => (object)null) + .Build() as TracerProviderSdk) + { + Assert.NotNull(provider); - Assert.Null(((MyInstrumentation)provider.Instrumentations[0]).Provider); - Assert.False(((MyInstrumentation)provider.Instrumentations[0]).Disposed); + Assert.Equal(3, provider.Instrumentations.Count); - Assert.NotNull(((MyInstrumentation)provider.Instrumentations[1]).Provider); - Assert.False(((MyInstrumentation)provider.Instrumentations[1]).Disposed); + Assert.Null(((MyInstrumentation)provider.Instrumentations[0]).Provider); + Assert.False(((MyInstrumentation)provider.Instrumentations[0]).Disposed); - Assert.Null(((MyInstrumentation)provider.Instrumentations[2]).Provider); - Assert.False(((MyInstrumentation)provider.Instrumentations[2]).Disposed); + Assert.NotNull(((MyInstrumentation)provider.Instrumentations[1]).Provider); + Assert.False(((MyInstrumentation)provider.Instrumentations[1]).Disposed); - instrumentation = new List(provider.Instrumentations); - } + Assert.Null(((MyInstrumentation)provider.Instrumentations[2]).Provider); + Assert.False(((MyInstrumentation)provider.Instrumentations[2]).Disposed); - Assert.NotNull(instrumentation); - Assert.True(((MyInstrumentation)instrumentation[0]).Disposed); - Assert.True(((MyInstrumentation)instrumentation[1]).Disposed); - Assert.True(((MyInstrumentation)instrumentation[2]).Disposed); + instrumentation = new List(provider.Instrumentations); } - [Fact] - public void SetAndConfigureResourceTest() - { - var builder = Sdk.CreateTracerProviderBuilder(); + Assert.NotNull(instrumentation); + Assert.True(((MyInstrumentation)instrumentation[0]).Disposed); + Assert.True(((MyInstrumentation)instrumentation[1]).Disposed); + Assert.True(((MyInstrumentation)instrumentation[2]).Disposed); + } - int configureInvocations = 0; - bool serviceProviderTestExecuted = false; + [Fact] + public void SetAndConfigureResourceTest() + { + var builder = Sdk.CreateTracerProviderBuilder(); - builder.SetResourceBuilder(ResourceBuilder.CreateEmpty().AddService("Test")); - builder.ConfigureResource(builder => - { - configureInvocations++; + int configureInvocations = 0; + bool serviceProviderTestExecuted = false; - Assert.Single(builder.ResourceDetectors); + builder.SetResourceBuilder(ResourceBuilder.CreateEmpty().AddService("Test")); + builder.ConfigureResource(builder => + { + configureInvocations++; - builder.AddAttributes(new Dictionary() { ["key1"] = "value1" }); + Assert.Single(builder.ResourceDetectors); - Assert.Equal(2, builder.ResourceDetectors.Count); - }); - builder.SetResourceBuilder(ResourceBuilder.CreateEmpty()); - builder.ConfigureResource(builder => - { - configureInvocations++; + builder.AddAttributes(new Dictionary() { ["key1"] = "value1" }); - Assert.Empty(builder.ResourceDetectors); + Assert.Equal(2, builder.ResourceDetectors.Count); + }); + builder.SetResourceBuilder(ResourceBuilder.CreateEmpty()); + builder.ConfigureResource(builder => + { + configureInvocations++; - builder.AddDetectorInternal(sp => - { - serviceProviderTestExecuted = true; - Assert.NotNull(sp); - return new ResourceBuilder.WrapperResourceDetector(new Resource(new Dictionary() { ["key2"] = "value2" })); - }); + Assert.Empty(builder.ResourceDetectors); - Assert.Single(builder.ResourceDetectors); + builder.AddDetectorInternal(sp => + { + serviceProviderTestExecuted = true; + Assert.NotNull(sp); + return new ResourceBuilder.WrapperResourceDetector(new Resource(new Dictionary() { ["key2"] = "value2" })); }); - using var provider = builder.Build() as TracerProviderSdk; - - Assert.True(serviceProviderTestExecuted); - Assert.Equal(2, configureInvocations); + Assert.Single(builder.ResourceDetectors); + }); - Assert.Single(provider.Resource.Attributes); - Assert.Contains(provider.Resource.Attributes, kvp => kvp.Key == "key2" && (string)kvp.Value == "value2"); - } - - [Fact] - public void ConfigureBuilderIConfigurationAvailableTest() - { - Environment.SetEnvironmentVariable("TEST_KEY", "TEST_KEY_VALUE"); + using var provider = builder.Build() as TracerProviderSdk; - bool configureBuilderCalled = false; + Assert.True(serviceProviderTestExecuted); + Assert.Equal(2, configureInvocations); - using var provider = Sdk.CreateTracerProviderBuilder() - .ConfigureBuilder((sp, builder) => - { - var configuration = sp.GetRequiredService(); + Assert.Single(provider.Resource.Attributes); + Assert.Contains(provider.Resource.Attributes, kvp => kvp.Key == "key2" && (string)kvp.Value == "value2"); + } - configureBuilderCalled = true; + [Fact] + public void ConfigureBuilderIConfigurationAvailableTest() + { + Environment.SetEnvironmentVariable("TEST_KEY", "TEST_KEY_VALUE"); - var testKeyValue = configuration.GetValue("TEST_KEY", null); + bool configureBuilderCalled = false; - Assert.Equal("TEST_KEY_VALUE", testKeyValue); - }) - .Build(); + using var provider = Sdk.CreateTracerProviderBuilder() + .ConfigureBuilder((sp, builder) => + { + var configuration = sp.GetRequiredService(); - Assert.True(configureBuilderCalled); + configureBuilderCalled = true; - Environment.SetEnvironmentVariable("TEST_KEY", null); - } + var testKeyValue = configuration.GetValue("TEST_KEY", null); - [Fact] - public void ConfigureBuilderIConfigurationModifiableTest() - { - bool configureBuilderCalled = false; + Assert.Equal("TEST_KEY_VALUE", testKeyValue); + }) + .Build(); - using var provider = Sdk.CreateTracerProviderBuilder() - .ConfigureServices(services => - { - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { ["TEST_KEY_2"] = "TEST_KEY_2_VALUE" }) - .Build(); + Assert.True(configureBuilderCalled); - services.AddSingleton(configuration); - }) - .ConfigureBuilder((sp, builder) => - { - var configuration = sp.GetRequiredService(); + Environment.SetEnvironmentVariable("TEST_KEY", null); + } - configureBuilderCalled = true; + [Fact] + public void ConfigureBuilderIConfigurationModifiableTest() + { + bool configureBuilderCalled = false; - var testKey2Value = configuration.GetValue("TEST_KEY_2", null); + using var provider = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { ["TEST_KEY_2"] = "TEST_KEY_2_VALUE" }) + .Build(); - Assert.Equal("TEST_KEY_2_VALUE", testKey2Value); - }) - .Build(); + services.AddSingleton(configuration); + }) + .ConfigureBuilder((sp, builder) => + { + var configuration = sp.GetRequiredService(); - Assert.True(configureBuilderCalled); - } + configureBuilderCalled = true; - [Theory] - [InlineData(true)] - [InlineData(false)] - public void TracerProviderNestedResolutionUsingBuilderTest(bool callNestedConfigure) - { - bool innerConfigureBuilderTestExecuted = false; - bool innerConfigureOpenTelemetryLoggerProviderTestExecuted = false; - bool innerConfigureOpenTelemetryLoggerProviderTestWithServiceProviderExecuted = false; + var testKey2Value = configuration.GetValue("TEST_KEY_2", null); - using var provider = Sdk.CreateTracerProviderBuilder() - .ConfigureServices(services => - { - if (callNestedConfigure) - { - services.ConfigureOpenTelemetryTracerProvider( - builder => - { - innerConfigureOpenTelemetryLoggerProviderTestExecuted = true; - builder.AddInstrumentation(); - }); - services.ConfigureOpenTelemetryTracerProvider( - (sp, builder) => - { - innerConfigureOpenTelemetryLoggerProviderTestWithServiceProviderExecuted = true; - Assert.Throws(() => builder.AddInstrumentation()); - }); - } - }) - .ConfigureBuilder((sp, builder) => - { - innerConfigureBuilderTestExecuted = true; - Assert.Throws(() => sp.GetService()); - }) - .Build() as TracerProviderSdk; + Assert.Equal("TEST_KEY_2_VALUE", testKey2Value); + }) + .Build(); - Assert.NotNull(provider); + Assert.True(configureBuilderCalled); + } - Assert.True(innerConfigureBuilderTestExecuted); - Assert.Equal(callNestedConfigure, innerConfigureOpenTelemetryLoggerProviderTestExecuted); - Assert.Equal(callNestedConfigure, innerConfigureOpenTelemetryLoggerProviderTestWithServiceProviderExecuted); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TracerProviderNestedResolutionUsingBuilderTest(bool callNestedConfigure) + { + bool innerConfigureBuilderTestExecuted = false; + bool innerConfigureOpenTelemetryLoggerProviderTestExecuted = false; + bool innerConfigureOpenTelemetryLoggerProviderTestWithServiceProviderExecuted = false; - if (callNestedConfigure) + using var provider = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => { - Assert.Single(provider.Instrumentations); - } - else + if (callNestedConfigure) + { + services.ConfigureOpenTelemetryTracerProvider( + builder => + { + innerConfigureOpenTelemetryLoggerProviderTestExecuted = true; + builder.AddInstrumentation(); + }); + services.ConfigureOpenTelemetryTracerProvider( + (sp, builder) => + { + innerConfigureOpenTelemetryLoggerProviderTestWithServiceProviderExecuted = true; + Assert.Throws(() => builder.AddInstrumentation()); + }); + } + }) + .ConfigureBuilder((sp, builder) => { - Assert.Empty(provider.Instrumentations); - } + innerConfigureBuilderTestExecuted = true; + Assert.Throws(() => sp.GetService()); + }) + .Build() as TracerProviderSdk; - Assert.Throws(() => provider.GetServiceProvider()?.GetService()); - } + Assert.NotNull(provider); - [Fact] - public void TracerProviderSetSamplerFactoryTest() + Assert.True(innerConfigureBuilderTestExecuted); + Assert.Equal(callNestedConfigure, innerConfigureOpenTelemetryLoggerProviderTestExecuted); + Assert.Equal(callNestedConfigure, innerConfigureOpenTelemetryLoggerProviderTestWithServiceProviderExecuted); + + if (callNestedConfigure) { - bool factoryInvoked = false; + Assert.Single(provider.Instrumentations); + } + else + { + Assert.Empty(provider.Instrumentations); + } - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .SetSampler(sp => - { - factoryInvoked = true; + Assert.Throws(() => provider.GetServiceProvider()?.GetService()); + } - Assert.NotNull(sp); + [Fact] + public void TracerProviderSetSamplerFactoryTest() + { + bool factoryInvoked = false; - return new MySampler(); - }) - .Build() as TracerProviderSdk; + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(sp => + { + factoryInvoked = true; - Assert.True(factoryInvoked); + Assert.NotNull(sp); - Assert.NotNull(tracerProvider); - Assert.True(tracerProvider.Sampler is MySampler); - } + return new MySampler(); + }) + .Build() as TracerProviderSdk; - [Fact] - public void TracerProviderAddProcessorFactoryTest() - { - bool factoryInvoked = false; + Assert.True(factoryInvoked); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddProcessor(sp => - { - factoryInvoked = true; + Assert.NotNull(tracerProvider); + Assert.True(tracerProvider.Sampler is MySampler); + } - Assert.NotNull(sp); + [Fact] + public void TracerProviderAddProcessorFactoryTest() + { + bool factoryInvoked = false; - return new MyProcessor(); - }) - .Build() as TracerProviderSdk; + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddProcessor(sp => + { + factoryInvoked = true; - Assert.True(factoryInvoked); + Assert.NotNull(sp); - Assert.NotNull(tracerProvider); - Assert.True(tracerProvider.Processor is MyProcessor); - } + return new MyProcessor(); + }) + .Build() as TracerProviderSdk; - private static void RunBuilderServiceLifecycleTest( - TracerProviderBuilder builder, - Func buildFunc, - Action postAction) - { - var baseBuilder = builder as TracerProviderBuilderBase; + Assert.True(factoryInvoked); - builder - .AddSource("TestSource1") - .AddLegacySource("TestLegacySource1") - .SetSampler(); + Assert.NotNull(tracerProvider); + Assert.True(tracerProvider.Processor is MyProcessor); + } - bool configureServicesCalled = false; - builder.ConfigureServices(services => - { - configureServicesCalled = true; + [Fact] + public void TracerProviderBuilderCustomImplementationBuildTest() + { + var builder = new MyTracerProviderBuilder(); - Assert.NotNull(services); + Assert.Throws(() => builder.Build()); + } - services.TryAddSingleton(); - services.TryAddSingleton(); + private static void RunBuilderServiceLifecycleTest( + TracerProviderBuilder builder, + Func buildFunc, + Action postAction) + { + var baseBuilder = builder as TracerProviderBuilderBase; - // Note: This is strange to call ConfigureOpenTelemetryTracerProvider here, but supported - services.ConfigureOpenTelemetryTracerProvider((sp, b) => - { - Assert.Throws(() => b.ConfigureServices(services => { })); + builder + .AddSource("TestSource1") + .AddLegacySource("TestLegacySource1") + .SetSampler(); - b.AddInstrumentation(sp.GetRequiredService()); - }); - }); + bool configureServicesCalled = false; + builder.ConfigureServices(services => + { + configureServicesCalled = true; + + Assert.NotNull(services); + + services.TryAddSingleton(); + services.TryAddSingleton(); - int configureBuilderInvocations = 0; - builder.ConfigureBuilder((sp, builder) => + // Note: This is strange to call ConfigureOpenTelemetryTracerProvider here, but supported + services.ConfigureOpenTelemetryTracerProvider((sp, b) => { - configureBuilderInvocations++; + Assert.Throws(() => b.ConfigureServices(services => { })); - var sdkBuilder = builder as TracerProviderBuilderSdk; - Assert.NotNull(sdkBuilder); + b.AddInstrumentation(sp.GetRequiredService()); + }); + }); - builder - .AddSource("TestSource2") - .AddLegacySource("TestLegacySource2"); + int configureBuilderInvocations = 0; + builder.ConfigureBuilder((sp, builder) => + { + configureBuilderInvocations++; - Assert.Contains(sdkBuilder.Sources, s => s == "TestSource1"); - Assert.Contains(sdkBuilder.Sources, s => s == "TestSource2"); - Assert.Contains(sdkBuilder.LegacyActivityOperationNames, s => s == "TestLegacySource1"); - Assert.Contains(sdkBuilder.LegacyActivityOperationNames, s => s == "TestLegacySource2"); + var sdkBuilder = builder as TracerProviderBuilderSdk; + Assert.NotNull(sdkBuilder); + + builder + .AddSource("TestSource2") + .AddLegacySource("TestLegacySource2"); - // Note: Services can't be configured at this stage - Assert.Throws( - () => builder.ConfigureServices(services => services.TryAddSingleton())); + Assert.Contains(sdkBuilder.Sources, s => s == "TestSource1"); + Assert.Contains(sdkBuilder.Sources, s => s == "TestSource2"); + Assert.Contains(sdkBuilder.LegacyActivityOperationNames, s => s == "TestLegacySource1"); + Assert.Contains(sdkBuilder.LegacyActivityOperationNames, s => s == "TestLegacySource2"); - builder.AddProcessor(sp.GetRequiredService()); + // Note: Services can't be configured at this stage + Assert.Throws( + () => builder.ConfigureServices(services => services.TryAddSingleton())); - builder.ConfigureBuilder((_, b) => + builder.AddProcessor(sp.GetRequiredService()); + + builder.ConfigureBuilder((_, b) => + { + // Note: ConfigureBuilder calls can be nested, this is supported + configureBuilderInvocations++; + + b.ConfigureBuilder((_, _) => { - // Note: ConfigureBuilder calls can be nested, this is supported configureBuilderInvocations++; - - b.ConfigureBuilder((_, _) => - { - configureBuilderInvocations++; - }); }); }); + }); - var provider = buildFunc(); + var provider = buildFunc(); - Assert.True(configureServicesCalled); - Assert.Equal(3, configureBuilderInvocations); + Assert.True(configureServicesCalled); + Assert.Equal(3, configureBuilderInvocations); - Assert.True(provider.Sampler is MySampler); - Assert.Single(provider.Instrumentations); - Assert.True(provider.Instrumentations[0] is MyInstrumentation); - Assert.True(provider.Processor is MyProcessor); + Assert.True(provider.Sampler is MySampler); + Assert.Single(provider.Instrumentations); + Assert.True(provider.Instrumentations[0] is MyInstrumentation); + Assert.True(provider.Processor is MyProcessor); + + postAction(provider); + } - postAction(provider); + private sealed class MySampler : Sampler + { + public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) + { + return new SamplingResult(SamplingDecision.RecordAndSample); } + } - private sealed class MySampler : Sampler + private sealed class MyInstrumentation : IDisposable + { + internal TracerProvider Provider; + internal bool Disposed; + + public void Dispose() { - public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) - { - return new SamplingResult(SamplingDecision.RecordAndSample); - } + this.Disposed = true; } + } + + private sealed class MyProcessor : BaseProcessor + { + } - private sealed class MyInstrumentation : IDisposable + private sealed class MyExporter : BaseExporter + { + public override ExportResult Export(in Batch batch) { - internal TracerProvider Provider; - internal bool Disposed; + return ExportResult.Success; + } + } - public void Dispose() - { - this.Disposed = true; - } + private sealed class MyTracerProviderBuilder : TracerProviderBuilder + { + public override TracerProviderBuilder AddInstrumentation(Func instrumentationFactory) + { + throw new NotImplementedException(); } - private sealed class MyProcessor : BaseProcessor + public override TracerProviderBuilder AddLegacySource(string operationName) { + throw new NotImplementedException(); } - private sealed class MyExporter : BaseExporter + public override TracerProviderBuilder AddSource(params string[] names) { - public override ExportResult Export(in Batch batch) - { - return ExportResult.Success; - } + throw new NotImplementedException(); } } } diff --git a/test/OpenTelemetry.Tests/Trace/TracerProviderExtensionsTest.cs b/test/OpenTelemetry.Tests/Trace/TracerProviderExtensionsTest.cs index cfeff0c7df8..5722284db99 100644 --- a/test/OpenTelemetry.Tests/Trace/TracerProviderExtensionsTest.cs +++ b/test/OpenTelemetry.Tests/Trace/TracerProviderExtensionsTest.cs @@ -1,70 +1,56 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Trace.Tests +namespace OpenTelemetry.Trace.Tests; + +public class TracerProviderExtensionsTest { - public class TracerProviderExtensionsTest + [Fact] + public void Verify_ForceFlush_HandlesException() { - [Fact] - public void Verify_ForceFlush_HandlesException() - { - using var testProcessor = new DelegatingProcessor(); + using var testProcessor = new DelegatingProcessor(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddProcessor(testProcessor) - .Build(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddProcessor(testProcessor) + .Build(); - Assert.True(tracerProvider.ForceFlush()); + Assert.True(tracerProvider.ForceFlush()); - testProcessor.OnForceFlushFunc = (timeout) => throw new Exception("test exception"); + testProcessor.OnForceFlushFunc = (timeout) => throw new Exception("test exception"); - Assert.False(tracerProvider.ForceFlush()); - } + Assert.False(tracerProvider.ForceFlush()); + } - [Fact] - public void Verify_Shutdown_HandlesSecond() - { - using var testProcessor = new DelegatingProcessor(); + [Fact] + public void Verify_Shutdown_HandlesSecond() + { + using var testProcessor = new DelegatingProcessor(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddProcessor(testProcessor) - .Build(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddProcessor(testProcessor) + .Build(); - Assert.True(tracerProvider.Shutdown()); - Assert.False(tracerProvider.Shutdown()); - } + Assert.True(tracerProvider.Shutdown()); + Assert.False(tracerProvider.Shutdown()); + } - [Fact] - public void Verify_Shutdown_HandlesException() + [Fact] + public void Verify_Shutdown_HandlesException() + { + using var testProcessor = new DelegatingProcessor { - using var testProcessor = new DelegatingProcessor - { - OnShutdownFunc = (timeout) => throw new Exception("test exception"), - }; + OnShutdownFunc = (timeout) => throw new Exception("test exception"), + }; - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddProcessor(testProcessor) - .Build(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddProcessor(testProcessor) + .Build(); - Assert.False(tracerProvider.Shutdown()); - } + Assert.False(tracerProvider.Shutdown()); } } diff --git a/test/OpenTelemetry.Tests/Trace/TracerProviderSdkTest.cs b/test/OpenTelemetry.Tests/Trace/TracerProviderSdkTest.cs index 7da31fffcc1..b78744b49b5 100644 --- a/test/OpenTelemetry.Tests/Trace/TracerProviderSdkTest.cs +++ b/test/OpenTelemetry.Tests/Trace/TracerProviderSdkTest.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using OpenTelemetry.Instrumentation; @@ -21,25 +8,48 @@ using OpenTelemetry.Tests; using Xunit; -namespace OpenTelemetry.Trace.Tests +namespace OpenTelemetry.Trace.Tests; + +public class TracerProviderSdkTest : IDisposable { - public class TracerProviderSdkTest : IDisposable + public TracerProviderSdkTest() + { + Activity.DefaultIdFormat = ActivityIdFormat.W3C; + } + + [Fact] + public void TracerProviderSdkAddSource() { - public TracerProviderSdkTest() + using var source1 = new ActivitySource($"{Utils.GetCurrentMethodName()}.1"); + using var source2 = new ActivitySource($"{Utils.GetCurrentMethodName()}.2"); + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(source1.Name) + .Build(); + + using (var activity = source1.StartActivity("test")) { - Activity.DefaultIdFormat = ActivityIdFormat.W3C; + Assert.NotNull(activity); } - [Fact] - public void TracerProviderSdkAddSource() + using (var activity = source2.StartActivity("test")) { - using var source1 = new ActivitySource($"{Utils.GetCurrentMethodName()}.1"); - using var source2 = new ActivitySource($"{Utils.GetCurrentMethodName()}.2"); - - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(source1.Name) - .Build(); + Assert.Null(activity); + } + } + [Fact] + public void TracerProviderSdkAddSourceWithWildcards() + { + using var source1 = new ActivitySource($"{Utils.GetCurrentMethodName()}.A"); + using var source2 = new ActivitySource($"{Utils.GetCurrentMethodName()}.Ab"); + using var source3 = new ActivitySource($"{Utils.GetCurrentMethodName()}.Abc"); + using var source4 = new ActivitySource($"{Utils.GetCurrentMethodName()}.B"); + + using (var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource($"{Utils.GetCurrentMethodName()}.*") + .Build()) + { using (var activity = source1.StartActivity("test")) { Assert.NotNull(activity); @@ -47,1235 +57,1211 @@ public void TracerProviderSdkAddSource() using (var activity = source2.StartActivity("test")) { - Assert.Null(activity); + Assert.NotNull(activity); } - } - [Fact] - public void TracerProviderSdkAddSourceWithWildcards() - { - using var source1 = new ActivitySource($"{Utils.GetCurrentMethodName()}.A"); - using var source2 = new ActivitySource($"{Utils.GetCurrentMethodName()}.Ab"); - using var source3 = new ActivitySource($"{Utils.GetCurrentMethodName()}.Abc"); - using var source4 = new ActivitySource($"{Utils.GetCurrentMethodName()}.B"); - - using (var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource($"{Utils.GetCurrentMethodName()}.*") - .Build()) + using (var activity = source3.StartActivity("test")) { - using (var activity = source1.StartActivity("test")) - { - Assert.NotNull(activity); - } - - using (var activity = source2.StartActivity("test")) - { - Assert.NotNull(activity); - } - - using (var activity = source3.StartActivity("test")) - { - Assert.NotNull(activity); - } - - using (var activity = source4.StartActivity("test")) - { - Assert.NotNull(activity); - } + Assert.NotNull(activity); } - using (var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource($"{Utils.GetCurrentMethodName()}.?") - .Build()) + using (var activity = source4.StartActivity("test")) { - using (var activity = source1.StartActivity("test")) - { - Assert.NotNull(activity); - } - - using (var activity = source2.StartActivity("test")) - { - Assert.Null(activity); - } - - using (var activity = source3.StartActivity("test")) - { - Assert.Null(activity); - } - - using (var activity = source4.StartActivity("test")) - { - Assert.NotNull(activity); - } + Assert.NotNull(activity); } } - [Fact] - public void TracerProviderSdkInvokesSamplingWithCorrectParameters() + using (var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource($"{Utils.GetCurrentMethodName()}.?") + .Build()) { - var activitySourceName = Utils.GetCurrentMethodName(); - var testSampler = new TestSampler(); - using var activitySource = new ActivitySource(activitySourceName); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .SetSampler(testSampler) - .Build(); - - // OpenTelemetry Sdk is expected to set default to W3C. - Assert.True(Activity.DefaultIdFormat == ActivityIdFormat.W3C); - - using (var rootActivity = activitySource.StartActivity("root")) - { - Assert.NotNull(rootActivity); - Assert.True(rootActivity.ParentSpanId == default); - - // Validate that the TraceId seen by Sampler is same as the - // Activity when it got created. - Assert.Equal(rootActivity.TraceId, testSampler.LatestSamplingParameters.TraceId); - Assert.Null(testSampler.LatestSamplingParameters.Tags); - Assert.Null(testSampler.LatestSamplingParameters.Links); - } - - using (var parent = activitySource.StartActivity("parent", ActivityKind.Client)) + using (var activity = source1.StartActivity("test")) { - Assert.Equal(parent.TraceId, testSampler.LatestSamplingParameters.TraceId); - using var child = activitySource.StartActivity("child"); - Assert.Equal(child.TraceId, testSampler.LatestSamplingParameters.TraceId); - Assert.Null(testSampler.LatestSamplingParameters.Tags); - Assert.Null(testSampler.LatestSamplingParameters.Links); - Assert.Equal(parent.TraceId, child.TraceId); - Assert.Equal(parent.SpanId, child.ParentSpanId); + Assert.NotNull(activity); } - var customContext = new ActivityContext( - ActivityTraceId.CreateRandom(), - ActivitySpanId.CreateRandom(), - ActivityTraceFlags.None); - - using (var fromCustomContext = - activitySource.StartActivity("customContext", ActivityKind.Client, customContext)) + using (var activity = source2.StartActivity("test")) { - Assert.Equal(fromCustomContext.TraceId, testSampler.LatestSamplingParameters.TraceId); - Assert.Null(testSampler.LatestSamplingParameters.Tags); - Assert.Null(testSampler.LatestSamplingParameters.Links); - Assert.Equal(customContext.TraceId, fromCustomContext.TraceId); - Assert.Equal(customContext.SpanId, fromCustomContext.ParentSpanId); - Assert.NotEqual(customContext.SpanId, fromCustomContext.SpanId); + Assert.Null(activity); } - // Validate that Samplers get the tags passed with Activity creation - var initialTags = new ActivityTagsCollection(); - initialTags["tagA"] = "tagAValue"; - using (var withInitialTags = activitySource.StartActivity("withInitialTags", ActivityKind.Client, default(ActivityContext), initialTags)) + using (var activity = source3.StartActivity("test")) { - Assert.Equal(withInitialTags.TraceId, testSampler.LatestSamplingParameters.TraceId); - Assert.Equal(initialTags, testSampler.LatestSamplingParameters.Tags); + Assert.Null(activity); } - // Validate that Samplers get the links passed with Activity creation - var links = new List(); - var linkContext1 = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded); - var linkContext2 = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded); - var link1 = new ActivityLink(linkContext1); - var link2 = new ActivityLink(linkContext2); - links.Add(link1); - links.Add(link2); - - using (var withInitialTags = activitySource.StartActivity("withLinks", ActivityKind.Client, default(ActivityContext), links: links)) + using (var activity = source4.StartActivity("test")) { - Assert.Equal(withInitialTags.TraceId, testSampler.LatestSamplingParameters.TraceId); - Assert.Null(testSampler.LatestSamplingParameters.Tags); - Assert.Equal(links, testSampler.LatestSamplingParameters.Links); + Assert.NotNull(activity); } + } + } - // Validate that when StartActivity is called using Parent as string, - // Sampling is called correctly. - using var act = new Activity("anything").Start(); - act.Stop(); - var customContextAsString = act.Id; - var expectedTraceId = act.TraceId; - var expectedParentSpanId = act.SpanId; + [Fact] + public void TracerProviderSdkInvokesSamplingWithCorrectParameters() + { + var activitySourceName = Utils.GetCurrentMethodName(); + var testSampler = new TestSampler(); + using var activitySource = new ActivitySource(activitySourceName); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .SetSampler(testSampler) + .Build(); + + // OpenTelemetry Sdk is expected to set default to W3C. + Assert.True(Activity.DefaultIdFormat == ActivityIdFormat.W3C); + + using (var rootActivity = activitySource.StartActivity("root")) + { + Assert.NotNull(rootActivity); + Assert.True(rootActivity.ParentSpanId == default); - using (var fromCustomContextAsString = - activitySource.StartActivity("customContext", ActivityKind.Client, customContextAsString)) - { - Assert.Equal(fromCustomContextAsString.TraceId, testSampler.LatestSamplingParameters.TraceId); - Assert.Equal(expectedTraceId, fromCustomContextAsString.TraceId); - Assert.Equal(expectedParentSpanId, fromCustomContextAsString.ParentSpanId); - } + // Validate that the TraceId seen by Sampler is same as the + // Activity when it got created. + Assert.Equal(rootActivity.TraceId, testSampler.LatestSamplingParameters.TraceId); + Assert.Null(testSampler.LatestSamplingParameters.Tags); + Assert.Null(testSampler.LatestSamplingParameters.Links); + } - // Verify that StartActivity returns an instance of Activity. - using var fromInvalidW3CIdParent = - activitySource.StartActivity("customContext", ActivityKind.Client, "InvalidW3CIdParent"); - Assert.NotNull(fromInvalidW3CIdParent); + using (var parent = activitySource.StartActivity("parent", ActivityKind.Client)) + { + Assert.Equal(parent.TraceId, testSampler.LatestSamplingParameters.TraceId); + using var child = activitySource.StartActivity("child"); + Assert.Equal(child.TraceId, testSampler.LatestSamplingParameters.TraceId); + Assert.Null(testSampler.LatestSamplingParameters.Tags); + Assert.Null(testSampler.LatestSamplingParameters.Links); + Assert.Equal(parent.TraceId, child.TraceId); + Assert.Equal(parent.SpanId, child.ParentSpanId); + } - // Verify that the TestSampler was invoked and received the correct params. - Assert.Equal(fromInvalidW3CIdParent.TraceId, testSampler.LatestSamplingParameters.TraceId); + var customContext = new ActivityContext( + ActivityTraceId.CreateRandom(), + ActivitySpanId.CreateRandom(), + ActivityTraceFlags.None); - // OpenTelemetry ActivityContext does not support non W3C Ids. - Assert.Null(fromInvalidW3CIdParent.ParentId); - Assert.Equal(default, fromInvalidW3CIdParent.ParentSpanId); + using (var fromCustomContext = + activitySource.StartActivity("customContext", ActivityKind.Client, customContext)) + { + Assert.Equal(fromCustomContext.TraceId, testSampler.LatestSamplingParameters.TraceId); + Assert.Null(testSampler.LatestSamplingParameters.Tags); + Assert.Null(testSampler.LatestSamplingParameters.Links); + Assert.Equal(customContext.TraceId, fromCustomContext.TraceId); + Assert.Equal(customContext.SpanId, fromCustomContext.ParentSpanId); + Assert.NotEqual(customContext.SpanId, fromCustomContext.SpanId); } - [Theory] - [InlineData(SamplingDecision.Drop)] - [InlineData(SamplingDecision.RecordOnly)] - [InlineData(SamplingDecision.RecordAndSample)] - public void TracerProviderSdkSamplerAttributesAreAppliedToActivity(SamplingDecision sampling) + // Validate that Samplers get the tags passed with Activity creation + var initialTags = new ActivityTagsCollection(); + initialTags["tagA"] = "tagAValue"; + using (var withInitialTags = activitySource.StartActivity("withInitialTags", ActivityKind.Client, default(ActivityContext), initialTags)) { - var testSampler = new TestSampler - { - SamplingAction = (samplingParams) => - { - var attributes = new Dictionary - { - { "tagkeybysampler", "tagvalueaddedbysampler" }, - }; - return new SamplingResult(sampling, attributes); - }, - }; + Assert.Equal(withInitialTags.TraceId, testSampler.LatestSamplingParameters.TraceId); + Assert.Equal(initialTags, testSampler.LatestSamplingParameters.Tags); + } - var activitySourceName = Utils.GetCurrentMethodName(); - using var activitySource = new ActivitySource(activitySourceName); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .SetSampler(testSampler) - .Build(); + // Validate that Samplers get the links passed with Activity creation + var links = new List(); + var linkContext1 = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded); + var linkContext2 = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded); + var link1 = new ActivityLink(linkContext1); + var link2 = new ActivityLink(linkContext2); + links.Add(link1); + links.Add(link2); - using var rootActivity = activitySource.StartActivity("root"); - Assert.NotNull(rootActivity); - Assert.Equal(rootActivity.TraceId, testSampler.LatestSamplingParameters.TraceId); - if (sampling != SamplingDecision.Drop) - { - Assert.Contains(new KeyValuePair("tagkeybysampler", "tagvalueaddedbysampler"), rootActivity.TagObjects); - } + using (var withInitialTags = activitySource.StartActivity("withLinks", ActivityKind.Client, default(ActivityContext), links: links)) + { + Assert.Equal(withInitialTags.TraceId, testSampler.LatestSamplingParameters.TraceId); + Assert.Null(testSampler.LatestSamplingParameters.Tags); + Assert.Equal(links, testSampler.LatestSamplingParameters.Links); } - [Fact] - public void TracerSdkSetsActivitySamplingResultAsPropagationWhenParentIsRemote() + // Validate that when StartActivity is called using Parent as string, + // Sampling is called correctly. + using var act = new Activity("anything").Start(); + act.Stop(); + var customContextAsString = act.Id; + var expectedTraceId = act.TraceId; + var expectedParentSpanId = act.SpanId; + + using (var fromCustomContextAsString = + activitySource.StartActivity("customContext", ActivityKind.Client, customContextAsString)) { - var activitySourceName = Utils.GetCurrentMethodName(); - var testSampler = new TestSampler(); - using var activitySource = new ActivitySource(activitySourceName); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .SetSampler(testSampler) - .Build(); + Assert.Equal(fromCustomContextAsString.TraceId, testSampler.LatestSamplingParameters.TraceId); + Assert.Equal(expectedTraceId, fromCustomContextAsString.TraceId); + Assert.Equal(expectedParentSpanId, fromCustomContextAsString.ParentSpanId); + } - testSampler.SamplingAction = (samplingParameters) => - { - return new SamplingResult(SamplingDecision.Drop); - }; + // Verify that StartActivity returns an instance of Activity. + using var fromInvalidW3CIdParent = + activitySource.StartActivity("customContext", ActivityKind.Client, "InvalidW3CIdParent"); + Assert.NotNull(fromInvalidW3CIdParent); - ActivityContext ctx = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None, isRemote: true); + // Verify that the TestSampler was invoked and received the correct params. + Assert.Equal(fromInvalidW3CIdParent.TraceId, testSampler.LatestSamplingParameters.TraceId); - using (var activity = activitySource.StartActivity("root", ActivityKind.Server, ctx)) - { - // Even if sampling returns false, for activities with remote parent, - // activity is still created with PropagationOnly. - Assert.NotNull(activity); - Assert.False(activity.IsAllDataRequested); - Assert.False(activity.Recorded); + // OpenTelemetry ActivityContext does not support non W3C Ids. + Assert.Null(fromInvalidW3CIdParent.ParentId); + Assert.Equal(default, fromInvalidW3CIdParent.ParentSpanId); + } - // This is not a root activity and parent is not remote. - // If sampling returns false, no activity is created at all. - using var innerActivity = activitySource.StartActivity("inner"); - Assert.Null(innerActivity); - } + [Theory] + [InlineData(SamplingDecision.Drop)] + [InlineData(SamplingDecision.RecordOnly)] + [InlineData(SamplingDecision.RecordAndSample)] + public void TracerProviderSdkSamplerAttributesAreAppliedToActivity(SamplingDecision sampling) + { + var testSampler = new TestSampler + { + SamplingAction = (samplingParams) => + { + var attributes = new Dictionary + { + { "tagkeybysampler", "tagvalueaddedbysampler" }, + }; + return new SamplingResult(sampling, attributes); + }, + }; + + var activitySourceName = Utils.GetCurrentMethodName(); + using var activitySource = new ActivitySource(activitySourceName); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .SetSampler(testSampler) + .Build(); + + using var rootActivity = activitySource.StartActivity("root"); + Assert.NotNull(rootActivity); + Assert.Equal(rootActivity.TraceId, testSampler.LatestSamplingParameters.TraceId); + if (sampling != SamplingDecision.Drop) + { + Assert.Contains(new KeyValuePair("tagkeybysampler", "tagvalueaddedbysampler"), rootActivity.TagObjects); } + } + + [Fact] + public void TracerSdkSetsActivitySamplingResultAsPropagationWhenParentIsRemote() + { + var activitySourceName = Utils.GetCurrentMethodName(); + var testSampler = new TestSampler(); + using var activitySource = new ActivitySource(activitySourceName); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .SetSampler(testSampler) + .Build(); - [Fact] - public void TracerSdkSetsActivitySamplingResultBasedOnSamplingDecision() + testSampler.SamplingAction = (samplingParameters) => { - var activitySourceName = Utils.GetCurrentMethodName(); - var testSampler = new TestSampler(); - using var activitySource = new ActivitySource(activitySourceName); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .SetSampler(testSampler) - .Build(); + return new SamplingResult(SamplingDecision.Drop); + }; - testSampler.SamplingAction = (samplingParameters) => - { - return new SamplingResult(SamplingDecision.RecordAndSample); - }; + ActivityContext ctx = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None, isRemote: true); - using (var activity = activitySource.StartActivity("root")) - { - Assert.NotNull(activity); - Assert.True(activity.IsAllDataRequested); - Assert.True(activity.Recorded); - } + using (var activity = activitySource.StartActivity("root", ActivityKind.Server, ctx)) + { + // Even if sampling returns false, for activities with remote parent, + // activity is still created with PropagationOnly. + Assert.NotNull(activity); + Assert.False(activity.IsAllDataRequested); + Assert.False(activity.Recorded); - testSampler.SamplingAction = (samplingParameters) => - { - return new SamplingResult(SamplingDecision.RecordOnly); - }; + // This is not a root activity and parent is not remote. + // If sampling returns false, no activity is created at all. + using var innerActivity = activitySource.StartActivity("inner"); + Assert.Null(innerActivity); + } + } - using (var activity = activitySource.StartActivity("root")) - { - // Even if sampling returns false, for root activities, - // activity is still created with PropagationOnly. - Assert.NotNull(activity); - Assert.True(activity.IsAllDataRequested); - Assert.False(activity.Recorded); - } + [Fact] + public void TracerSdkSetsActivitySamplingResultBasedOnSamplingDecision() + { + var activitySourceName = Utils.GetCurrentMethodName(); + var testSampler = new TestSampler(); + using var activitySource = new ActivitySource(activitySourceName); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .SetSampler(testSampler) + .Build(); - testSampler.SamplingAction = (samplingParameters) => - { - return new SamplingResult(SamplingDecision.Drop); - }; + testSampler.SamplingAction = (samplingParameters) => + { + return new SamplingResult(SamplingDecision.RecordAndSample); + }; - using (var activity = activitySource.StartActivity("root")) - { - // Even if sampling returns false, for root activities, - // activity is still created with PropagationOnly. - Assert.NotNull(activity); - Assert.False(activity.IsAllDataRequested); - Assert.False(activity.Recorded); + using (var activity = activitySource.StartActivity("root")) + { + Assert.NotNull(activity); + Assert.True(activity.IsAllDataRequested); + Assert.True(activity.Recorded); + } - // This is not a root activity. - // If sampling returns false, no activity is created at all. - using var innerActivity = activitySource.StartActivity("inner"); - Assert.Null(innerActivity); - } + testSampler.SamplingAction = (samplingParameters) => + { + return new SamplingResult(SamplingDecision.RecordOnly); + }; + + using (var activity = activitySource.StartActivity("root")) + { + // Even if sampling returns false, for root activities, + // activity is still created with PropagationOnly. + Assert.NotNull(activity); + Assert.True(activity.IsAllDataRequested); + Assert.False(activity.Recorded); } - [Fact] - public void TracerSdkSetsActivitySamplingResultToNoneWhenSuppressInstrumentationIsTrue() + testSampler.SamplingAction = (samplingParameters) => { - using var scope = SuppressInstrumentationScope.Begin(); + return new SamplingResult(SamplingDecision.Drop); + }; - var activitySourceName = Utils.GetCurrentMethodName(); - var testSampler = new TestSampler(); - using var activitySource = new ActivitySource(activitySourceName); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .SetSampler(testSampler) - .Build(); + using (var activity = activitySource.StartActivity("root")) + { + // Even if sampling returns false, for root activities, + // activity is still created with PropagationOnly. + Assert.NotNull(activity); + Assert.False(activity.IsAllDataRequested); + Assert.False(activity.Recorded); - using var activity = activitySource.StartActivity("root"); - Assert.Null(activity); + // This is not a root activity. + // If sampling returns false, no activity is created at all. + using var innerActivity = activitySource.StartActivity("inner"); + Assert.Null(innerActivity); } + } - [Fact] - public void TracerSdkSetsActivityDataRequestedToFalseWhenSuppressInstrumentationIsTrueForLegacyActivity() - { - using TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); + [Fact] + public void TracerSdkSetsActivitySamplingResultToNoneWhenSuppressInstrumentationIsTrue() + { + using var scope = SuppressInstrumentationScope.Begin(); - bool startCalled = false; - bool endCalled = false; + var activitySourceName = Utils.GetCurrentMethodName(); + var testSampler = new TestSampler(); + using var activitySource = new ActivitySource(activitySourceName); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .SetSampler(testSampler) + .Build(); - testActivityProcessor.StartAction = - (a) => - { - startCalled = true; - }; + using var activity = activitySource.StartActivity("root"); + Assert.Null(activity); + } - testActivityProcessor.EndAction = - (a) => - { - endCalled = true; - }; + [Fact] + public void TracerSdkSetsActivityDataRequestedToFalseWhenSuppressInstrumentationIsTrueForLegacyActivity() + { + using TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); - var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); + bool startCalled = false; + bool endCalled = false; - using var openTelemetry = Sdk.CreateTracerProviderBuilder() - .AddLegacySource(operationNameForLegacyActivity) - .AddProcessor(testActivityProcessor) - .SetSampler(new AlwaysOnSampler()) - .Build(); + testActivityProcessor.StartAction = + (a) => + { + startCalled = true; + }; - using (SuppressInstrumentationScope.Begin(true)) + testActivityProcessor.EndAction = + (a) => { - using var activity = new Activity(operationNameForLegacyActivity).Start(); - Assert.False(activity.IsAllDataRequested); - } + endCalled = true; + }; - Assert.False(startCalled); - Assert.False(endCalled); - } + var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); - [Fact] - public void ProcessorDoesNotReceiveNotRecordDecisionSpan() - { - var activitySourceName = Utils.GetCurrentMethodName(); - var testSampler = new TestSampler(); - using TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); + using var openTelemetry = Sdk.CreateTracerProviderBuilder() + .AddLegacySource(operationNameForLegacyActivity) + .AddProcessor(testActivityProcessor) + .SetSampler(new AlwaysOnSampler()) + .Build(); - bool startCalled = false; - bool endCalled = false; + using (SuppressInstrumentationScope.Begin(true)) + { + using var activity = new Activity(operationNameForLegacyActivity).Start(); + Assert.False(activity.IsAllDataRequested); + } - testActivityProcessor.StartAction = - (a) => - { - startCalled = true; - }; + Assert.False(startCalled); + Assert.False(endCalled); + } - testActivityProcessor.EndAction = - (a) => - { - endCalled = true; - }; + [Fact] + public void ProcessorDoesNotReceiveNotRecordDecisionSpan() + { + var activitySourceName = Utils.GetCurrentMethodName(); + var testSampler = new TestSampler(); + using TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); - using var openTelemetry = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceName) - .AddProcessor(testActivityProcessor) - .SetSampler(testSampler) - .Build(); + bool startCalled = false; + bool endCalled = false; - testSampler.SamplingAction = (samplingParameters) => + testActivityProcessor.StartAction = + (a) => { - return new SamplingResult(SamplingDecision.Drop); + startCalled = true; }; - using ActivitySource source = new ActivitySource(activitySourceName); - using var activity = source.StartActivity("somename"); - activity.Stop(); + testActivityProcessor.EndAction = + (a) => + { + endCalled = true; + }; - Assert.False(activity.IsAllDataRequested); - Assert.Equal(ActivityTraceFlags.None, activity.ActivityTraceFlags); - Assert.False(activity.Recorded); - Assert.False(startCalled); - Assert.False(endCalled); - } + using var openTelemetry = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .AddProcessor(testActivityProcessor) + .SetSampler(testSampler) + .Build(); - // Test to check that TracerProvider does not call Processor.OnStart or Processor.OnEnd for a legacy activity when no legacy OperationName is - // provided to TracerProviderBuilder. - [Fact] - public void SdkDoesNotProcessLegacyActivityWithNoAdditionalConfig() + testSampler.SamplingAction = (samplingParameters) => { - using TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); + return new SamplingResult(SamplingDecision.Drop); + }; + + using ActivitySource source = new ActivitySource(activitySourceName); + using var activity = source.StartActivity("somename"); + activity.Stop(); + + Assert.False(activity.IsAllDataRequested); + Assert.Equal(ActivityTraceFlags.None, activity.ActivityTraceFlags); + Assert.False(activity.Recorded); + Assert.False(startCalled); + Assert.False(endCalled); + } - bool startCalled = false; - bool endCalled = false; + // Test to check that TracerProvider does not call Processor.OnStart or Processor.OnEnd for a legacy activity when no legacy OperationName is + // provided to TracerProviderBuilder. + [Fact] + public void SdkDoesNotProcessLegacyActivityWithNoAdditionalConfig() + { + using TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); - testActivityProcessor.StartAction = - (a) => - { - Assert.False(Sdk.SuppressInstrumentation); - Assert.True(a.IsAllDataRequested); // If Processor.OnStart is called, activity's IsAllDataRequested is set to true - startCalled = true; - }; + bool startCalled = false; + bool endCalled = false; - testActivityProcessor.EndAction = - (a) => - { - Assert.False(Sdk.SuppressInstrumentation); - Assert.True(a.IsAllDataRequested); // If Processor.OnEnd is called, activity's IsAllDataRequested is set to true - endCalled = true; - }; + testActivityProcessor.StartAction = + (a) => + { + Assert.False(Sdk.SuppressInstrumentation); + Assert.True(a.IsAllDataRequested); // If Processor.OnStart is called, activity's IsAllDataRequested is set to true + startCalled = true; + }; - using var emptyActivitySource = new ActivitySource(string.Empty); - Assert.False(emptyActivitySource.HasListeners()); // No ActivityListener for empty ActivitySource added yet + testActivityProcessor.EndAction = + (a) => + { + Assert.False(Sdk.SuppressInstrumentation); + Assert.True(a.IsAllDataRequested); // If Processor.OnEnd is called, activity's IsAllDataRequested is set to true + endCalled = true; + }; - // No AddLegacyOperationName chained to TracerProviderBuilder - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddProcessor(testActivityProcessor) - .Build(); + using var emptyActivitySource = new ActivitySource(string.Empty); + Assert.False(emptyActivitySource.HasListeners()); // No ActivityListener for empty ActivitySource added yet - Assert.False(emptyActivitySource.HasListeners()); // No listener for empty ActivitySource even after build + // No AddLegacyOperationName chained to TracerProviderBuilder + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddProcessor(testActivityProcessor) + .Build(); - using var activity = new Activity("Test"); - activity.Start(); - activity.Stop(); + Assert.False(emptyActivitySource.HasListeners()); // No listener for empty ActivitySource even after build - Assert.False(startCalled); // Processor.OnStart is not called since we did not add any legacy OperationName - Assert.False(endCalled); // Processor.OnEnd is not called since we did not add any legacy OperationName - } + using var activity = new Activity("Test"); + activity.Start(); + activity.Stop(); - // Test to check that TracerProvider samples a legacy activity using a custom Sampler and calls Processor.OnStart and Processor.OnEnd for the - // legacy activity when the correct legacy OperationName is provided to TracerProviderBuilder. - [Fact] - public void SdkSamplesAndProcessesLegacyActivityWithRightConfig() - { - bool samplerCalled = false; + Assert.False(startCalled); // Processor.OnStart is not called since we did not add any legacy OperationName + Assert.False(endCalled); // Processor.OnEnd is not called since we did not add any legacy OperationName + } - var sampler = new TestSampler + // Test to check that TracerProvider samples a legacy activity using a custom Sampler and calls Processor.OnStart and Processor.OnEnd for the + // legacy activity when the correct legacy OperationName is provided to TracerProviderBuilder. + [Fact] + public void SdkSamplesAndProcessesLegacyActivityWithRightConfig() + { + bool samplerCalled = false; + + var sampler = new TestSampler + { + SamplingAction = + (samplingParameters) => { - SamplingAction = - (samplingParameters) => - { - samplerCalled = true; - return new SamplingResult(SamplingDecision.RecordAndSample); - }, - }; + samplerCalled = true; + return new SamplingResult(SamplingDecision.RecordAndSample); + }, + }; - using TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); + using TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); - bool startCalled = false; - bool endCalled = false; + bool startCalled = false; + bool endCalled = false; - testActivityProcessor.StartAction = - (a) => - { - Assert.True(samplerCalled); - Assert.False(Sdk.SuppressInstrumentation); - Assert.True(a.IsAllDataRequested); // If Processor.OnStart is called, activity's IsAllDataRequested is set to true - startCalled = true; - }; + testActivityProcessor.StartAction = + (a) => + { + Assert.True(samplerCalled); + Assert.False(Sdk.SuppressInstrumentation); + Assert.True(a.IsAllDataRequested); // If Processor.OnStart is called, activity's IsAllDataRequested is set to true + startCalled = true; + }; - testActivityProcessor.EndAction = - (a) => - { - Assert.False(Sdk.SuppressInstrumentation); - Assert.True(a.IsAllDataRequested); // If Processor.OnEnd is called, activity's IsAllDataRequested is set to true - endCalled = true; - }; + testActivityProcessor.EndAction = + (a) => + { + Assert.False(Sdk.SuppressInstrumentation); + Assert.True(a.IsAllDataRequested); // If Processor.OnEnd is called, activity's IsAllDataRequested is set to true + endCalled = true; + }; - using var emptyActivitySource = new ActivitySource(string.Empty); - Assert.False(emptyActivitySource.HasListeners()); // No ActivityListener for empty ActivitySource added yet + using var emptyActivitySource = new ActivitySource(string.Empty); + Assert.False(emptyActivitySource.HasListeners()); // No ActivityListener for empty ActivitySource added yet - var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); + var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); - // AddLegacyOperationName chained to TracerProviderBuilder - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .SetSampler(sampler) - .AddProcessor(testActivityProcessor) - .AddLegacySource(operationNameForLegacyActivity) - .Build(); + // AddLegacyOperationName chained to TracerProviderBuilder + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(sampler) + .AddProcessor(testActivityProcessor) + .AddLegacySource(operationNameForLegacyActivity) + .Build(); - Assert.True(emptyActivitySource.HasListeners()); // Listener for empty ActivitySource added after TracerProvider build + Assert.True(emptyActivitySource.HasListeners()); // Listener for empty ActivitySource added after TracerProvider build - using var activity = new Activity(operationNameForLegacyActivity); - activity.Start(); - activity.Stop(); + using var activity = new Activity(operationNameForLegacyActivity); + activity.Start(); + activity.Stop(); - Assert.True(startCalled); // Processor.OnStart is called since we added a legacy OperationName - Assert.True(endCalled); // Processor.OnEnd is called since we added a legacy OperationName - } + Assert.True(startCalled); // Processor.OnStart is called since we added a legacy OperationName + Assert.True(endCalled); // Processor.OnEnd is called since we added a legacy OperationName + } - // Test to check that TracerProvider samples a legacy activity using a custom Sampler and calls Processor.OnStart and Processor.OnEnd for the - // legacy activity when the correct legacy OperationName is provided to TracerProviderBuilder and a wildcard Source is added - [Fact] - public void SdkSamplesAndProcessesLegacyActivityWithRightConfigOnWildCardMode() - { - bool samplerCalled = false; + // Test to check that TracerProvider samples a legacy activity using a custom Sampler and calls Processor.OnStart and Processor.OnEnd for the + // legacy activity when the correct legacy OperationName is provided to TracerProviderBuilder and a wildcard Source is added + [Fact] + public void SdkSamplesAndProcessesLegacyActivityWithRightConfigOnWildCardMode() + { + bool samplerCalled = false; - var sampler = new TestSampler + var sampler = new TestSampler + { + SamplingAction = + (samplingParameters) => { - SamplingAction = - (samplingParameters) => - { - samplerCalled = true; - return new SamplingResult(SamplingDecision.RecordAndSample); - }, - }; + samplerCalled = true; + return new SamplingResult(SamplingDecision.RecordAndSample); + }, + }; - using TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); + using TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); - bool startCalled = false; - bool endCalled = false; + bool startCalled = false; + bool endCalled = false; - testActivityProcessor.StartAction = - (a) => - { - Assert.True(samplerCalled); - Assert.False(Sdk.SuppressInstrumentation); - Assert.True(a.IsAllDataRequested); // If Processor.OnStart is called, activity's IsAllDataRequested is set to true - startCalled = true; - }; + testActivityProcessor.StartAction = + (a) => + { + Assert.True(samplerCalled); + Assert.False(Sdk.SuppressInstrumentation); + Assert.True(a.IsAllDataRequested); // If Processor.OnStart is called, activity's IsAllDataRequested is set to true + startCalled = true; + }; - testActivityProcessor.EndAction = - (a) => - { - Assert.False(Sdk.SuppressInstrumentation); - Assert.True(a.IsAllDataRequested); // If Processor.OnEnd is called, activity's IsAllDataRequested is set to true - endCalled = true; - }; + testActivityProcessor.EndAction = + (a) => + { + Assert.False(Sdk.SuppressInstrumentation); + Assert.True(a.IsAllDataRequested); // If Processor.OnEnd is called, activity's IsAllDataRequested is set to true + endCalled = true; + }; - using var emptyActivitySource = new ActivitySource(string.Empty); - Assert.False(emptyActivitySource.HasListeners()); // No ActivityListener for empty ActivitySource added yet + using var emptyActivitySource = new ActivitySource(string.Empty); + Assert.False(emptyActivitySource.HasListeners()); // No ActivityListener for empty ActivitySource added yet - var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); + var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); - // AddLegacyOperationName chained to TracerProviderBuilder - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .SetSampler(sampler) - .AddSource("ABCCompany.XYZProduct.*") // Adding a wild card source - .AddProcessor(testActivityProcessor) - .AddLegacySource(operationNameForLegacyActivity) - .Build(); + // AddLegacyOperationName chained to TracerProviderBuilder + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(sampler) + .AddSource("ABCCompany.XYZProduct.*") // Adding a wild card source + .AddProcessor(testActivityProcessor) + .AddLegacySource(operationNameForLegacyActivity) + .Build(); - Assert.True(emptyActivitySource.HasListeners()); // Listener for empty ActivitySource added after TracerProvider build + Assert.True(emptyActivitySource.HasListeners()); // Listener for empty ActivitySource added after TracerProvider build - using var activity = new Activity(operationNameForLegacyActivity); - activity.Start(); - activity.Stop(); + using var activity = new Activity(operationNameForLegacyActivity); + activity.Start(); + activity.Stop(); - Assert.True(startCalled); // Processor.OnStart is called since we added a legacy OperationName - Assert.True(endCalled); // Processor.OnEnd is called since we added a legacy OperationName - } + Assert.True(startCalled); // Processor.OnStart is called since we added a legacy OperationName + Assert.True(endCalled); // Processor.OnEnd is called since we added a legacy OperationName + } - // Test to check that TracerProvider does not call Processor.OnEnd for a legacy activity whose ActivitySource got updated before Activity.Stop and - // the updated source was not added to the Provider - [Fact] - public void SdkCallsOnlyProcessorOnStartForLegacyActivityWhenActivitySourceIsUpdatedWithoutAddSource() - { - using TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); + // Test to check that TracerProvider does not call Processor.OnEnd for a legacy activity whose ActivitySource got updated before Activity.Stop and + // the updated source was not added to the Provider + [Fact] + public void SdkCallsOnlyProcessorOnStartForLegacyActivityWhenActivitySourceIsUpdatedWithoutAddSource() + { + using TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); - bool startCalled = false; - bool endCalled = false; + bool startCalled = false; + bool endCalled = false; - testActivityProcessor.StartAction = - (a) => - { - Assert.False(Sdk.SuppressInstrumentation); - Assert.True(a.IsAllDataRequested); // If Processor.OnStart is called, activity's IsAllDataRequested is set to true - startCalled = true; - }; + testActivityProcessor.StartAction = + (a) => + { + Assert.False(Sdk.SuppressInstrumentation); + Assert.True(a.IsAllDataRequested); // If Processor.OnStart is called, activity's IsAllDataRequested is set to true + startCalled = true; + }; - testActivityProcessor.EndAction = - (a) => - { - Assert.False(Sdk.SuppressInstrumentation); - Assert.True(a.IsAllDataRequested); // If Processor.OnEnd is called, activity's IsAllDataRequested is set to true - endCalled = true; - }; + testActivityProcessor.EndAction = + (a) => + { + Assert.False(Sdk.SuppressInstrumentation); + Assert.True(a.IsAllDataRequested); // If Processor.OnEnd is called, activity's IsAllDataRequested is set to true + endCalled = true; + }; - using var emptyActivitySource = new ActivitySource(string.Empty); - Assert.False(emptyActivitySource.HasListeners()); // No ActivityListener for empty ActivitySource added yet + using var emptyActivitySource = new ActivitySource(string.Empty); + Assert.False(emptyActivitySource.HasListeners()); // No ActivityListener for empty ActivitySource added yet - var activitySourceName = Utils.GetCurrentMethodName(); - var operationNameForLegacyActivity = $"legacyActivitySource-{activitySourceName}"; - using var activitySourceForLegacyActivity = new ActivitySource(activitySourceName, "1.0.0"); + var activitySourceName = Utils.GetCurrentMethodName(); + var operationNameForLegacyActivity = $"legacyActivitySource-{activitySourceName}"; + using var activitySourceForLegacyActivity = new ActivitySource(activitySourceName, "1.0.0"); - // AddLegacyOperationName chained to TracerProviderBuilder - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddLegacySource(operationNameForLegacyActivity) - .AddProcessor(testActivityProcessor) - .Build(); + // AddLegacyOperationName chained to TracerProviderBuilder + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddLegacySource(operationNameForLegacyActivity) + .AddProcessor(testActivityProcessor) + .Build(); - Assert.True(emptyActivitySource.HasListeners()); // Listener for empty ActivitySource added after TracerProvider build + Assert.True(emptyActivitySource.HasListeners()); // Listener for empty ActivitySource added after TracerProvider build - using var activity = new Activity(operationNameForLegacyActivity); - activity.Start(); - ActivityInstrumentationHelper.SetActivitySourceProperty(activity, activitySourceForLegacyActivity); - activity.Stop(); + using var activity = new Activity(operationNameForLegacyActivity); + activity.Start(); + ActivityInstrumentationHelper.SetActivitySourceProperty(activity, activitySourceForLegacyActivity); + activity.Stop(); - Assert.True(startCalled); // Processor.OnStart is called since we provided the legacy OperationName - Assert.False(endCalled); // Processor.OnEnd is not called since the ActivitySource is updated and the updated source name is not added as a Source to the provider - } + Assert.True(startCalled); // Processor.OnStart is called since we provided the legacy OperationName + Assert.False(endCalled); // Processor.OnEnd is not called since the ActivitySource is updated and the updated source name is not added as a Source to the provider + } - // Test to check that TracerProvider calls Processor.OnStart and Processor.OnEnd for a legacy activity whose ActivitySource got updated before Activity.Stop and - // the updated source was added to the Provider - [Fact] - public void SdkProcessesLegacyActivityWhenActivitySourceIsUpdatedWithAddSource() - { - using TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); + // Test to check that TracerProvider calls Processor.OnStart and Processor.OnEnd for a legacy activity whose ActivitySource got updated before Activity.Stop and + // the updated source was added to the Provider + [Fact] + public void SdkProcessesLegacyActivityWhenActivitySourceIsUpdatedWithAddSource() + { + using TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); - bool startCalled = false; - bool endCalled = false; + bool startCalled = false; + bool endCalled = false; - testActivityProcessor.StartAction = - (a) => - { - Assert.False(Sdk.SuppressInstrumentation); - Assert.True(a.IsAllDataRequested); // If Processor.OnStart is called, activity's IsAllDataRequested is set to true - startCalled = true; - }; + testActivityProcessor.StartAction = + (a) => + { + Assert.False(Sdk.SuppressInstrumentation); + Assert.True(a.IsAllDataRequested); // If Processor.OnStart is called, activity's IsAllDataRequested is set to true + startCalled = true; + }; - testActivityProcessor.EndAction = - (a) => - { - Assert.False(Sdk.SuppressInstrumentation); - Assert.True(a.IsAllDataRequested); // If Processor.OnEnd is called, activity's IsAllDataRequested is set to true - endCalled = true; - }; + testActivityProcessor.EndAction = + (a) => + { + Assert.False(Sdk.SuppressInstrumentation); + Assert.True(a.IsAllDataRequested); // If Processor.OnEnd is called, activity's IsAllDataRequested is set to true + endCalled = true; + }; - using var emptyActivitySource = new ActivitySource(string.Empty); - Assert.False(emptyActivitySource.HasListeners()); // No ActivityListener for empty ActivitySource added yet + using var emptyActivitySource = new ActivitySource(string.Empty); + Assert.False(emptyActivitySource.HasListeners()); // No ActivityListener for empty ActivitySource added yet - var activitySourceName = Utils.GetCurrentMethodName(); - var operationNameForLegacyActivity = $"legacyActivitySource-{activitySourceName}"; - using var activitySourceForLegacyActivity = new ActivitySource(activitySourceName, "1.0.0"); + var activitySourceName = Utils.GetCurrentMethodName(); + var operationNameForLegacyActivity = $"legacyActivitySource-{activitySourceName}"; + using var activitySourceForLegacyActivity = new ActivitySource(activitySourceName, "1.0.0"); - // AddLegacyOperationName chained to TracerProviderBuilder - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource(activitySourceForLegacyActivity.Name) // Add the updated ActivitySource as a Source - .AddLegacySource(operationNameForLegacyActivity) - .AddProcessor(testActivityProcessor) - .Build(); + // AddLegacyOperationName chained to TracerProviderBuilder + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceForLegacyActivity.Name) // Add the updated ActivitySource as a Source + .AddLegacySource(operationNameForLegacyActivity) + .AddProcessor(testActivityProcessor) + .Build(); - Assert.True(emptyActivitySource.HasListeners()); // Listener for empty ActivitySource added after TracerProvider build + Assert.True(emptyActivitySource.HasListeners()); // Listener for empty ActivitySource added after TracerProvider build - using var activity = new Activity(operationNameForLegacyActivity); - activity.Start(); - ActivityInstrumentationHelper.SetActivitySourceProperty(activity, activitySourceForLegacyActivity); - activity.Stop(); + using var activity = new Activity(operationNameForLegacyActivity); + activity.Start(); + ActivityInstrumentationHelper.SetActivitySourceProperty(activity, activitySourceForLegacyActivity); + activity.Stop(); - Assert.True(startCalled); // Processor.OnStart is called since we provided the legacy OperationName - Assert.True(endCalled); // Processor.OnEnd is not called since the ActivitySource is updated and the updated source name is added as a Source to the provider - } + Assert.True(startCalled); // Processor.OnStart is called since we provided the legacy OperationName + Assert.True(endCalled); // Processor.OnEnd is not called since the ActivitySource is updated and the updated source name is added as a Source to the provider + } - // Test to check that TracerProvider continues to process legacy activities even after a new Processor is added after the building the provider. - [Fact] - public void SdkProcessesLegacyActivityEvenAfterAddingNewProcessor() - { - using TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); + // Test to check that TracerProvider continues to process legacy activities even after a new Processor is added after the building the provider. + [Fact] + public void SdkProcessesLegacyActivityEvenAfterAddingNewProcessor() + { + using TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); - bool startCalled = false; - bool endCalled = false; + bool startCalled = false; + bool endCalled = false; - testActivityProcessor.StartAction = - (a) => - { - Assert.False(Sdk.SuppressInstrumentation); - Assert.True(a.IsAllDataRequested); // If Processor.OnStart is called, activity's IsAllDataRequested is set to true - startCalled = true; - }; + testActivityProcessor.StartAction = + (a) => + { + Assert.False(Sdk.SuppressInstrumentation); + Assert.True(a.IsAllDataRequested); // If Processor.OnStart is called, activity's IsAllDataRequested is set to true + startCalled = true; + }; - testActivityProcessor.EndAction = - (a) => - { - Assert.False(Sdk.SuppressInstrumentation); - Assert.True(a.IsAllDataRequested); // If Processor.OnEnd is called, activity's IsAllDataRequested is set to true - endCalled = true; - }; + testActivityProcessor.EndAction = + (a) => + { + Assert.False(Sdk.SuppressInstrumentation); + Assert.True(a.IsAllDataRequested); // If Processor.OnEnd is called, activity's IsAllDataRequested is set to true + endCalled = true; + }; - var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); + var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); - // AddLegacyOperationName chained to TracerProviderBuilder - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddProcessor(testActivityProcessor) - .AddLegacySource(operationNameForLegacyActivity) - .Build(); + // AddLegacyOperationName chained to TracerProviderBuilder + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddProcessor(testActivityProcessor) + .AddLegacySource(operationNameForLegacyActivity) + .Build(); - Assert.Equal(tracerProvider, testActivityProcessor.ParentProvider); + Assert.Equal(tracerProvider, testActivityProcessor.ParentProvider); - using var activity = new Activity(operationNameForLegacyActivity); - activity.Start(); - activity.Stop(); + using var activity = new Activity(operationNameForLegacyActivity); + activity.Start(); + activity.Stop(); - Assert.True(startCalled); - Assert.True(endCalled); + Assert.True(startCalled); + Assert.True(endCalled); - // As Processors can be added anytime after Provider construction, the following validates - // the following validates that updated processors are processing the legacy activities created from here on. - using var testActivityProcessorNew = new TestActivityProcessor(); + // As Processors can be added anytime after Provider construction, the following validates + // the following validates that updated processors are processing the legacy activities created from here on. + using var testActivityProcessorNew = new TestActivityProcessor(); - bool startCalledNew = false; - bool endCalledNew = false; + bool startCalledNew = false; + bool endCalledNew = false; - testActivityProcessorNew.StartAction = - (a) => - { - Assert.False(Sdk.SuppressInstrumentation); - Assert.True(a.IsAllDataRequested); // If Processor.OnStart is called, activity's IsAllDataRequested is set to true - startCalledNew = true; - }; + testActivityProcessorNew.StartAction = + (a) => + { + Assert.False(Sdk.SuppressInstrumentation); + Assert.True(a.IsAllDataRequested); // If Processor.OnStart is called, activity's IsAllDataRequested is set to true + startCalledNew = true; + }; - testActivityProcessorNew.EndAction = - (a) => - { - Assert.False(Sdk.SuppressInstrumentation); - Assert.True(a.IsAllDataRequested); // If Processor.OnEnd is called, activity's IsAllDataRequested is set to true - endCalledNew = true; - }; + testActivityProcessorNew.EndAction = + (a) => + { + Assert.False(Sdk.SuppressInstrumentation); + Assert.True(a.IsAllDataRequested); // If Processor.OnEnd is called, activity's IsAllDataRequested is set to true + endCalledNew = true; + }; - tracerProvider.AddProcessor(testActivityProcessorNew); + tracerProvider.AddProcessor(testActivityProcessorNew); - var sdkProvider = (TracerProviderSdk)tracerProvider; + var sdkProvider = (TracerProviderSdk)tracerProvider; - Assert.True(sdkProvider.Processor is CompositeProcessor); - Assert.Equal(tracerProvider, sdkProvider.Processor.ParentProvider); - Assert.Equal(tracerProvider, testActivityProcessorNew.ParentProvider); + Assert.True(sdkProvider.Processor is CompositeProcessor); + Assert.Equal(tracerProvider, sdkProvider.Processor.ParentProvider); + Assert.Equal(tracerProvider, testActivityProcessorNew.ParentProvider); - using var activityNew = new Activity(operationNameForLegacyActivity); // Create a new Activity with the same operation name - activityNew.Start(); - activityNew.Stop(); + using var activityNew = new Activity(operationNameForLegacyActivity); // Create a new Activity with the same operation name + activityNew.Start(); + activityNew.Stop(); - Assert.True(startCalledNew); - Assert.True(endCalledNew); - } + Assert.True(startCalledNew); + Assert.True(endCalledNew); + } - [Fact] - public void SdkSamplesLegacyActivityWithAlwaysOnSampler() - { - var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .SetSampler(new AlwaysOnSampler()) - .AddLegacySource(operationNameForLegacyActivity) - .Build(); + [Fact] + public void SdkSamplesLegacyActivityWithAlwaysOnSampler() + { + var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddLegacySource(operationNameForLegacyActivity) + .Build(); - using var activity = new Activity(operationNameForLegacyActivity); - activity.Start(); + using var activity = new Activity(operationNameForLegacyActivity); + activity.Start(); - Assert.True(activity.IsAllDataRequested); - Assert.True(activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); + Assert.True(activity.IsAllDataRequested); + Assert.True(activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); - // Validating ActivityTraceFlags is not enough as it does not get reflected on - // Id, If the Id is accessed before the sampler runs. - // https://github.com/open-telemetry/opentelemetry-dotnet/issues/2700 - Assert.EndsWith("-01", activity.Id); + // Validating ActivityTraceFlags is not enough as it does not get reflected on + // Id, If the Id is accessed before the sampler runs. + // https://github.com/open-telemetry/opentelemetry-dotnet/issues/2700 + Assert.EndsWith("-01", activity.Id); - activity.Stop(); - } + activity.Stop(); + } - [Fact] - public void SdkSamplesLegacyActivityWithAlwaysOffSampler() - { - var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .SetSampler(new AlwaysOffSampler()) - .AddLegacySource(operationNameForLegacyActivity) - .Build(); + [Fact] + public void SdkSamplesLegacyActivityWithAlwaysOffSampler() + { + var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOffSampler()) + .AddLegacySource(operationNameForLegacyActivity) + .Build(); - using var activity = new Activity(operationNameForLegacyActivity); - activity.Start(); + using var activity = new Activity(operationNameForLegacyActivity); + activity.Start(); - Assert.False(activity.IsAllDataRequested); - Assert.False(activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); + Assert.False(activity.IsAllDataRequested); + Assert.False(activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); - // Validating ActivityTraceFlags is not enough as it does not get reflected on - // Id, If the Id is accessed before the sampler runs. - // https://github.com/open-telemetry/opentelemetry-dotnet/issues/2700 - Assert.EndsWith("-00", activity.Id); + // Validating ActivityTraceFlags is not enough as it does not get reflected on + // Id, If the Id is accessed before the sampler runs. + // https://github.com/open-telemetry/opentelemetry-dotnet/issues/2700 + Assert.EndsWith("-00", activity.Id); - activity.Stop(); - } + activity.Stop(); + } - [Theory] - [InlineData(SamplingDecision.Drop, false, false)] - [InlineData(SamplingDecision.RecordOnly, true, false)] - [InlineData(SamplingDecision.RecordAndSample, true, true)] - public void SdkSamplesLegacyActivityWithCustomSampler(SamplingDecision samplingDecision, bool isAllDataRequested, bool hasRecordedFlag) - { - var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); - var sampler = new TestSampler() { SamplingAction = (samplingParameters) => new SamplingResult(samplingDecision) }; + [Theory] + [InlineData(SamplingDecision.Drop, false, false)] + [InlineData(SamplingDecision.RecordOnly, true, false)] + [InlineData(SamplingDecision.RecordAndSample, true, true)] + public void SdkSamplesLegacyActivityWithCustomSampler(SamplingDecision samplingDecision, bool isAllDataRequested, bool hasRecordedFlag) + { + var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); + var sampler = new TestSampler() { SamplingAction = (samplingParameters) => new SamplingResult(samplingDecision) }; - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .SetSampler(sampler) - .AddLegacySource(operationNameForLegacyActivity) - .Build(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(sampler) + .AddLegacySource(operationNameForLegacyActivity) + .Build(); - using var activity = new Activity(operationNameForLegacyActivity); - activity.Start(); + using var activity = new Activity(operationNameForLegacyActivity); + activity.Start(); - Assert.Equal(isAllDataRequested, activity.IsAllDataRequested); - Assert.Equal(hasRecordedFlag, activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); + Assert.Equal(isAllDataRequested, activity.IsAllDataRequested); + Assert.Equal(hasRecordedFlag, activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); - // Validating ActivityTraceFlags is not enough as it does not get reflected on - // Id, If the Id is accessed before the sampler runs. - // https://github.com/open-telemetry/opentelemetry-dotnet/issues/2700 - Assert.EndsWith(hasRecordedFlag ? "-01" : "-00", activity.Id); + // Validating ActivityTraceFlags is not enough as it does not get reflected on + // Id, If the Id is accessed before the sampler runs. + // https://github.com/open-telemetry/opentelemetry-dotnet/issues/2700 + Assert.EndsWith(hasRecordedFlag ? "-01" : "-00", activity.Id); - activity.Stop(); - } + activity.Stop(); + } - [Fact] - public void SdkPopulatesSamplingParamsCorrectlyForRootLegacyActivity() + [Fact] + public void SdkPopulatesSamplingParamsCorrectlyForRootLegacyActivity() + { + var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); + var sampler = new TestSampler() { - var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); - var sampler = new TestSampler() + SamplingAction = (samplingParameters) => { - SamplingAction = (samplingParameters) => - { - Assert.Equal(default, samplingParameters.ParentContext); - return new SamplingResult(SamplingDecision.RecordAndSample); - }, - }; + Assert.Equal(default, samplingParameters.ParentContext); + return new SamplingResult(SamplingDecision.RecordAndSample); + }, + }; - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .SetSampler(sampler) - .AddLegacySource(operationNameForLegacyActivity) - .Build(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(sampler) + .AddLegacySource(operationNameForLegacyActivity) + .Build(); - // Start activity without setting parent. i.e it'll have null parent - // and becomes root activity - using var activity = new Activity(operationNameForLegacyActivity); - activity.Start(); - activity.Stop(); - } + // Start activity without setting parent. i.e it'll have null parent + // and becomes root activity + using var activity = new Activity(operationNameForLegacyActivity); + activity.Start(); + activity.Stop(); + } - [Theory] - [InlineData(SamplingDecision.Drop, ActivityTraceFlags.None, false, false)] - [InlineData(SamplingDecision.Drop, ActivityTraceFlags.Recorded, false, false)] - [InlineData(SamplingDecision.RecordOnly, ActivityTraceFlags.None, true, false)] - [InlineData(SamplingDecision.RecordOnly, ActivityTraceFlags.Recorded, true, false)] - [InlineData(SamplingDecision.RecordAndSample, ActivityTraceFlags.None, true, true)] - [InlineData(SamplingDecision.RecordAndSample, ActivityTraceFlags.Recorded, true, true)] - public void SdkSamplesLegacyActivityWithRemoteParentWithCustomSampler(SamplingDecision samplingDecision, ActivityTraceFlags parentTraceFlags, bool expectedIsAllDataRequested, bool hasRecordedFlag) + [Theory] + [InlineData(SamplingDecision.Drop, ActivityTraceFlags.None, false, false)] + [InlineData(SamplingDecision.Drop, ActivityTraceFlags.Recorded, false, false)] + [InlineData(SamplingDecision.RecordOnly, ActivityTraceFlags.None, true, false)] + [InlineData(SamplingDecision.RecordOnly, ActivityTraceFlags.Recorded, true, false)] + [InlineData(SamplingDecision.RecordAndSample, ActivityTraceFlags.None, true, true)] + [InlineData(SamplingDecision.RecordAndSample, ActivityTraceFlags.Recorded, true, true)] + public void SdkSamplesLegacyActivityWithRemoteParentWithCustomSampler(SamplingDecision samplingDecision, ActivityTraceFlags parentTraceFlags, bool expectedIsAllDataRequested, bool hasRecordedFlag) + { + var parentTraceId = ActivityTraceId.CreateRandom(); + var parentSpanId = ActivitySpanId.CreateRandom(); + var parentTraceFlag = (parentTraceFlags == ActivityTraceFlags.Recorded) ? "01" : "00"; + string remoteParentId = $"00-{parentTraceId}-{parentSpanId}-{parentTraceFlag}"; + string tracestate = "a=b;c=d"; + + var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); + var sampler = new TestSampler() { - var parentTraceId = ActivityTraceId.CreateRandom(); - var parentSpanId = ActivitySpanId.CreateRandom(); - var parentTraceFlag = (parentTraceFlags == ActivityTraceFlags.Recorded) ? "01" : "00"; - string remoteParentId = $"00-{parentTraceId}-{parentSpanId}-{parentTraceFlag}"; - string tracestate = "a=b;c=d"; - - var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); - var sampler = new TestSampler() + SamplingAction = (samplingParameters) => { - SamplingAction = (samplingParameters) => - { - // Ensure that SDK populates the sampling parameters correctly - Assert.Equal(parentTraceId, samplingParameters.ParentContext.TraceId); - Assert.Equal(parentSpanId, samplingParameters.ParentContext.SpanId); - Assert.Equal(parentTraceFlags, samplingParameters.ParentContext.TraceFlags); - Assert.Equal(tracestate, samplingParameters.ParentContext.TraceState); - return new SamplingResult(samplingDecision); - }, - }; + // Ensure that SDK populates the sampling parameters correctly + Assert.Equal(parentTraceId, samplingParameters.ParentContext.TraceId); + Assert.Equal(parentSpanId, samplingParameters.ParentContext.SpanId); + Assert.Equal(parentTraceFlags, samplingParameters.ParentContext.TraceFlags); + Assert.Equal(tracestate, samplingParameters.ParentContext.TraceState); + return new SamplingResult(samplingDecision); + }, + }; + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(sampler) + .AddLegacySource(operationNameForLegacyActivity) + .Build(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .SetSampler(sampler) - .AddLegacySource(operationNameForLegacyActivity) - .Build(); + // Create an activity with remote parent id. + // The sampling parameters are expected to be that of the + // parent context i.e the remote parent. - // Create an activity with remote parent id. - // The sampling parameters are expected to be that of the - // parent context i.e the remote parent. + using var activity = new Activity(operationNameForLegacyActivity).SetParentId(remoteParentId); + activity.TraceStateString = tracestate; - using var activity = new Activity(operationNameForLegacyActivity).SetParentId(remoteParentId); - activity.TraceStateString = tracestate; + // At this point SetParentId has set the ActivityTraceFlags to that of the parent activity. The activity is now passed to the sampler. + activity.Start(); + Assert.Equal(expectedIsAllDataRequested, activity.IsAllDataRequested); + Assert.Equal(hasRecordedFlag, activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); - // At this point SetParentId has set the ActivityTraceFlags to that of the parent activity. The activity is now passed to the sampler. - activity.Start(); - Assert.Equal(expectedIsAllDataRequested, activity.IsAllDataRequested); - Assert.Equal(hasRecordedFlag, activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); + // Validating ActivityTraceFlags is not enough as it does not get reflected on + // Id, If the Id is accessed before the sampler runs. + // https://github.com/open-telemetry/opentelemetry-dotnet/issues/2700 + Assert.EndsWith(hasRecordedFlag ? "-01" : "-00", activity.Id); + activity.Stop(); + } - // Validating ActivityTraceFlags is not enough as it does not get reflected on - // Id, If the Id is accessed before the sampler runs. - // https://github.com/open-telemetry/opentelemetry-dotnet/issues/2700 - Assert.EndsWith(hasRecordedFlag ? "-01" : "-00", activity.Id); - activity.Stop(); - } + [Theory] + [InlineData(ActivityTraceFlags.None)] + [InlineData(ActivityTraceFlags.Recorded)] + public void SdkSamplesLegacyActivityWithRemoteParentWithAlwaysOnSampler(ActivityTraceFlags parentTraceFlags) + { + var parentTraceId = ActivityTraceId.CreateRandom(); + var parentSpanId = ActivitySpanId.CreateRandom(); + var parentTraceFlag = (parentTraceFlags == ActivityTraceFlags.Recorded) ? "01" : "00"; + string remoteParentId = $"00-{parentTraceId}-{parentSpanId}-{parentTraceFlag}"; - [Theory] - [InlineData(ActivityTraceFlags.None)] - [InlineData(ActivityTraceFlags.Recorded)] - public void SdkSamplesLegacyActivityWithRemoteParentWithAlwaysOnSampler(ActivityTraceFlags parentTraceFlags) - { - var parentTraceId = ActivityTraceId.CreateRandom(); - var parentSpanId = ActivitySpanId.CreateRandom(); - var parentTraceFlag = (parentTraceFlags == ActivityTraceFlags.Recorded) ? "01" : "00"; - string remoteParentId = $"00-{parentTraceId}-{parentSpanId}-{parentTraceFlag}"; + var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); - var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddLegacySource(operationNameForLegacyActivity) + .Build(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .SetSampler(new AlwaysOnSampler()) - .AddLegacySource(operationNameForLegacyActivity) - .Build(); + // Create an activity with remote parent id. + // The sampling parameters are expected to be that of the + // parent context i.e the remote parent. - // Create an activity with remote parent id. - // The sampling parameters are expected to be that of the - // parent context i.e the remote parent. + using var activity = new Activity(operationNameForLegacyActivity).SetParentId(remoteParentId); - using var activity = new Activity(operationNameForLegacyActivity).SetParentId(remoteParentId); + // At this point SetParentId has set the ActivityTraceFlags to that of the parent activity. The activity is now passed to the sampler. + activity.Start(); + Assert.True(activity.IsAllDataRequested); + Assert.True(activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); - // At this point SetParentId has set the ActivityTraceFlags to that of the parent activity. The activity is now passed to the sampler. - activity.Start(); - Assert.True(activity.IsAllDataRequested); - Assert.True(activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); + // Validating ActivityTraceFlags is not enough as it does not get reflected on + // Id, If the Id is accessed before the sampler runs. + // https://github.com/open-telemetry/opentelemetry-dotnet/issues/2700 + Assert.EndsWith("-01", activity.Id); + activity.Stop(); + } - // Validating ActivityTraceFlags is not enough as it does not get reflected on - // Id, If the Id is accessed before the sampler runs. - // https://github.com/open-telemetry/opentelemetry-dotnet/issues/2700 - Assert.EndsWith("-01", activity.Id); - activity.Stop(); - } + [Theory] + [InlineData(ActivityTraceFlags.None)] + [InlineData(ActivityTraceFlags.Recorded)] + public void SdkSamplesLegacyActivityWithRemoteParentWithAlwaysOffSampler(ActivityTraceFlags parentTraceFlags) + { + var parentTraceId = ActivityTraceId.CreateRandom(); + var parentSpanId = ActivitySpanId.CreateRandom(); + var parentTraceFlag = (parentTraceFlags == ActivityTraceFlags.Recorded) ? "01" : "00"; + string remoteParentId = $"00-{parentTraceId}-{parentSpanId}-{parentTraceFlag}"; - [Theory] - [InlineData(ActivityTraceFlags.None)] - [InlineData(ActivityTraceFlags.Recorded)] - public void SdkSamplesLegacyActivityWithRemoteParentWithAlwaysOffSampler(ActivityTraceFlags parentTraceFlags) - { - var parentTraceId = ActivityTraceId.CreateRandom(); - var parentSpanId = ActivitySpanId.CreateRandom(); - var parentTraceFlag = (parentTraceFlags == ActivityTraceFlags.Recorded) ? "01" : "00"; - string remoteParentId = $"00-{parentTraceId}-{parentSpanId}-{parentTraceFlag}"; + var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); - var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOffSampler()) + .AddLegacySource(operationNameForLegacyActivity) + .Build(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .SetSampler(new AlwaysOffSampler()) - .AddLegacySource(operationNameForLegacyActivity) - .Build(); + // Create an activity with remote parent id. + // The sampling parameters are expected to be that of the + // parent context i.e the remote parent. - // Create an activity with remote parent id. - // The sampling parameters are expected to be that of the - // parent context i.e the remote parent. + using var activity = new Activity(operationNameForLegacyActivity).SetParentId(remoteParentId); - using var activity = new Activity(operationNameForLegacyActivity).SetParentId(remoteParentId); + // At this point SetParentId has set the ActivityTraceFlags to that of the parent activity. The activity is now passed to the sampler. + activity.Start(); + Assert.False(activity.IsAllDataRequested); + Assert.False(activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); - // At this point SetParentId has set the ActivityTraceFlags to that of the parent activity. The activity is now passed to the sampler. - activity.Start(); - Assert.False(activity.IsAllDataRequested); - Assert.False(activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); + // Validating ActivityTraceFlags is not enough as it does not get reflected on + // Id, If the Id is accessed before the sampler runs. + // https://github.com/open-telemetry/opentelemetry-dotnet/issues/2700 + Assert.EndsWith("-00", activity.Id); + activity.Stop(); + } - // Validating ActivityTraceFlags is not enough as it does not get reflected on - // Id, If the Id is accessed before the sampler runs. - // https://github.com/open-telemetry/opentelemetry-dotnet/issues/2700 - Assert.EndsWith("-00", activity.Id); - activity.Stop(); - } + [Theory] + [InlineData(ActivityTraceFlags.None)] + [InlineData(ActivityTraceFlags.Recorded)] + public void SdkPopulatesSamplingParamsCorrectlyForLegacyActivityWithInProcParent(ActivityTraceFlags traceFlags) + { + // Create some parent activity. + string tracestate = "a=b;c=d"; + using var activityLocalParent = new Activity("TestParent") + { + ActivityTraceFlags = traceFlags, + TraceStateString = tracestate, + }; + activityLocalParent.Start(); - [Theory] - [InlineData(ActivityTraceFlags.None)] - [InlineData(ActivityTraceFlags.Recorded)] - public void SdkPopulatesSamplingParamsCorrectlyForLegacyActivityWithInProcParent(ActivityTraceFlags traceFlags) + var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); + var sampler = new TestSampler() { - // Create some parent activity. - string tracestate = "a=b;c=d"; - using var activityLocalParent = new Activity("TestParent") + SamplingAction = (samplingParameters) => { - ActivityTraceFlags = traceFlags, - TraceStateString = tracestate, - }; - activityLocalParent.Start(); + Assert.Equal(activityLocalParent.TraceId, samplingParameters.ParentContext.TraceId); + Assert.Equal(activityLocalParent.SpanId, samplingParameters.ParentContext.SpanId); + Assert.Equal(activityLocalParent.ActivityTraceFlags, samplingParameters.ParentContext.TraceFlags); + Assert.Equal(tracestate, samplingParameters.ParentContext.TraceState); + return new SamplingResult(SamplingDecision.RecordAndSample); + }, + }; - var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); - var sampler = new TestSampler() - { - SamplingAction = (samplingParameters) => - { - Assert.Equal(activityLocalParent.TraceId, samplingParameters.ParentContext.TraceId); - Assert.Equal(activityLocalParent.SpanId, samplingParameters.ParentContext.SpanId); - Assert.Equal(activityLocalParent.ActivityTraceFlags, samplingParameters.ParentContext.TraceFlags); - Assert.Equal(tracestate, samplingParameters.ParentContext.TraceState); - return new SamplingResult(SamplingDecision.RecordAndSample); - }, - }; + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(sampler) + .AddLegacySource(operationNameForLegacyActivity) + .Build(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .SetSampler(sampler) - .AddLegacySource(operationNameForLegacyActivity) - .Build(); - - // This activity will have a inproc parent. - // activity.Parent will be equal to the activity created at the beginning of this test. - // Sampling parameters are expected to be that of the parentContext. - // i.e of the parent Activity - using var activity = new Activity(operationNameForLegacyActivity); - activity.Start(); - activity.Stop(); - } + // This activity will have a inproc parent. + // activity.Parent will be equal to the activity created at the beginning of this test. + // Sampling parameters are expected to be that of the parentContext. + // i.e of the parent Activity + using var activity = new Activity(operationNameForLegacyActivity); + activity.Start(); + activity.Stop(); + } - [Fact] - public void TracerProvideSdkCreatesAndDiposesInstrumentation() - { - TestInstrumentation testInstrumentation = null; - var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddInstrumentation(() => - { - testInstrumentation = new TestInstrumentation(); - return testInstrumentation; - }) - .Build(); - - Assert.NotNull(testInstrumentation); - Assert.False(testInstrumentation.IsDisposed); - tracerProvider.Dispose(); - Assert.True(testInstrumentation.IsDisposed); - } + [Fact] + public void TracerProvideSdkCreatesAndDiposesInstrumentation() + { + TestInstrumentation testInstrumentation = null; + var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddInstrumentation(() => + { + testInstrumentation = new TestInstrumentation(); + return testInstrumentation; + }) + .Build(); - [Fact] - public void TracerProviderSdkBuildsWithDefaultResource() - { - var tracerProvider = Sdk.CreateTracerProviderBuilder().Build(); - var resource = tracerProvider.GetResource(); + Assert.NotNull(testInstrumentation); + Assert.False(testInstrumentation.IsDisposed); + tracerProvider.Dispose(); + Assert.True(testInstrumentation.IsDisposed); + } - Assert.NotNull(resource); - Assert.NotEqual(Resource.Empty, resource); + [Fact] + public void TracerProviderSdkBuildsWithDefaultResource() + { + var tracerProvider = Sdk.CreateTracerProviderBuilder().Build(); + var resource = tracerProvider.GetResource(); - var attributes = resource.Attributes; - Assert.Equal(4, attributes.Count()); - ResourceTest.ValidateDefaultAttributes(attributes); - ResourceTest.ValidateTelemetrySdkAttributes(attributes); - } + Assert.NotNull(resource); + Assert.NotEqual(Resource.Empty, resource); - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public void AddLegacyOperationName_BadArgs(string operationName) - { - var builder = Sdk.CreateTracerProviderBuilder(); - Assert.Throws(() => builder.AddLegacySource(operationName)); - } + var attributes = resource.Attributes; + Assert.Equal(4, attributes.Count()); + ResourceTest.ValidateDefaultAttributes(attributes); + ResourceTest.ValidateTelemetrySdkAttributes(attributes); + } - [Fact] - public void AddLegacyOperationNameAddsActivityListenerForEmptyActivitySource() - { - using var emptyActivitySource = new ActivitySource(string.Empty); - var builder = Sdk.CreateTracerProviderBuilder(); - builder.AddLegacySource("TestOperationName"); + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void AddLegacyOperationName_BadArgs(string operationName) + { + var builder = Sdk.CreateTracerProviderBuilder(); + Assert.Throws(() => builder.AddLegacySource(operationName)); + } - Assert.False(emptyActivitySource.HasListeners()); - using var provider = builder.Build(); - Assert.True(emptyActivitySource.HasListeners()); - } + [Fact] + public void AddLegacyOperationNameAddsActivityListenerForEmptyActivitySource() + { + using var emptyActivitySource = new ActivitySource(string.Empty); + var builder = Sdk.CreateTracerProviderBuilder(); + builder.AddLegacySource("TestOperationName"); - [Theory] - [InlineData(true)] - [InlineData(false)] - public void TracerProviderSdkBuildsWithSDKResource(bool useConfigure) - { - using var tracerProvider = useConfigure ? - Sdk.CreateTracerProviderBuilder().SetResourceBuilder( - ResourceBuilder.CreateDefault().AddTelemetrySdk()).Build() : - Sdk.CreateTracerProviderBuilder().ConfigureResource(r => r.AddTelemetrySdk()).Build(); - var resource = tracerProvider.GetResource(); - var attributes = resource.Attributes; - - Assert.NotNull(resource); - Assert.NotEqual(Resource.Empty, resource); - Assert.Contains(new KeyValuePair("telemetry.sdk.name", "opentelemetry"), attributes); - Assert.Contains(new KeyValuePair("telemetry.sdk.language", "dotnet"), attributes); - var versionAttribute = attributes.Where(pair => pair.Key.Equals("telemetry.sdk.version")); - Assert.Single(versionAttribute); - } + Assert.False(emptyActivitySource.HasListeners()); + using var provider = builder.Build(); + Assert.True(emptyActivitySource.HasListeners()); + } - [Fact] - public void TracerProviderSdkFlushesProcessorForcibly() - { - using TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TracerProviderSdkBuildsWithSDKResource(bool useConfigure) + { + using var tracerProvider = useConfigure ? + Sdk.CreateTracerProviderBuilder().SetResourceBuilder( + ResourceBuilder.CreateDefault().AddTelemetrySdk()).Build() : + Sdk.CreateTracerProviderBuilder().ConfigureResource(r => r.AddTelemetrySdk()).Build(); + var resource = tracerProvider.GetResource(); + var attributes = resource.Attributes; + + Assert.NotNull(resource); + Assert.NotEqual(Resource.Empty, resource); + Assert.Contains(new KeyValuePair("telemetry.sdk.name", "opentelemetry"), attributes); + Assert.Contains(new KeyValuePair("telemetry.sdk.language", "dotnet"), attributes); + var versionAttribute = attributes.Where(pair => pair.Key.Equals("telemetry.sdk.version")); + Assert.Single(versionAttribute); + } - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddProcessor(testActivityProcessor) - .Build(); + [Fact] + public void TracerProviderSdkFlushesProcessorForcibly() + { + using TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); - var isFlushed = tracerProvider.ForceFlush(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddProcessor(testActivityProcessor) + .Build(); - Assert.True(isFlushed); - Assert.True(testActivityProcessor.ForceFlushCalled); - } + var isFlushed = tracerProvider.ForceFlush(); - [Fact] - public void SdkSamplesAndProcessesLegacySourceWhenAddLegacySourceIsCalledWithWildcardValue() + Assert.True(isFlushed); + Assert.True(testActivityProcessor.ForceFlushCalled); + } + + [Fact] + public void SdkSamplesAndProcessesLegacySourceWhenAddLegacySourceIsCalledWithWildcardValue() + { + var sampledActivities = new List(); + var sampler = new TestSampler { - var sampledActivities = new List(); - var sampler = new TestSampler + SamplingAction = + (samplingParameters) => { - SamplingAction = - (samplingParameters) => - { - sampledActivities.Add(samplingParameters.Name); - return new SamplingResult(SamplingDecision.RecordAndSample); - }, - }; + sampledActivities.Add(samplingParameters.Name); + return new SamplingResult(SamplingDecision.RecordAndSample); + }, + }; - using TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); + using TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); - var onStartProcessedActivities = new List(); - var onStopProcessedActivities = new List(); - testActivityProcessor.StartAction = - (a) => - { - Assert.Contains(a.OperationName, sampledActivities); - Assert.False(Sdk.SuppressInstrumentation); - Assert.True(a.IsAllDataRequested); // If Processor.OnStart is called, activity's IsAllDataRequested is set to true - onStartProcessedActivities.Add(a.OperationName); - }; + var onStartProcessedActivities = new List(); + var onStopProcessedActivities = new List(); + testActivityProcessor.StartAction = + (a) => + { + Assert.Contains(a.OperationName, sampledActivities); + Assert.False(Sdk.SuppressInstrumentation); + Assert.True(a.IsAllDataRequested); // If Processor.OnStart is called, activity's IsAllDataRequested is set to true + onStartProcessedActivities.Add(a.OperationName); + }; - testActivityProcessor.EndAction = - (a) => - { - Assert.False(Sdk.SuppressInstrumentation); - Assert.True(a.IsAllDataRequested); // If Processor.OnEnd is called, activity's IsAllDataRequested is set to true - onStopProcessedActivities.Add(a.OperationName); - }; + testActivityProcessor.EndAction = + (a) => + { + Assert.False(Sdk.SuppressInstrumentation); + Assert.True(a.IsAllDataRequested); // If Processor.OnEnd is called, activity's IsAllDataRequested is set to true + onStopProcessedActivities.Add(a.OperationName); + }; - var legacySourceNamespaces = new[] { "LegacyNamespace.*", "Namespace.*.Operation" }; - var activitySourceName = Utils.GetCurrentMethodName(); - using var activitySource = new ActivitySource(activitySourceName); + var legacySourceNamespaces = new[] { "LegacyNamespace.*", "Namespace.*.Operation" }; + var activitySourceName = Utils.GetCurrentMethodName(); + using var activitySource = new ActivitySource(activitySourceName); - // AddLegacyOperationName chained to TracerProviderBuilder - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .SetSampler(sampler) - .AddProcessor(testActivityProcessor) - .AddLegacySource(legacySourceNamespaces[0]) - .AddLegacySource(legacySourceNamespaces[1]) - .AddSource(activitySourceName) - .Build(); + // AddLegacyOperationName chained to TracerProviderBuilder + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(sampler) + .AddProcessor(testActivityProcessor) + .AddLegacySource(legacySourceNamespaces[0]) + .AddLegacySource(legacySourceNamespaces[1]) + .AddSource(activitySourceName) + .Build(); - foreach (var ns in legacySourceNamespaces) - { - var startOpName = ns.Replace("*", "Start"); - using var startOperation = new Activity(startOpName); - startOperation.Start(); - startOperation.Stop(); + foreach (var ns in legacySourceNamespaces) + { + var startOpName = ns.Replace("*", "Start"); + using var startOperation = new Activity(startOpName); + startOperation.Start(); + startOperation.Stop(); - Assert.Contains(startOpName, onStartProcessedActivities); // Processor.OnStart is called since we added a legacy OperationName - Assert.Contains(startOpName, onStopProcessedActivities); // Processor.OnEnd is called since we added a legacy OperationName + Assert.Contains(startOpName, onStartProcessedActivities); // Processor.OnStart is called since we added a legacy OperationName + Assert.Contains(startOpName, onStopProcessedActivities); // Processor.OnEnd is called since we added a legacy OperationName - var stopOpName = ns.Replace("*", "Stop"); - using var stopOperation = new Activity(stopOpName); - stopOperation.Start(); - stopOperation.Stop(); + var stopOpName = ns.Replace("*", "Stop"); + using var stopOperation = new Activity(stopOpName); + stopOperation.Start(); + stopOperation.Stop(); - Assert.Contains(stopOpName, onStartProcessedActivities); // Processor.OnStart is called since we added a legacy OperationName - Assert.Contains(stopOpName, onStopProcessedActivities); // Processor.OnEnd is called since we added a legacy OperationName - } + Assert.Contains(stopOpName, onStartProcessedActivities); // Processor.OnStart is called since we added a legacy OperationName + Assert.Contains(stopOpName, onStopProcessedActivities); // Processor.OnEnd is called since we added a legacy OperationName + } - // Ensure we can still process "normal" activities when in legacy wildcard mode. - using var nonLegacyActivity = activitySource.StartActivity("TestActivity"); - nonLegacyActivity.Start(); - nonLegacyActivity.Stop(); + // Ensure we can still process "normal" activities when in legacy wildcard mode. + using var nonLegacyActivity = activitySource.StartActivity("TestActivity"); + nonLegacyActivity.Start(); + nonLegacyActivity.Stop(); - Assert.Contains(nonLegacyActivity.OperationName, onStartProcessedActivities); // Processor.OnStart is called since we added a legacy OperationName - Assert.Contains(nonLegacyActivity.OperationName, onStopProcessedActivities); // Processor.OnEnd is called since we added a legacy OperationName - } + Assert.Contains(nonLegacyActivity.OperationName, onStartProcessedActivities); // Processor.OnStart is called since we added a legacy OperationName + Assert.Contains(nonLegacyActivity.OperationName, onStopProcessedActivities); // Processor.OnEnd is called since we added a legacy OperationName + } - [Fact] - public void BuilderTypeDoesNotChangeTest() - { - var originalBuilder = new TestTracerProviderBuilder(); + [Fact] + public void BuilderTypeDoesNotChangeTest() + { + var originalBuilder = new TestTracerProviderBuilder(); - // Tests the protected version of AddInstrumentation on TracerProviderBuilderBase - var currentBuilder = originalBuilder.AddInstrumentation(); - Assert.True(ReferenceEquals(originalBuilder, currentBuilder)); + // Tests the protected version of AddInstrumentation on TracerProviderBuilderBase + var currentBuilder = originalBuilder.AddInstrumentation(); + Assert.True(ReferenceEquals(originalBuilder, currentBuilder)); - var deferredBuilder = currentBuilder as IDeferredTracerProviderBuilder; - Assert.NotNull(deferredBuilder); + var deferredBuilder = currentBuilder as IDeferredTracerProviderBuilder; + Assert.NotNull(deferredBuilder); - currentBuilder = deferredBuilder.Configure((sp, innerBuilder) => { }); - Assert.True(ReferenceEquals(originalBuilder, currentBuilder)); + currentBuilder = deferredBuilder.Configure((sp, innerBuilder) => { }); + Assert.True(ReferenceEquals(originalBuilder, currentBuilder)); - currentBuilder = currentBuilder.ConfigureServices(s => { }); - Assert.True(ReferenceEquals(originalBuilder, currentBuilder)); + currentBuilder = currentBuilder.ConfigureServices(s => { }); + Assert.True(ReferenceEquals(originalBuilder, currentBuilder)); - currentBuilder = currentBuilder.AddInstrumentation(() => new object()); - Assert.True(ReferenceEquals(originalBuilder, currentBuilder)); + currentBuilder = currentBuilder.AddInstrumentation(() => new object()); + Assert.True(ReferenceEquals(originalBuilder, currentBuilder)); - currentBuilder = currentBuilder.AddSource("MySource"); - Assert.True(ReferenceEquals(originalBuilder, currentBuilder)); + currentBuilder = currentBuilder.AddSource("MySource"); + Assert.True(ReferenceEquals(originalBuilder, currentBuilder)); - currentBuilder = currentBuilder.AddLegacySource("MyLegacySource"); - Assert.True(ReferenceEquals(originalBuilder, currentBuilder)); + currentBuilder = currentBuilder.AddLegacySource("MyLegacySource"); + Assert.True(ReferenceEquals(originalBuilder, currentBuilder)); - using var provider = currentBuilder.Build(); + using var provider = currentBuilder.Build(); - Assert.NotNull(provider); - } + Assert.NotNull(provider); + } - public void Dispose() + public void Dispose() + { + GC.SuppressFinalize(this); + } + + private sealed class TestTracerProviderBuilder : TracerProviderBuilderBase + { + public TracerProviderBuilder AddInstrumentation() { - GC.SuppressFinalize(this); + return this.AddInstrumentation("SomeInstrumentation", "1.0.0", () => new object()); } + } + + private class TestInstrumentation : IDisposable + { + public bool IsDisposed; - private sealed class TestTracerProviderBuilder : TracerProviderBuilderBase + public TestInstrumentation() { - public TracerProviderBuilder AddInstrumentation() - { - return this.AddInstrumentation("SomeInstrumentation", "1.0.0", () => new object()); - } + this.IsDisposed = false; } - private class TestInstrumentation : IDisposable + public void Dispose() { - public bool IsDisposed; - - public TestInstrumentation() - { - this.IsDisposed = false; - } - - public void Dispose() - { - this.IsDisposed = true; - } + this.IsDisposed = true; } } } diff --git a/test/TestApp.AspNetCore/ActivityMiddleware.cs b/test/TestApp.AspNetCore/ActivityMiddleware.cs index 9d7204686fe..591bc388f34 100644 --- a/test/TestApp.AspNetCore/ActivityMiddleware.cs +++ b/test/TestApp.AspNetCore/ActivityMiddleware.cs @@ -1,58 +1,44 @@ -// // 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. -// - -namespace TestApp.AspNetCore +// SPDX-License-Identifier: Apache-2.0 + +namespace TestApp.AspNetCore; + +public class ActivityMiddleware { - public class ActivityMiddleware + private readonly ActivityMiddlewareImpl impl; + private readonly RequestDelegate next; + + public ActivityMiddleware(RequestDelegate next, ActivityMiddlewareImpl impl) { - private readonly ActivityMiddlewareImpl impl; - private readonly RequestDelegate next; + this.next = next; + this.impl = impl; + } - public ActivityMiddleware(RequestDelegate next, ActivityMiddlewareImpl impl) + public async Task InvokeAsync(HttpContext context) + { + if (this.impl != null) { - this.next = next; - this.impl = impl; + this.impl.PreProcess(context); } - public async Task InvokeAsync(HttpContext context) - { - if (this.impl != null) - { - this.impl.PreProcess(context); - } + await this.next(context); - await this.next(context).ConfigureAwait(false); + if (this.impl != null) + { + this.impl.PostProcess(context); + } + } - if (this.impl != null) - { - this.impl.PostProcess(context); - } + public class ActivityMiddlewareImpl + { + public virtual void PreProcess(HttpContext context) + { + // Do nothing } - public class ActivityMiddlewareImpl + public virtual void PostProcess(HttpContext context) { - public virtual void PreProcess(HttpContext context) - { - // Do nothing - } - - public virtual void PostProcess(HttpContext context) - { - // Do nothing - } + // Do nothing } } } diff --git a/test/TestApp.AspNetCore/CallbackMiddleware.cs b/test/TestApp.AspNetCore/CallbackMiddleware.cs index 13a0763f1fc..fee8569d5d0 100644 --- a/test/TestApp.AspNetCore/CallbackMiddleware.cs +++ b/test/TestApp.AspNetCore/CallbackMiddleware.cs @@ -1,46 +1,32 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 -namespace TestApp.AspNetCore +namespace TestApp.AspNetCore; + +public class CallbackMiddleware { - public class CallbackMiddleware - { - private readonly CallbackMiddlewareImpl impl; - private readonly RequestDelegate next; + private readonly CallbackMiddlewareImpl impl; + private readonly RequestDelegate next; - public CallbackMiddleware(RequestDelegate next, CallbackMiddlewareImpl impl) - { - this.next = next; - this.impl = impl; - } + public CallbackMiddleware(RequestDelegate next, CallbackMiddlewareImpl impl) + { + this.next = next; + this.impl = impl; + } - public async Task InvokeAsync(HttpContext context) + public async Task InvokeAsync(HttpContext context) + { + if (this.impl == null || await this.impl.ProcessAsync(context)) { - if (this.impl == null || await this.impl.ProcessAsync(context).ConfigureAwait(false)) - { - await this.next(context).ConfigureAwait(false); - } + await this.next(context); } + } - public class CallbackMiddlewareImpl + public class CallbackMiddlewareImpl + { + public virtual async Task ProcessAsync(HttpContext context) { - public virtual async Task ProcessAsync(HttpContext context) - { - return await Task.FromResult(true).ConfigureAwait(false); - } + return await Task.FromResult(true); } } } diff --git a/test/TestApp.AspNetCore/Controllers/ChildActivityController.cs b/test/TestApp.AspNetCore/Controllers/ChildActivityController.cs index e7c8c1a1959..b55927000f3 100644 --- a/test/TestApp.AspNetCore/Controllers/ChildActivityController.cs +++ b/test/TestApp.AspNetCore/Controllers/ChildActivityController.cs @@ -1,51 +1,47 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc; using OpenTelemetry; -namespace TestApp.AspNetCore.Controllers +namespace TestApp.AspNetCore.Controllers; + +public class ChildActivityController : Controller { - public class ChildActivityController : Controller + [HttpGet] + [Route("api/GetChildActivityTraceContext")] + public Dictionary GetChildActivityTraceContext() { - [HttpGet] - [Route("api/GetChildActivityTraceContext")] - public Dictionary GetChildActivityTraceContext() + var result = new Dictionary(); + var activity = new Activity("ActivityInsideHttpRequest"); + activity.Start(); + result["TraceId"] = activity.Context.TraceId.ToString(); + result["ParentSpanId"] = activity.ParentSpanId.ToString(); + if (activity.Context.TraceState != null) { - var result = new Dictionary(); - var activity = new Activity("ActivityInsideHttpRequest"); - activity.Start(); - result["TraceId"] = activity.Context.TraceId.ToString(); - result["ParentSpanId"] = activity.ParentSpanId.ToString(); - if (activity.Context.TraceState != null) - { - result["TraceState"] = activity.Context.TraceState; - } - - activity.Stop(); - return result; + result["TraceState"] = activity.Context.TraceState; } - [HttpGet] - [Route("api/GetChildActivityBaggageContext")] - public IReadOnlyDictionary GetChildActivityBaggageContext() - { - var result = Baggage.Current.GetBaggage(); - return result; - } + activity.Stop(); + return result; + } + + [HttpGet] + [Route("api/GetChildActivityBaggageContext")] + public IReadOnlyDictionary GetChildActivityBaggageContext() + { + var result = Baggage.Current.GetBaggage(); + return result; + } + + [HttpGet] + [Route("api/GetActivityEquality")] + public bool GetActivityEquality() + { + var activity = this.HttpContext.Features.Get()?.Activity; + var equal = Activity.Current == activity; + return equal; } } diff --git a/test/TestApp.AspNetCore/Controllers/ErrorController.cs b/test/TestApp.AspNetCore/Controllers/ErrorController.cs index 174888fec05..24c904cfe9f 100644 --- a/test/TestApp.AspNetCore/Controllers/ErrorController.cs +++ b/test/TestApp.AspNetCore/Controllers/ErrorController.cs @@ -1,30 +1,17 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + using Microsoft.AspNetCore.Mvc; -namespace TestApp.AspNetCore.Controllers +namespace TestApp.AspNetCore.Controllers; + +[Route("api/[controller]")] +public class ErrorController : Controller { - [Route("api/[controller]")] - public class ErrorController : Controller + // GET api/error + [HttpGet] + public string Get() { - // GET api/error - [HttpGet] - public string Get() - { - throw new Exception("something's wrong!"); - } + throw new Exception("something's wrong!"); } } diff --git a/test/TestApp.AspNetCore/Controllers/ValuesController.cs b/test/TestApp.AspNetCore/Controllers/ValuesController.cs index 12154514eab..27a9ab0d2d3 100644 --- a/test/TestApp.AspNetCore/Controllers/ValuesController.cs +++ b/test/TestApp.AspNetCore/Controllers/ValuesController.cs @@ -1,55 +1,42 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 + using Microsoft.AspNetCore.Mvc; -namespace TestApp.AspNetCore.Controllers +namespace TestApp.AspNetCore.Controllers; + +[Route("api/[controller]")] +public class ValuesController : Controller { - [Route("api/[controller]")] - public class ValuesController : Controller + // GET api/values + [HttpGet] + public IEnumerable Get() { - // GET api/values - [HttpGet] - public IEnumerable Get() - { - return new string[] { "value1", "value2" }; - } + return new string[] { "value1", "value2" }; + } - // GET api/values/5 - [HttpGet("{id}")] - public string Get(int id) - { - return "value"; - } + // GET api/values/5 + [HttpGet("{id}")] + public string Get(int id) + { + return "value"; + } - // POST api/values - [HttpPost] - public void Post([FromBody] string value) - { - } + // POST api/values + [HttpPost] + public void Post([FromBody] string value) + { + } - // PUT api/values/5 - [HttpPut("{id}")] - public void Put(int id, [FromBody] string value) - { - } + // PUT api/values/5 + [HttpPut("{id}")] + public void Put(int id, [FromBody] string value) + { + } - // DELETE api/values/5 - [HttpDelete("{id}")] - public void Delete(int id) - { - } + // DELETE api/values/5 + [HttpDelete("{id}")] + public void Delete(int id) + { } } diff --git a/test/TestApp.AspNetCore/Filters/ExceptionFilter1.cs b/test/TestApp.AspNetCore/Filters/ExceptionFilter1.cs index 74a597a06ad..1f058860693 100644 --- a/test/TestApp.AspNetCore/Filters/ExceptionFilter1.cs +++ b/test/TestApp.AspNetCore/Filters/ExceptionFilter1.cs @@ -1,28 +1,14 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.AspNetCore.Mvc.Filters; -namespace TestApp.AspNetCore.Filters +namespace TestApp.AspNetCore.Filters; + +public class ExceptionFilter1 : IExceptionFilter { - public class ExceptionFilter1 : IExceptionFilter + public void OnException(ExceptionContext context) { - public void OnException(ExceptionContext context) - { - // test the behaviour when an application has two ExceptionFilters defined - } + // test the behaviour when an application has two ExceptionFilters defined } } diff --git a/test/TestApp.AspNetCore/Filters/ExceptionFilter2.cs b/test/TestApp.AspNetCore/Filters/ExceptionFilter2.cs index 96934bcb551..fc81905c650 100644 --- a/test/TestApp.AspNetCore/Filters/ExceptionFilter2.cs +++ b/test/TestApp.AspNetCore/Filters/ExceptionFilter2.cs @@ -1,28 +1,14 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using Microsoft.AspNetCore.Mvc.Filters; -namespace TestApp.AspNetCore.Filters +namespace TestApp.AspNetCore.Filters; + +public class ExceptionFilter2 : IExceptionFilter { - public class ExceptionFilter2 : IExceptionFilter + public void OnException(ExceptionContext context) { - public void OnException(ExceptionContext context) - { - // test the behaviour when an application has two ExceptionFilters defined - } + // test the behaviour when an application has two ExceptionFilters defined } } diff --git a/test/TestApp.AspNetCore/Program.cs b/test/TestApp.AspNetCore/Program.cs index b68fcf238fa..06071eab4bd 100644 --- a/test/TestApp.AspNetCore/Program.cs +++ b/test/TestApp.AspNetCore/Program.cs @@ -1,18 +1,5 @@ -// // 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. -// +// SPDX-License-Identifier: Apache-2.0 using TestApp.AspNetCore; @@ -60,6 +47,8 @@ public static void Main(string[] args) app.UseMiddleware(); + app.AddTestMiddleware(); + app.Run(); } } diff --git a/test/TestApp.AspNetCore/TestApp.AspNetCore.csproj b/test/TestApp.AspNetCore/TestApp.AspNetCore.csproj index 241bacbbd47..93a3a0a972d 100644 --- a/test/TestApp.AspNetCore/TestApp.AspNetCore.csproj +++ b/test/TestApp.AspNetCore/TestApp.AspNetCore.csproj @@ -1,18 +1,19 @@ - net7.0;net6.0 + $(TargetFrameworksForAspNetCoreTests) - - - - + + + + + diff --git a/test/TestApp.AspNetCore/TestMiddleware.cs b/test/TestApp.AspNetCore/TestMiddleware.cs new file mode 100644 index 00000000000..39acf58db3d --- /dev/null +++ b/test/TestApp.AspNetCore/TestMiddleware.cs @@ -0,0 +1,24 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace TestApp.AspNetCore; + +public static class TestMiddleware +{ + private static readonly AsyncLocal?> Current = new(); + + public static IApplicationBuilder AddTestMiddleware(this IApplicationBuilder builder) + { + if (Current.Value is { } configure) + { + configure(builder); + } + + return builder; + } + + public static void Create(Action action) + { + Current.Value = action; + } +}