diff --git a/.clomonitor.yml b/.clomonitor.yml new file mode 100644 index 00000000000..4bc35d87f7d --- /dev/null +++ b/.clomonitor.yml @@ -0,0 +1,4 @@ +# see https://github.com/cncf/clomonitor/blob/main/docs/checks.md#exemptions +exemptions: + - check: artifacthub_badge + reason: "Artifact Hub doesn't support .NET packages" diff --git a/.editorconfig b/.editorconfig index 5bc89604c76..e3f693c1d13 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,6 +10,9 @@ indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true +[*.sh] +end_of_line = lf + [*.{cs,cshtml,htm,html,md,py,sln,xml}] indent_size = 4 @@ -41,7 +44,7 @@ csharp_indent_labels = flush_left # Modifier preferences 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 +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion # this. preferences dotnet_style_qualification_for_field = true:suggestion @@ -53,8 +56,8 @@ dotnet_style_qualification_for_event = true:suggestion csharp_style_var_for_built_in_types = true:silent csharp_style_var_when_type_is_apparent = true:silent csharp_style_var_elsewhere = true:silent -dotnet_style_predefined_type_for_locals_parameters_members = true:silent -dotnet_style_predefined_type_for_member_access = true:silent +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion # name all constant fields using PascalCase dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion @@ -75,6 +78,7 @@ dotnet_style_readonly_field = true:suggestion csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion dotnet_style_prefer_simplified_interpolation = true:suggestion dotnet_style_object_initializer = true:suggestion +csharp_style_prefer_primary_constructors = false:none # Expression-level preferences dotnet_style_object_initializer = true:suggestion @@ -82,21 +86,23 @@ dotnet_style_collection_initializer = true:suggestion dotnet_style_explicit_tuple_names = true:suggestion dotnet_style_coalesce_expression = true:suggestion dotnet_style_null_propagation = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion dotnet_style_prefer_inferred_tuple_names = true:suggestion dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion -dotnet_style_prefer_auto_properties = true:silent -dotnet_style_prefer_conditional_expression_over_assignment = true:silent -dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion csharp_prefer_simple_default_expression = true:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:none # Expression-bodied members -csharp_style_expression_bodied_methods = false:silent -csharp_style_expression_bodied_constructors = false:silent -csharp_style_expression_bodied_operators = false:silent -csharp_style_expression_bodied_properties = true:silent -csharp_style_expression_bodied_indexers = true:silent -csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_methods = true:suggestion +dotnet_diagnostic.IDE0022.severity = suggestion # dotnet format doesn't respect the suggestion in the line above +csharp_style_expression_bodied_constructors = false:warning +csharp_style_expression_bodied_operators = true:suggestion +csharp_style_expression_bodied_properties = true:suggestion +csharp_style_expression_bodied_indexers = true:suggestion +csharp_style_expression_bodied_accessors = true:suggestion # Pattern matching csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion @@ -113,6 +119,7 @@ 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 +dotnet_style_namespace_match_folder = false:none # Space preferences csharp_space_after_cast = false @@ -128,10 +135,10 @@ csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false # Parentheses preferences -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion # Code analyzers # CA1031: Do not catch general exception types @@ -152,7 +159,14 @@ dotnet_diagnostic.IDE0005.severity = warning # RS0041: Public members should not use oblivious types dotnet_diagnostic.RS0041.severity = suggestion -[obj/**.cs] +[*Tests.cs] +# CA1515: Disable making types internal for Tests classes. It is required by xunit +dotnet_diagnostic.CA1515.severity = none + +# CA2007: Disable Consider calling ConfigureAwait on the awaited task. It is not working with xunit +dotnet_diagnostic.CA2007.severity = none + +[**/obj/**.cs] generated_code = true [*.csproj] diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..50ca329f24b --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sh eol=lf diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 3bad5b876a7..1d9f2dd277a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,6 +1,7 @@ name: Bug report +title: "[bug] " description: Create a report to help us improve -labels: ["bug"] +labels: [bug,needs-triage] body: - type: markdown attributes: @@ -47,7 +48,7 @@ body: - type: input attributes: label: Runtime Version - description: What .NET runtime version did you use? (e.g. `net462`, `net48`, `netcoreapp3.1`, `net6.0` etc. You can find this information from the `*.csproj` file) + description: What .NET runtime version did you use? (e.g. `net462`, `net48`, `net8.0`, etc. You can find this information from the `*.csproj` file) validations: required: true @@ -61,7 +62,7 @@ body: - type: textarea attributes: label: Steps to Reproduce - description: Provide a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). We will close the issue if the repro project you share with us is complex or we cannot reproduce the behavior you are reporting. We cannot investigate custom projects, so don't point us to such, please. + description: Provide a [minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). We will close the issue if the repro project you share with us is complex or we cannot reproduce the behavior you are reporting. We cannot investigate custom projects, so don't point us to such, please. validations: required: true @@ -83,3 +84,11 @@ body: attributes: label: Additional Context description: Any additional information you think may be relevant to this issue. + + - type: dropdown + attributes: + label: Tip + description: This element is static, used to render a helpful sub-heading for end-users and community members to help prioritize issues. Please leave as is. + options: + - "[React](https://github.blog/news-insights/product-news/add-reactions-to-pull-requests-issues-and-comments/) with :+1: to help prioritize this issue. Please use comments to provide useful context, avoiding `+1` or `me too`, to help us triage it. Learn more [here](https://opentelemetry.io/community/end-user/issue-participation/)." + default: 0 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..3104c7c9769 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,7 @@ +# https://docs.github.com/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository#configuring-the-template-chooser + +blank_issues_enabled: false +contact_links: + - name: Question + url: https://github.com/open-telemetry/opentelemetry-dotnet/discussions/new?category=q-a + about: Ask a question to help us improve our knowledge base and documentation. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 7fd9b2f4eda..9ca53fe8e7e 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,6 +1,7 @@ name: Feature request +title: "[feature request] " description: Suggest an idea for this project -labels: ["enhancement"] +labels: [enhancement,needs-triage] body: - type: markdown attributes: @@ -52,3 +53,11 @@ body: attributes: label: Additional context description: Any additional information you think may be relevant to this feature request. + + - type: dropdown + attributes: + label: Tip + description: This element is static, used to render a helpful sub-heading for end-users and community members to help prioritize issues. Please leave as is. + options: + - "[React](https://github.blog/news-insights/product-news/add-reactions-to-pull-requests-issues-and-comments/) with :+1: to help prioritize this issue. Please use comments to provide useful context, avoiding `+1` or `me too`, to help us triage it. Learn more [here](https://opentelemetry.io/community/end-user/issue-participation/)." + default: 0 diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml deleted file mode 100644 index 76eea553517..00000000000 --- a/.github/ISSUE_TEMPLATE/question.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Question -description: Ask a question to help us improve our knowledge base and documentation -labels: ["question"] -body: - - type: markdown - attributes: - value: | - > [!NOTE] - > Please ask questions using [GitHub Discussions](https://github.com/open-telemetry/opentelemetry-dotnet/discussions/new) instead of GitHub Issues. - - - type: textarea - attributes: - label: What is the question? - description: Describe the question you have. - validations: - required: true - - - type: textarea - attributes: - label: Additional context - description: Any additional information you think may be relevant to this question. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2ac5ee67846..dda94ddd1f0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,8 +1,10 @@ +# This file is retained solely for automated tooling to see we do automated +# dependency updates as not all such scanners recognize the use of Renovate. version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "daily" + interval: yearly labels: - "infra" diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 00000000000..996d775ec7f --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,70 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "additionalBranchPrefix": "{{manager}}/", + "automerge": false, + "commitBodyTable": true, + "commitMessageAction": "Bump", + "dependencyDashboard": false, + "extends": [ + "config:best-practices", + "customManagers:dockerfileVersions", + "customManagers:githubActionsVersions", + ":automergeRequireAllStatusChecks", + ":disableRateLimiting", + ":enableVulnerabilityAlerts", + ":gitSignOff", + ":ignoreUnstable" + ], + "ignorePresets": [ + ":ignoreModulesAndTests" + ], + "ignorePaths": [ + "**/node_modules/**", + "**/bower_components/**", + "**/vendor/**", + "**/__tests__/**", + "**/__fixtures__/**" + ], + "labels": ["dependencies", "infra"], + "packageRules": [ + { + "matchManagers": ["dockerfile"], + "addLabels": ["docker"] + }, + { + "matchManagers": ["github-actions"], + "addLabels": ["github_actions"] + }, + { + "matchManagers": ["nuget"], + "addLabels": [".NET"] + }, + { + "matchManagers": ["pypi"], + "addLabels": ["python"] + }, + { + "description": ["Skip pinned NuGet package versions"], + "matchManagers": ["nuget"], + "matchCurrentValue": "^\\[[^,]+,\\)$", + "enabled": false + }, + { + "extends": ["monorepo:dotnet"], + "description": ["Disable major version updates for .NET"], + "matchUpdateTypes": ["major"], + "enabled": false + }, + { + "matchDepNames": ["xunit"], + "description": ["Disable major version updates for xunit"], + "matchUpdateTypes": ["major"], + "enabled": false + } + ], + "schedule": ["* 8-17 * * 3"], + "timezone": "Etc/UTC", + "vulnerabilityAlerts": { + "addLabels": ["security"] + } +} diff --git a/.github/workflows/Component.BuildTest.yml b/.github/workflows/Component.BuildTest.yml index f5ef85e0dc3..406be841884 100644 --- a/.github/workflows/Component.BuildTest.yml +++ b/.github/workflows/Component.BuildTest.yml @@ -20,14 +20,17 @@ on: required: false type: string os-list: - default: '[ "windows-latest", "ubuntu-latest" ]' + default: '[ "windows-latest", "windows-11-arm", "ubuntu-22.04", "ubuntu-22.04-arm" ]' required: false type: string tfm-list: - default: '[ "net462", "net6.0", "net7.0", "net8.0" ]' + default: '[ "net462", "net8.0", "net9.0" ]' required: false type: string +permissions: + contents: read + jobs: build-test: @@ -37,20 +40,33 @@ jobs: os: ${{ fromJSON(inputs.os-list) }} version: ${{ fromJSON(inputs.tfm-list) }} exclude: - - os: ubuntu-latest + - os: ubuntu-22.04 + version: net462 + - os: ubuntu-22.04-arm version: net462 + - os: ubuntu-22.04-arm + version: net8.0 + - os: windows-11-arm + version: net8.0 runs-on: ${{ matrix.os }} + timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 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: Setup previous .NET runtimes + uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + with: + dotnet-version: | + 8.0.x + + - name: Setup .NET + uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 - name: dotnet restore ${{ inputs.project-name }} run: dotnet restore ${{ inputs.project-name }} ${{ inputs.project-build-commands }} @@ -59,7 +75,18 @@ jobs: 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 + 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" + --logger:"GitHubActions;report-warnings=false" + --logger:"junit;LogFilePath=TestResults/junit.xml" + -- RunConfiguration.DisableAppDomain=true - name: Install coverage tool run: dotnet tool install -g dotnet-coverage @@ -68,15 +95,24 @@ jobs: run: dotnet-coverage merge -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 + uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 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 + files: 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 + token: ${{ secrets.CODECOV_TOKEN }} + + - name: Upload test results ${{ inputs.code-cov-prefix }}-${{ inputs.code-cov-name }} + if: ${{ !cancelled() && hashFiles('./**/TestResults/junit.xml') != '' }} + uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1 + with: + env_vars: OS,TFM + flags: ${{ inputs.code-cov-prefix }}-${{ inputs.code-cov-name }} + name: Test results for ${{ inputs.code-cov-prefix }}-${{ inputs.code-cov-name }} on [${{ matrix.os }}.${{ matrix.version }}] + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/add-labels.yml b/.github/workflows/add-labels.yml index cae26b876b4..19d25be7e44 100644 --- a/.github/workflows/add-labels.yml +++ b/.github/workflows/add-labels.yml @@ -7,18 +7,19 @@ on: branches: [ 'main*' ] permissions: - issues: write - pull-requests: write + contents: read jobs: add-labels-on-issues: + permissions: + issues: write if: github.event_name == 'issues' && !github.event.issue.pull_request - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: check out code - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Add labels for package found in bug issue descriptions shell: pwsh @@ -33,13 +34,15 @@ jobs: ISSUE_BODY: ${{ github.event.issue.body }} add-labels-on-pull-requests: + permissions: + pull-requests: write if: github.event_name == 'pull_request_target' - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: check out code - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.repository.default_branch }} # Note: Do not run on the PR branch we want to execute add-labels.psm1 from main on the base repo only because pull_request_target can see secrets diff --git a/.github/workflows/automation.yml b/.github/workflows/automation.yml index 1385474892a..2daa116be62 100644 --- a/.github/workflows/automation.yml +++ b/.github/workflows/automation.yml @@ -5,30 +5,33 @@ on: outputs: enabled: value: ${{ jobs.resolve-automation.outputs.enabled == 'true' }} - token-secret-name: - value: ${{ jobs.resolve-automation.outputs.token-secret-name }} username: value: ${{ vars.AUTOMATION_USERNAME }} email: value: ${{ vars.AUTOMATION_EMAIL }} + application-name: + value: ${{ vars.AUTOMATION_APPLICATION_NAME }} + application-username: + value: ${{ vars.AUTOMATION_APPLICATION_USERNAME }} secrets: - OPENTELEMETRYBOT_GITHUB_TOKEN: + OTELBOT_DOTNET_PRIVATE_KEY: required: false +permissions: + contents: read + jobs: resolve-automation: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 outputs: enabled: ${{ steps.evaluate.outputs.enabled }} - token-secret-name: ${{ steps.evaluate.outputs.token-secret-name }} env: - OPENTELEMETRYBOT_GITHUB_TOKEN_EXISTS: ${{ secrets.OPENTELEMETRYBOT_GITHUB_TOKEN != '' }} + OTELBOT_DOTNET_PRIVATE_KEY_EXISTS: ${{ secrets.OTELBOT_DOTNET_PRIVATE_KEY != '' }} steps: - id: evaluate run: | - echo "enabled=${{ env.OPENTELEMETRYBOT_GITHUB_TOKEN_EXISTS == 'true' }}" >> "$GITHUB_OUTPUT" - echo "token-secret-name=OPENTELEMETRYBOT_GITHUB_TOKEN" >> "$GITHUB_OUTPUT" + echo "enabled=${{ env.OTELBOT_DOTNET_PRIVATE_KEY_EXISTS == 'true' }}" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f222b07cc3..3110eef0cc6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,22 +9,32 @@ on: pull_request: branches: [ 'main*' ] +permissions: + contents: read + jobs: lint-misspell-sanitycheck: uses: ./.github/workflows/sanitycheck.yml + code-ql: + uses: ./.github/workflows/codeql-analysis-steps.yml + permissions: + actions: read + contents: read + security-events: write + detect-changes: runs-on: windows-latest outputs: changes: ${{ steps.changes.outputs.changes }} steps: - - uses: actions/checkout@v4 - - uses: AurorNZ/paths-filter@v4 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: AurorNZ/paths-filter@3b1f3abc3371cca888d8eb03dfa70bc8a9867629 # v4.0.0 id: changes with: filters: | md: ['**.md'] - build: ['build/**', '.github/**/*.yml', '**/*.targets', '**/*.props'] + build: ['build/**', '.github/**/*.yml', '**/*.targets', '**/*.props', 'global.json'] shared: ['src/Shared/**'] code: ['**.cs', '**.csproj', '.editorconfig'] solution: ['OpenTelemetry.sln'] @@ -107,13 +117,13 @@ jobs: || 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 + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: - version: [ net6.0, net7.0, net8.0 ] + version: [ net8.0, net9.0 ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - 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 @@ -125,13 +135,13 @@ jobs: || 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 + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: - version: [ net6.0, net7.0 ] + version: [ net8.0, net9.0 ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - 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 @@ -171,8 +181,9 @@ jobs: build-test: needs: [ - lint-misspell-sanitycheck, detect-changes, + code-ql, + lint-misspell-sanitycheck, lint-md, lint-dotnet-format, build-test-solution, @@ -186,7 +197,18 @@ jobs: verify-aot-compat, concurrency-tests ] - if: always() && !cancelled() && !contains(needs.*.result, 'failure') - runs-on: windows-latest + if: always() && !cancelled() + runs-on: ubuntu-24.04 steps: - - run: echo 'build complete' + - name: Report CI status + shell: bash + env: + CI_SUCCESS: ${{ !contains(needs.*.result, 'failure') }} + run: | + if [ "${CI_SUCCESS}" == "true" ] + then + echo 'Build complete' + else + echo 'Build failed' + exit 1 + fi diff --git a/.github/workflows/codeql-analysis-steps.yml b/.github/workflows/codeql-analysis-steps.yml new file mode 100644 index 00000000000..47a6ba61535 --- /dev/null +++ b/.github/workflows/codeql-analysis-steps.yml @@ -0,0 +1,65 @@ +name: codeql-analysis-steps + +on: + workflow_call: + +permissions: {} + +jobs: + analyze: + permissions: + actions: read # for github/codeql-action/init to get workflow details + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/analyze to upload SARIF results + runs-on: windows-latest + + strategy: + fail-fast: false + matrix: + language: ['actions', 'csharp'] + + steps: + - name: Configure Pagefile + if: matrix.language == 'csharp' + uses: al-cheb/configure-pagefile-action@a3b6ebd6b634da88790d9c58d4b37a7f4a7b8708 # v1.4 + with: + minimum-size: 8GB + maximum-size: 32GB + disk-root: "D:" + + - name: Checkout repository + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + filter: 'tree:0' + persist-credentials: false + show-progress: false + + - name: Initialize CodeQL + uses: github/codeql-action/init@303c0aef88fc2fe5ff6d63d3b1596bfd83dfa1f9 # v3.30.4 + with: + build-mode: none + languages: ${{ matrix.language }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@303c0aef88fc2fe5ff6d63d3b1596bfd83dfa1f9 # v3.30.4 + with: + category: '/language:${{ matrix.language }}' + + results: + if: ${{ !cancelled() }} + needs: [ analyze ] + runs-on: ubuntu-latest + + steps: + - name: Report status + shell: bash + env: + SCAN_SUCCESS: ${{ !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} + run: | + if [ "${SCAN_SUCCESS}" == "true" ] + then + echo 'CodeQL analysis successful' + else + echo 'CodeQL analysis failed' + exit 1 + fi diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 3cdd6a57b6d..44fc56c2444 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,8 +1,3 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. name: "CodeQL" on: @@ -10,37 +5,12 @@ on: - cron: '0 0 * * *' # once in a day at 00:00 workflow_dispatch: -jobs: - analyze: - name: Analyze - runs-on: windows-latest - - strategy: - fail-fast: false - matrix: - language: ['csharp'] - - steps: - - name: configure Pagefile - uses: al-cheb/configure-pagefile-action@v1.4 - with: - minimum-size: 8GB - maximum-size: 32GB - disk-root: "D:" - - - name: Checkout repository - uses: actions/checkout@v4 +permissions: {} - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - - - name: Setup dotnet - uses: actions/setup-dotnet@v4 - - - name: dotnet pack - run: dotnet pack ./build/OpenTelemetry.proj --configuration Release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 +jobs: + code-ql: + uses: ./.github/workflows/codeql-analysis-steps.yml + permissions: + actions: read + contents: read + security-events: write diff --git a/.github/workflows/concurrency-tests.yml b/.github/workflows/concurrency-tests.yml index 4ff616911ce..ae1e64bfcf9 100644 --- a/.github/workflows/concurrency-tests.yml +++ b/.github/workflows/concurrency-tests.yml @@ -5,22 +5,25 @@ name: Concurrency Tests on: workflow_call: +permissions: + contents: read + 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 ] + os: [ windows-latest, ubuntu-22.04 ] version: [ net8.0 ] project: [ OpenTelemetry.Tests, OpenTelemetry.Api.Tests ] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup dotnet - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 - name: Run Coyote Tests shell: pwsh @@ -28,7 +31,7 @@ jobs: - name: Publish Artifacts if: always() && !cancelled() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: ${{ matrix.os }}-${{ matrix.project }}-${{ matrix.version }}-coyoteoutput path: '**/*_CoyoteOutput.*' diff --git a/.github/workflows/docfx.yml b/.github/workflows/docfx.yml index 4c2031c9303..fcf07c097b3 100644 --- a/.github/workflows/docfx.yml +++ b/.github/workflows/docfx.yml @@ -5,13 +5,19 @@ name: Build docfx on: workflow_call: +permissions: + contents: read + jobs: run-docfx-build: runs-on: windows-latest steps: - name: check out code - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - name: Setup dotnet + uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 - name: install docfx run: dotnet tool install -g docfx diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index 2ea4834570f..dc72da32565 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -5,16 +5,19 @@ name: Lint - dotnet format on: workflow_call: +permissions: + contents: read + jobs: run-dotnet-format-stable: runs-on: windows-latest steps: - name: check out code - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup dotnet - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 - name: dotnet restore run: dotnet restore OpenTelemetry.sln @@ -29,10 +32,10 @@ jobs: steps: - name: check out code - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup dotnet - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 - name: dotnet restore run: dotnet restore OpenTelemetry.sln diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml new file mode 100644 index 00000000000..f86b5a99322 --- /dev/null +++ b/.github/workflows/fossa.yml @@ -0,0 +1,20 @@ +name: FOSSA scanning + +on: + push: + branches: + - main + +permissions: + contents: read + +jobs: + fossa: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - uses: fossas/fossa-action@3ebcea1862c6ffbd5cf1b4d0bd6b3fe7bd6f2cac # v1.7.0 + with: + api-key: ${{secrets.FOSSA_API_KEY}} + team: OpenTelemetry diff --git a/.github/workflows/markdownlint.yml b/.github/workflows/markdownlint.yml index 2764b8cc937..2ce6431d9cc 100644 --- a/.github/workflows/markdownlint.yml +++ b/.github/workflows/markdownlint.yml @@ -5,16 +5,19 @@ name: Lint - Markdown on: workflow_call: +permissions: + contents: read + jobs: run-markdownlint: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: check out code - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: run markdownlint - uses: DavidAnson/markdownlint-cli2-action@v16.0.0 + uses: DavidAnson/markdownlint-cli2-action@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e # v20.0.0 with: globs: | **/*.md diff --git a/.github/workflows/ossf-scorecard.yml b/.github/workflows/ossf-scorecard.yml new file mode 100644 index 00000000000..3d950859b51 --- /dev/null +++ b/.github/workflows/ossf-scorecard.yml @@ -0,0 +1,47 @@ +name: OSSF Scorecard + +on: + push: + branches: + - main + schedule: + - cron: "24 5 * * 0" # once a week + workflow_dispatch: + +permissions: read-all + +jobs: + analysis: + runs-on: ubuntu-latest + permissions: + # Needed for Code scanning upload + security-events: write + # Needed for GitHub OIDC token if publish_results is true + id-token: write + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable + # uploads of run results in SARIF format to the repository Actions tab. + # https://docs.github.com/en/actions/advanced-guides/storing-workflow-data-as-artifacts + - name: "Upload artifact" + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard (optional). + # Commenting out will disable upload of results to your repo's Code Scanning dashboard + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@303c0aef88fc2fe5ff6d63d3b1596bfd83dfa1f9 # v3.30.4 + with: + sarif_file: results.sarif diff --git a/.github/workflows/package-validation.yml b/.github/workflows/package-validation.yml index d1f7658ee2d..3216646f45c 100644 --- a/.github/workflows/package-validation.yml +++ b/.github/workflows/package-validation.yml @@ -5,12 +5,15 @@ name: Package Validation on: workflow_call: +permissions: + contents: read + jobs: run-package-validation-stable: runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 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 @@ -18,16 +21,23 @@ jobs: fetch-depth: 0 - name: Setup dotnet - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 - name: dotnet pack run: dotnet pack ./build/OpenTelemetry.proj --configuration Release /p:EnablePackageValidation=true /p:ExposeExperimentalFeatures=false /p:RunningDotNetPack=true + - name: Publish stable NuGet packages to Artifacts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: packages-stable + path: ./artifacts/package/release + if-no-files-found: error + run-package-validation-experimental: runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 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 @@ -35,7 +45,14 @@ jobs: fetch-depth: 0 - name: Setup dotnet - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 - name: dotnet pack run: dotnet pack ./build/OpenTelemetry.proj --configuration Release /p:EnablePackageValidation=true /p:ExposeExperimentalFeatures=true /p:RunningDotNetPack=true + + - name: Publish experimental NuGet packages to Artifacts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: packages-experimental + path: ./artifacts/package/release + if-no-files-found: error diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml index e824e87d724..7a06a0c6580 100644 --- a/.github/workflows/post-release.yml +++ b/.github/workflows/post-release.yml @@ -16,104 +16,148 @@ on: types: - created +permissions: + contents: read + jobs: automation: uses: ./.github/workflows/automation.yml - secrets: inherit + secrets: + OTELBOT_DOTNET_PRIVATE_KEY: ${{ secrets.OTELBOT_DOTNET_PRIVATE_KEY }} push-packages-and-publish-release: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 needs: automation + permissions: + id-token: write + if: | - github.event_name == 'issue_comment' - && github.event.issue.pull_request - && github.event.issue.locked == true - && github.event.comment.user.login != needs.automation.outputs.username - && contains(github.event.comment.body, '/PushPackages') - && startsWith(github.event.issue.title, '[release] Prepare release ') - && github.event.issue.pull_request.merged_at - && needs.automation.outputs.enabled - - env: - GH_TOKEN: ${{ secrets[needs.automation.outputs.token-secret-name] }} + github.event_name == 'issue_comment' && + github.event.issue.pull_request && + github.event.issue.locked == true && + github.event.comment.user.login != needs.automation.outputs.application-username && + contains(github.event.comment.body, '/PushPackages') && + startsWith(github.event.issue.title, '[release] Prepare release ') && + github.event.issue.pull_request.merged_at && + needs.automation.outputs.enabled steps: - - name: check out code - uses: actions/checkout@v4 + - uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 + id: otelbot-token + with: + app-id: ${{ vars.OTELBOT_DOTNET_APP_ID }} + private-key: ${{ secrets.OTELBOT_DOTNET_PRIVATE_KEY }} + + - name: Check out code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: - token: ${{ secrets[needs.automation.outputs.token-secret-name] }} + token: ${{ steps.otelbot-token.outputs.token }} ref: ${{ github.event.repository.default_branch }} + - name: Setup .NET + uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + + - name: NuGet log in + uses: NuGet/login@d22cc5f58ff5b88bf9bd452535b4335137e24544 # v1.1.0 + id: nuget-login + with: + user: ${{ secrets.NUGET_USER }} + - name: Push packages and publish release shell: pwsh env: - NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }} + GH_TOKEN: ${{ steps.otelbot-token.outputs.token }} + EXPECTED_PR_AUTHOR_USER_NAME: ${{ needs.automation.outputs.application-name }} + COMMENT_USER_NAME: ${{ github.event.comment.user.login }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + NUGET_TOKEN: ${{ steps.nuget-login.outputs.NUGET_API_KEY }} run: | Import-Module .\build\scripts\post-release.psm1 + $HasToken = -Not [string]::IsNullOrEmpty($env:NUGET_TOKEN) PushPackagesPublishReleaseUnlockAndPostNoticeOnPrepareReleasePullRequest ` - -gitRepository '${{ github.repository }}' ` - -pullRequestNumber '${{ github.event.issue.number }}' ` - -botUserName '${{ needs.automation.outputs.username }}' ` - -commentUserName '${{ github.event.comment.user.login }}' ` - -artifactDownloadPath '${{ github.workspace }}/artifacts' ` - -pushToNuget '${{ secrets.NUGET_TOKEN != '' }}' + -gitRepository ${env:GITHUB_REPOSITORY} ` + -pullRequestNumber ${env:ISSUE_NUMBER} ` + -expectedPrAuthorUserName ${env:EXPECTED_PR_AUTHOR_USER_NAME} ` + -commentUserName ${env:COMMENT_USER_NAME} ` + -artifactDownloadPath "${env:GITHUB_WORKSPACE}/artifacts" ` + -pushToNuget $HasToken post-release-published: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 needs: - automation if: | - needs.automation.outputs.enabled - && (github.event_name == 'release' || github.event_name == 'workflow_dispatch') - - env: - GH_TOKEN: ${{ secrets[needs.automation.outputs.token-secret-name] }} + needs.automation.outputs.enabled && + (github.event_name == 'release' || github.event_name == 'workflow_dispatch') steps: - - uses: actions/checkout@v4 + - uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 + id: otelbot-token + with: + app-id: ${{ vars.OTELBOT_DOTNET_APP_ID }} + private-key: ${{ secrets.OTELBOT_DOTNET_PRIVATE_KEY }} + + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: # Note: By default GitHub only fetches 1 commit. We need all the tags # for this work. fetch-depth: 0 ref: ${{ github.event.repository.default_branch }} - token: ${{ secrets[needs.automation.outputs.token-secret-name] }} + token: ${{ steps.otelbot-token.outputs.token }} + + - name: Setup dotnet + uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 - name: Create GitHub Pull Request to update stable build version in props if: | (github.ref_type == 'tag' && startsWith(github.ref_name, 'core-') && !contains(github.ref_name, '-alpha') && !contains(github.ref_name, '-beta') && !contains(github.ref_name, '-rc')) || (inputs.tag && startsWith(inputs.tag, 'core-') && !contains(inputs.tag, '-alpha') && !contains(inputs.tag, '-beta') && !contains(inputs.tag, '-rc')) shell: pwsh + env: + GH_TOKEN: ${{ steps.otelbot-token.outputs.token }} + BOT_USER_EMAIL: ${{ needs.automation.outputs.email }} + BOT_USER_NAME: ${{ needs.automation.outputs.username }} + TAG: ${{ inputs.tag || github.ref_name }} + TARGET_BRANCH: ${{ github.event.repository.default_branch }} run: | Import-Module .\build\scripts\post-release.psm1 CreateStableVersionUpdatePullRequest ` - -gitRepository '${{ github.repository }}' ` - -tag '${{ inputs.tag || github.ref_name }}' ` - -targetBranch '${{ github.event.repository.default_branch }}' ` - -gitUserName '${{ needs.automation.outputs.username }}' ` - -gitUserEmail '${{ needs.automation.outputs.email }}' + -gitRepository ${env:GITHUB_REPOSITORY} ` + -tag ${env:TAG} ` + -targetBranch ${env:TARGET_BRANCH} ` + -gitUserName ${env:BOT_USER_NAME} ` + -gitUserEmail ${env:BOT_USER_EMAIL} - name: Invoke core version update workflow in opentelemetry-dotnet-contrib repository if: vars.CONTRIB_REPO shell: pwsh + env: + GH_TOKEN: ${{ steps.otelbot-token.outputs.token }} + CONTRIB_REPO: ${{ vars.CONTRIB_REPO }} + TAG: ${{ inputs.tag || github.ref_name }} run: | Import-Module .\build\scripts\post-release.psm1 InvokeCoreVersionUpdateWorkflowInRemoteRepository ` - -remoteGitRepository '${{ vars.CONTRIB_REPO }}' ` - -tag '${{ inputs.tag || github.ref_name }}' + -remoteGitRepository ${env:CONTRIB_REPO} ` + -tag ${env:TAG} - name: Post notice when release is published shell: pwsh + env: + GH_TOKEN: ${{ steps.otelbot-token.outputs.token }} + EXPECTED_PR_AUTHOR_USER_NAME: ${{ needs.automation.outputs.application-name }} + TAG: ${{ inputs.tag || github.ref_name }} run: | Import-Module .\build\scripts\post-release.psm1 TryPostReleasePublishedNoticeOnPrepareReleasePullRequest ` - -gitRepository '${{ github.repository }}' ` - -botUserName '${{ needs.automation.outputs.username }}' ` - -tag '${{ inputs.tag || github.ref_name }}' + -gitRepository ${env:GITHUB_REPOSITORY} ` + -expectedPrAuthorUserName ${env:EXPECTED_PR_AUTHOR_USER_NAME} ` + -tag ${env:TAG} diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index bd8211e645e..5d8eb56ae39 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -23,108 +23,238 @@ on: types: - created +permissions: + contents: read + jobs: automation: uses: ./.github/workflows/automation.yml - secrets: inherit + secrets: + OTELBOT_DOTNET_PRIVATE_KEY: ${{ secrets.OTELBOT_DOTNET_PRIVATE_KEY }} prepare-release-pr: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 needs: automation if: github.event_name == 'workflow_dispatch' && needs.automation.outputs.enabled - env: - GH_TOKEN: ${{ secrets[needs.automation.outputs.token-secret-name] }} - steps: - - name: check out code - uses: actions/checkout@v4 + - uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 + id: otelbot-token + with: + app-id: ${{ vars.OTELBOT_DOTNET_APP_ID }} + private-key: ${{ secrets.OTELBOT_DOTNET_PRIVATE_KEY }} + + - name: Check out code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: - token: ${{ secrets[needs.automation.outputs.token-secret-name] }} + token: ${{ steps.otelbot-token.outputs.token }} - name: Create GitHub Pull Request to prepare release shell: pwsh + env: + GH_TOKEN: ${{ steps.otelbot-token.outputs.token }} + BOT_USER_EMAIL: ${{ needs.automation.outputs.email }} + BOT_USER_NAME: ${{ needs.automation.outputs.username }} + COMMENT_USER_NAME: ${{ github.event.sender.login }} + TAG_PREFIX: ${{ inputs.tag-prefix }} + VERSION: ${{ inputs.version }} run: | Import-Module .\build\scripts\prepare-release.psm1 CreatePullRequestToUpdateChangelogsAndPublicApis ` - -gitRepository '${{ github.repository }}' ` - -minVerTagPrefix '${{ inputs.tag-prefix }}' ` - -version '${{ inputs.version }}' ` - -targetBranch '${{ github.ref_name }}' ` - -gitUserName '${{ needs.automation.outputs.username }}' ` - -gitUserEmail '${{ needs.automation.outputs.email }}' + -gitRepository ${env:GITHUB_REPOSITORY} ` + -minVerTagPrefix ${env:TAG_PREFIX} ` + -version ${env:VERSION} ` + -requestedByUserName ${env:COMMENT_USER_NAME} ` + -targetBranch ${env:GITHUB_REF_NAME} ` + -gitUserName ${env:BOT_USER_NAME} ` + -gitUserEmail ${env:BOT_USER_EMAIL} lock-pr-and-post-notice-to-create-release-tag: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 needs: automation if: | - github.event_name == 'pull_request' - && github.event.action == 'closed' - && github.event.pull_request.user.login == needs.automation.outputs.username - && github.event.pull_request.merged == true - && startsWith(github.event.pull_request.title, '[release] Prepare release ') - && needs.automation.outputs.enabled - - env: - GH_TOKEN: ${{ secrets[needs.automation.outputs.token-secret-name] }} + github.event_name == 'pull_request' && + github.event.action == 'closed' && + github.event.pull_request.user.login == needs.automation.outputs.application-username && + github.event.pull_request.merged == true && + startsWith(github.event.pull_request.title, '[release] Prepare release ') && + needs.automation.outputs.enabled steps: - - name: check out code - uses: actions/checkout@v4 + - uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 + id: otelbot-token with: - token: ${{ secrets[needs.automation.outputs.token-secret-name] }} + app-id: ${{ vars.OTELBOT_DOTNET_APP_ID }} + private-key: ${{ secrets.OTELBOT_DOTNET_PRIVATE_KEY }} + + - name: Check out code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + token: ${{ steps.otelbot-token.outputs.token }} - name: Lock GitHub Pull Request to prepare release shell: pwsh + env: + GH_TOKEN: ${{ steps.otelbot-token.outputs.token }} + EXPECTED_PR_AUTHOR_USER_NAME: ${{ needs.automation.outputs.application-name }} + ISSUE_NUMBER: ${{ github.event.pull_request.number }} run: | Import-Module .\build\scripts\prepare-release.psm1 LockPullRequestAndPostNoticeToCreateReleaseTag ` - -gitRepository '${{ github.repository }}' ` - -pullRequestNumber '${{ github.event.pull_request.number }}' ` - -botUserName '${{ needs.automation.outputs.username }}' + -gitRepository ${env:GITHUB_REPOSITORY} ` + -pullRequestNumber ${env:ISSUE_NUMBER} ` + -expectedPrAuthorUserName ${env:EXPECTED_PR_AUTHOR_USER_NAME} create-release-tag-pr-post-notice: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 needs: automation if: | - github.event_name == 'issue_comment' - && github.event.issue.pull_request - && github.event.issue.locked == true - && github.event.comment.user.login != needs.automation.outputs.username - && contains(github.event.comment.body, '/CreateReleaseTag') - && startsWith(github.event.issue.title, '[release] Prepare release ') - && github.event.issue.pull_request.merged_at - && needs.automation.outputs.enabled - - env: - GH_TOKEN: ${{ secrets[needs.automation.outputs.token-secret-name] }} + github.event_name == 'issue_comment' && + github.event.issue.pull_request && + github.event.issue.locked == true && + github.event.comment.user.login != needs.automation.outputs.application-username && + contains(github.event.comment.body, '/CreateReleaseTag') && + startsWith(github.event.issue.title, '[release] Prepare release ') && + github.event.issue.pull_request.merged_at && + needs.automation.outputs.enabled steps: - - name: check out code - uses: actions/checkout@v4 + - uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 + id: otelbot-token + with: + app-id: ${{ vars.OTELBOT_DOTNET_APP_ID }} + private-key: ${{ secrets.OTELBOT_DOTNET_PRIVATE_KEY }} + + - name: Check out code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: # Note: By default GitHub only fetches 1 commit which fails the git tag operation below fetch-depth: 0 - token: ${{ secrets[needs.automation.outputs.token-secret-name] }} + token: ${{ steps.otelbot-token.outputs.token }} - name: Create release tag id: create-tag shell: pwsh + env: + GH_TOKEN: ${{ steps.otelbot-token.outputs.token }} + BOT_USER_EMAIL: ${{ needs.automation.outputs.email }} + BOT_USER_NAME: ${{ needs.automation.outputs.username }} + EXPECTED_PR_AUTHOR_USER_NAME: ${{ needs.automation.outputs.application-name }} + ISSUE_NUMBER: ${{ github.event.issue.number }} run: | Import-Module .\build\scripts\prepare-release.psm1 CreateReleaseTagAndPostNoticeOnPullRequest ` - -gitRepository '${{ github.repository }}' ` - -pullRequestNumber '${{ github.event.issue.number }}' ` - -botUserName '${{ needs.automation.outputs.username }}' ` - -gitUserName '${{ needs.automation.outputs.username }}' ` - -gitUserEmail '${{ needs.automation.outputs.email }}' + -gitRepository ${env:GITHUB_REPOSITORY} ` + -pullRequestNumber ${env:ISSUE_NUMBER} ` + -expectedPrAuthorUserName ${env:EXPECTED_PR_AUTHOR_USER_NAME} ` + -gitUserName ${env:BOT_USER_NAME} ` + -gitUserEmail ${env:BOT_USER_EMAIL} + + update-changelog-release-dates-on-prepare-pr-post-notice: + runs-on: ubuntu-24.04 + + needs: automation + + if: | + github.event_name == 'issue_comment' && + github.event.issue.pull_request && + github.event.issue.state == 'open' && + github.event.comment.user.login != needs.automation.outputs.application-username && + contains(github.event.comment.body, '/UpdateReleaseDates') && + startsWith(github.event.issue.title, '[release] Prepare release ') && + github.event.issue.pull_request.merged_at == null && + needs.automation.outputs.enabled + + steps: + - uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 + id: otelbot-token + with: + app-id: ${{ vars.OTELBOT_DOTNET_APP_ID }} + private-key: ${{ secrets.OTELBOT_DOTNET_PRIVATE_KEY }} + + - name: Check out code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + # Note: By default GitHub only fetches 1 commit which fails the git tag operation below + fetch-depth: 0 + token: ${{ steps.otelbot-token.outputs.token }} + + - name: Update release date + shell: pwsh + env: + GH_TOKEN: ${{ steps.otelbot-token.outputs.token }} + BOT_USER_EMAIL: ${{ needs.automation.outputs.email }} + BOT_USER_NAME: ${{ needs.automation.outputs.username }} + EXPECTED_PR_AUTHOR_USER_NAME: ${{ needs.automation.outputs.application-name }} + COMMENT_USER_NAME: ${{ github.event.comment.user.login }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + run: | + Import-Module .\build\scripts\prepare-release.psm1 + + UpdateChangelogReleaseDatesAndPostNoticeOnPullRequest ` + -gitRepository ${env:GITHUB_REPOSITORY} ` + -pullRequestNumber ${env:ISSUE_NUMBER} ` + -expectedPrAuthorUserName ${env:EXPECTED_PR_AUTHOR_USER_NAME} ` + -commentUserName ${env:COMMENT_USER_NAME} ` + -gitUserName ${env:BOT_USER_NAME} ` + -gitUserEmail ${env:BOT_USER_EMAIL} + + update-releasenotes-on-prepare-pr-post-notice: + runs-on: ubuntu-24.04 + + needs: automation + + if: | + github.event_name == 'issue_comment' && + github.event.issue.pull_request && + github.event.issue.state == 'open' && + github.event.comment.user.login != needs.automation.outputs.application-username && + contains(github.event.comment.body, '/UpdateReleaseNotes') && + startsWith(github.event.issue.title, '[release] Prepare release ') && + github.event.issue.pull_request.merged_at == null && + needs.automation.outputs.enabled + + steps: + - uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 + id: otelbot-token + with: + app-id: ${{ vars.OTELBOT_DOTNET_APP_ID }} + private-key: ${{ secrets.OTELBOT_DOTNET_PRIVATE_KEY }} + + - name: Check out code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + # Note: By default GitHub only fetches 1 commit which fails the git tag operation below + fetch-depth: 0 + token: ${{ steps.otelbot-token.outputs.token }} + + - name: Update release notes + env: + GH_TOKEN: ${{ steps.otelbot-token.outputs.token }} + BOT_USER_EMAIL: ${{ needs.automation.outputs.email }} + BOT_USER_NAME: ${{ needs.automation.outputs.username }} + EXPECTED_PR_AUTHOR_USER_NAME: ${{ needs.automation.outputs.application-name }} + COMMENT_BODY: ${{ github.event.comment.body }} + COMMENT_USER_NAME: ${{ github.event.comment.user.login }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + shell: pwsh + run: | + Import-Module .\build\scripts\prepare-release.psm1 + UpdateReleaseNotesAndPostNoticeOnPullRequest ` + -gitRepository ${env:GITHUB_REPOSITORY} ` + -pullRequestNumber ${env:ISSUE_NUMBER} ` + -expectedPrAuthorUserName ${env:EXPECTED_PR_AUTHOR_USER_NAME} ` + -commentUserName ${env:COMMENT_USER_NAME} ` + -commentBody $Env:COMMENT_BODY ` + -gitUserName ${env:BOT_USER_NAME} ` + -gitUserEmail ${env:BOT_USER_EMAIL} diff --git a/.github/workflows/publish-packages-1.0.yml b/.github/workflows/publish-packages-1.0.yml index 7553bd66627..86661100080 100644 --- a/.github/workflows/publish-packages-1.0.yml +++ b/.github/workflows/publish-packages-1.0.yml @@ -16,20 +16,30 @@ on: schedule: - cron: '0 0 * * *' # once in a day at 00:00 +permissions: + contents: read + jobs: automation: uses: ./.github/workflows/automation.yml - secrets: inherit + secrets: + OTELBOT_DOTNET_PRIVATE_KEY: ${{ secrets.OTELBOT_DOTNET_PRIVATE_KEY }} build-pack-publish: runs-on: windows-latest + permissions: + contents: read + id-token: write + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COSIGN_YES: "yes" outputs: artifact-url: ${{ steps.upload-artifacts.outputs.artifact-url }} artifact-id: ${{ steps.upload-artifacts.outputs.artifact-id }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 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 @@ -37,34 +47,62 @@ jobs: fetch-depth: 0 - name: Setup dotnet - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + + - name: Install Cosign + uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0 + with: + cosign-release: v2.5.3 - name: dotnet restore run: dotnet restore ./build/OpenTelemetry.proj -p:RunningDotNetPack=true - name: dotnet build - run: dotnet build ./build/OpenTelemetry.proj --configuration Release --no-restore -p:Deterministic=true -p:BuildNumber=${{ github.run_number }} -p:RunningDotNetPack=true + shell: pwsh + run: dotnet build ./build/OpenTelemetry.proj --configuration Release --no-restore -p:Deterministic=true -p:"BuildNumber=${env:GITHUB_RUN_NUMBER}" -p:RunningDotNetPack=true + + - name: Sign DLLs with Cosign Keyless + shell: pwsh + run: | + $projectFiles = Get-ChildItem -Path src/*/*.csproj -File + + foreach ($projectFile in $projectFiles) { + $projectName = [System.IO.Path]::GetFileNameWithoutExtension($projectFile) + + Get-ChildItem -Path artifacts/bin/$projectName/release_*/$projectName.dll -File | ForEach-Object { + $fileFullPath = $_.FullName + Write-Host "Signing $fileFullPath" + + cosign.exe sign-blob $fileFullPath --yes --output-signature $fileFullPath-keyless.sig --output-certificate $fileFullPath-keyless.pem + } + } - name: dotnet pack - run: dotnet pack ./build/OpenTelemetry.proj --configuration Release --no-restore --no-build -p:PackTag=${{ github.ref_type == 'tag' && github.ref_name || '' }} + shell: pwsh + env: + PACK_TAG: ${{ github.ref_type == 'tag' && github.ref_name || '' }} + run: dotnet pack ./build/OpenTelemetry.proj --configuration Release --no-restore --no-build -p:"PackTag=${env:PACK_TAG}" - name: Publish Artifacts id: upload-artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: ${{ github.ref_name }}-packages - path: 'src/**/*.*nupkg' + path: ./artifacts/package/release + if-no-files-found: error - name: Publish MyGet + working-directory: ./artifacts/package/release env: MYGET_TOKEN_EXISTS: ${{ secrets.MYGET_TOKEN != '' }} + API_KEY: ${{ secrets.MYGET_TOKEN }} + SOURCE: https://www.myget.org/F/opentelemetry/api/v2/package if: env.MYGET_TOKEN_EXISTS == 'true' # Skip MyGet publish if run on a fork without the secret - run: | - nuget setApiKey ${{ secrets.MYGET_TOKEN }} -Source https://www.myget.org/F/opentelemetry/api/v2/package - nuget push src/**/*.nupkg -Source https://www.myget.org/F/opentelemetry/api/v2/package + shell: pwsh + run: dotnet nuget push *.nupkg --api-key ${env:API_KEY} --skip-duplicate --source ${env:SOURCE} post-build: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 needs: - automation @@ -72,45 +110,55 @@ jobs: if: needs.automation.outputs.enabled && github.event_name == 'push' - env: - GH_TOKEN: ${{ secrets[needs.automation.outputs.token-secret-name] }} - steps: - - name: check out code - uses: actions/checkout@v4 + - uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 + id: otelbot-token + with: + app-id: ${{ vars.OTELBOT_DOTNET_APP_ID }} + private-key: ${{ secrets.OTELBOT_DOTNET_PRIVATE_KEY }} + + - name: Check out code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: - token: ${{ secrets[needs.automation.outputs.token-secret-name] }} + token: ${{ steps.otelbot-token.outputs.token }} - name: Download Artifacts + env: + ARTIFACT_ID: ${{ needs.build-pack-publish.outputs.artifact-id }} + ARTIFACT_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | curl \ -H "Accept: application/vnd.github+json" \ - -H "Authorization: token ${{ github.token }}" \ + -H "Authorization: token ${ARTIFACT_TOKEN}" \ -L \ - -o '${{ github.workspace }}/artifacts/${{ github.ref_name }}-packages.zip' \ + -o "${GITHUB_WORKSPACE}/artifacts/${GITHUB_REF_NAME}-packages.zip" \ --create-dirs \ - "https://api.github.com/repos/${{ github.repository }}/actions/artifacts/${{ needs.build-pack-publish.outputs.artifact-id }}/zip" + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}/zip" - name: Create GitHub Release draft shell: pwsh + env: + GH_TOKEN: ${{ steps.otelbot-token.outputs.token }} run: | Import-Module .\build\scripts\post-release.psm1 CreateDraftRelease ` - -gitRepository '${{ github.repository }}' ` - -tag '${{ github.ref_name }}' ` - -releaseFiles '${{ github.workspace }}/artifacts/${{ github.ref_name }}-packages.zip#Packages' + -gitRepository ${env:GITHUB_REPOSITORY} ` + -tag ${env:GITHUB_REF_NAME} ` + -releaseFiles "${env:GITHUB_WORKSPACE}/artifacts/${env:GITHUB_REF_NAME}-packages.zip" - name: Post notice when packages are ready shell: pwsh + env: + GH_TOKEN: ${{ steps.otelbot-token.outputs.token }} + EXPECTED_PR_AUTHOR_USER_NAME: ${{ needs.automation.outputs.application-name }} + PACKAGES_URL: ${{ needs.build-pack-publish.outputs.artifact-url }} run: | Import-Module .\build\scripts\post-release.psm1 TryPostPackagesReadyNoticeOnPrepareReleasePullRequest ` - -gitRepository '${{ github.repository }}' ` - -tag '${{ github.ref_name }}' ` - -tagSha '${{ github.sha }}' ` - -packagesUrl '${{ needs.build-pack-publish.outputs.artifact-url }}' ` - -botUserName '${{ needs.automation.outputs.username }}' - - + -gitRepository ${env:GITHUB_REPOSITORY} ` + -tag ${env:GITHUB_REF_NAME} ` + -tagSha ${env:GITHUB_SHA} ` + -packagesUrl ${env:PACKAGES_URL} ` + -expectedPrAuthorUserName ${env:EXPECTED_PR_AUTHOR_USER_NAME} diff --git a/.github/workflows/sanitycheck.yml b/.github/workflows/sanitycheck.yml index d98f3fea2c9..54f89668af6 100644 --- a/.github/workflows/sanitycheck.yml +++ b/.github/workflows/sanitycheck.yml @@ -5,13 +5,16 @@ name: Lint - Spelling & Encoding on: workflow_call: +permissions: + contents: read + jobs: run-misspell: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: check out code - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: install misspell run: | @@ -22,11 +25,11 @@ jobs: run: ./bin/misspell -error . run-sanitycheck: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: check out code - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: detect non-ASCII encoding and trailing space run: python3 ./build/scripts/sanitycheck.py diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index f5657aa9f54..16b246d66aa 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,21 +1,32 @@ # Syntax: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions # Github Actions Stale: https://github.com/actions/stale -name: "Close stale pull requests" +name: "Manage stale issues and pull requests" on: schedule: - cron: "12 3 * * *" # arbitrary time not to DDOS GitHub +permissions: + contents: read + jobs: stale: - runs-on: ubuntu-latest + permissions: + issues: write # for actions/stale to close stale issues + pull-requests: write # for actions/stale to close stale PRs + runs-on: ubuntu-24.04 steps: - - uses: actions/stale@v9 + - uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 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.' + stale-issue-message: 'This issue was marked stale due to lack of activity and will be closed in 7 days. Commenting will instruct the bot to automatically remove the label. This bot runs once per day.' + close-issue-message: 'Closed as inactive. Feel free to reopen if this issue is still a concern.' + 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.' operations-per-run: 400 days-before-pr-stale: 7 - days-before-issue-stale: -1 + days-before-issue-stale: 300 days-before-pr-close: 7 - days-before-issue-close: -1 + days-before-issue-close: 7 + exempt-all-issue-milestones: true + exempt-issue-labels: 'keep-open,needs-triage' + exempt-pr-labels: 'keep-open' diff --git a/.github/workflows/survey-on-merged-pr.yml b/.github/workflows/survey-on-merged-pr.yml new file mode 100644 index 00000000000..3b3c80be6c5 --- /dev/null +++ b/.github/workflows/survey-on-merged-pr.yml @@ -0,0 +1,29 @@ +name: Survey on Merged PR by Non-Member + +on: + pull_request_target: + branches: [main] + types: [closed] + +permissions: {} + +jobs: + comment-on-pr: + name: Add survey to PR if author is not a member + permissions: + pull-requests: write + runs-on: ubuntu-latest + # Only run for merged pull requests by non-members users (i.e. not bots) + if: | + github.event.pull_request.merged && + github.event.pull_request.user.type == 'User' && + contains(fromJson('["CONTRIBUTOR", "FIRST_TIME_CONTRIBUTOR", "FIRST_TIMER"]'), github.event.pull_request.author_association) + steps: + - name: Add comment + run: | + gh pr comment "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --body "Thank you for your contribution @${USERNAME}! :tada: We would like to hear from you about your experience contributing to OpenTelemetry by taking a few minutes to fill out this [survey](${SURVEY_URL})." + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + SURVEY_URL: https://docs.google.com/forms/d/e/1FAIpQLSf2FfCsW-DimeWzdQgfl0KDzT2UEAqu69_f7F2BVPSxVae1cQ/viewform?entry.1540511742=${{ github.repository }} + USERNAME: ${{ github.event.pull_request.user.login }} diff --git a/.github/workflows/verifyaotcompat.yml b/.github/workflows/verifyaotcompat.yml index 24991b9fe3c..affabafaf72 100644 --- a/.github/workflows/verifyaotcompat.yml +++ b/.github/workflows/verifyaotcompat.yml @@ -5,21 +5,24 @@ name: Publish & Verify AOT Compatibility on: workflow_call: +permissions: + contents: read + 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, windows-latest ] - version: [ net8.0 ] + os: [ ubuntu-22.04, windows-latest ] + version: [ net8.0, net9.0 ] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup dotnet - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 - name: publish AOT testApp, assert static analysis warning count, and run the app shell: pwsh diff --git a/.gitignore b/.gitignore index e06229e460f..851ee867e69 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ x64/ x86/ bld/ +[Aa]rtifacts/ [Bb]in/ [Oo]bj/ [Ll]og/ @@ -351,3 +352,6 @@ tempo-data/ # Coyote Rewrite Files rewrite.coyote.json + +# Test results +TestResults/ diff --git a/.markdownlint.yaml b/.markdownlint.yaml index c388b69fd5f..6efb6aeb05c 100644 --- a/.markdownlint.yaml +++ b/.markdownlint.yaml @@ -10,3 +10,6 @@ MD013: MD033: # Allowed elements allowed_elements: [ 'details', 'summary' ] + +# MD059/link-text-should-be-descriptive : Inline HTML : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md059.md +MD059: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7a9590b068a..9ca8868d46a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,16 +10,39 @@ for a summary description of past meetings. To request edit access, join the meeting or get in touch on [Slack](https://cloud-native.slack.com/archives/C01N3BC2W7Q). -Even though, anybody can contribute, there are benefits of being a member of our -community. See to the [community membership +Anyone may contribute but there are benefits of being a member of our community. +See the [community membership document](https://github.com/open-telemetry/community/blob/main/community-membership.md) on how to become a -[**Member**](https://github.com/open-telemetry/community/blob/main/community-membership.md#member), -[**Approver**](https://github.com/open-telemetry/community/blob/main/community-membership.md#approver) +[**Member**](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#member), +[**Triager**](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#triager), +[**Approver**](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#approver), and -[**Maintainer**](https://github.com/open-telemetry/community/blob/main/community-membership.md#maintainer). +[**Maintainer**](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#maintainer). -## Find a Buddy and Get Started Quickly +## Give feedback + +We are always looking for your feedback. + +You can do this by [submitting a GitHub issue](https://github.com/open-telemetry/opentelemetry-dotnet/issues/new). + +You may also prefer writing on [#otel-dotnet Slack channel](https://cloud-native.slack.com/archives/C01N3BC2W7Q). + +### Report a bug + +Reporting bugs is an important contribution. Please make sure to include: + +* Expected and actual behavior; +* OpenTelemetry, OS, and .NET versions you are using; +* Steps to reproduce; +* [Minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). + +### Request a feature + +If you would like to work on something that is not listed as an issue +(e.g. a new feature or enhancement) please create an issue and describe your proposal. + +## Find a buddy and get started quickly If you are looking for someone to help you find a starting point and be a resource for your first contribution, join our Slack channel and find a buddy! @@ -34,17 +57,24 @@ resource for your first contribution, join our Slack channel and find a buddy! Your OpenTelemetry buddy is your resource to talk to directly on all aspects of contributing to OpenTelemetry: providing context, reviewing PRs, and helping -those get merged. Buddies will not be available 24/7, but is committed to -responding during their normal contribution hours. +those get merged. Buddies will not be available 24/7, but are committed to +responding during their normal working hours. ## Development Environment -You can contribute to this project from a Windows, macOS or Linux machine. +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 8.0 +* Git client and command line tools + +* [.NET SDK (latest stable version)](https://dotnet.microsoft.com/download) + + > [!NOTE] + > At times a pre-release version of the .NET SDK may be required to build code + in this repository. Check + [global.json](https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/global.json) + to verify the current required version. ### Linux or MacOS @@ -59,42 +89,97 @@ of Windows. * Visual Studio 2022+ or Visual Studio Code * .NET Framework 4.6.2+ -### Public API - -It is critical to keep public API surface small and clean. This repository is -using `Microsoft.CodeAnalysis.PublicApiAnalyzers` to validate the public APIs. -This analyzer will check if you changed a public property/method so the change -will be easily spotted in pull request. It will also ensure that OpenTelemetry -doesn't expose APIs outside of the library primary concerns like a generic -helper methods. - -#### How to enable and configure - -* Create a folder in your project called `.publicApi` with the frameworks that - as folders you target. -* Create two files called `PublicAPI.Shipped.txt` and `PublicAPI.Unshipped.txt` - in each framework that you target. -* Add the following lines to your csproj: - -```xml - - - - -``` - -* Use - [IntelliSense](https://docs.microsoft.com/visualstudio/ide/using-intellisense) - to update the publicApi files. +## Public API validation + +It is critical to **NOT** make breaking changes to public APIs which have been +released in stable builds. We also strive to keep a minimal public API surface. +This repository is using +[Microsoft.CodeAnalysis.PublicApiAnalyzers](https://github.com/dotnet/roslyn-analyzers/blob/main/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) +and [Package +validation](https://learn.microsoft.com/dotnet/fundamentals/apicompat/package-validation/overview) +to validate public APIs. + +* `Microsoft.CodeAnalysis.PublicApiAnalyzers` will validate public API + changes/additions against a set of "public API files" which capture the + shipped/unshipped public APIs. These files must be maintained manually (not + recommended) or by using tooling/code fixes built into the package (see below + for details). + + Public API files are also used to perform public API reviews by repo + approvers/maintainers before releasing stable builds. + +* `Package validation` will validate public API changes/additions against + previously released NuGet packages. + + This is performed automatically by the build/CI + [package-validation](https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/.github/workflows/package-validation.yml) + workflow. + + By default package validation is **NOT** run for local builds. To enable + package validation in local builds set the `EnablePackageValidation` property + to `true` in + [Common.prod.props](https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/build/Common.prod.props) + (please do not check in this change). + +### Working with Microsoft.CodeAnalysis.PublicApiAnalyzers + +#### Update public API files when writing code + +[IntelliSense](https://docs.microsoft.com/visualstudio/ide/using-intellisense) +will [suggest +modifications](https://github.com/dotnet/roslyn-analyzers/issues/3322#issuecomment-591031429) +to the `PublicAPI.Unshipped.txt` file when you make changes. After reviewing +these changes, ensure they are reflected across all targeted frameworks. You can +do this by: + +* Using the "Fix all occurrences in Project" feature in Visual Studio. + +* Manually cycling through each framework using Visual Studio's target framework + dropdown (in the upper right corner) and applying the IntelliSense + suggestions. + +> [!IMPORTANT] +> Do **NOT** modify `PublicAPI.Shipped.txt` files. New features and bug fixes + **SHOULD** only require changes to `PublicAPI.Unshipped.txt` files. If you + have to modify a "shipped" file it likely means you made a mistake and broke a + stable API. Typically only maintainers modify the `PublicAPI.Shipped.txt` file + while performing stable releases. If you need help reach out to an approver or + maintainer on Slack or open a draft PR. + +#### Enable public API validation in new projects + +1. If you are **NOT** using experimental APIs: + * If your API is the same across all target frameworks: + * You only need two files: `.publicApi/PublicAPI.Shipped.txt` and + `.publicApi/PublicAPI.Unshipped.txt`. + * If your APIs differ between target frameworks: + * Place the shared APIs in `.publicApi/PublicAPI.Shipped.txt` and + `.publicApi/PublicAPI.Unshipped.txt`. + * Create framework-specific files for API differences (e.g., + `.publicApi/net462/PublicAPI.Shipped.txt` and + `.publicApi/net462/PublicAPI.Unshipped.txt`). + +2. If you are using experimental APIs: + * Follow the rules above, but create an additional layer in your folder + structure: + * For stable APIs: `.publicApi/Stable/*`. + * For experimental APIs: `.publicApi/Experimental/*`. + * The `Experimental` folder should contain APIs that are public only in + pre-release builds. Typically the `Experimental` folder only contains + `PublicAPI.Unshipped.txt` files as experimental APIs are never shipped + stable. + + Example folder structure can be found + [here](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src/OpenTelemetry.Api/.publicApi). ## Pull Requests -### How to Send Pull Requests +### How to create pull requests Everyone is welcome to contribute code to `opentelemetry-dotnet` via GitHub pull requests (PRs). -To create a new PR, fork the project in GitHub and clone the upstream repo: +To create a new PR, fork the project on GitHub and clone the upstream repo: ```sh git clone https://github.com/open-telemetry/opentelemetry-dotnet.git @@ -125,7 +210,7 @@ If you made changes to the Markdown documents (`*.md` files), install the latest markdownlint . ``` -Check out a new branch, make modifications and push the branch to your fork: +Check out a new branch, make modifications, and push the branch to your fork: ```sh $ git checkout -b feature @@ -136,20 +221,26 @@ $ git push fork feature Open a pull request against the main `opentelemetry-dotnet` repo. -### How to Receive Comments +#### Tips and best practices for pull requests * If the PR is not ready for review, please mark it as [`draft`](https://github.blog/2019-02-14-introducing-draft-pull-requests/). * Make sure CLA is signed and all required CI checks are clear. -* Submit small, focused PRs addressing a single - concern/issue. +* Submit small, focused PRs addressing a single concern/issue. * Make sure the PR title reflects the contribution. * Write a summary that helps understand the change. * Include usage examples in the summary, where applicable. * Include benchmarks (before/after) in the summary, for contributions that are performance enhancements. +* We are open to bot generated PRs or AI/LLM assisted PRs. Actually, we are + using + [dependabot](https://docs.github.com/en/code-security/dependabot/dependabot-security-updates/configuring-dependabot-security-updates) + to automate the security updates. However, if you use bots to generate spam + PRs (e.g. incorrect, noisy, non-improvements, unintelligible, trying to sell + your product, etc.), we might close the PR right away with a warning, and if + you keep doing so, we might block your user account. -### How to Get PRs Merged +### How to get pull requests merged A PR is considered to be **ready to merge** when: @@ -157,18 +248,21 @@ A PR is considered to be **ready to merge** when: [Approvers](https://github.com/open-telemetry/community/blob/main/community-membership.md#approver). / [Maintainers](https://github.com/open-telemetry/community/blob/main/community-membership.md#maintainer). -* Major feedbacks are resolved. +* Major feedback/comments are resolved. * It has been open for review for at least one working day. This gives people reasonable time to review. -* Trivial change (typo, cosmetic, doc, etc.) doesn't have to wait for one day. -* Urgent fix can take exception as long as it has been actively communicated. + * Trivial change (typo, cosmetic, doc, etc.) doesn't have to wait for one day. + * Urgent fix can take exception as long as it has been actively communicated. -Any Maintainer can merge the PR once it is **ready to merge**. Note, that some -PRs may not be merged immediately if the repo is in the process of a release and -the maintainers decided to defer the PR to the next release train. +Any maintainer can merge PRs once they are **ready to merge** however +maintainers might decide to wait on merging changes until there are more +approvals and/or dicussion, or based on other factors such as release timing and +risk to users. For example if a stable release is planned and a new change is +introduced adding public API(s) or behavioral changes it might be held until the +next alpha/beta release. -If a PR has been stuck (e.g. there are lots of debates and people couldn't agree -on each other), the owner should try to get people aligned by: +If a PR has become stuck (e.g. there is a lot of debate and people couldn't +agree on the direction), the owner should try to get people aligned by: * Consolidating the perspectives and putting a summary in the PR. It is recommended to add a link into the PR description, which points to a comment @@ -183,7 +277,7 @@ on each other), the owner should try to get people aligned by: the owner should bring it to the OpenTelemetry .NET SIG [meeting](README.md#contributing). -## Design Choices +## Design choices As with other OpenTelemetry clients, opentelemetry-dotnet follows the [opentelemetry-specification](https://github.com/open-telemetry/opentelemetry-specification). @@ -191,7 +285,7 @@ As with other OpenTelemetry clients, opentelemetry-dotnet follows the It's especially valuable to read through the [library guidelines](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/library-guidelines.md). -### Focus on Capabilities, Not Structure Compliance +### Focus on capabilities not structural compliance OpenTelemetry is an evolving specification, one where the desires and use cases are clear, but the method to satisfy those uses cases are not. @@ -205,10 +299,10 @@ than conform to specific API names or argument patterns in the spec. For a deeper discussion, see [this spec issue](https://github.com/open-telemetry/opentelemetry-specification/issues/165). -## Style Guide +## Style guide This project includes a [`.editorconfig`](./.editorconfig) file which is -supported by all the IDEs/editor mentioned above. It works with the IDE/editor +supported by all the IDEs/editors mentioned above. It works with the IDE/editor only and does not affect the actual build of the project. This repository also includes StyleCop ruleset files under the `./build` folder. @@ -229,31 +323,11 @@ types](https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-type * Pass [static 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] > 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. -## New code - -New code files MUST enable [nullable reference -types](https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-types/nullable-reference-types) -manually in projects where it is not automatically enabled project-wide. This is -done by specifying `#nullable enable` towards the top of the file (usually after -the copyright header). We are currently working towards enabling nullable -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] -> 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 diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000000..e157295e687 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,6 @@ + + + $([System.IO.Path]::Combine('$(MSBuildThisFileDirectory)', 'artifacts')) + true + + diff --git a/Directory.Packages.props b/Directory.Packages.props index 032d5eb04a2..0e3f4ce78a8 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,7 +1,8 @@ + true - 1.8.1 + 1.12.0 - - - - - - + + + + - - - - - - - - - - - - - - - + + + + + + + - + + - - + + - + + - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + diff --git a/NuGet.config b/NuGet.config index ca17f1b8088..ec945cd0620 100644 --- a/NuGet.config +++ b/NuGet.config @@ -3,19 +3,14 @@ - - - - - diff --git a/OpenTelemetry.sln b/OpenTelemetry.sln index 61963889e01..0384bcce8aa 100644 --- a/OpenTelemetry.sln +++ b/OpenTelemetry.sln @@ -12,12 +12,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .dockerignore = .dockerignore .editorconfig = .editorconfig .gitignore = .gitignore - .github\workflows\ci-concurrency.yml = .github\workflows\ci-concurrency.yml CONTRIBUTING.md = CONTRIBUTING.md global.json = global.json LICENSE.TXT = LICENSE.TXT NuGet.config = NuGet.config README.md = README.md + RELEASENOTES.md = RELEASENOTES.md THIRD-PARTY-NOTICES.TXT = THIRD-PARTY-NOTICES.TXT VERSIONING.md = VERSIONING.md EndProjectSection @@ -27,12 +27,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{7CB2F02E build\Common.nonprod.props = build\Common.nonprod.props build\Common.prod.props = build\Common.prod.props build\Common.props = build\Common.props + build\Common.targets = build\Common.targets 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\docker-compose.net9.0.yml = build\docker-compose.net9.0.yml 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 @@ -73,6 +73,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{F1D0 ProjectSection(SolutionItems) = preProject .github\codecov.yml = .github\codecov.yml .github\CODEOWNERS = .github\CODEOWNERS + .github\dependabot.yml = .github\dependabot.yml .github\PULL_REQUEST_TEMPLATE.md = .github\PULL_REQUEST_TEMPLATE.md EndProjectSection EndProject @@ -80,7 +81,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ISSUE_TEMPLATE", "ISSUE_TEM ProjectSection(SolutionItems) = preProject .github\ISSUE_TEMPLATE\bug_report.yml = .github\ISSUE_TEMPLATE\bug_report.yml .github\ISSUE_TEMPLATE\feature_request.yml = .github\ISSUE_TEMPLATE\feature_request.yml - .github\ISSUE_TEMPLATE\question.yml = .github\ISSUE_TEMPLATE\question.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{E69578EB-B456-4062-A645-877CD964528B}" @@ -113,7 +113,6 @@ 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}") = "Examples.Console", "examples\Console\Examples.Console.csproj", "{FF3E6E08-E8E4-4523-B526-847CD989279F}" @@ -123,14 +122,20 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "test\Benchmarks\Benchmarks.csproj", "{DE9130A4-F30A-49D7-8834-41DE3021218B}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{7C87CAF9-79D7-4C26-9FFB-F3F1FB6911F1}" + ProjectSection(SolutionItems) = preProject + docs\README.md = docs\README.md + EndProjectSection 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 + examples\Directory.Build.targets = examples\Directory.Build.targets EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "trace", "trace", "{5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818}" + ProjectSection(SolutionItems) = preProject + docs\trace\README.md = docs\trace\README.md + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "metrics", "metrics", "{3277B1C0-BDFE-4460-9B0D-D9A661FB48DB}" ProjectSection(SolutionItems) = preProject @@ -249,9 +254,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A49299 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\ResourceSemanticConventions.cs = src\Shared\ResourceSemanticConventions.cs src\Shared\SemanticConventions.cs = src\Shared\SemanticConventions.cs src\Shared\SpanAttributeConstants.cs = src\Shared\SpanAttributeConstants.cs @@ -275,7 +278,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Options", "Options", "{4949 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shims", "Shims", "{A0CB9A10-F22D-4E66-A449-74B3D0361A9C}" ProjectSection(SolutionItems) = preProject + src\Shared\Shims\ExperimentalAttribute.cs = src\Shared\Shims\ExperimentalAttribute.cs src\Shared\Shims\IsExternalInit.cs = src\Shared\Shims\IsExternalInit.cs + src\Shared\Shims\Lock.cs = src\Shared\Shims\Lock.cs src\Shared\Shims\NullableAttributes.cs = src\Shared\Shims\NullableAttributes.cs EndProjectSection EndProject @@ -299,7 +304,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "experimental-apis", "experi 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\OTEL1003.md = docs\diagnostics\experimental-apis\OTEL1003.md docs\diagnostics\experimental-apis\OTEL1004.md = docs\diagnostics\experimental-apis\OTEL1004.md docs\diagnostics\experimental-apis\README.md = docs\diagnostics\experimental-apis\README.md EndProjectSection @@ -340,6 +344,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{4498 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "exemplars", "docs\metrics\exemplars\exemplars.csproj", "{79C12C80-B27B-41FB-AE79-A3BB74CFA782}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Proto", "Proto", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" + ProjectSection(SolutionItems) = preProject + src\Shared\Proto\README.md = src\Shared\Proto\README.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -656,6 +665,7 @@ Global {993E65E5-E71B-40FD-871C-60A9EBD59724} = {A49299FB-C5CD-4E0E-B7E1-B7867BBD67CC} {44982E0D-C8C6-42DC-9F8F-714981F27CE6} = {7CB2F02E-03FA-4FFF-89A5-C51F107623FD} {79C12C80-B27B-41FB-AE79-A3BB74CFA782} = {3277B1C0-BDFE-4460-9B0D-D9A661FB48DB} + {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {A49299FB-C5CD-4E0E-B7E1-B7867BBD67CC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {55639B5C-0770-4A22-AB56-859604650521} diff --git a/README.md b/README.md index 06490b1eeb3..303f2ce3f79 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,32 @@ [![NuGet](https://img.shields.io/nuget/dt/OpenTelemetry.svg)](https://www.nuget.org/profiles/OpenTelemetry) [![Build](https://github.com/open-telemetry/opentelemetry-dotnet/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/open-telemetry/opentelemetry-dotnet/actions/workflows/ci.yml) -The .NET [OpenTelemetry](https://opentelemetry.io/) client. +[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/open-telemetry/opentelemetry-dotnet/badge)](https://scorecard.dev/viewer/?uri=github.com/open-telemetry/opentelemetry-dotnet) +[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/10017/badge)](https://www.bestpractices.dev/projects/10017) +[![FOSSA License Status](https://app.fossa.com/api/projects/custom%2B162%2Fgithub.com%2Fopen-telemetry%2Fopentelemetry-dotnet.svg?type=shield&issueType=license)](https://app.fossa.com/projects/custom%2B162%2Fgithub.com%2Fopen-telemetry%2Fopentelemetry-dotnet?ref=badge_shield&issueType=license) +[![FOSSA Security Status](https://app.fossa.com/api/projects/custom%2B162%2Fgithub.com%2Fopen-telemetry%2Fopentelemetry-dotnet.svg?type=shield&issueType=security)](https://app.fossa.com/projects/custom%2B162%2Fgithub.com%2Fopen-telemetry%2Fopentelemetry-dotnet?ref=badge_shield&issueType=security) -## Supported .NET Versions +The .NET [OpenTelemetry](https://opentelemetry.io/) implementation. + +
+Table of Contents + +* [Supported .NET versions](#supported-net-versions) +* [Project status](#project-status) +* [Getting started](#getting-started) + * [Getting started with Logging](#getting-started-with-logging) + * [Getting started with Metrics](#getting-started-with-metrics) + * [Getting started with Tracing](#getting-started-with-tracing) +* [Repository structure](#repository-structure) +* [Troubleshooting](#troubleshooting) +* [Extensibility](#extensibility) +* [Releases](#releases) +* [Contributing](#contributing) +* [References](#references) + +
+ +## Supported .NET versions Packages shipped from this repository generally support all the officially supported versions of [.NET](https://dotnet.microsoft.com/download/dotnet) and @@ -17,36 +40,88 @@ older Windows-based .NET implementation), except `.NET Framework 3.5`. Any exceptions to this are noted in the individual `README.md` files. -## Project Status +## Project status -**Stable** across all 3 signals i.e. `Logs`, `Metrics`, and `Traces`. +**Stable** across all 3 signals (`Logs`, `Metrics`, and `Traces`). -See [Spec Compliance -Matrix](https://github.com/open-telemetry/opentelemetry-specification/blob/main/spec-compliance-matrix.md) -to understand which portions of the specification has been implemented in this -repo. +> [!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. -## Getting Started +To understand which portions of the [OpenTelemetry +Specification](https://github.com/open-telemetry/opentelemetry-specification) +have been implemented in OpenTelemetry .NET see: [Spec Compliance +Matrix](https://github.com/open-telemetry/opentelemetry-specification/blob/main/spec-compliance-matrix.md). -If you are new here, please read the getting started docs: +## Getting started -* [Logs](./docs/logs/README.md) -* [Metrics](./docs/metrics/README.md) -* [Traces](./docs/trace/README.md) +If you are new here, please read the getting started docs: -This repository includes multiple installable components, available on -[NuGet](https://www.nuget.org/profiles/OpenTelemetry). Each component has its -individual `README.md` file, which covers the instruction on how to install and -how to get started. To find all the available components, please take a look at -the `src` folder. +### Getting started with Logging + +If you are new to +[logging](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/README.md), +it is recommended to first follow the [getting started in 5 minutes - ASP.NET +Core Application](./docs/logs/getting-started-aspnetcore/README.md) guide or +the [getting started in 5 minutes - Console +Application](./docs/logs/getting-started-console/README.md) guide to get up +and running. + +For general information and best practices see: [OpenTelemetry .NET +Logs](./docs/logs/README.md). For a more detailed explanation of SDK logging +features see: [Customizing OpenTelemetry .NET SDK for +Logs](./docs/logs/customizing-the-sdk/README.md). + +### Getting started with Metrics + +If you are new to +[metrics](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/README.md), +it is recommended to first follow the [getting started in 5 minutes - ASP.NET +Core Application](./docs/metrics/getting-started-aspnetcore/README.md) guide +or the [getting started in 5 minutes - Console +Application](./docs/metrics/getting-started-console/README.md) guide to get +up and running. + +For general information and best practices see: [OpenTelemetry .NET +Metrics](./docs/metrics/README.md). For a more detailed explanation of SDK +metric features see: [Customizing OpenTelemetry .NET SDK for +Metrics](./docs/metrics/customizing-the-sdk/README.md). + +### Getting started with Tracing + +If you are new to +[traces](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/README.md), +it is recommended to first follow the [getting started in 5 minutes - ASP.NET +Core Application](./docs/trace/getting-started-aspnetcore/README.md) guide +or the [getting started in 5 minutes - Console +Application](./docs/trace/getting-started-console/README.md) guide to get up +and running. + +For general information and best practices see: [OpenTelemetry .NET +Traces](./docs/trace/README.md). For a more detailed explanation of SDK tracing +features see: [Customizing OpenTelemetry .NET SDK for +Tracing](./docs/trace/customizing-the-sdk/README.md). + +## Repository structure + +This repository includes only what is defined in the [OpenTelemetry +Specification](https://github.com/open-telemetry/opentelemetry-specification) +and is shipped as separate packages through +[NuGet](https://www.nuget.org/profiles/OpenTelemetry). Each component has an +individual `README.md` and `CHANGELOG.md` file which covers the instructions on +how to install and get started, and details about the individual changes made +(respectively). To find all the available components, please take a look at the +`src` folder. Here are the most commonly used components: -* [OpenTelemetry .NET API](./src/OpenTelemetry.Api/README.md) -* [OpenTelemetry .NET SDK](./src/OpenTelemetry/README.md) - -[Instrumentation libraries](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#instrumentation-library) -can be found in [contrib repository](https://github.com/open-telemetry/opentelemetry-dotnet-contrib). +* [OpenTelemetry API](./src/OpenTelemetry.Api/README.md) +* [OpenTelemetry SDK](./src/OpenTelemetry/README.md) +* [OpenTelemetry Hosting + Extensions](./src/OpenTelemetry.Extensions.Hosting/README.md) Here are the [exporter libraries](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#exporter-library): @@ -59,16 +134,19 @@ libraries](https://github.com/open-telemetry/opentelemetry-specification/blob/ma * [Prometheus HttpListener](./src/OpenTelemetry.Exporter.Prometheus.HttpListener/README.md) * [Zipkin](./src/OpenTelemetry.Exporter.Zipkin/README.md) -See the [OpenTelemetry -registry](https://opentelemetry.io/ecosystem/registry/?language=dotnet) and -[OpenTelemetry .NET Contrib -repo](https://github.com/open-telemetry/opentelemetry-dotnet-contrib) for more -components. +Additional packages including [instrumentation +libraries](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#instrumentation-library), +exporters, resource detectors, and extensions can be found in the +[opentelemetry-dotnet-contrib +repository](https://github.com/open-telemetry/opentelemetry-dotnet-contrib) +and/or the [OpenTelemetry +registry](https://opentelemetry.io/ecosystem/registry/?language=dotnet). ## Troubleshooting -See [Troubleshooting](./src/OpenTelemetry/README.md#troubleshooting). -Additionally check readme file for the individual components for any additional +For general instructions see: +[Troubleshooting](./src/OpenTelemetry/README.md#troubleshooting). Additionally +`README.md` files for individual components may contain more detailed troubleshooting information. ## Extensibility @@ -80,7 +158,7 @@ extension scenarios: library](./docs/trace/extending-the-sdk/README.md#instrumentation-library). * Building a custom exporter for [logs](./docs/logs/extending-the-sdk/README.md#exporter), - [metrics](./docs/metrics/extending-the-sdk/README.md#exporter) and + [metrics](./docs/metrics/extending-the-sdk/README.md#exporter), and [traces](./docs/trace/extending-the-sdk/README.md#exporter). * Building a custom processor for [logs](./docs/logs/extending-the-sdk/README.md#processor) and @@ -88,9 +166,59 @@ extension scenarios: * Building a custom sampler for [traces](./docs/trace/extending-the-sdk/README.md#sampler). +## Releases + +For details about upcoming planned releases see: +[Milestones](https://github.com/open-telemetry/opentelemetry-dotnet/milestones). +The dates and features described in issues and milestones are estimates and +subject to change. + +For highlights and annoucements for stable releases see: [Release +Notes](./RELEASENOTES.md). + +To access packages, source code, and/or view a list of changes for all +components in a release see: +[Releases](https://github.com/open-telemetry/opentelemetry-dotnet/releases). + +Nightly builds from this repo are published to [MyGet](https://www.myget.org), +and can be installed using the +`https://www.myget.org/F/opentelemetry/api/v3/index.json` source. + +### Digital signing + +Starting with the `1.10.0` release the DLLs included in the packages pushed to +NuGet are digitally signed using [Sigstore](https://www.sigstore.dev/). Within +each NuGet package the digital signature and its corresponding certificate file +are placed alongside the shipped DLL(s) in the `/lib` folder. When a project +targets multiple frameworks each target outputs a dedicated DLL and signing +artifacts into a sub folder based on the +[TFM](https://learn.microsoft.com/dotnet/standard/frameworks). + +The digitial signature and certificate files share the same name prefix as the +DLL to ensure easy identification and association. + +To verify the integrity of a DLL inside a NuGet package use the +[cosign](https://github.com/sigstore/cosign) tool from Sigstore: + +```bash +cosign verify-blob \ + --signature OpenTelemetry.dll-keyless.sig \ + --certificate OpenTelemetry.dll-keyless.pem.cer \ + --certificate-identity "https://github.com/open-telemetry/opentelemetry-dotnet/.github/workflows/publish-packages-1.0.yml@refs/tags/core-1.10.0-rc.1" \ + --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ + OpenTelemetry.dll +``` + +> [!NOTE] +> A successful verification outputs `Verify OK`. + +For more verification options please refer to the [cosign +documentation](https://github.com/sigstore/cosign/blob/main/doc/cosign_verify-blob.md). + ## Contributing -See [CONTRIBUTING.md](CONTRIBUTING.md) +For information about contributing to the project see: +[CONTRIBUTING.md](CONTRIBUTING.md). 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' @@ -108,59 +236,60 @@ 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)): +### Maintainers * [Alan West](https://github.com/alanwest), New Relic -* [Mikel Blanchard](https://github.com/CodeBlanch), Microsoft +* [Piotr Kiełkowicz](https://github.com/Kielek), Splunk +* [Rajkumar Rangaraj](https://github.com/rajkumar-rangaraj), Microsoft + +For more information about the maintainer role, see the [community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#maintainer). -[Approvers](https://github.com/open-telemetry/community/blob/main/community-membership.md#approver) -([@open-telemetry/dotnet-approvers](https://github.com/orgs/open-telemetry/teams/dotnet-approvers)): +### Approvers * [Cijo Thomas](https://github.com/cijothomas), Microsoft -* [Reiley Yang](https://github.com/reyang), Microsoft -* [Utkarsh Umesan Pillai](https://github.com/utpilla), Microsoft -* [Vishwesh Bankwar](https://github.com/vishweshbankwar), Microsoft +* [Martin Costello](https://github.com/martincostello), Grafana Labs +* [Mikel Blanchard](https://github.com/CodeBlanch), Microsoft + +For more information about the approver role, see the [community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#approver). -[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)): +### Triagers * [Martin Thwaites](https://github.com/martinjt), Honeycomb -* [Piotr Kiełkowicz](https://github.com/Kielek), Splunk +* [Timothy "Mothra" Lee](https://github.com/TimothyMothra) + +For more information about the triager role, see the [community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#triager). -[Emeritus -Maintainer/Approver/Triager](https://github.com/open-telemetry/community/blob/main/community-membership.md#emeritus-maintainerapprovertriager): +### Emeritus Maintainers + +* [Mike Goldsmith](https://github.com/MikeGoldsmith) +* [Sergey Kanzhelev](https://github.com/SergeyKanzhelev) +* [Utkarsh Umesan Pillai](https://github.com/utpilla) + +For more information about the emeritus role, see the [community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#emeritus-maintainerapprovertriager). + +### Emeritus Approvers * [Bruno Garcia](https://github.com/bruno-garcia) * [Eddy Nakamura](https://github.com/eddynaka) * [Liudmila Molkova](https://github.com/lmolkova) -* [Mike Goldsmith](https://github.com/MikeGoldsmith) * [Paulo Janotti](https://github.com/pjanotti) +* [Reiley Yang](https://github.com/reyang) * [Robert Pająk](https://github.com/pellared) -* [Sergey Kanzhelev](https://github.com/SergeyKanzhelev) -* [Victor Lu](https://github.com/victlu) +* [Vishwesh Bankwar](https://github.com/vishweshbankwar) -### Thanks to all the people who have contributed +For more information about the emeritus role, see the [community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#emeritus-maintainerapprovertriager). -[![contributors](https://contributors-img.web.app/image?repo=open-telemetry/opentelemetry-dotnet)](https://github.com/open-telemetry/opentelemetry-dotnet/graphs/contributors) +### Emeritus Triagers -## Release Schedule +* [Victor Lu](https://github.com/victlu) -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. +For more information about the emeritus role, see the [community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#emeritus-maintainerapprovertriager). -See the [release -notes](https://github.com/open-telemetry/opentelemetry-dotnet/releases) for -existing releases. +### Thanks to all the people who have contributed -> [!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. +[![contributors](https://contributors-img.web.app/image?repo=open-telemetry/opentelemetry-dotnet)](https://github.com/open-telemetry/opentelemetry-dotnet/graphs/contributors) + +## References -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). +* [OpenTelemetry Project](https://opentelemetry.io/) +* [OpenTelemetry Specification](https://github.com/open-telemetry/opentelemetry-specification) diff --git a/RELEASENOTES.md b/RELEASENOTES.md new file mode 100644 index 00000000000..3a26d50aec8 --- /dev/null +++ b/RELEASENOTES.md @@ -0,0 +1,143 @@ +# Release Notes + +This file contains highlights and announcements covering all components. +For more details see `CHANGELOG.md` files maintained in the root source +directory of each individual package. + +## 1.13.0 + +Release details: [1.13.0](https://github.com/open-telemetry/opentelemetry-dotnet/releases/tag/core-1.13.0) + +* gRPC calls to export traces, logs, and metrics using `OtlpExportProtocol.Grpc` + now set the `TE=trailers` HTTP request header to improve interoperability. +* `EventName` is now exported by default as `EventName` instead of + `logrecord.event.name` when specified through `ILogger` or the experimental + log bridge API. + +## 1.12.0 + +Release details: [1.12.0](https://github.com/open-telemetry/opentelemetry-dotnet/releases/tag/core-1.12.0) + +* **Breaking Change**: `OpenTelemetry.Exporter.OpenTelemetryProtocol` now + defaults to using OTLP/HTTP instead of OTLP/gRPC when targeting .NET Framework + and .NET Standard. This change may cause telemetry export to fail unless + appropriate adjustments are made. Explicitly setting OTLP/gRPC may result in a + `NotSupportedException` unless further configuration is applied. See + [#6209](https://github.com/open-telemetry/opentelemetry-dotnet/issues/6209) for + full details and mitigation guidance. [#6229](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6229) + +## 1.11.1 + +Release details: [1.11.1](https://github.com/open-telemetry/opentelemetry-dotnet/releases/tag/core-1.11.1) + +* Fixed a bug preventing `OpenTelemetry.Exporter.OpenTelemetryProtocol` from + exporting telemetry on .NET Framework. + +## 1.11.0 + +Release details: [1.11.0](https://github.com/open-telemetry/opentelemetry-dotnet/releases/tag/core-1.11.0) + +* `OpenTelemetry.Exporter.OpenTelemetryProtocol` no longer depends on the + `Google.Protobuf`, `Grpc`, or `Grpc.Net.Client` packages. Serialization and + transmission of outgoing data is now performed manually to improve the overall + performance. + +## 1.10.0 + +Release details: [1.10.0](https://github.com/open-telemetry/opentelemetry-dotnet/releases/tag/core-1.10.0) + +* Bumped the package versions of `System.Diagnostic.DiagnosticSource` and other + Microsoft.Extensions.* packages to `9.0.0`. + +* Added support for new APIs introduced in `System.Diagnostics.DiagnosticSource` + `9.0.0`: + + * [InstrumentAdvice<T>](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.instrumentadvice-1) + + For details see: [Explicit bucket histogram + aggregation](./docs/metrics/customizing-the-sdk/README.md#explicit-bucket-histogram-aggregation). + + * [Gauge<T>](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.gauge-1) + + * [ActivitySource.Tags](https://learn.microsoft.com/dotnet/api/system.diagnostics.activitysource.tags) + (supported in OtlpExporter & ConsoleExporter) + +* Experimental features promoted to stable: + + * `CardinalityLimit` can now be managed for individual metrics via the View + API. For details see: [Changing cardinality limit for a + Metric](./docs/metrics/customizing-the-sdk/README.md#changing-the-cardinality-limit-for-a-metric). + + * The [overflow + attribute](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#overflow-attribute) + (`otel.metric.overflow`) behavior is now enabled by default. The + `OTEL_DOTNET_EXPERIMENTAL_METRICS_EMIT_OVERFLOW_ATTRIBUTE` environment + variable is no longer required. For details see: [Cardinality + Limits](./docs/metrics/README.md#cardinality-limits). + + * The MetricPoint reclaim behavior is now enabled by default when Delta + aggregation temporality is used. The + `OTEL_DOTNET_EXPERIMENTAL_METRICS_RECLAIM_UNUSED_METRIC_POINTS` environment + variable is no longer required. For details see: [Cardinality + Limits](./docs/metrics/README.md#cardinality-limits). + +* Added `OpenTelemetrySdk.Create` API for configuring OpenTelemetry .NET signals + (logging, tracing, and metrics) via a single builder. This new API simplifies + bootstrap and teardown, and supports cross-cutting extensions targeting + `IOpenTelemetryBuilder`. + +* Removed out of support `net6.0` target and added `net9.0` target. + +## 1.9.0 + +Release details: [1.9.0](https://github.com/open-telemetry/opentelemetry-dotnet/releases/tag/core-1.9.0) + +* `Exemplars` are now part of the stable API! For details see: [customizing + exemplars + collection](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/metrics/customizing-the-sdk#exemplars). + +* `WithLogging` is now part of the stable API! Logging, Metrics, and Tracing can + now all be configured using the `With` style and the builders finally have + parity in their APIs. + +## 1.8.0 + +Release details: [1.8.0](https://github.com/open-telemetry/opentelemetry-dotnet/releases/tag/core-1.8.0) + +* `TracerProvider` sampler can now be configured via the `OTEL_TRACES_SAMPLER` & + `OTEL_TRACES_SAMPLER_ARG` envvars. + +* A new `UseOtlpExporter` cross-cutting extension has been added to register the + `OtlpExporter` and enable all signals in a single call. + +* `exception.type`, `exception.message`, `exception.stacktrace` will now + automatically be included by the `OtlpLogExporter` when logging exceptions. + Previously an experimental environment variable had to be set. + +## 1.7.0 + +Release details: [1.7.0](https://github.com/open-telemetry/opentelemetry-dotnet/releases/tag/core-1.7.0) + +* Bumped the package versions of System.Diagnostic.DiagnosticSource and other + Microsoft.Extensions.* packages to `8.0.0`. + +* Added `net8.0` targets to all the components. + +* OTLP Exporter + * Updated to use `ILogger` `CategoryName` as the instrumentation scope for + logs. + * Added named options support for OTLP Log Exporter. + * Added support for instrumentation scope attributes in metrics. + * Added support under an experimental flag to emit log exception attributes. + * Added support under an experimental flag to emit log eventId and eventName. + attributes. + +* Added support for the + [IMetricsBuilder](https://learn.microsoft.com/dotnet/api/microsoft.extensions.diagnostics.metrics.imetricsbuilder) + API. + +* Added an experimental opt-in metrics feature to reclaim unused MetricPoints + which enables a higher number of unique dimension combinations to be emitted. + See [reclaim unused metric + points](https://github.com/open-telemetry/opentelemetry-dotnet/blob/32c64d04defb5c92d056fd8817638151168b10da/docs/metrics/README.md#cardinality-limits) + for more details. diff --git a/THIRD-PARTY-NOTICES.TXT b/THIRD-PARTY-NOTICES.TXT index b65f5723308..38eb675f542 100644 --- a/THIRD-PARTY-NOTICES.TXT +++ b/THIRD-PARTY-NOTICES.TXT @@ -29,3 +29,20 @@ 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. + +License notice for gRPC for .NET (https://github.com/grpc/grpc-dotnet) +---------------------------------------------------------------------------------------------- + +Copyright 2019 The gRPC Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/build/Common.nonprod.props b/build/Common.nonprod.props index 6de6e014c88..d5a959fd253 100644 --- a/build/Common.nonprod.props +++ b/build/Common.nonprod.props @@ -2,13 +2,13 @@ - $(NoWarn),1574,1591 + $(NoWarn),CS1574,CS1591 false $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), 'OpenTelemetry.sln'))\build\OpenTelemetry.test.ruleset - net8.0 + net9.0 @@ -21,6 +21,18 @@ + + + + + + + + + + + + diff --git a/build/Common.prod.props b/build/Common.prod.props index 2f7b7cc900f..5a13bf20617 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -3,6 +3,9 @@ $(MSBuildThisFileDirectory)/OpenTelemetry.prod.ruleset + + + true false @@ -15,13 +18,15 @@ https://opentelemetry.io OpenTelemetry Authors Copyright The OpenTelemetry Authors - $(Build_ArtifactStagingDirectory) true snupkg Apache-2.0 true $(RepoRoot)\LICENSE.TXT $(RepoRoot)\THIRD-PARTY-NOTICES.TXT + README.md + CHANGELOG.md + $(RepoRoot)\RELEASENOTES.md @@ -32,7 +37,6 @@ - @@ -47,7 +51,20 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + \[([^]]+?)\]\(\.(.+?)\) + $(GitOriginConsoleOutput.Replace('.git','')) + $(GitHubRepoUrl)/blob/$(PackTag) + $(GitHubRepoUrl)/blob/$(GitCommitConsoleOutput) + + + + + + + + + + + + + + + + <_PackageReleaseNotesFilePath>$([System.IO.Path]::GetFullPath('$(PackageReleaseNotesFile)').Replace('$(RepoRoot)', '').Replace('\', '/')) + <_PackageChangelogFilePath>$([System.IO.Path]::GetFullPath('$(PackageChangelogFile)').Replace('$(RepoRoot)', '').Replace('\', '/')) + + For highlights and announcements see: $(GitHubPermalinkUrl)$(_PackageReleaseNotesFilePath). + + For detailed changes see: $(GitHubPermalinkUrl)$(_PackageChangelogFilePath). + + + + + + + + - $(NoWarn);OTEL1000;OTEL1001;OTEL1002;OTEL1003;OTEL1004 - - + $(NoWarn);OTEL1000;OTEL1001;OTEL1002;OTEL1004 + latest-All + + + + 002400000480000094000000060200000024000052534131000400000100010051C1562A090FB0C9F391012A32198B5E5D9A60E9B80FA2D7B434C9E5CCB7259BD606E66F9660676AFC6692B8CDC6793D190904551D2103B7B22FA636DCBB8208839785BA402EA08FC00C8F1500CCEF28BBF599AA64FFB1E1D5DC1BF3420A3777BADFE697856E9D52070A50C3EA5821C80BEF17CA3ACFFA28F89DD413F096F898 + + + + false @@ -21,18 +30,19 @@ net481;net48;net472;net471;net47;net462 - net8.0;net6.0;netstandard2.0;$(NetFrameworkMinimumSupportedVersion) - net8.0;net6.0;netstandard2.1;netstandard2.0;$(NetFrameworkMinimumSupportedVersion) - net8.0;net6.0 + net9.0;net8.0;netstandard2.0;$(NetFrameworkMinimumSupportedVersion) + net9.0;net8.0;netstandard2.1;netstandard2.0;$(NetFrameworkMinimumSupportedVersion) + net9.0;net8.0 + net8.0;netstandard2.1;netstandard2.0;$(NetFrameworkMinimumSupportedVersion) - net8.0;net7.0;net6.0 - net8.0 - net8.0;net7.0;net6.0 + net9.0;net8.0 + net9.0;net8.0 + net9.0;net8.0 $(TargetFrameworksForDocs);$(NetFrameworkSupportedVersions) - net8.0;net7.0;net6.0 + net9.0;net8.0 $(TargetFrameworksForTests);$(NetFrameworkMinimumSupportedVersion) diff --git a/build/Common.targets b/build/Common.targets new file mode 100644 index 00000000000..63286efffbf --- /dev/null +++ b/build/Common.targets @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/build/RELEASING.md b/build/RELEASING.md index f9075727bd6..bb430aaf9e8 100644 --- a/build/RELEASING.md +++ b/build/RELEASING.md @@ -80,11 +80,21 @@ Maintainers (admins) are needed to merge PRs and for the push to NuGet.** for the projects being released. - 5. :stop_sign: Wait for the [Build, pack, and publish to + 6. :stop_sign: Wait for the [Build, pack, and publish to MyGet](https://github.com/open-telemetry/opentelemetry-dotnet/actions/workflows/publish-packages-1.0.yml) workflow to complete. When complete a trigger will automatically add a comment on the PR opened by [Prepare for a @@ -187,14 +197,14 @@ Maintainers (admins) are needed to merge PRs and for the push to NuGet.** draft Release and click `Publish release`. - 6. If a new stable version of the core packages was released, a PR should have + 7. If a new stable version of the core packages was released, a PR should have been automatically created by the [Complete release](https://github.com/open-telemetry/opentelemetry-dotnet/actions/workflows/post-release.yml) workflow to update the `OTelLatestStableVer` property in `Directory.Packages.props` to the just released stable version. Merge that PR once the build passes (this requires the packages be available on NuGet). - 7. The [Complete + 8. The [Complete release](https://github.com/open-telemetry/opentelemetry-dotnet/actions/workflows/post-release.yml) workflow should have invoked the [Core version update](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/actions/workflows/core-version-update.yml) @@ -203,6 +213,6 @@ Maintainers (admins) are needed to merge PRs and for the push to NuGet.** repository which opens a PR to update dependencies. Verify this PR was opened successfully. - 8. Post an announcement in the [Slack - channel](https://cloud-native.slack.com/archives/C01N3BC2W7Q). Note any big - or interesting new features as part of the announcement. + 9. For stable releases post an announcement in the [Slack + channel](https://cloud-native.slack.com/archives/C01N3BC2W7Q) announcing the + release and link to the release notes. diff --git a/build/docker-compose.net6.0.yml b/build/docker-compose.net6.0.yml deleted file mode 100644 index 099f1007277..00000000000 --- a/build/docker-compose.net6.0.yml +++ /dev/null @@ -1,9 +0,0 @@ -version: '3.7' - -services: - tests: - build: - args: - PUBLISH_FRAMEWORK: net6.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 deleted file mode 100644 index 48a2589cda9..00000000000 --- a/build/docker-compose.net7.0.yml +++ /dev/null @@ -1,9 +0,0 @@ -version: '3.7' - -services: - tests: - build: - args: - PUBLISH_FRAMEWORK: net7.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 index a5ac999e43e..b93fc24655e 100644 --- a/build/docker-compose.net8.0.yml +++ b/build/docker-compose.net8.0.yml @@ -1,9 +1,7 @@ -version: '3.7' - services: tests: build: args: PUBLISH_FRAMEWORK: net8.0 TEST_SDK_VERSION: "8.0" - BUILD_SDK_VERSION: "8.0" + BUILD_SDK_VERSION: "9.0" diff --git a/build/docker-compose.net9.0.yml b/build/docker-compose.net9.0.yml new file mode 100644 index 00000000000..94176258b24 --- /dev/null +++ b/build/docker-compose.net9.0.yml @@ -0,0 +1,7 @@ +services: + tests: + build: + args: + PUBLISH_FRAMEWORK: net9.0 + TEST_SDK_VERSION: "9.0" + BUILD_SDK_VERSION: "9.0" diff --git a/build/scripts/add-labels.psm1 b/build/scripts/add-labels.psm1 index 60c07bc4637..1b14854adb5 100644 --- a/build/scripts/add-labels.psm1 +++ b/build/scripts/add-labels.psm1 @@ -38,7 +38,7 @@ function AddLabelsOnPullRequestsBasedOnFilesChanged { # it automatically in order to also allow manual inclusion after reviewing files $managedLabels = 'infra', 'documentation', 'dependencies' $rootInfraFiles = 'global.json', 'NuGet.config', 'codeowners' - $documentationFiles = 'readme.md', 'contributing.md', 'releasing.md', 'versioning.md' + $documentationFiles = 'readme.md', 'contributing.md', 'releasing.md', 'versioning.md', 'releasenotes.md' foreach ($fileChanged in $filesChangedOnPullRequest) { @@ -99,12 +99,12 @@ function AddLabelsOnPullRequestsBasedOnFilesChanged { $rootInfraFiles.Contains($fullFileName) -or $fileExtension -eq ".props" -or $fileExtension -eq ".targets" -or - $fileChanged.StartsWith('test\openTelemetry.aotcompatibility')) + $fileChanged.StartsWith('test/openTelemetry.aotcompatibility')) { $added = $labelsToAdd.Add("infra") } - if ($fileChanged.StartsWith('test\benchmarks')) + if ($fileChanged.StartsWith('test/benchmarks')) { $added = $labelsToAdd.Add("perf") } diff --git a/build/scripts/post-release.psm1 b/build/scripts/post-release.psm1 index 57f0f341c8f..6edbe955474 100644 --- a/build/scripts/post-release.psm1 +++ b/build/scripts/post-release.psm1 @@ -13,6 +13,7 @@ function CreateDraftRelease { $tagPrefix = $match.Groups[1].Value $version = $match.Groups[2].Value + $isPrerelease = $version -match '-alpha' -or $version -match '-beta' -or $version -match '-rc' $projects = @(Get-ChildItem -Path src/**/*.csproj | Select-String "$tagPrefix" -List | Select Path) @@ -22,6 +23,7 @@ function CreateDraftRelease { } $notes = '' + $previousVersion = '' foreach ($project in $projects) { @@ -44,6 +46,11 @@ function CreateDraftRelease { } elseif ($line -like "## *" -and $started -eq $true) { + $match = [regex]::Match($line, '^##\s*(.*)$') + if ($match.Success -eq $true) + { + $previousVersion = $match.Groups[1].Value + } break } else @@ -57,7 +64,7 @@ function CreateDraftRelease { if ([string]::IsNullOrWhitespace($content) -eq $true) { - $content = " No notable changes." + $content = " No notable changes." } $content = $content.trimend() @@ -70,11 +77,19 @@ $content See [CHANGELOG](https://github.com/$gitRepository/blob/$tag/src/$projectName/CHANGELOG.md) for details. + "@ } - if ($version -match '-alpha' -or $version -match '-beta' -or $version -match '-rc') + if ($isPrerelease -eq $true) { + $notes = +@" +The following changes are from the previous release [$previousVersion](https://github.com/$gitRepository/releases/tag/$tagPrefix$previousVersion). + + +"@ + $notes + gh release create $tag $releaseFiles ` --title $tag ` --verify-tag ` @@ -84,6 +99,15 @@ $content } else { + $notes = +@" +For highlights and announcements pertaining to this release see: [Release Notes > $version](https://github.com/$gitRepository/blob/main/RELEASENOTES.md#$($version.Replace('.',''))). + +The following changes are from the previous release [$previousVersion](https://github.com/$gitRepository/releases/tag/$tagPrefix$previousVersion). + + +"@ + $notes + gh release create $tag $releaseFiles ` --title $tag ` --verify-tag ` @@ -101,7 +125,7 @@ function TryPostPackagesReadyNoticeOnPrepareReleasePullRequest { [Parameter(Mandatory=$true)][string]$tag, [Parameter(Mandatory=$true)][string]$tagSha, [Parameter(Mandatory=$true)][string]$packagesUrl, - [Parameter(Mandatory=$true)][string]$botUserName + [Parameter(Mandatory=$true)][string]$expectedPrAuthorUserName ) $prListResponse = gh pr list --search $tagSha --state merged --json number,author,title,comments | ConvertFrom-Json @@ -114,7 +138,7 @@ function TryPostPackagesReadyNoticeOnPrepareReleasePullRequest { foreach ($pr in $prListResponse) { - if ($pr.author.login -ne $botUserName -or $pr.title -ne "[release] Prepare release $tag") + if ($pr.author.login -ne $expectedPrAuthorUserName -or $pr.title -ne "[release] Prepare release $tag") { continue } @@ -122,7 +146,7 @@ function TryPostPackagesReadyNoticeOnPrepareReleasePullRequest { $foundComment = $false foreach ($comment in $pr.comments) { - if ($comment.author.login -eq $botUserName -and $comment.body.StartsWith("I just pushed the [$tag]")) + if ($comment.author.login -eq $expectedPrAuthorUserName -and $comment.body.StartsWith("I just pushed the [$tag]")) { $foundComment = $true break @@ -156,15 +180,15 @@ function PushPackagesPublishReleaseUnlockAndPostNoticeOnPrepareReleasePullReques param( [Parameter(Mandatory=$true)][string]$gitRepository, [Parameter(Mandatory=$true)][string]$pullRequestNumber, - [Parameter(Mandatory=$true)][string]$botUserName, + [Parameter(Mandatory=$true)][string]$expectedPrAuthorUserName, [Parameter(Mandatory=$true)][string]$commentUserName, [Parameter(Mandatory=$true)][string]$artifactDownloadPath, - [Parameter(Mandatory=$true)][string]$pushToNuget + [Parameter(Mandatory=$true)][bool]$pushToNuget ) $prViewResponse = gh pr view $pullRequestNumber --json author,title,comments | ConvertFrom-Json - if ($prViewResponse.author.login -ne $botUserName) + if ($prViewResponse.author.login -ne $expectedPrAuthorUserName) { throw 'PR author was unexpected' } @@ -189,7 +213,7 @@ function PushPackagesPublishReleaseUnlockAndPostNoticeOnPrepareReleasePullReques $packagesUrl = '' foreach ($comment in $prViewResponse.comments) { - if ($comment.author.login -eq $botUserName -and $comment.body.StartsWith("The packages for [$tag](https://github.com/$gitRepository/releases/tag/$tag) are now available:")) + if ($comment.author.login -eq $expectedPrAuthorUserName -and $comment.body.StartsWith("The packages for [$tag](https://github.com/$gitRepository/releases/tag/$tag) are now available:")) { $foundComment = $true break @@ -207,12 +231,12 @@ function PushPackagesPublishReleaseUnlockAndPostNoticeOnPrepareReleasePullReques Expand-Archive -LiteralPath "$artifactDownloadPath/$tag-packages.zip" -DestinationPath "$artifactDownloadPath\" - if ($pushToNuget -eq 'true') + if ($pushToNuget) { gh pr comment $pullRequestNumber ` --body "I am uploading the packages for ``$tag`` to NuGet and then I will publish the release." - nuget push "$artifactDownloadPath/**/*.nupkg" -Source https://api.nuget.org/v3/index.json -ApiKey "$env:NUGET_TOKEN" -SymbolApiKey "$env:NUGET_TOKEN" + dotnet nuget push "$artifactDownloadPath/**/*.nupkg" --source https://api.nuget.org/v3/index.json --api-key "$env:NUGET_TOKEN" --symbol-api-key "$env:NUGET_TOKEN" if ($LASTEXITCODE -gt 0) { @@ -239,6 +263,7 @@ function CreateStableVersionUpdatePullRequest { [Parameter(Mandatory=$true)][string]$gitRepository, [Parameter(Mandatory=$true)][string]$tag, [Parameter()][string]$targetBranch="main", + [Parameter()][string]$lineEnding="`n", [Parameter()][string]$gitUserName, [Parameter()][string]$gitUserEmail ) @@ -249,9 +274,9 @@ function CreateStableVersionUpdatePullRequest { throw 'Could not parse version from tag' } - $packageVersion = $match.Groups[1].Value + $version = $match.Groups[1].Value - $branch="release/post-stable-${tag}-update" + $branch="otelbot/post-stable-${tag}-update" if ([string]::IsNullOrEmpty($gitUserName) -eq $false) { @@ -268,17 +293,38 @@ function CreateStableVersionUpdatePullRequest { throw 'git switch failure' } + $projectsAndDependenciesBefore = GetCoreDependenciesForProjects + (Get-Content Directory.Packages.props) ` - -replace '.*<\/OTelLatestStableVer>', "$packageVersion" | + -replace '.*<\/OTelLatestStableVer>', "$version" | Set-Content Directory.Packages.props + $projectsAndDependenciesAfter = GetCoreDependenciesForProjects + + $changedProjects = @{} + + $projectsAndDependenciesBefore.GetEnumerator() | ForEach-Object { + $projectDir = $_.Key + $projectDependenciesBefore = $_.Value + $projectDependenciesAfter = $projectsAndDependenciesAfter[$projectDir] + + $projectDependenciesBefore.GetEnumerator() | ForEach-Object { + $packageName = $_.Key + $packageVersionBefore = $_.Value + if ($projectDependenciesAfter[$packageName] -ne $packageVersionBefore) + { + $changedProjects[$projectDir] = $true + } + } + } + git add Directory.Packages.props 2>&1 | % ToString if ($LASTEXITCODE -gt 0) { throw 'git add failure' } - git commit -m "Update OTelLatestStableVer in Directory.Packages.props to $packageVersion." 2>&1 | % ToString + git commit -m "Update OTelLatestStableVer in Directory.Packages.props to $version." 2>&1 | % ToString if ($LASTEXITCODE -gt 0) { throw 'git commit failure' @@ -298,19 +344,177 @@ Merge once packages are available on NuGet and the build passes. ## Changes -* Sets ``OTelLatestStableVer`` in ``Directory.Packages.props`` to ``$packageVersion``. +* Sets ``OTelLatestStableVer`` in ``Directory.Packages.props`` to ``$version``. "@ - gh pr create ` - --title "[release] Core stable release $packageVersion updates" ` + $createPullRequestResponse = gh pr create ` + --title "[release] Core stable release $version updates" ` --body $body ` --base $targetBranch ` --head $branch ` --label release + + Write-Host $createPullRequestResponse + + $match = [regex]::Match($createPullRequestResponse, "\/pull\/(.*)$") + if ($match.Success -eq $false) + { + throw 'Could not parse pull request number from gh pr create response' + } + + $pullRequestNumber = $match.Groups[1].Value + + if ($changedProjects.Count -eq 0) + { + Return + } + + $entry = @" +* Updated OpenTelemetry core component version(s) to ``$version``. + ([#$pullRequestNumber](https://github.com/$gitRepository/pull/$pullRequestNumber)) + + +"@ + + $lastLineBlank = $true + $changelogFilesUpdated = 0 + + foreach ($projectDir in $changedProjects.Keys) + { + $path = Join-Path -Path $projectDir -ChildPath "CHANGELOG.md" + + if ([System.IO.File]::Exists($path) -eq $false) + { + Write-Host "No CHANGELOG found in $projectDir" + continue + } + + $changelogContent = Get-Content -Path $path + + $started = $false + $isRemoving = $false + $content = "" + + foreach ($line in $changelogContent) + { + if ($line -like "## Unreleased" -and $started -ne $true) + { + $started = $true + } + elseif ($line -like "## *" -and $started -eq $true) + { + if ($lastLineBlank -eq $false) + { + $content += $lineEnding + } + $content += $entry + $started = $false + $isRemoving = $false + } + elseif ($started -eq $true -and ($line -like '*Update* OpenTelemetry SDK version to*' -or $line -like '*Updated OpenTelemetry core component version(s) to*')) + { + $isRemoving = $true + continue + } + + if ($line.StartsWith('* ')) + { + if ($isRemoving -eq $true) + { + $isRemoving = $false + } + + if ($lastLineBlank -eq $false) + { + $content += $lineEnding + } + } + + if ($isRemoving -eq $true) + { + continue + } + + $content += $line + $lineEnding + + $lastLineBlank = [string]::IsNullOrWhitespace($line) + } + + if ($started -eq $true) + { + # Note: If we never wrote the entry it means the file ended in the unreleased section + if ($lastLineBlank -eq $false) + { + $content += $lineEnding + } + $content += $entry + } + + Set-Content -Path $path -Value $content.TrimEnd() + + git add $path 2>&1 | % ToString + if ($LASTEXITCODE -gt 0) + { + throw 'git add failure' + } + + $changelogFilesUpdated++ + } + + if ($changelogFilesUpdated -gt 0) + { + git commit -m "Update CHANGELOGs for projects using OTelLatestStableVer." 2>&1 | % ToString + if ($LASTEXITCODE -gt 0) + { + throw 'git commit failure' + } + + git push -u origin $branch 2>&1 | % ToString + if ($LASTEXITCODE -gt 0) + { + throw 'git push failure' + } + } } Export-ModuleMember -Function CreateStableVersionUpdatePullRequest +function GetCoreDependenciesForProjects { + $projects = @(Get-ChildItem -Path 'src/*/*.csproj') + + $projectsAndDependencies = @{} + + foreach ($project in $projects) + { + # Note: dotnet restore may fail if the core packages aren't available yet but that is fine, we just want to generate project.assets.json for these projects. + $output = dotnet restore $project -p:RunningDotNetPack=true + + $projectDir = $project | Split-Path -Parent + + $content = (Get-Content "$projectDir/obj/project.assets.json" -Raw) + + $projectDependencies = @{} + + $matches = [regex]::Matches($content, '"(OpenTelemetry(?:.*))?": {[\S\s]*?"target": "Package",[\S\s]*?"version": "(.*)"[\S\s]*?}') + foreach ($match in $matches) + { + $packageName = $match.Groups[1].Value + $packageVersion = $match.Groups[2].Value + if ($packageName -eq 'OpenTelemetry' -or + $packageName -eq 'OpenTelemetry.Api' -or + $packageName -eq 'OpenTelemetry.Api.ProviderBuilderExtensions' -or + $packageName -eq 'OpenTelemetry.Extensions.Hosting' -or + $packageName -eq 'OpenTelemetry.Extensions.Propagators') + { + $projectDependencies[$packageName.ToString()] = $packageVersion.ToString() + } + } + $projectsAndDependencies[$projectDir.ToString()] = $projectDependencies + } + + return $projectsAndDependencies +} + function InvokeCoreVersionUpdateWorkflowInRemoteRepository { param( [Parameter(Mandatory=$true)][string]$remoteGitRepository, @@ -335,7 +539,7 @@ Export-ModuleMember -Function InvokeCoreVersionUpdateWorkflowInRemoteRepository function TryPostReleasePublishedNoticeOnPrepareReleasePullRequest { param( [Parameter(Mandatory=$true)][string]$gitRepository, - [Parameter(Mandatory=$true)][string]$botUserName, + [Parameter(Mandatory=$true)][string]$expectedPrAuthorUserName, [Parameter(Mandatory=$true)][string]$tag ) @@ -355,7 +559,7 @@ function TryPostReleasePublishedNoticeOnPrepareReleasePullRequest { foreach ($pr in $prListResponse) { - if ($pr.author.login -ne $botUserName -or $pr.title -ne "[release] Prepare release $tag") + if ($pr.author.login -ne $expectedPrAuthorUserName -or $pr.title -ne "[release] Prepare release $tag") { continue } @@ -363,7 +567,7 @@ function TryPostReleasePublishedNoticeOnPrepareReleasePullRequest { $foundComment = $false foreach ($comment in $pr.comments) { - if ($comment.author.login -eq $botUserName -and $comment.body.StartsWith("The packages for [$tag](https://github.com/$gitRepository/releases/tag/$tag) are now available:")) + if ($comment.author.login -eq $expectedPrAuthorUserName -and $comment.body.StartsWith("The packages for [$tag](https://github.com/$gitRepository/releases/tag/$tag) are now available:")) { $foundComment = $true break diff --git a/build/scripts/prepare-release.psm1 b/build/scripts/prepare-release.psm1 index 88e6f7ad11f..4a6ee4678f4 100644 --- a/build/scripts/prepare-release.psm1 +++ b/build/scripts/prepare-release.psm1 @@ -3,13 +3,21 @@ function CreatePullRequestToUpdateChangelogsAndPublicApis { [Parameter(Mandatory=$true)][string]$gitRepository, [Parameter(Mandatory=$true)][string]$minVerTagPrefix, [Parameter(Mandatory=$true)][string]$version, + [Parameter(Mandatory=$true)][string]$requestedByUserName, [Parameter()][string]$targetBranch="main", [Parameter()][string]$gitUserName, [Parameter()][string]$gitUserEmail ) + $match = [regex]::Match($version, '^(\d+\.\d+\.\d+)(?:-((?:alpha)|(?:beta)|(?:rc))\.(\d+))?$') + if ($match.Success -eq $false) + { + throw 'Input version did not match expected format' + } + + $isPrerelease = $version -match '-alpha' -or $version -match '-beta' -or $version -match '-rc' $tag="${minVerTagPrefix}${version}" - $branch="release/prepare-${tag}-release" + $branch="otelbot/prepare-${tag}-release" if ([string]::IsNullOrEmpty($gitUserName) -eq $false) { @@ -30,6 +38,8 @@ function CreatePullRequestToUpdateChangelogsAndPublicApis { @" Note: This PR was opened automatically by the [prepare release workflow](https://github.com/$gitRepository/actions/workflows/prepare-release.yml). +Requested by: @$requestedByUserName + ## Changes * CHANGELOG files updated for projects being released. @@ -39,13 +49,37 @@ Note: This PR was opened automatically by the [prepare release workflow](https:/ & ./build/scripts/update-changelogs.ps1 -minVerTagPrefix $minVerTagPrefix -version $version # Update publicApi files for stable releases - if ($version -notlike "*-alpha*" -and $version -notlike "*-beta*" -and $version -notlike "*-rc*") + if ($isPrerelease -ne $true) { & ./build/scripts/finalize-publicapi.ps1 -minVerTagPrefix $minVerTagPrefix $body += "`r`n* Public API files updated for projects being released (only performed for stable releases)." } + $body += +@" + +## Commands + +``/UpdateReleaseDates``: Use to update release dates in CHANGELOGs before merging [``approvers``, ``maintainers``] +"@ + + if ($minVerTagPrefix -eq 'core-' -and $isPrerelease -ne $true) + { + $body += +@" + +``/UpdateReleaseNotes``: Use to update ``RELEASENOTES.md`` before merging [``approvers``, ``maintainers``] +"@ + } + + $body += +@" + +``/CreateReleaseTag``: Use after merging to push the release tag and trigger the job to create packages [``approvers``, ``maintainers``] +``/PushPackages``: Use after the created packages have been validated to push to NuGet [``maintainers``] +"@ + git commit -a -m "Prepare repo to release $tag." 2>&1 | % ToString if ($LASTEXITCODE -gt 0) { @@ -58,12 +92,42 @@ Note: This PR was opened automatically by the [prepare release workflow](https:/ throw 'git push failure' } - gh pr create ` + $createPullRequestResponse = gh pr create ` --title "[release] Prepare release $tag" ` --body $body ` --base $targetBranch ` --head $branch ` --label release + + Write-Host $createPullRequestResponse + + $match = [regex]::Match($createPullRequestResponse, "\/pull\/(.*)$") + if ($match.Success -eq $false) + { + throw 'Could not parse pull request number from gh pr create response' + } + + $pullRequestNumber = $match.Groups[1].Value + + if ($minVerTagPrefix -eq 'core-' -and $isPrerelease -ne $true) + { + $found = Select-String -Path "RELEASENOTES.md" -Pattern "## $version" -Quiet + if ($found -eq $false) + { + $body = +@" +I noticed this PR is releasing a stable version of core packages but there isn't any content in ``RELEASENOTES.md`` for the version being released. + +It is important to update ``RELEASENOTES.md`` before creating the release tag because a permalink will become part of the package(s). + +Post a comment with "/UpdateReleaseNotes" in the body if you would like me to update release notes. + +Note: In the comment everything below "/UpdateReleaseNotes" will be added to ``RELEASENOTES.md`` for the version being released. If something is already there it will be replaced. +"@ + + gh pr comment $pullRequestNumber --body $body + } + } } Export-ModuleMember -Function CreatePullRequestToUpdateChangelogsAndPublicApis @@ -72,12 +136,12 @@ function LockPullRequestAndPostNoticeToCreateReleaseTag { param( [Parameter(Mandatory=$true)][string]$gitRepository, [Parameter(Mandatory=$true)][string]$pullRequestNumber, - [Parameter(Mandatory=$true)][string]$botUserName + [Parameter(Mandatory=$true)][string]$expectedPrAuthorUserName ) $prViewResponse = gh pr view $pullRequestNumber --json mergeCommit,author,title | ConvertFrom-Json - if ($prViewResponse.author.login -ne $botUserName) + if ($prViewResponse.author.login -ne $expectedPrAuthorUserName) { throw 'PR author was unexpected' } @@ -114,14 +178,14 @@ function CreateReleaseTagAndPostNoticeOnPullRequest { param( [Parameter(Mandatory=$true)][string]$gitRepository, [Parameter(Mandatory=$true)][string]$pullRequestNumber, - [Parameter(Mandatory=$true)][string]$botUserName, + [Parameter(Mandatory=$true)][string]$expectedPrAuthorUserName, [Parameter()][string]$gitUserName, [Parameter()][string]$gitUserEmail ) $prViewResponse = gh pr view $pullRequestNumber --json mergeCommit,author,title | ConvertFrom-Json - if ($prViewResponse.author.login -ne $botUserName) + if ($prViewResponse.author.login -ne $expectedPrAuthorUserName) { throw 'PR author was unexpected' } @@ -172,3 +236,228 @@ The [package workflow](https://github.com/$gitRepository/actions/workflows/publi } Export-ModuleMember -Function CreateReleaseTagAndPostNoticeOnPullRequest + +function UpdateChangelogReleaseDatesAndPostNoticeOnPullRequest { + param( + [Parameter(Mandatory=$true)][string]$gitRepository, + [Parameter(Mandatory=$true)][string]$pullRequestNumber, + [Parameter(Mandatory=$true)][string]$expectedPrAuthorUserName, + [Parameter(Mandatory=$true)][string]$commentUserName, + [Parameter()][string]$gitUserName, + [Parameter()][string]$gitUserEmail + ) + + $prViewResponse = gh pr view $pullRequestNumber --json headRefName,author,title | ConvertFrom-Json + + if ($prViewResponse.author.login -ne $expectedPrAuthorUserName) + { + throw 'PR author was unexpected' + } + + $match = [regex]::Match($prViewResponse.title, '^\[release\] Prepare release (.*)$') + if ($match.Success -eq $false) + { + throw 'Could not parse tag from PR title' + } + + $tag = $match.Groups[1].Value + + $match = [regex]::Match($tag, '^(.*?-)(.*)$') + if ($match.Success -eq $false) + { + throw 'Could not parse prefix or version from tag' + } + + $tagPrefix = $match.Groups[1].Value + $version = $match.Groups[2].Value + + $commentUserPermission = gh api "repos/$gitRepository/collaborators/$commentUserName/permission" | ConvertFrom-Json + if ($commentUserPermission.permission -ne 'admin' -and $commentUserPermission.permission -ne 'write') + { + gh pr comment $pullRequestNumber ` + --body "I'm sorry @$commentUserName but you don't have permission to update this PR. Only maintainers and approvers can update this PR." + return + } + + if ([string]::IsNullOrEmpty($gitUserName) -eq $false) + { + git config user.name $gitUserName + } + if ([string]::IsNullOrEmpty($gitUserEmail) -eq $false) + { + git config user.email $gitUserEmail + } + + git switch $prViewResponse.headRefName 2>&1 | % ToString + if ($LASTEXITCODE -gt 0) + { + throw 'git switch failure' + } + + $updatedFiles = 0 + $newHeader = +@" +## $version + +Released $(Get-Date -UFormat '%Y-%b-%d') +"@ + + $projectDirs = Get-ChildItem -Path src/**/*.csproj | Select-String "$tagPrefix" -List | Select Path | Split-Path -Parent + + foreach ($projectDir in $projectDirs) + { + $content = (Get-Content "$projectDir/CHANGELOG.md" -Raw) + + $newContent = $content -replace "## $version\s*Released .*", $newHeader + + if ($content -ne $newContent) + { + $updatedFiles++ + Set-Content -Path "$projectDir/CHANGELOG.md" $newContent.Trim() + } + } + + if ($updatedFiles -eq 0) + { + gh pr comment $pullRequestNumber --body "All of the CHANGELOG files have valid release dates." + return + } + + git commit -a -m "Update CHANGELOG release dates for $tag." 2>&1 | % ToString + if ($LASTEXITCODE -gt 0) + { + throw 'git commit failure' + } + + git push -u origin $prViewResponse.headRefName 2>&1 | % ToString + if ($LASTEXITCODE -gt 0) + { + throw 'git push failure' + } + + gh pr comment $pullRequestNumber --body "I updated the CHANGELOG release dates." +} + +Export-ModuleMember -Function UpdateChangelogReleaseDatesAndPostNoticeOnPullRequest + +function UpdateReleaseNotesAndPostNoticeOnPullRequest { + param( + [Parameter(Mandatory=$true)][string]$gitRepository, + [Parameter(Mandatory=$true)][string]$pullRequestNumber, + [Parameter(Mandatory=$true)][string]$expectedPrAuthorUserName, + [Parameter(Mandatory=$true)][string]$commentUserName, + [Parameter(Mandatory=$true)][string]$commentBody, + [Parameter()][string]$gitUserName, + [Parameter()][string]$gitUserEmail + ) + + $prViewResponse = gh pr view $pullRequestNumber --json headRefName,author,title | ConvertFrom-Json + + if ($prViewResponse.author.login -ne $expectedPrAuthorUserName) + { + throw 'PR author was unexpected' + } + + $match = [regex]::Match($prViewResponse.title, '^\[release\] Prepare release (.*)$') + if ($match.Success -eq $false) + { + throw 'Could not parse tag from PR title' + } + + $tag = $match.Groups[1].Value + + $match = [regex]::Match($tag, '^(.*?-)(.*)$') + if ($match.Success -eq $false) + { + throw 'Could not parse prefix or version from tag' + } + + $tagPrefix = $match.Groups[1].Value + $version = $match.Groups[2].Value + $isPrerelease = $version -match '-alpha' -or $version -match '-beta' -or $version -match '-rc' + + $commentUserPermission = gh api "repos/$gitRepository/collaborators/$commentUserName/permission" | ConvertFrom-Json + if ($commentUserPermission.permission -ne 'admin' -and $commentUserPermission.permission -ne 'write') + { + gh pr comment $pullRequestNumber ` + --body "I'm sorry @$commentUserName but you don't have permission to update this PR. Only maintainers and approvers can update this PR." + return + } + + if ($tagPrefix -ne 'core-' -or $isPrerelease -eq $true) + { + gh pr comment $pullRequestNumber ` + --body "I'm sorry @$commentUserName but we don't typically add release notes for prereleases or unstable packages." + return + } + + if ([string]::IsNullOrEmpty($gitUserName) -eq $false) + { + git config user.name $gitUserName + } + if ([string]::IsNullOrEmpty($gitUserEmail) -eq $false) + { + git config user.email $gitUserEmail + } + + git switch $prViewResponse.headRefName 2>&1 | % ToString + if ($LASTEXITCODE -gt 0) + { + throw 'git switch failure' + } + + $releaseNotesContent = (Get-Content -Path "RELEASENOTES.md" -Raw) + + $match = [regex]::Match($commentBody, '[\w\W\s]*\/UpdateReleaseNotes.*$([\w\W\s]*)', [Text.RegularExpressions.RegexOptions]::Multiline) + if ($match.Success -eq $false) + { + throw 'Could not find release notes content' + } + + $content = $match.Groups[1].Value.Trim() -replace "`r`n", "`n" + + $body = +@" +## $version + +Release details: [$version](https://github.com/$gitRepository/releases/tag/$tagPrefix$version) + +$content + +## +"@ + + $match = [regex]::Match($releaseNotesContent, "(## $version[\w\W\s]*?)##", [Text.RegularExpressions.RegexOptions]::Multiline) + if ($match.Success -eq $true) + { + $content = [regex]::Replace($releaseNotesContent, "(## $version[\w\W\s]*?)##", $body, [Text.RegularExpressions.RegexOptions]::Multiline) + Set-Content -Path "RELEASENOTES.md" -Value $content.TrimEnd() + } + else { + $match = [regex]::Match($releaseNotesContent, '(# Release Notes[\w\W\s]*?)##', [Text.RegularExpressions.RegexOptions]::Multiline) + if ($match.Success -eq $false) + { + throw 'Could not find release notes header' + } + + $body = $match.Groups[1].Value + $body + $content = [regex]::Replace($releaseNotesContent, '(# Release Notes[\w\W\s]*?)##', $body, [Text.RegularExpressions.RegexOptions]::Multiline) + Set-Content -Path "RELEASENOTES.md" -Value $content.TrimEnd() + } + + git commit -a -m "Update RELEASENOTES for $tag." 2>&1 | % ToString + if ($LASTEXITCODE -gt 0) + { + throw 'git commit failure' + } + + git push -u origin $prViewResponse.headRefName 2>&1 | % ToString + if ($LASTEXITCODE -gt 0) + { + throw 'git push failure' + } + + gh pr comment $pullRequestNumber --body "I updated ``RELEASENOTES.md``." +} + +Export-ModuleMember -Function UpdateReleaseNotesAndPostNoticeOnPullRequest diff --git a/build/scripts/test-aot-compatibility.ps1 b/build/scripts/test-aot-compatibility.ps1 index 895055a1b48..99def7badd5 100644 --- a/build/scripts/test-aot-compatibility.ps1 +++ b/build/scripts/test-aot-compatibility.ps1 @@ -24,10 +24,9 @@ if ($LastExitCode -ne 0) Write-Host $publishOutput } -$runtime = $IsWindows ? "win-x64" : ($IsMacOS ? "macos-x64" : "linux-x64") $app = $IsWindows ? "./OpenTelemetry.AotCompatibility.TestApp.exe" : "./OpenTelemetry.AotCompatibility.TestApp" -Push-Location $rootDirectory/test/OpenTelemetry.AotCompatibility.TestApp/bin/Release/$targetNetFramework/$runtime +Push-Location $rootDirectory/artifacts/publish/OpenTelemetry.AotCompatibility.TestApp/release_$targetNetFramework Write-Host "Executing test App..." $app diff --git a/build/scripts/test-threadSafety.ps1 b/build/scripts/test-threadSafety.ps1 index 73cc1f27d27..6fbaba60e5c 100644 --- a/build/scripts/test-threadSafety.ps1 +++ b/build/scripts/test-threadSafety.ps1 @@ -1,22 +1,32 @@ param( - [Parameter()][string]$coyoteVersion="1.7.10", + [Parameter()][string]$coyoteVersion="1.7.11", [Parameter(Mandatory=$true)][string]$testProjectName, [Parameter(Mandatory=$true)][string]$targetFramework, [Parameter()][string]$categoryName="CoyoteConcurrencyTests", [Parameter()][string]$configuration="Release" ) +$ErrorActionPreference = "Stop" + $env:OTEL_RUN_COYOTE_TESTS = 'true' $rootDirectory = Get-Location Write-Host "Install Coyote CLI." -dotnet tool install --global Microsoft.Coyote.CLI +dotnet tool install --global Microsoft.Coyote.CLI --version $coyoteVersion + +if ($LASTEXITCODE -ne 0) { + throw "Microsoft.Coyote.CLI installation failed with exit code $LASTEXITCODE" +} Write-Host "Build $testProjectName project." dotnet build "$rootDirectory/test/$testProjectName/$testProjectName.csproj" --configuration $configuration -$artifactsPath = Join-Path $rootDirectory "test/$testProjectName/bin/$configuration/$targetFramework" +if ($LASTEXITCODE -ne 0) { + throw "dotnet build failed with exit code $LASTEXITCODE" +} + +$artifactsPath = Join-Path $rootDirectory "artifacts/bin/$testProjectName/$($configuration.ToLowerInvariant())_$targetFramework" Write-Host "Generate Coyote rewriting options JSON file." $assemblies = Get-ChildItem $artifactsPath -Filter OpenTelemetry*.dll | ForEach-Object {$_.Name} @@ -29,6 +39,13 @@ $RewriteOptionsJson | ConvertTo-Json -Compress | Set-Content -Path "$rootDirecto Write-Host "Run Coyote rewrite." coyote rewrite "$rootDirectory/test/$testProjectName/rewrite.coyote.json" +if ($LASTEXITCODE -ne 0) { + throw "coyote rewrite failed with exit code $LASTEXITCODE" +} + Write-Host "Execute re-written binary." dotnet test "$artifactsPath/$testProjectName.dll" --framework $targetFramework --filter CategoryName=$categoryName +if ($LASTEXITCODE -ne 0) { + throw "dotnet test failed with exit code $LASTEXITCODE" +} diff --git a/docs/Directory.Build.props b/docs/Directory.Build.props index 942eea5974c..9bc6a965577 100644 --- a/docs/Directory.Build.props +++ b/docs/Directory.Build.props @@ -1,8 +1,11 @@ + Exe $(TargetFrameworksForDocs) + + false diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000000..23d80c8a577 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,134 @@ +# OpenTelemetry .NET SDK + +## Initialize the SDK + +There are two different common initialization styles supported by OpenTelemetry. + +### Initialize the SDK using a host + +Users building applications based on +[Microsoft.Extensions.Hosting](https://www.nuget.org/packages/Microsoft.Extensions.Hosting) +should utilize the +[OpenTelemetry.Extensions.Hosting](../src/OpenTelemetry.Extensions.Hosting/README.md) +package to initialize OpenTelemetry. This style provides a deep integration +between the host infrastructure (`IServiceCollection`, `IServiceProvider`, +`IConfiguration`, etc.) and OpenTelemetry. + +[AspNetCore](https://learn.microsoft.com/aspnet/core/fundamentals/host/web-host) +applications are the most common to use the hosting model but there is also a +[Generic Host](https://learn.microsoft.com/dotnet/core/extensions/generic-host) +which may be used in console, service, and worker applications. + +> [!NOTE] +> When using `OpenTelemetry.Extensions.Hosting` only a single pipeline will be +> created for each configured signal (logging, metrics, and/or tracing). Users +> who need more granular control can create additional pipelines using the +> manual style below. + +First install the +[OpenTelemetry.Extensions.Hosting](../src/OpenTelemetry.Extensions.Hosting/README.md) +package. + +Second call the `AddOpenTelemetry` extension using the host +`IServiceCollection`: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +// Clear the default logging providers added by the host +builder.Logging.ClearProviders(); + +// Initialize OpenTelemetry +builder.Services.AddOpenTelemetry() + .ConfigureResource(resource => /* Resource configuration goes here */) + .WithLogging(logging => /* Logging configuration goes here */) + .WithMetrics(metrics => /* Metrics configuration goes here */) + .WithTracing(tracing => /* Tracing configuration goes here */)); +``` + +> [!NOTE] +> Calling `WithLogging` automatically registers the OpenTelemetry +> `ILoggerProvider` and enables `ILogger` integration. + +### Initialize the SDK manually + +Users running on .NET Framework or running without a host may initialize +OpenTelemetry manually. + +> [!IMPORTANT] +> When initializing OpenTelemetry manually make sure to ALWAYS dispose the SDK +> and/or providers when the application is shutting down. Disposing +> OpenTelemetry gives the SDK a chance to flush any telemetry held in memory. +> Skipping this step may result in data loss. + +First install the [OpenTelemetry SDK](../src/OpenTelemetry/README.md) package or +an exporter package such as +[OpenTelemetry.Exporter.OpenTelemetryProtocol](../src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md). +Exporter packages typically reference the SDK and will make it available via +transitive reference. + +Second use one of the following initialization APIs (depending on the SDK +version being used): + +#### Using 1.10.0 or newer + +The `OpenTelemetrySdk.Create` API can be used to initialize all signals off a +single root builder and supports cross-cutting extensions such as +`ConfigureResource` which configures a `Resource` to be used by all enabled +signals. An `OpenTelemetrySdk` instance is returned which may be used to access +providers for each signal. Calling `Dispose` on the returned instance will +gracefully shutdown the SDK and flush any telemetry held in memory. + +> [!NOTE] +> When calling `OpenTelemetrySdk.Create` a dedicated `IServiceCollection` and +> `IServiceProvider` will be created for the SDK and shared by all signals. An +> `IConfiguration` is created automatically from environment variables. + +```csharp +using OpenTelemetry; + +var sdk = OpenTelemetrySdk.Create(builder => builder + .ConfigureResource(resource => /* Resource configuration goes here */) + .WithLogging(logging => /* Logging configuration goes here */) + .WithMetrics(metrics => /* Metrics configuration goes here */) + .WithTracing(tracing => /* Tracing configuration goes here */)); + +// During application shutdown +sdk.Dispose(); +``` + +To obtain an `ILogger` instance for emitting logs when using the +`OpenTelemetrySdk.Create` API call the `GetLoggerFactory` extension method using +the returned `OpenTelemetrySdk` instance: + +```csharp +var logger = sdk.GetLoggerFactory().CreateLogger(); +logger.LogInformation("Application started"); +``` + +#### Using 1.9.0 or older + +The following shows how to create providers for each individual signal. Each +provider is independent and must be managed and disposed explicitly. There is no +mechanism using this style to perform cross-cutting actions across signals. + +```csharp +using Microsoft.Extensions.Logging; +using OpenTelemetry; + +var tracerProvider = Sdk.CreateTracerProviderBuilder() + /* Tracing configuration goes here */ + .Build(); + +var meterProvider = Sdk.CreateMeterProviderBuilder() + /* Metrics configuration goes here */ + .Build(); + +var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetry(options => /* Logging configuration goes here */)); + +// During application shutdown +tracerProvider.Dispose(); +meterProvider.Dispose(); +loggerFactory.Dispose(); +``` diff --git a/docs/diagnostics/experimental-apis/OTEL1003.md b/docs/diagnostics/experimental-apis/OTEL1003.md deleted file mode 100644 index 5f62f03575f..00000000000 --- a/docs/diagnostics/experimental-apis/OTEL1003.md +++ /dev/null @@ -1,47 +0,0 @@ -# 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 index 6c581030b4c..daa80d34b38 100644 --- a/docs/diagnostics/experimental-apis/README.md +++ b/docs/diagnostics/experimental-apis/README.md @@ -27,12 +27,6 @@ Description: Logs Bridge API Details: [OTEL1001](./OTEL1001.md) -### OTEL1003 - -Description: MetricStreamConfiguration CardinalityLimit Support - -Details: [OTEL1003](./OTEL1003.md) - ### OTEL1004 Description: ExemplarReservoir Support @@ -58,3 +52,11 @@ Description: Metrics Exemplar Support Details: [OTEL1002](https://github.com/open-telemetry/opentelemetry-dotnet/blob/b8ea807bae1a5d9b0f3d6d23b1e1e10f5e096a25/docs/diagnostics/experimental-apis/OTEL1002.md) Released stable: `1.9.0` + +### OTEL1003 + +Description: MetricStreamConfiguration CardinalityLimit Support + +Details: [OTEL1003](https://github.com/open-telemetry/opentelemetry-dotnet/blob/9f41eadf03f3dcc5e76c686b61fb39849f046312/docs/diagnostics/experimental-apis/OTEL1003.md) + +Released stable: `1.10.0` diff --git a/docs/logs/README.md b/docs/logs/README.md index 55c6c06c0b2..d5a6064c310 100644 --- a/docs/logs/README.md +++ b/docs/logs/README.md @@ -25,33 +25,17 @@ OpenTelemetry .NET: * [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; +## Logging API -logger.LogInformation($"Hello from {food} {price}."); -``` +### ILogger -Refer to the [logging performance -benchmark](../../test/Benchmarks/Logs/LogBenchmarks.cs) for more details. +.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. -## Package Version +#### Package Version :heavy_check_mark: You should always use the [`ILogger`](https://docs.microsoft.com/dotnet/api/microsoft.extensions.logging.ilogger) @@ -69,16 +53,6 @@ package, regardless of the .NET runtime version being used: 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 @@ -125,7 +99,38 @@ 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 +#### Write log messages + +: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. + +```csharp +var food = "tomato"; +var price = 2.99; + +logger.LogInformation("Hello from {food} {price}.", food, price); +``` + +: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. :heavy_check_mark: You should use [compile-time logging source generation](https://docs.microsoft.com/dotnet/core/extensions/logger-message-generator) @@ -222,6 +227,111 @@ code is now depending on which logger is being enabled, not to mention the argument evaluation might have significant side effects that are now depending on the logging configuration. +:heavy_check_mark: You should use a dedicated parameter to log exceptions when +using the compile-time source generator. + +```csharp +var food = "tomato"; +var price = 2.99; + +try +{ + // Execute some logic + + logger.SayHello(food, price); +} +catch (Exception ex) +{ + logger.SayHelloFailure(ex, 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); + + [LoggerMessage(Level = LogLevel.Error, Message = "Could not say hello from {food} {price}.")] + public static partial void SayHelloFailure(this ILogger logger, Exception exception, string food, double price); +} +``` + +> [!NOTE] +> When using the compile-time source generator the first `Exception` parameter +> detected is automatically given special handling. It **SHOULD NOT** be part of +> the message template. For details see: [Log method +> anatomy](https://learn.microsoft.com/dotnet/core/extensions/logger-message-generator#log-method-anatomy). + +:heavy_check_mark: You should use the dedicated overloads to log exceptions when +using the logging extensions methods. + +```csharp +var food = "tomato"; +var price = 2.99; + +try +{ + // Execute some logic + + logger.LogInformation("Hello from {food} {price}.", food, price); +} +catch (Exception ex) +{ + logger.LogError(ex, "Could not say hello from {food} {price}.", food, price); +} +``` + +:stop_sign: You should avoid adding exception details into the message template. + +You want to use the correct `Exception` APIs because the OpenTelemetry +Specification [defines dedicated +attributes](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/exceptions/exceptions-logs.md) +for `Exception` details. The following examples show what **NOT** to do. In +these cases the details won't be lost, but the dedicated attributes also won't +be added. + +```csharp +var food = "tomato"; +var price = 2.99; + +try +{ + // Execute some logic + + logger.SayHello(food, price); +} +catch (Exception ex) +{ + logger.SayHelloFailure(food, price, ex.Message); +} + +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); + + // BAD - Exception should not be part of the message template. Use the dedicated parameter. + [LoggerMessage(Level = LogLevel.Error, Message = "Could not say hello from {food} {price} {message}.")] + public static partial void SayHelloFailure(this ILogger logger, string food, double price, string message); +} +``` + +```csharp +var food = "tomato"; +var price = 2.99; + +try +{ + // Execute some logic + + logger.LogInformation("Hello from {food} {price}.", food, price); +} +catch (Exception ex) +{ + // BAD - Exception should not be part of the message template. Use the dedicated parameter. + logger.LogError("Could not say hello from {food} {price} {message}.", food, price, ex.Message); +} +``` + ## LoggerFactory In many cases, you can use [ILogger](#ilogger) without having to interact with diff --git a/docs/logs/complex-objects/FoodRecallNotice.cs b/docs/logs/complex-objects/FoodRecallNotice.cs index 40ca59e3b8d..e771fc7adc4 100644 --- a/docs/logs/complex-objects/FoodRecallNotice.cs +++ b/docs/logs/complex-objects/FoodRecallNotice.cs @@ -1,7 +1,9 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -public struct FoodRecallNotice +namespace ComplexObjects; + +internal struct FoodRecallNotice { public string? BrandName { get; set; } diff --git a/docs/logs/complex-objects/Program.cs b/docs/logs/complex-objects/Program.cs index c09cfc5c772..d4aa278f89e 100644 --- a/docs/logs/complex-objects/Program.cs +++ b/docs/logs/complex-objects/Program.cs @@ -1,6 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using ComplexObjects; using Microsoft.Extensions.Logging; using OpenTelemetry.Logs; diff --git a/docs/logs/correlation/Program.cs b/docs/logs/correlation/Program.cs index b1a3284fbcc..6eee75f3657 100644 --- a/docs/logs/correlation/Program.cs +++ b/docs/logs/correlation/Program.cs @@ -7,7 +7,9 @@ using OpenTelemetry.Logs; using OpenTelemetry.Trace; -public class Program +namespace Correlation; + +internal sealed class Program { private static readonly ActivitySource MyActivitySource = new("MyCompany.MyProduct.MyLibrary"); diff --git a/docs/logs/dedicated-pipeline/DedicatedLogging/DedicatedLoggingServiceCollectionExtensions.cs b/docs/logs/dedicated-pipeline/DedicatedLogging/DedicatedLoggingServiceCollectionExtensions.cs index c9343185e21..6652c195586 100644 --- a/docs/logs/dedicated-pipeline/DedicatedLogging/DedicatedLoggingServiceCollectionExtensions.cs +++ b/docs/logs/dedicated-pipeline/DedicatedLogging/DedicatedLoggingServiceCollectionExtensions.cs @@ -6,7 +6,7 @@ namespace DedicatedLogging; -public static class DedicatedLoggingServiceCollectionExtensions +internal static class DedicatedLoggingServiceCollectionExtensions { public static IServiceCollection AddDedicatedLogging( this IServiceCollection services, diff --git a/docs/logs/dedicated-pipeline/DedicatedLogging/IDedicatedLogger.cs b/docs/logs/dedicated-pipeline/DedicatedLogging/IDedicatedLogger.cs index ba09f27e271..975300a60e0 100644 --- a/docs/logs/dedicated-pipeline/DedicatedLogging/IDedicatedLogger.cs +++ b/docs/logs/dedicated-pipeline/DedicatedLogging/IDedicatedLogger.cs @@ -3,10 +3,10 @@ namespace DedicatedLogging; -public interface IDedicatedLogger : ILogger +internal interface IDedicatedLogger : ILogger { } -public interface IDedicatedLogger : IDedicatedLogger +internal interface IDedicatedLogger : IDedicatedLogger { } diff --git a/docs/logs/dedicated-pipeline/dedicated-pipeline.csproj b/docs/logs/dedicated-pipeline/dedicated-pipeline.csproj index cebc8460c42..6426d0f7ddd 100644 --- a/docs/logs/dedicated-pipeline/dedicated-pipeline.csproj +++ b/docs/logs/dedicated-pipeline/dedicated-pipeline.csproj @@ -1,4 +1,8 @@ + + $(NoWarn);CA1812;CA2213 + + diff --git a/docs/logs/extending-the-sdk/MyExporter.cs b/docs/logs/extending-the-sdk/MyExporter.cs index 6093f57394b..25b2d9809e1 100644 --- a/docs/logs/extending-the-sdk/MyExporter.cs +++ b/docs/logs/extending-the-sdk/MyExporter.cs @@ -5,7 +5,7 @@ using OpenTelemetry; using OpenTelemetry.Logs; -internal class MyExporter : BaseExporter +internal sealed class MyExporter : BaseExporter { private readonly string name; @@ -59,6 +59,7 @@ protected override bool OnShutdown(int timeoutMilliseconds) protected override void Dispose(bool disposing) { + base.Dispose(disposing); Console.WriteLine($"{this.name}.Dispose({disposing})"); } } diff --git a/docs/logs/extending-the-sdk/MyProcessor.cs b/docs/logs/extending-the-sdk/MyProcessor.cs index 5fcd34e1d8d..739f9f6f232 100644 --- a/docs/logs/extending-the-sdk/MyProcessor.cs +++ b/docs/logs/extending-the-sdk/MyProcessor.cs @@ -4,7 +4,7 @@ using OpenTelemetry; using OpenTelemetry.Logs; -internal class MyProcessor : BaseProcessor +internal sealed class MyProcessor : BaseProcessor { private readonly string name; @@ -32,6 +32,7 @@ protected override bool OnShutdown(int timeoutMilliseconds) protected override void Dispose(bool disposing) { + base.Dispose(disposing); Console.WriteLine($"{this.name}.Dispose({disposing})"); } } diff --git a/docs/logs/extending-the-sdk/Program.cs b/docs/logs/extending-the-sdk/Program.cs index cada6ec1f5d..1de67683841 100644 --- a/docs/logs/extending-the-sdk/Program.cs +++ b/docs/logs/extending-the-sdk/Program.cs @@ -6,7 +6,7 @@ namespace ExtendingTheSdk; -public class Program +internal sealed class Program { public static void Main() { @@ -29,16 +29,16 @@ public static void Main() // logger.LogInformation($"Hello from potato {0.99}."); // structured log with template - logger.LogInformation("Hello from {name} {price}.", "tomato", 2.99); + logger.LogInformation("Hello from {Name} {Price}.", "tomato", 2.99); // structured log with strong type - logger.LogInformation("{food}", new Food { Name = "artichoke", Price = 3.99 }); + logger.LogInformation("{Food}", new Food { Name = "artichoke", Price = 3.99 }); // structured log with anonymous type - logger.LogInformation("{food}", new { Name = "pumpkin", Price = 5.99 }); + logger.LogInformation("{Food}", new { Name = "pumpkin", Price = 5.99 }); // structured log with general type - logger.LogInformation("{food}", new Dictionary + logger.LogInformation("{Food}", new Dictionary { ["Name"] = "truffle", ["Price"] = 299.99, @@ -48,11 +48,11 @@ public static void Main() using (logger.BeginScope("[operation]")) using (logger.BeginScope("[hardware]")) { - logger.LogError("{name} is broken.", "refrigerator"); + logger.LogError("{Name} is broken.", "refrigerator"); } // message will be redacted by MyRedactionProcessor - logger.LogInformation("OpenTelemetry {sensitiveString}.", ""); + logger.LogInformation("OpenTelemetry {SensitiveString}.", ""); } internal struct Food diff --git a/docs/logs/extending-the-sdk/extending-the-sdk.csproj b/docs/logs/extending-the-sdk/extending-the-sdk.csproj index 4d96c349671..e775de84b5d 100644 --- a/docs/logs/extending-the-sdk/extending-the-sdk.csproj +++ b/docs/logs/extending-the-sdk/extending-the-sdk.csproj @@ -1,4 +1,8 @@  + + $(NoWarn);CA2000;CA1848;CA1510;CA1305 + + diff --git a/docs/logs/redaction/MyRedactionProcessor.cs b/docs/logs/redaction/MyRedactionProcessor.cs index 7959faf3d60..cec1edc4504 100644 --- a/docs/logs/redaction/MyRedactionProcessor.cs +++ b/docs/logs/redaction/MyRedactionProcessor.cs @@ -15,18 +15,18 @@ public override void OnEnd(LogRecord logRecord) } } - internal sealed class MyClassWithRedactionEnumerator : IReadOnlyList> + internal sealed class MyClassWithRedactionEnumerator : IReadOnlyList> { - private readonly IReadOnlyList> state; + private readonly IReadOnlyList> state; - public MyClassWithRedactionEnumerator(IReadOnlyList> state) + public MyClassWithRedactionEnumerator(IReadOnlyList> state) { this.state = state; } public int Count => this.state.Count; - public KeyValuePair this[int index] + public KeyValuePair this[int index] { get { @@ -34,14 +34,14 @@ public KeyValuePair this[int index] var entryVal = item.Value?.ToString(); if (entryVal != null && entryVal.Contains("")) { - return new KeyValuePair(item.Key, "newRedactedValueHere"); + return new KeyValuePair(item.Key, "newRedactedValueHere"); } return item; } } - public IEnumerator> GetEnumerator() + public IEnumerator> GetEnumerator() { for (var i = 0; i < this.Count; i++) { diff --git a/docs/logs/redaction/redaction.csproj b/docs/logs/redaction/redaction.csproj index 2dc5d8deb63..f9c04fa7bff 100644 --- a/docs/logs/redaction/redaction.csproj +++ b/docs/logs/redaction/redaction.csproj @@ -1,8 +1,8 @@ - - disable + $(NoWarn);CA2213;CA1812;CA1307 + diff --git a/docs/metrics/README.md b/docs/metrics/README.md index a96ade061b3..a51c76319bb 100644 --- a/docs/metrics/README.md +++ b/docs/metrics/README.md @@ -10,11 +10,13 @@ * [Instruments](#instruments) * [MeterProvider Management](#meterprovider-management) * [Memory Management](#memory-management) + * [Example](#example) * [Pre-Aggregation](#pre-aggregation) * [Cardinality Limits](#cardinality-limits) * [Memory Preallocation](#memory-preallocation) * [Metrics Correlation](#metrics-correlation) * [Metrics Enrichment](#metrics-enrichment) +* [Common issues that lead to missing metrics](#common-issues-that-lead-to-missing-metrics) @@ -57,7 +59,7 @@ 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)). +[InstrumentationSource.cs](../../examples/AspNetCore/InstrumentationSource.cs)). :heavy_check_mark: You should use dot-separated [UpperCamelCase](https://en.wikipedia.org/wiki/Camel_case) as the @@ -86,7 +88,7 @@ static readonly Meter MyMeter = new("MyCompany.MyProduct.MyLibrary", "1.0"); | [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 | + | [Gauge](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#gauge) | [`Gauge`](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.gauge-1) | | [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) | @@ -95,7 +97,7 @@ 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)). +[InstrumentationSource.cs](../../examples/AspNetCore/InstrumentationSource.cs)). :stop_sign: You should avoid invalid instrument names. @@ -127,7 +129,7 @@ There are two different ways of passing tags to an instrument API: * Pass the tags directly to the instrument API: ```csharp - counter.Add(100, ("Key1", "Value1"), ("Key2", "Value2")); + counter.Add(100, new("Key1", "Value1"), new("Key2", "Value2")); ``` * Use @@ -386,37 +388,25 @@ 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 +As of `1.10.0` once a metric has reached the cardinality limit, any new +measurement that could not be independently aggregated will be automatically 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. +attribute](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#overflow-attribute). > [!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 +> In SDK versions `1.6.0` - `1.9.0` the overflow attribute was an experimental + feature that could be enabled by setting the environment variable + `OTEL_DOTNET_EXPERIMENTAL_METRICS_EMIT_OVERFLOW_ATTRIBUTE=true`. + +As of `1.10.0` 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. +is used, it is possible to choose a smaller cardinality limit because the SDK +will 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. +> In SDK versions `1.7.0` - `1.9.0`, metric point reclaim was an experimental + feature that could be enabled by setting the environment variable + `OTEL_DOTNET_EXPERIMENTAL_METRICS_RECLAIM_UNUSED_METRIC_POINTS=true`. ### Memory Preallocation diff --git a/docs/metrics/customizing-the-sdk/Program.cs b/docs/metrics/customizing-the-sdk/Program.cs index c88a295fdd7..cdcb6dcbbaf 100644 --- a/docs/metrics/customizing-the-sdk/Program.cs +++ b/docs/metrics/customizing-the-sdk/Program.cs @@ -8,7 +8,7 @@ namespace CustomizingTheSdk; -public class Program +internal static class Program { private static readonly Meter Meter1 = new("CompanyA.ProductA.Library1", "1.0"); private static readonly Meter Meter2 = new("CompanyA.ProductB.Library2", "1.0"); @@ -29,17 +29,23 @@ public static void Main() .AddView(instrumentName: "MyCounter", name: "MyCounterRenamed") // Change Histogram boundaries using the Explicit Bucket Histogram aggregation. - .AddView(instrumentName: "MyHistogram", new ExplicitBucketHistogramConfiguration() { Boundaries = new double[] { 10, 20 } }) + .AddView(instrumentName: "MyHistogram", new ExplicitBucketHistogramConfiguration() { Boundaries = [10.0, 20.0] }) // Change Histogram to use the Base2 Exponential Bucket Histogram aggregation. .AddView(instrumentName: "MyExponentialBucketHistogram", new Base2ExponentialBucketHistogramConfiguration()) // For the instrument "MyCounterCustomTags", aggregate with only the keys "tag1", "tag2". - .AddView(instrumentName: "MyCounterCustomTags", new MetricStreamConfiguration() { TagKeys = new string[] { "tag1", "tag2" } }) + .AddView(instrumentName: "MyCounterCustomTags", new MetricStreamConfiguration() { TagKeys = ["tag1", "tag2"] }) // Drop the instrument "MyCounterDrop". .AddView(instrumentName: "MyCounterDrop", MetricStreamConfiguration.Drop) + // Configure the Explicit Bucket Histogram aggregation with custom boundaries and new name. + .AddView(instrumentName: "histogramWithMultipleAggregations", new ExplicitBucketHistogramConfiguration() { Boundaries = [10.0, 20.0], Name = "MyHistogramWithExplicitHistogram" }) + + // Use Base2 Exponential Bucket Histogram aggregation and new name. + .AddView(instrumentName: "histogramWithMultipleAggregations", new Base2ExponentialBucketHistogramConfiguration() { Name = "MyHistogramWithBase2ExponentialBucketHistogram" }) + // An instrument which does not match any views // gets processed with default behavior. (SDK default) // Uncommenting the following line will @@ -70,6 +76,12 @@ public static void Main() exponentialBucketHistogram.Record(random.Next(1, 1000), new("tag1", "value1"), new("tag2", "value2")); } + var histogramWithMultipleAggregations = Meter1.CreateHistogram("histogramWithMultipleAggregations"); + for (int i = 0; i < 20000; i++) + { + histogramWithMultipleAggregations.Record(random.Next(1, 1000), new("tag1", "value1"), new("tag2", "value2")); + } + var counterCustomTags = Meter1.CreateCounter("MyCounterCustomTags"); for (int i = 0; i < 20000; i++) { diff --git a/docs/metrics/customizing-the-sdk/README.md b/docs/metrics/customizing-the-sdk/README.md index 830ad66957c..75d55077a15 100644 --- a/docs/metrics/customizing-the-sdk/README.md +++ b/docs/metrics/customizing-the-sdk/README.md @@ -200,29 +200,56 @@ used. ##### Explicit bucket histogram aggregation -By default, the boundaries used for a Histogram are [`{ 0, 5, 10, 25, 50, 75, -100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000}`](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.14.0/specification/metrics/sdk.md#explicit-bucket-histogram-aggregation). -Views can be used to provide custom boundaries for a Histogram. The measurements -are then aggregated using the custom boundaries provided instead of the the -default boundaries. This requires the use of -`ExplicitBucketHistogramConfiguration`. +By default, the [OpenTelemetry +Specification](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.14.0/specification/metrics/sdk.md#explicit-bucket-histogram-aggregation) +defines explicit buckets (aka boundaries) for Histograms as: `[ 0, 5, 10, 25, +50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000 ]`. -```csharp - // Change Histogram boundaries to count measurements under the following buckets: - // (-inf, 10] - // (10, 20] - // (20, +inf) - .AddView( - instrumentName: "MyHistogram", - new ExplicitBucketHistogramConfiguration { Boundaries = new double[] { 10, 20 } }) +###### Customizing explicit buckets when using histogram aggregation - // If you provide an empty `double` array as `Boundaries` to the `ExplicitBucketHistogramConfiguration`, - // the SDK will only export the sum, count, min and max for the measurements. - // There are no buckets exported in this case. - .AddView( - instrumentName: "MyHistogram", - new ExplicitBucketHistogramConfiguration { Boundaries = Array.Empty() }) -``` +There are two mechanisms available to configure explicit buckets when using +histogram aggregation: + +* View API - Part of the OpenTelemetry .NET SDK. +* Advice API - Part of the `System.Diagnostics.DiagnosticSource` package + starting with version `9.0.0`. + +> [!IMPORTANT] +> When both the View API and Advice API are used, the View API takes precedence. + If explicit buckets are not provided by either the View API or the Advice API + then the SDK defaults apply. + +* View API + + Views can be used to provide custom explicit buckets for a Histogram. This + requires the use of `ExplicitBucketHistogramConfiguration`. + + ```csharp + // Change Histogram boundaries to count measurements under the following buckets: + // (-inf, 10] + // (10, 20] + // (20, +inf) + .AddView( + instrumentName: "MyHistogram", + new ExplicitBucketHistogramConfiguration { Boundaries = new double[] { 10, 20 } }) + + // If you provide an empty `double` array as `Boundaries` to the `ExplicitBucketHistogramConfiguration`, + // the SDK will only export the sum, count, min and max for the measurements. + // There are no buckets exported in this case. + .AddView( + instrumentName: "MyHistogram", + new ExplicitBucketHistogramConfiguration { Boundaries = Array.Empty() }) + ``` + +* Advice API + + Starting with the `1.10.0` SDK, explicit buckets for a Histogram may be provided + by instrumentation authors when the instrument is created. This is generally + recommended to be used by library authors when the SDK defaults don't match the + required granularity for the histogram being emitted. + + See: [Using Advice to customize Histogram + instruments](https://learn.microsoft.com/dotnet/core/diagnostics/metrics-instrumentation#using-advice-to-customize-histogram-instruments). ##### Base2 exponential bucket histogram aggregation @@ -245,6 +272,90 @@ within the maximum number of buckets defined by `MaxSize`. The default new Base2ExponentialBucketHistogramConfiguration { MaxSize = 40 }) ``` +#### Produce multiple metrics from single instrument + +When an instrument matches multiple views, it can generate multiple metrics. For +instance, if an instrument is matched by two different view configurations, it +will result in two separate metrics being produced from that single instrument. +Below is an example demonstrating how to leverage this capability to create two +independent metrics from a single instrument. In this example, a histogram +instrument is used to report measurements, and views are configured to produce +two metrics : one aggregated using `ExplicitBucketHistogramConfiguration` and the +other using `Base2ExponentialBucketHistogramConfiguration`. + +```csharp + var histogramWithMultipleAggregations = meter.CreateHistogram("HistogramWithMultipleAggregations"); + + // Configure the Explicit Bucket Histogram aggregation with custom boundaries and new name. + .AddView(instrumentName: "HistogramWithMultipleAggregations", new ExplicitBucketHistogramConfiguration() { Boundaries = new double[] { 10, 20 }, Name = "MyHistogramWithExplicitHistogram" }) + + // Use Base2 Exponential Bucket Histogram aggregation and new name. + .AddView(instrumentName: "HistogramWithMultipleAggregations", new Base2ExponentialBucketHistogramConfiguration() { Name = "MyHistogramWithBase2ExponentialBucketHistogram" }) + + // Both views rename the metric to avoid name conflicts. However, in this case, + // renaming one would be sufficient. + + // This measurement will be aggregated into two separate metrics. + histogramWithMultipleAggregations.Record(10, new("tag1", "value1"), new("tag2", "value2")); +``` + +When using views that produce multiple metrics from single instrument, it's +crucial to rename the metric to prevent conflicts. In the event of conflict, +OpenTelemetry will emit an internal warning but will still export both metrics. +The impact of this behavior depends on the backend or receiver being used. You +can refer to [OpenTelemetry's +specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#opentelemetry-protocol-data-model-consumer-recommendations) +for more details. + +Below example is showing the *BAD* practice. DO NOT FOLLOW it. + +```csharp + var histogram = meter.CreateHistogram("MyHistogram"); + + // Configure a view to aggregate based only on the "location" tag. + .AddView(instrumentName: "MyHistogram", metricStreamConfiguration: new MetricStreamConfiguration + { + TagKeys = new string[] { "location" }, + }) + + // Configure another view to aggregate based only on the "status" tag. + .AddView(instrumentName: "MyHistogram", metricStreamConfiguration: new MetricStreamConfiguration + { + TagKeys = new string[] { "status" }, + }) + + // The measurement below will be aggregated into two metric streams, but both will have the same name. + // OpenTelemetry will issue a warning about this conflict and pass both streams to the exporter. + // However, this may cause issues depending on the backend. + histogram.Record(10, new("location", "seattle"), new("status", "OK")); +``` + +The modified version, avoiding name conflict is shown below: + +```csharp + var histogram = meter.CreateHistogram("MyHistogram"); + + // Configure a view to aggregate based only on the "location" tag, + // and rename the metric. + .AddView(instrumentName: "MyHistogram", metricStreamConfiguration: new MetricStreamConfiguration + { + Name = "MyHistogramWithLocation", + TagKeys = new string[] { "location" }, + }) + + // Configure a view to aggregate based only on the "status" tag, + // and rename the metric. + .AddView(instrumentName: "MyHistogram", metricStreamConfiguration: new MetricStreamConfiguration + { + Name = "MyHistogramWithStatus", + TagKeys = new string[] { "status" }, + }) + + // The measurement below will be aggregated into two separate metrics, "MyHistogramWithLocation" + // and "MyHistogramWithStatus". + histogram.Record(10, new("location", "seattle"), new("status", "OK")); +``` + > [!NOTE] > The SDK currently does not support any changes to `Aggregation` type by using Views. @@ -319,9 +430,8 @@ metrics managed by a given `MeterProvider`, use the > [!CAUTION] > `MeterProviderBuilder.SetMaxMetricPointsPerMetricStream` is marked `Obsolete` - in pre-release builds and has been replaced by - `MetricStreamConfiguration.CardinalityLimit`. For details see: - [OTEL1003](../../diagnostics/experimental-apis/OTEL1003.md). + in stable builds since 1.10.0 and has been replaced by + `MetricStreamConfiguration.CardinalityLimit`. ```csharp using var meterProvider = Sdk.CreateMeterProviderBuilder() @@ -337,11 +447,6 @@ To set the [cardinality limit](../README.md#cardinality-limits) for an individual metric, use the `MetricStreamConfiguration.CardinalityLimit` property on the View API: -> [!NOTE] -> `MetricStreamConfiguration.CardinalityLimit` is an experimental API only - available in pre-release builds. For details see: - [OTEL1003](../../diagnostics/experimental-apis/OTEL1003.md). - ```csharp var meterProvider = Sdk.CreateMeterProviderBuilder() .AddMeter("MyCompany.MyProduct.MyLibrary") diff --git a/docs/metrics/customizing-the-sdk/customizing-the-sdk.csproj b/docs/metrics/customizing-the-sdk/customizing-the-sdk.csproj index 19aa9791432..97876c06b9c 100644 --- a/docs/metrics/customizing-the-sdk/customizing-the-sdk.csproj +++ b/docs/metrics/customizing-the-sdk/customizing-the-sdk.csproj @@ -1,4 +1,8 @@ + + $(NoWarn);CA5394 + + diff --git a/docs/metrics/exemplars/Program.cs b/docs/metrics/exemplars/Program.cs index cd63ac77ecc..d54e0ff6a2b 100644 --- a/docs/metrics/exemplars/Program.cs +++ b/docs/metrics/exemplars/Program.cs @@ -8,7 +8,9 @@ using OpenTelemetry.Metrics; using OpenTelemetry.Trace; -public class Program +namespace Exemplars; + +internal static class Program { private static readonly ActivitySource MyActivitySource = new("OpenTelemetry.Demo.Exemplar"); private static readonly Meter MyMeter = new("OpenTelemetry.Demo.Exemplar"); diff --git a/docs/metrics/exemplars/README.md b/docs/metrics/exemplars/README.md index 89f88866560..1f532c2ff0e 100644 --- a/docs/metrics/exemplars/README.md +++ b/docs/metrics/exemplars/README.md @@ -58,7 +58,7 @@ and enabled: ```sh -./prometheus --enable-feature=exemplar-storage --enable-feature=otlp-write-receiver +./prometheus --enable-feature=exemplar-storage --web.enable-otlp-receiver ``` ## Install and configure Grafana diff --git a/docs/metrics/exemplars/exemplars.csproj b/docs/metrics/exemplars/exemplars.csproj index cce12eec60d..686278e7a42 100644 --- a/docs/metrics/exemplars/exemplars.csproj +++ b/docs/metrics/exemplars/exemplars.csproj @@ -1,4 +1,8 @@ + + $(NoWarn);CA5394 + + diff --git a/docs/metrics/extending-the-sdk/MyExporter.cs b/docs/metrics/extending-the-sdk/MyExporter.cs index 228d3e0c342..376fa2f5b97 100644 --- a/docs/metrics/extending-the-sdk/MyExporter.cs +++ b/docs/metrics/extending-the-sdk/MyExporter.cs @@ -5,7 +5,7 @@ using OpenTelemetry; using OpenTelemetry.Metrics; -internal class MyExporter : BaseExporter +internal sealed class MyExporter : BaseExporter { private readonly string name; @@ -28,7 +28,7 @@ public override ExportResult Export(in Batch batch) sb.Append(", "); } - sb.Append($"{metric.Name}"); + sb.Append(metric.Name); foreach (ref readonly var metricPoint in metric.GetMetricPoints()) { @@ -52,6 +52,7 @@ protected override bool OnShutdown(int timeoutMilliseconds) protected override void Dispose(bool disposing) { + base.Dispose(disposing); Console.WriteLine($"{this.name}.Dispose({disposing})"); } } diff --git a/docs/metrics/extending-the-sdk/Program.cs b/docs/metrics/extending-the-sdk/Program.cs index a2952062261..0a7cf838cd1 100644 --- a/docs/metrics/extending-the-sdk/Program.cs +++ b/docs/metrics/extending-the-sdk/Program.cs @@ -8,7 +8,7 @@ namespace ExtendingTheSdk; -public class Program +internal static class Program { private static readonly Meter MyMeter = new("MyCompany.MyProduct.MyLibrary", "1.0"); private static readonly Counter MyFruitCounter = MyMeter.CreateCounter("MyFruitCounter"); diff --git a/docs/metrics/extending-the-sdk/extending-the-sdk.csproj b/docs/metrics/extending-the-sdk/extending-the-sdk.csproj index 4d96c349671..98a53a13c5a 100644 --- a/docs/metrics/extending-the-sdk/extending-the-sdk.csproj +++ b/docs/metrics/extending-the-sdk/extending-the-sdk.csproj @@ -1,4 +1,8 @@  + + $(NoWarn);CA2000;CA1510;CA1305 + + diff --git a/docs/metrics/getting-started-console/Program.cs b/docs/metrics/getting-started-console/Program.cs index 89425c6d41c..dbc42339877 100644 --- a/docs/metrics/getting-started-console/Program.cs +++ b/docs/metrics/getting-started-console/Program.cs @@ -5,7 +5,9 @@ using OpenTelemetry; using OpenTelemetry.Metrics; -public class Program +namespace GettingStartedConsole; + +internal static class Program { private static readonly Meter MyMeter = new("MyCompany.MyProduct.MyLibrary", "1.0"); private static readonly Counter MyFruitCounter = MyMeter.CreateCounter("MyFruitCounter"); diff --git a/docs/metrics/getting-started-prometheus-grafana/Program.cs b/docs/metrics/getting-started-prometheus-grafana/Program.cs index 0fd2437eefd..848116636c0 100644 --- a/docs/metrics/getting-started-prometheus-grafana/Program.cs +++ b/docs/metrics/getting-started-prometheus-grafana/Program.cs @@ -6,7 +6,9 @@ using OpenTelemetry.Exporter; using OpenTelemetry.Metrics; -public class Program +namespace GettingStartedPrometheusGrafana; + +internal static class Program { private static readonly Meter MyMeter = new("MyCompany.MyProduct.MyLibrary", "1.0"); private static readonly Counter MyFruitCounter = MyMeter.CreateCounter("MyFruitCounter"); diff --git a/docs/metrics/getting-started-prometheus-grafana/README.md b/docs/metrics/getting-started-prometheus-grafana/README.md index dfc3e09c1a3..75a09cb1226 100644 --- a/docs/metrics/getting-started-prometheus-grafana/README.md +++ b/docs/metrics/getting-started-prometheus-grafana/README.md @@ -88,7 +88,7 @@ access. Run the `prometheus(.exe)` server executable with feature flag enabled: ```sh -./prometheus --enable-feature=otlp-write-receiver +./prometheus --web.enable-otlp-receiver ``` ### View results in Prometheus diff --git a/docs/metrics/learning-more-instruments/Program.cs b/docs/metrics/learning-more-instruments/Program.cs index c887e281461..9acbcac5639 100644 --- a/docs/metrics/learning-more-instruments/Program.cs +++ b/docs/metrics/learning-more-instruments/Program.cs @@ -8,7 +8,7 @@ namespace LearningMoreInstruments; -public class Program +internal static class Program { private static readonly Meter MyMeter = new("MyCompany.MyProduct.MyLibrary", "1.0"); private static readonly Histogram MyHistogram = MyMeter.CreateHistogram("MyHistogram"); diff --git a/docs/metrics/learning-more-instruments/learning-more-instruments.csproj b/docs/metrics/learning-more-instruments/learning-more-instruments.csproj index 9f5b6b79bc3..001e041f0f7 100644 --- a/docs/metrics/learning-more-instruments/learning-more-instruments.csproj +++ b/docs/metrics/learning-more-instruments/learning-more-instruments.csproj @@ -1,4 +1,8 @@ + + $(NoWarn);CA5394 + + diff --git a/docs/resources/extending-the-sdk/LoggerExtensions.cs b/docs/resources/extending-the-sdk/LoggerExtensions.cs new file mode 100644 index 00000000000..a57502fc0f8 --- /dev/null +++ b/docs/resources/extending-the-sdk/LoggerExtensions.cs @@ -0,0 +1,12 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.Logging; + +namespace ExtendingTheSdk; + +internal static partial class LoggerExtensions +{ + [LoggerMessage(LogLevel.Information, "Hello from {Name} {Price}")] + public static partial void HelloFrom(this ILogger logger, string name, double price); +} diff --git a/docs/resources/extending-the-sdk/MyResourceDetector.cs b/docs/resources/extending-the-sdk/MyResourceDetector.cs index df40d9b3302..978e4a7edb6 100644 --- a/docs/resources/extending-the-sdk/MyResourceDetector.cs +++ b/docs/resources/extending-the-sdk/MyResourceDetector.cs @@ -3,7 +3,7 @@ using OpenTelemetry.Resources; -internal class MyResourceDetector : IResourceDetector +internal sealed class MyResourceDetector : IResourceDetector { public Resource Detect() { diff --git a/docs/resources/extending-the-sdk/Program.cs b/docs/resources/extending-the-sdk/Program.cs index 849bdcc182b..6877d0c5cc7 100644 --- a/docs/resources/extending-the-sdk/Program.cs +++ b/docs/resources/extending-the-sdk/Program.cs @@ -11,7 +11,7 @@ namespace ExtendingTheSdk; -public class Program +internal static class Program { private static readonly ActivitySource DemoSource = new("OTel.Demo"); private static readonly Meter MeterDemoSource = new("OTel.Demo"); @@ -55,7 +55,6 @@ public static void Main() } var logger = loggerFactory.CreateLogger("OTel.Demo"); - logger - .LogInformation("Hello from {Name} {Price}", "tomato", 2.99); + logger.HelloFrom("tomato", 2.99); } } diff --git a/docs/trace/README.md b/docs/trace/README.md index f0cdb973e1e..80f7c48978e 100644 --- a/docs/trace/README.md +++ b/docs/trace/README.md @@ -49,7 +49,7 @@ 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)). +[InstrumentationSource.cs](../../examples/AspNetCore/InstrumentationSource.cs)). :heavy_check_mark: You should use dot-separated [UpperCamelCase](https://en.wikipedia.org/wiki/Camel_case) as the diff --git a/docs/trace/customizing-the-sdk/Program.cs b/docs/trace/customizing-the-sdk/Program.cs index fead3aa0be2..1cc63b0fcc5 100644 --- a/docs/trace/customizing-the-sdk/Program.cs +++ b/docs/trace/customizing-the-sdk/Program.cs @@ -8,7 +8,7 @@ namespace CustomizingTheSdk; -public class Program +internal static class Program { private static readonly ActivitySource MyLibraryActivitySource = new( "MyCompany.MyProduct.MyLibrary"); diff --git a/docs/trace/customizing-the-sdk/README.md b/docs/trace/customizing-the-sdk/README.md index d78ed1cd569..b9cec40e949 100644 --- a/docs/trace/customizing-the-sdk/README.md +++ b/docs/trace/customizing-the-sdk/README.md @@ -477,8 +477,8 @@ When using the `AddOpenTelemetry` & `WithTracing` extension methods the into an existing collection (typically the collection used is the one managed by the application host). The `TracerProviderBuilder` will be able to access all services registered into that collection. For lifecycle management, the -`AddOpenTelemetry` registers an [IHostedService -](https://learn.microsoft.com/dotnet/api/microsoft.extensions.hosting.ihostedservice) +`AddOpenTelemetry` registers an +[IHostedService](https://learn.microsoft.com/dotnet/api/microsoft.extensions.hosting.ihostedservice) 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. diff --git a/docs/trace/customizing-the-sdk/customizing-the-sdk.csproj b/docs/trace/customizing-the-sdk/customizing-the-sdk.csproj index 2dc5d8deb63..19aa9791432 100644 --- a/docs/trace/customizing-the-sdk/customizing-the-sdk.csproj +++ b/docs/trace/customizing-the-sdk/customizing-the-sdk.csproj @@ -1,8 +1,4 @@ - - - disable - diff --git a/docs/trace/extending-the-sdk/MyEnrichingProcessor.cs b/docs/trace/extending-the-sdk/MyEnrichingProcessor.cs index a6107330485..4372f9e59f3 100644 --- a/docs/trace/extending-the-sdk/MyEnrichingProcessor.cs +++ b/docs/trace/extending-the-sdk/MyEnrichingProcessor.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using OpenTelemetry; -internal class MyEnrichingProcessor : BaseProcessor +internal sealed class MyEnrichingProcessor : BaseProcessor { public override void OnEnd(Activity activity) { diff --git a/docs/trace/extending-the-sdk/MyExporter.cs b/docs/trace/extending-the-sdk/MyExporter.cs index d37b9daac46..f00674d1ffc 100644 --- a/docs/trace/extending-the-sdk/MyExporter.cs +++ b/docs/trace/extending-the-sdk/MyExporter.cs @@ -5,7 +5,7 @@ using System.Text; using OpenTelemetry; -internal class MyExporter : BaseExporter +internal sealed class MyExporter : BaseExporter { private readonly string name; @@ -43,6 +43,7 @@ protected override bool OnShutdown(int timeoutMilliseconds) protected override void Dispose(bool disposing) { + base.Dispose(disposing); Console.WriteLine($"{this.name}.Dispose({disposing})"); } } diff --git a/docs/trace/extending-the-sdk/MyProcessor.cs b/docs/trace/extending-the-sdk/MyProcessor.cs index 4171821d49a..d6ab394ce03 100644 --- a/docs/trace/extending-the-sdk/MyProcessor.cs +++ b/docs/trace/extending-the-sdk/MyProcessor.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using OpenTelemetry; -internal class MyProcessor : BaseProcessor +internal sealed class MyProcessor : BaseProcessor { private readonly string name; @@ -37,6 +37,7 @@ protected override bool OnShutdown(int timeoutMilliseconds) protected override void Dispose(bool disposing) { + base.Dispose(disposing); Console.WriteLine($"{this.name}.Dispose({disposing})"); } } diff --git a/docs/trace/extending-the-sdk/MyResourceDetector.cs b/docs/trace/extending-the-sdk/MyResourceDetector.cs index df40d9b3302..978e4a7edb6 100644 --- a/docs/trace/extending-the-sdk/MyResourceDetector.cs +++ b/docs/trace/extending-the-sdk/MyResourceDetector.cs @@ -3,7 +3,7 @@ using OpenTelemetry.Resources; -internal class MyResourceDetector : IResourceDetector +internal sealed class MyResourceDetector : IResourceDetector { public Resource Detect() { diff --git a/docs/trace/extending-the-sdk/MySampler.cs b/docs/trace/extending-the-sdk/MySampler.cs index bbb4ae04793..2d733aed25d 100644 --- a/docs/trace/extending-the-sdk/MySampler.cs +++ b/docs/trace/extending-the-sdk/MySampler.cs @@ -3,7 +3,7 @@ using OpenTelemetry.Trace; -internal class MySampler : Sampler +internal sealed class MySampler : Sampler { public override SamplingResult ShouldSample(in SamplingParameters param) { diff --git a/docs/trace/extending-the-sdk/Program.cs b/docs/trace/extending-the-sdk/Program.cs index ce001aee3e3..e051d4ed71a 100644 --- a/docs/trace/extending-the-sdk/Program.cs +++ b/docs/trace/extending-the-sdk/Program.cs @@ -8,7 +8,7 @@ namespace ExtendingTheSdk; -public class Program +internal static class Program { private static readonly ActivitySource DemoSource = new("OTel.Demo"); diff --git a/docs/trace/extending-the-sdk/README.md b/docs/trace/extending-the-sdk/README.md index 4f7380ee64b..db75bad7131 100644 --- a/docs/trace/extending-the-sdk/README.md +++ b/docs/trace/extending-the-sdk/README.md @@ -320,10 +320,10 @@ cases, it is recommended to use that option as it offers higher performance. OpenTelemetry .NET SDK has provided the following built-in samplers: -* [AlwaysOffSampler](../../../src/OpenTelemetry/Trace/AlwaysOffSampler.cs) -* [AlwaysOnSampler](../../../src/OpenTelemetry/Trace/AlwaysOnSampler.cs) -* [ParentBasedSampler](../../../src/OpenTelemetry/Trace/ParentBasedSampler.cs) -* [TraceIdRatioBasedSampler](../../../src/OpenTelemetry/Trace/TraceIdRatioBasedSampler.cs) +* [AlwaysOffSampler](../../../src/OpenTelemetry/Trace/Sampler/AlwaysOffSampler.cs) +* [AlwaysOnSampler](../../../src/OpenTelemetry/Trace/Sampler/AlwaysOnSampler.cs) +* [ParentBasedSampler](../../../src/OpenTelemetry/Trace/Sampler/ParentBasedSampler.cs) +* [TraceIdRatioBasedSampler](../../../src/OpenTelemetry/Trace/Sampler/TraceIdRatioBasedSampler.cs) Custom samplers can be implemented to cover more scenarios: @@ -338,7 +338,7 @@ class MySampler : Sampler { public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) { - return new SamplingResult(SamplingDecision.RecordAndSampled); + return new SamplingResult(SamplingDecision.RecordAndSample); } } ``` diff --git a/docs/trace/extending-the-sdk/extending-the-sdk.csproj b/docs/trace/extending-the-sdk/extending-the-sdk.csproj index 85aab7a7ed3..2f56fd33163 100644 --- a/docs/trace/extending-the-sdk/extending-the-sdk.csproj +++ b/docs/trace/extending-the-sdk/extending-the-sdk.csproj @@ -1,4 +1,8 @@ + + $(NoWarn);CA2000;CA1812;CA1510 + + diff --git a/docs/trace/getting-started-aspnetcore/README.md b/docs/trace/getting-started-aspnetcore/README.md index a9b7adf0b3d..7febcf4d55f 100644 --- a/docs/trace/getting-started-aspnetcore/README.md +++ b/docs/trace/getting-started-aspnetcore/README.md @@ -30,30 +30,30 @@ in the console for your application (ex `http://localhost:5154`). You should see the trace output from the console. ```text -Activity.TraceId: c1572aa14ee9c0ac037dbdc3e91e5dd7 -Activity.SpanId: 45406137f33cc279 +Activity.TraceId: c28f7b480d5c7dfc30cfbd80ad29028d +Activity.SpanId: 27e478bbf9fdec10 Activity.TraceFlags: Recorded -Activity.ActivitySourceName: OpenTelemetry.Instrumentation.AspNetCore -Activity.DisplayName: / +Activity.ActivitySourceName: Microsoft.AspNetCore +Activity.DisplayName: GET / Activity.Kind: Server -Activity.StartTime: 2023-01-13T19:38:11.5417593Z -Activity.Duration: 00:00:00.0167407 +Activity.StartTime: 2024-07-04T13:03:37.3318740Z +Activity.Duration: 00:00:00.3693734 Activity.Tags: - net.host.name: localhost - net.host.port: 5154 - http.method: GET - http.scheme: http - http.target: / - http.url: http://localhost:5154/ - http.flavor: 1.1 - http.user_agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.76 - http.status_code: 200 + server.address: localhost + server.port: 5154 + http.request.method: GET + url.scheme: https + url.path: / + network.protocol.version: 2 + user_agent.original: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 + http.route: / + http.response.status_code: 200 Resource associated with Activity: service.name: getting-started-aspnetcore - service.instance.id: 32c9371c-ed9d-474c-a698-b169e87a5577 + service.instance.id: a388466b-4969-4bb0-ad96-8f39527fa66b telemetry.sdk.name: opentelemetry telemetry.sdk.language: dotnet - telemetry.sdk.version: 1.5.1 + telemetry.sdk.version: 1.9.0 ``` Congratulations! You are now collecting traces using OpenTelemetry. diff --git a/docs/trace/getting-started-console/Program.cs b/docs/trace/getting-started-console/Program.cs index 4acaf7b8a80..3ab95d4d24f 100644 --- a/docs/trace/getting-started-console/Program.cs +++ b/docs/trace/getting-started-console/Program.cs @@ -5,7 +5,9 @@ using OpenTelemetry; using OpenTelemetry.Trace; -public class Program +namespace GettingStartedConsole; + +internal static class Program { private static readonly ActivitySource MyActivitySource = new("MyCompany.MyProduct.MyLibrary"); @@ -18,9 +20,10 @@ public static void Main() using (var activity = MyActivitySource.StartActivity("SayHello")) { + int[] intArray = [1, 2, 3]; activity?.SetTag("foo", 1); activity?.SetTag("bar", "Hello, World!"); - activity?.SetTag("baz", new int[] { 1, 2, 3 }); + activity?.SetTag("baz", intArray); activity?.SetStatus(ActivityStatusCode.Ok); } diff --git a/docs/trace/getting-started-jaeger/Program.cs b/docs/trace/getting-started-jaeger/Program.cs index d4603a71bd0..bff8079f132 100644 --- a/docs/trace/getting-started-jaeger/Program.cs +++ b/docs/trace/getting-started-jaeger/Program.cs @@ -11,7 +11,7 @@ namespace GettingStartedJaeger; -public class Program +internal static class Program { private static readonly ActivitySource MyActivitySource = new("OpenTelemetry.Demo.Jaeger"); @@ -33,13 +33,13 @@ public static async Task Main() { using (var slow = MyActivitySource.StartActivity("SomethingSlow")) { - await client.GetStringAsync("https://httpstat.us/200?sleep=1000"); - await client.GetStringAsync("https://httpstat.us/200?sleep=1000"); + await client.GetStringAsync(new Uri("https://httpstat.us/200?sleep=1000")).ConfigureAwait(false); + await client.GetStringAsync(new Uri("https://httpstat.us/200?sleep=1000")).ConfigureAwait(false); } using (var fast = MyActivitySource.StartActivity("SomethingFast")) { - await client.GetStringAsync("https://httpstat.us/301"); + await client.GetStringAsync(new Uri("https://httpstat.us/301")).ConfigureAwait(false); } } } diff --git a/docs/trace/getting-started-jaeger/README.md b/docs/trace/getting-started-jaeger/README.md index b291933df00..92752ff66da 100644 --- a/docs/trace/getting-started-jaeger/README.md +++ b/docs/trace/getting-started-jaeger/README.md @@ -47,25 +47,29 @@ Run the application again and we should see the trace output from the console: ```text > dotnet run -Activity.TraceId: a80c920e0aabb50b547e2bb7455cfd39 -Activity.SpanId: 4e45a1d51744f329 -Activity.TraceFlags: Recorded -Activity.ParentSpanId: 4f7e9b78c55dcfad -Activity.ActivitySourceName: OpenTelemetry.Instrumentation.Http -Activity.DisplayName: HTTP GET -Activity.Kind: Client -Activity.StartTime: 2022-05-07T02:54:25.7840762Z -Activity.Duration: 00:00:01.9615540 +Activity.TraceId: 693f1d15634bfe6ba3254d6f9d20df27 +Activity.SpanId: 429cc5a90a753fb3 +Activity.TraceFlags: Recorded +Activity.ParentSpanId: 0d64498b736c9a11 +Activity.ActivitySourceName: System.Net.Http +Activity.DisplayName: GET +Activity.Kind: Client +Activity.StartTime: 2024-07-04T13:18:12.2408786Z +Activity.Duration: 00:00:02.1028562 Activity.Tags: - http.method: GET - http.host: httpstat.us - http.url: https://httpstat.us/200?sleep=1000 - http.status_code: 200 + http.request.method: GET + server.address: httpstat.us + server.port: 443 + url.full: https://httpstat.us/200?sleep=Redacted + network.protocol.version: 1.1 + http.response.status_code: 200 Resource associated with Activity: service.name: DemoApp service.version: 1.0.0 - service.instance.id: 1b3b3a6f-be43-46b0-819a-4db1200c633d - + service.instance.id: 03ccafab-e9a7-440a-a9cd-9a0163e0d06c + telemetry.sdk.name: opentelemetry + telemetry.sdk.language: dotnet + telemetry.sdk.version: 1.9.0 ... ``` diff --git a/docs/trace/links-based-sampler/LinksAndParentBasedSampler.cs b/docs/trace/links-based-sampler/LinksAndParentBasedSampler.cs index bca076a2734..af44cb92072 100644 --- a/docs/trace/links-based-sampler/LinksAndParentBasedSampler.cs +++ b/docs/trace/links-based-sampler/LinksAndParentBasedSampler.cs @@ -13,7 +13,7 @@ namespace LinksAndParentBasedSamplerExample; /// links based sampler. If either of these samplers decide to sample, /// this composite sampler decides to sample. /// -internal class LinksAndParentBasedSampler : Sampler +internal sealed class LinksAndParentBasedSampler : Sampler { private readonly ParentBasedSampler parentBasedSampler; private readonly LinksBasedSampler linksBasedSampler; diff --git a/docs/trace/links-based-sampler/LinksBasedSampler.cs b/docs/trace/links-based-sampler/LinksBasedSampler.cs index b9306fab232..226b6591693 100644 --- a/docs/trace/links-based-sampler/LinksBasedSampler.cs +++ b/docs/trace/links-based-sampler/LinksBasedSampler.cs @@ -10,7 +10,7 @@ namespace LinksAndParentBasedSamplerExample; /// A non-probabilistic sampler that samples an activity if ANY of the linked activities /// is sampled. /// -internal class LinksBasedSampler : Sampler +internal sealed class LinksBasedSampler : Sampler { public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) { diff --git a/docs/trace/links-based-sampler/Program.cs b/docs/trace/links-based-sampler/Program.cs index 7ef89aa5fdb..03827633820 100644 --- a/docs/trace/links-based-sampler/Program.cs +++ b/docs/trace/links-based-sampler/Program.cs @@ -7,7 +7,7 @@ namespace LinksAndParentBasedSamplerExample; -internal class Program +internal static class Program { private static readonly ActivitySource MyActivitySource = new("LinksAndParentBasedSampler.Example"); @@ -37,7 +37,7 @@ public static void Main(string[] args) /// Generates a list of activity links. A linked activity is sampled with a probability of 0.1. /// /// A list of links. - private static IEnumerable GetActivityLinks(int seed) + private static List GetActivityLinks(int seed) { var random = new Random(seed); var linkedActivitiesList = new List(); diff --git a/docs/trace/links-based-sampler/links-sampler.csproj b/docs/trace/links-based-sampler/links-sampler.csproj index 19aa9791432..97876c06b9c 100644 --- a/docs/trace/links-based-sampler/links-sampler.csproj +++ b/docs/trace/links-based-sampler/links-sampler.csproj @@ -1,4 +1,8 @@ + + $(NoWarn);CA5394 + + diff --git a/docs/trace/links-creation-with-new-activities/Program.cs b/docs/trace/links-creation-with-new-activities/Program.cs index c03af7bcc82..1568930d998 100644 --- a/docs/trace/links-creation-with-new-activities/Program.cs +++ b/docs/trace/links-creation-with-new-activities/Program.cs @@ -7,7 +7,7 @@ namespace LinksCreationWithNewRootActivitiesDemo; -internal class Program +internal static class Program { private static readonly ActivitySource MyActivitySource = new("LinksCreationWithNewRootActivities"); @@ -21,7 +21,7 @@ public static async Task Main(string[] args) using (var activity = MyActivitySource.StartActivity("OrchestratingActivity")) { activity?.SetTag("foo", 1); - await DoFanoutAsync(); + await DoFanoutAsync().ConfigureAwait(false); using (var nestedActivity = MyActivitySource.StartActivity("WrapUp")) { @@ -77,7 +77,7 @@ public static async Task DoFanoutAsync() } // Wait for all tasks to complete - await Task.WhenAll(tasks); + await Task.WhenAll(tasks).ConfigureAwait(false); // 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. diff --git a/docs/trace/reporting-exceptions/Program.cs b/docs/trace/reporting-exceptions/Program.cs index 62f2a11914e..a6e4997ff79 100644 --- a/docs/trace/reporting-exceptions/Program.cs +++ b/docs/trace/reporting-exceptions/Program.cs @@ -7,7 +7,7 @@ namespace ReportingExceptions; -public class Program +internal static class Program { private static readonly ActivitySource MyActivitySource = new( "MyCompany.MyProduct.MyLibrary"); @@ -27,7 +27,7 @@ public static void Main() { using (MyActivitySource.StartActivity("Bar")) { - throw new Exception("Oops!"); + throw new InvalidOperationException("Oops!"); } } } diff --git a/docs/trace/stratified-sampling-example/Program.cs b/docs/trace/stratified-sampling-example/Program.cs index 49d8e1b7e7b..8063dd4f046 100644 --- a/docs/trace/stratified-sampling-example/Program.cs +++ b/docs/trace/stratified-sampling-example/Program.cs @@ -7,7 +7,7 @@ namespace StratifiedSamplingByQueryTypeDemo; -internal class Program +internal sealed class Program { private static readonly ActivitySource MyActivitySource = new("StratifiedSampling.POC"); diff --git a/docs/trace/stratified-sampling-example/StratifiedSampler.cs b/docs/trace/stratified-sampling-example/StratifiedSampler.cs index cb01a23dee0..64cbe8a4c84 100644 --- a/docs/trace/stratified-sampling-example/StratifiedSampler.cs +++ b/docs/trace/stratified-sampling-example/StratifiedSampler.cs @@ -5,7 +5,7 @@ namespace StratifiedSamplingByQueryTypeDemo; -internal class StratifiedSampler : Sampler +internal sealed class StratifiedSampler : Sampler { // For this POC, we have two groups. // 0 is the group corresponding to user-initiated queries where we want a 100% sampling rate. diff --git a/docs/trace/stratified-sampling-example/stratified-sampling-example.csproj b/docs/trace/stratified-sampling-example/stratified-sampling-example.csproj index 19aa9791432..97876c06b9c 100644 --- a/docs/trace/stratified-sampling-example/stratified-sampling-example.csproj +++ b/docs/trace/stratified-sampling-example/stratified-sampling-example.csproj @@ -1,4 +1,8 @@ + + $(NoWarn);CA5394 + + diff --git a/docs/trace/tail-based-sampling-span-level/ParentBasedElseAlwaysRecordSampler.cs b/docs/trace/tail-based-sampling-span-level/ParentBasedElseAlwaysRecordSampler.cs index bce691b0c16..4d933dbf2ec 100644 --- a/docs/trace/tail-based-sampling-span-level/ParentBasedElseAlwaysRecordSampler.cs +++ b/docs/trace/tail-based-sampling-span-level/ParentBasedElseAlwaysRecordSampler.cs @@ -15,7 +15,7 @@ namespace SDKBasedSpanLevelTailSamplingSample; /// changed later by a span processor based on span attributes (e.g., failure) that become /// available only by the end of the span. /// -internal class ParentBasedElseAlwaysRecordSampler : Sampler +internal sealed class ParentBasedElseAlwaysRecordSampler : Sampler { private const double DefaultSamplingProbabilityForRootSpan = 0.1; private readonly ParentBasedSampler parentBasedSampler; diff --git a/docs/trace/tail-based-sampling-span-level/Program.cs b/docs/trace/tail-based-sampling-span-level/Program.cs index de4f8ea9f17..d2114248442 100644 --- a/docs/trace/tail-based-sampling-span-level/Program.cs +++ b/docs/trace/tail-based-sampling-span-level/Program.cs @@ -7,7 +7,7 @@ namespace SDKBasedSpanLevelTailSamplingSample; -internal class Program +internal static class Program { private static readonly ActivitySource MyActivitySource = new("SDK.TailSampling.POC"); diff --git a/docs/trace/tail-based-sampling-span-level/TailSamplingProcessor.cs b/docs/trace/tail-based-sampling-span-level/TailSamplingProcessor.cs index 5aa22092e75..10aee54e38d 100644 --- a/docs/trace/tail-based-sampling-span-level/TailSamplingProcessor.cs +++ b/docs/trace/tail-based-sampling-span-level/TailSamplingProcessor.cs @@ -26,7 +26,7 @@ public override void OnEnd(Activity activity) } else { - this.IncludeForExportIfFailedActivity(activity); + IncludeForExportIfFailedActivity(activity); } base.OnEnd(activity); @@ -42,7 +42,7 @@ public override void OnEnd(Activity activity) // 2. Traces will not be complete: Since this sampling is at a span level, the generated trace will be partial and won't be complete. // For example, if another part of the call tree is successful, those spans may not be sampled in leading to a partial trace. // 3. If multiple exporters are used, this decision will impact all of them: https://github.com/open-telemetry/opentelemetry-dotnet/issues/3861. - private void IncludeForExportIfFailedActivity(Activity activity) + private static void IncludeForExportIfFailedActivity(Activity activity) { if (activity.Status == ActivityStatusCode.Error) { diff --git a/docs/trace/tail-based-sampling-span-level/tail-based-sampling-example.csproj b/docs/trace/tail-based-sampling-span-level/tail-based-sampling-example.csproj index 19aa9791432..b7947e79e17 100644 --- a/docs/trace/tail-based-sampling-span-level/tail-based-sampling-example.csproj +++ b/docs/trace/tail-based-sampling-span-level/tail-based-sampling-example.csproj @@ -1,4 +1,8 @@ + + $(NoWarn);CA5394;CA2000 + + diff --git a/examples/AspNetCore/Controllers/WeatherForecastController.cs b/examples/AspNetCore/Controllers/WeatherForecastController.cs index 3d09fe9ed61..bbb257334e7 100644 --- a/examples/AspNetCore/Controllers/WeatherForecastController.cs +++ b/examples/AspNetCore/Controllers/WeatherForecastController.cs @@ -5,6 +5,7 @@ namespace Examples.AspNetCore.Controllers; using System.Diagnostics; using System.Diagnostics.Metrics; +using System.Security.Cryptography; using Examples.AspNetCore; using Microsoft.AspNetCore.Mvc; @@ -12,10 +13,10 @@ namespace Examples.AspNetCore.Controllers; [Route("[controller]")] public class WeatherForecastController : ControllerBase { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching", - }; + private static readonly string[] Summaries = + [ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + ]; private static readonly HttpClient HttpClient = new(); @@ -23,24 +24,24 @@ public class WeatherForecastController : ControllerBase private readonly ActivitySource activitySource; private readonly Counter freezingDaysCounter; - public WeatherForecastController(ILogger logger, Instrumentation instrumentation) + public WeatherForecastController(ILogger logger, InstrumentationSource instrumentationSource) { this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - ArgumentNullException.ThrowIfNull(instrumentation); - this.activitySource = instrumentation.ActivitySource; - this.freezingDaysCounter = instrumentation.FreezingDaysCounter; + ArgumentNullException.ThrowIfNull(instrumentationSource); + this.activitySource = instrumentationSource.ActivitySource; + this.freezingDaysCounter = instrumentationSource.FreezingDaysCounter; } [HttpGet] public IEnumerable Get() { - using var scope = this.logger.BeginScope("{Id}", Guid.NewGuid().ToString("N")); + using var scope = this.logger.BeginIdScope(Guid.NewGuid().ToString("N")); - // Making an http call here to serve as an example of + // Making a http call here to serve as an example of // how dependency calls will be captured and treated // automatically as child of incoming request. - var res = HttpClient.GetStringAsync("http://google.com").Result; + var res = HttpClient.GetStringAsync(new Uri("http://google.com")).Result; // Optional: Manually create an activity. This will become a child of // the activity created from the instrumentation library for AspNetCore. @@ -52,22 +53,18 @@ public IEnumerable Get() // a manual activity using Activity.Current?.SetTag() using var activity = this.activitySource.StartActivity("calculate forecast"); - var rng = new Random(); var forecast = Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), - TemperatureC = rng.Next(-20, 55), - Summary = Summaries[rng.Next(Summaries.Length)], + TemperatureC = RandomNumberGenerator.GetInt32(-20, 55), + Summary = Summaries[RandomNumberGenerator.GetInt32(Summaries.Length)], }) .ToArray(); // Optional: Count the freezing days this.freezingDaysCounter.Add(forecast.Count(f => f.TemperatureC < 0)); - this.logger.LogInformation( - "WeatherForecasts generated {count}: {forecasts}", - forecast.Length, - forecast); + this.logger.WeatherForecastGenerated(LogLevel.Information, forecast.Length, forecast); return forecast; } diff --git a/examples/AspNetCore/Controllers/WeatherForecastControllerLog.cs b/examples/AspNetCore/Controllers/WeatherForecastControllerLog.cs new file mode 100644 index 00000000000..eb6bc292673 --- /dev/null +++ b/examples/AspNetCore/Controllers/WeatherForecastControllerLog.cs @@ -0,0 +1,14 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace Examples.AspNetCore.Controllers; + +internal static partial class WeatherForecastControllerLog +{ + private static readonly Func Scope = LoggerMessage.DefineScope("{Id}"); + + public static IDisposable? BeginIdScope(this ILogger logger, string id) => Scope(logger, id); + + [LoggerMessage(EventId = 1, Message = "WeatherForecasts generated {Count}: {Forecasts}")] + public static partial void WeatherForecastGenerated(this ILogger logger, LogLevel logLevel, int count, WeatherForecast[] forecasts); +} diff --git a/examples/AspNetCore/Examples.AspNetCore.csproj b/examples/AspNetCore/Examples.AspNetCore.csproj index e476b1ed4d8..459213fe2ba 100644 --- a/examples/AspNetCore/Examples.AspNetCore.csproj +++ b/examples/AspNetCore/Examples.AspNetCore.csproj @@ -2,6 +2,7 @@ $(DefaultTargetFrameworkForExampleApps) + $(NoWarn);CA1515 diff --git a/examples/AspNetCore/Instrumentation.cs b/examples/AspNetCore/InstrumentationSource.cs similarity index 85% rename from examples/AspNetCore/Instrumentation.cs rename to examples/AspNetCore/InstrumentationSource.cs index 4b0ede1157f..be6d5764033 100644 --- a/examples/AspNetCore/Instrumentation.cs +++ b/examples/AspNetCore/InstrumentationSource.cs @@ -11,15 +11,15 @@ namespace Examples.AspNetCore; /// ActivitySource and Instruments. This avoids possible type collisions /// with other components in the DI container. /// -public class Instrumentation : IDisposable +public sealed class InstrumentationSource : IDisposable { internal const string ActivitySourceName = "Examples.AspNetCore"; internal const string MeterName = "Examples.AspNetCore"; private readonly Meter meter; - public Instrumentation() + public InstrumentationSource() { - string? version = typeof(Instrumentation).Assembly.GetName().Version?.ToString(); + string? version = typeof(InstrumentationSource).Assembly.GetName().Version?.ToString(); this.ActivitySource = new ActivitySource(ActivitySourceName, version); this.meter = new Meter(MeterName, version); this.FreezingDaysCounter = this.meter.CreateCounter("weather.days.freezing", description: "The number of days where the temperature is below freezing"); diff --git a/examples/AspNetCore/Program.cs b/examples/AspNetCore/Program.cs index 809534f5c76..afd59db31d7 100644 --- a/examples/AspNetCore/Program.cs +++ b/examples/AspNetCore/Program.cs @@ -13,20 +13,20 @@ var appBuilder = WebApplication.CreateBuilder(args); // Note: Switch between Zipkin/OTLP/Console by setting UseTracingExporter in appsettings.json. -var tracingExporter = appBuilder.Configuration.GetValue("UseTracingExporter", defaultValue: "console")!.ToLowerInvariant(); +var tracingExporter = appBuilder.Configuration.GetValue("UseTracingExporter", defaultValue: "CONSOLE").ToUpperInvariant(); // Note: Switch between Prometheus/OTLP/Console by setting UseMetricsExporter in appsettings.json. -var metricsExporter = appBuilder.Configuration.GetValue("UseMetricsExporter", defaultValue: "console")!.ToLowerInvariant(); +var metricsExporter = appBuilder.Configuration.GetValue("UseMetricsExporter", defaultValue: "CONSOLE").ToUpperInvariant(); // Note: Switch between Console/OTLP by setting UseLogExporter in appsettings.json. -var logExporter = appBuilder.Configuration.GetValue("UseLogExporter", defaultValue: "console")!.ToLowerInvariant(); +var logExporter = appBuilder.Configuration.GetValue("UseLogExporter", defaultValue: "CONSOLE").ToUpperInvariant(); // Note: Switch between Explicit/Exponential by setting HistogramAggregation in appsettings.json -var histogramAggregation = appBuilder.Configuration.GetValue("HistogramAggregation", defaultValue: "explicit")!.ToLowerInvariant(); +var histogramAggregation = appBuilder.Configuration.GetValue("HistogramAggregation", defaultValue: "EXPLICIT").ToUpperInvariant(); // Create a service to expose ActivitySource, and Metric Instruments // for manual instrumentation -appBuilder.Services.AddSingleton(); +appBuilder.Services.AddSingleton(); // Clear default logging providers used by WebApplication host. appBuilder.Logging.ClearProviders(); @@ -45,7 +45,7 @@ // Ensure the TracerProvider subscribes to any custom ActivitySources. builder - .AddSource(Instrumentation.ActivitySourceName) + .AddSource(InstrumentationSource.ActivitySourceName) .SetSampler(new AlwaysOnSampler()) .AddHttpClientInstrumentation() .AddAspNetCoreInstrumentation(); @@ -55,7 +55,7 @@ switch (tracingExporter) { - case "zipkin": + case "ZIPKIN": builder.AddZipkinExporter(); builder.ConfigureServices(services => @@ -65,11 +65,11 @@ }); break; - case "otlp": + case "OTLP": builder.AddOtlpExporter(otlpOptions => { // Use IConfiguration directly for Otlp exporter endpoint option. - otlpOptions.Endpoint = new Uri(appBuilder.Configuration.GetValue("Otlp:Endpoint", defaultValue: "http://localhost:4317")!); + otlpOptions.Endpoint = new Uri(appBuilder.Configuration.GetValue("Otlp:Endpoint", defaultValue: "http://localhost:4317")); }); break; @@ -84,7 +84,7 @@ // Ensure the MeterProvider subscribes to any custom Meters. builder - .AddMeter(Instrumentation.MeterName) + .AddMeter(InstrumentationSource.MeterName) .SetExemplarFilter(ExemplarFilterType.TraceBased) .AddRuntimeInstrumentation() .AddHttpClientInstrumentation() @@ -92,7 +92,7 @@ switch (histogramAggregation) { - case "exponential": + case "EXPONENTIAL": builder.AddView(instrument => { return instrument.GetType().GetGenericTypeDefinition() == typeof(Histogram<>) @@ -108,10 +108,10 @@ switch (metricsExporter) { - case "prometheus": + case "PROMETHEUS": builder.AddPrometheusExporter(); break; - case "otlp": + case "OTLP": builder.AddOtlpExporter(otlpOptions => { // Use IConfiguration directly for Otlp exporter endpoint option. @@ -129,11 +129,11 @@ switch (logExporter) { - case "otlp": + case "OTLP": builder.AddOtlpExporter(otlpOptions => { // Use IConfiguration directly for Otlp exporter endpoint option. - otlpOptions.Endpoint = new Uri(appBuilder.Configuration.GetValue("Otlp:Endpoint", defaultValue: "http://localhost:4317")!); + 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 001d3cdea08..8ab5c23a0ad 100644 --- a/examples/AspNetCore/README.md +++ b/examples/AspNetCore/README.md @@ -1,6 +1,6 @@ -# OpenTelemetry ASP.NET Core 7 Web API Example +# OpenTelemetry ASP.NET Core Web API Example -This example uses the new WebApplication host that ships with .NET 7 +This example uses the new WebApplication host that ships with .NET and shows how to setup 1. OpenTelemetry logging @@ -13,7 +13,27 @@ service name, version and the machine on which this program is running. The sample rate is set to emit all the traces using `AlwaysOnSampler`. You can try out different samplers like `TraceIdRatioBasedSampler`. +## Running Dependencies via Docker + +The example by default writes telemetry to stdout. To enable telemetry export +via OTLP, update the `appsettings.json` file to replace `"console"` with +`"otlp"`. Launching the application will then send telemetry data via OTLP. + +Use the provided "docker-compose.yaml" file to spin up the +required dependencies, including: + +- **OTel Collector** Accept telemetry and forwards them to Tempo, Prometheus +- **Prometheus** to store metrics +- **Grafana (UI)** UI to view metrics, traces. (Exemplars can be used to jump + from metrics to traces) +- **Tempo** to store traces // TODO: Add a logging store also. + +Once the Docker containers are running, you can access the **Grafana UI** at: +[http://localhost:3000/](http://localhost:3000/) + ## References -* [ASP.NET Core 3.1 Example](https://github.com/open-telemetry/opentelemetry-dotnet/tree/98cb28974af43fc893ab80a8cead6e2d4163e144/examples/AspNetCore) -* [OpenTelemetry Project](https://opentelemetry.io/) +- [ASP.NET Core](https://learn.microsoft.com/aspnet/core/introduction-to-aspnet-core) +- [Docker](http://docker.com) +- [Prometheus](http://prometheus.io/docs) +- [Tempo](https://github.com/grafana/tempo) diff --git a/examples/AspNetCore/docker-compose.yaml b/examples/AspNetCore/docker-compose.yaml index c8cc94fa4b1..444e79eca81 100644 --- a/examples/AspNetCore/docker-compose.yaml +++ b/examples/AspNetCore/docker-compose.yaml @@ -1,9 +1,8 @@ -version: "3" services: # OTEL Collector to receive logs, metrics and traces from the application otel-collector: - image: otel/opentelemetry-collector:0.70.0 + image: otel/opentelemetry-collector:0.136.0@sha256:98fd3b410ae8a939be9588f1580c4b7c3da6ebba49f5363df4259a827aabb779 command: [ "--config=/etc/otel-collector.yaml" ] volumes: - ./otel-collector.yaml:/etc/otel-collector.yaml @@ -14,7 +13,7 @@ services: # Exports Traces to Tempo tempo: - image: grafana/tempo:latest + image: grafana/tempo:2.8.2@sha256:0ef775495967cd5d7a6b2e146b6ea695d624803c8db8349fb8ce4164f719f9b7 command: [ "-config.file=/etc/tempo.yaml" ] volumes: - ./tempo.yaml:/etc/tempo.yaml @@ -26,7 +25,7 @@ services: # Exports Metrics to Prometheus prometheus: - image: prom/prometheus:latest + image: prom/prometheus:v3.6.0@sha256:76947e7ef22f8a698fc638f706685909be425dbe09bd7a2cd7aca849f79b5f64 command: - --config.file=/etc/prometheus.yaml - --web.enable-remote-write-receiver @@ -38,7 +37,7 @@ services: # UI to query traces and metrics grafana: - image: grafana/grafana:9.3.2 + image: grafana/grafana:12.2.0@sha256:74144189b38447facf737dfd0f3906e42e0776212bf575dc3334c3609183adf7 volumes: - ./grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml environment: diff --git a/examples/AspNetCore/otel-collector.yaml b/examples/AspNetCore/otel-collector.yaml index 15df605c542..fa9999d66c9 100644 --- a/examples/AspNetCore/otel-collector.yaml +++ b/examples/AspNetCore/otel-collector.yaml @@ -2,7 +2,9 @@ receivers: otlp: protocols: grpc: + endpoint: 0.0.0.0:4317 http: + endpoint: 0.0.0.0:4318 exporters: debug: diff --git a/examples/AspNetCore/tempo.yaml b/examples/AspNetCore/tempo.yaml index 0d46d4718e8..1217bc05760 100644 --- a/examples/AspNetCore/tempo.yaml +++ b/examples/AspNetCore/tempo.yaml @@ -6,7 +6,9 @@ distributor: otlp: protocols: http: + endpoint: 0.0.0.0:4318 grpc: + endpoint: 0.0.0.0:4317 storage: trace: diff --git a/examples/Console/Examples.Console.csproj b/examples/Console/Examples.Console.csproj index d2b74f2e648..ade79a55665 100644 --- a/examples/Console/Examples.Console.csproj +++ b/examples/Console/Examples.Console.csproj @@ -1,12 +1,9 @@ - + Exe $(DefaultTargetFrameworkForExampleApps) - $(NoWarn),CS0618 - - - disable + $(NoWarn),CA1812 diff --git a/examples/Console/InstrumentationWithActivitySource.cs b/examples/Console/InstrumentationWithActivitySource.cs index 240f58289d8..e591ea12505 100644 --- a/examples/Console/InstrumentationWithActivitySource.cs +++ b/examples/Console/InstrumentationWithActivitySource.cs @@ -8,7 +8,7 @@ namespace Examples.Console; -internal class InstrumentationWithActivitySource : IDisposable +internal sealed class InstrumentationWithActivitySource : IDisposable { private const string RequestPath = "/api/request"; private readonly SampleServer server = new(); @@ -27,7 +27,7 @@ public void Dispose() this.server.Dispose(); } - private class SampleServer : IDisposable + private sealed class SampleServer : IDisposable { private readonly HttpListener listener = new(); @@ -47,13 +47,13 @@ public void Start(string url) var context = this.listener.GetContext(); using var activity = source.StartActivity( - $"{context.Request.HttpMethod}:{context.Request.Url.AbsolutePath}", + $"{context.Request.HttpMethod}:{context.Request.Url!.AbsolutePath}", ActivityKind.Server); var headerKeys = context.Request.Headers.AllKeys; foreach (var headerKey in headerKeys) { - string headerValue = context.Request.Headers[headerKey]; + string? headerValue = context.Request.Headers[headerKey]; activity?.SetTag($"http.header.{headerKey}", headerValue); } @@ -62,11 +62,11 @@ public void Start(string url) using (var reader = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding)) { requestContent = reader.ReadToEnd(); - childSpan.AddEvent(new ActivityEvent("StreamReader.ReadToEnd")); + childSpan?.AddEvent(new ActivityEvent("StreamReader.ReadToEnd")); } activity?.SetTag("request.content", requestContent); - activity?.SetTag("request.length", requestContent.Length.ToString()); + activity?.SetTag("request.length", requestContent.Length); var echo = Encoding.UTF8.GetBytes("echo: " + requestContent); context.Response.ContentEncoding = Encoding.UTF8; @@ -88,10 +88,10 @@ public void Dispose() } } - private class SampleClient : IDisposable + private sealed class SampleClient : IDisposable { - private CancellationTokenSource cts; - private Task requestTask; + private CancellationTokenSource? cts; + private Task? requestTask; public void Start(string url) { @@ -114,7 +114,7 @@ public void Start(string url) count++; activity?.AddEvent(new ActivityEvent("PostAsync:Started")); - using var response = await client.PostAsync(url, content, cancellationToken).ConfigureAwait(false); + using var response = await client.PostAsync(new Uri(url, UriKind.Absolute), content, cancellationToken).ConfigureAwait(false); activity?.AddEvent(new ActivityEvent("PostAsync:Ended")); activity?.SetTag("http.status_code", (int)response.StatusCode); @@ -154,7 +154,7 @@ public void Dispose() if (this.cts != null) { this.cts.Cancel(); - this.requestTask.Wait(); + this.requestTask!.Wait(); this.requestTask.Dispose(); this.cts.Dispose(); } diff --git a/examples/Console/Program.cs b/examples/Console/Program.cs index 2b93979e071..a7d0e822f47 100644 --- a/examples/Console/Program.cs +++ b/examples/Console/Program.cs @@ -8,7 +8,7 @@ namespace Examples.Console; /// /// Main samples entry point. /// -public class Program +internal static class Program { /// /// Main method - invoke this using command line. @@ -33,16 +33,16 @@ public static void Main(string[] args) { Parser.Default.ParseArguments(args) .MapResult( - (ZipkinOptions options) => TestZipkinExporter.Run(options.Uri), - (PrometheusOptions options) => TestPrometheusExporter.Run(options.Port), + (ZipkinOptions options) => TestZipkinExporter.Run(options), + (PrometheusOptions options) => TestPrometheusExporter.Run(options), (MetricsOptions options) => TestMetrics.Run(options), (LogsOptions options) => TestLogs.Run(options), - (GrpcNetClientOptions options) => TestGrpcNetClient.Run(), - (HttpClientOptions options) => TestHttpClient.Run(), + (GrpcNetClientOptions options) => TestGrpcNetClient.Run(options), + (HttpClientOptions options) => TestHttpClient.Run(options), (ConsoleOptions options) => TestConsoleExporter.Run(options), (OpenTelemetryShimOptions options) => TestOTelShimWithConsoleExporter.Run(options), (OpenTracingShimOptions options) => TestOpenTracingShim.Run(options), - (OtlpOptions options) => TestOtlpExporter.Run(options.Endpoint, options.Protocol), + (OtlpOptions options) => TestOtlpExporter.Run(options), (InMemoryOptions options) => TestInMemoryExporter.Run(options), errs => 1); } @@ -51,21 +51,21 @@ public static void Main(string[] args) #pragma warning disable SA1402 // File may only contain a single type [Verb("zipkin", HelpText = "Specify the options required to test Zipkin exporter")] -internal class ZipkinOptions +internal sealed class ZipkinOptions { [Option('u', "uri", HelpText = "Please specify the uri of Zipkin backend", Required = true)] - public string Uri { get; set; } + public required string Uri { get; set; } } [Verb("prometheus", HelpText = "Specify the options required to test Prometheus")] -internal class PrometheusOptions +internal sealed 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("metrics", HelpText = "Specify the options required to test Metrics")] -internal class MetricsOptions +internal sealed class MetricsOptions { [Option('d', "IsDelta", HelpText = "Export Delta metrics", Required = false, Default = false)] public bool IsDelta { get; set; } @@ -83,71 +83,71 @@ internal class MetricsOptions public int DefaultCollectionPeriodMilliseconds { get; set; } [Option("useExporter", Default = "console", HelpText = "Options include otlp or console.", Required = false)] - public string UseExporter { get; set; } + public string? UseExporter { 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; } + public string? Endpoint { get; set; } [Option('p', "useGrpc", HelpText = "Use gRPC or HTTP when using the OTLP exporter", Required = false, Default = true)] public bool UseGrpc { get; set; } } [Verb("grpc", HelpText = "Specify the options required to test Grpc.Net.Client")] -internal class GrpcNetClientOptions +internal sealed class GrpcNetClientOptions { } [Verb("httpclient", HelpText = "Specify the options required to test HttpClient")] -internal class HttpClientOptions +internal sealed class HttpClientOptions { } [Verb("console", HelpText = "Specify the options required to test console exporter")] -internal class ConsoleOptions +internal sealed class ConsoleOptions { } [Verb("otelshim", HelpText = "Specify the options required to test OpenTelemetry Shim with console exporter")] -internal class OpenTelemetryShimOptions +internal sealed class OpenTelemetryShimOptions { } [Verb("opentracing", HelpText = "Specify the options required to test OpenTracing Shim with console exporter")] -internal class OpenTracingShimOptions +internal sealed class OpenTracingShimOptions { } [Verb("otlp", HelpText = "Specify the options required to test OpenTelemetry Protocol (OTLP)")] -internal class OtlpOptions +internal sealed 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; } + public string? Endpoint { get; set; } [Option('p', "protocol", HelpText = "Transport protocol used by exporter. Supported values: grpc and http/protobuf.", Default = "grpc")] - public string Protocol { get; set; } + public string? Protocol { get; set; } } [Verb("logs", HelpText = "Specify the options required to test Logs")] -internal class LogsOptions +internal sealed class LogsOptions { [Option("useExporter", Default = "otlp", HelpText = "Options include otlp or console.", Required = false)] - public string UseExporter { get; set; } + public string? UseExporter { 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; } + public string? Endpoint { 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; } + public string? Protocol { get; set; } [Option("processorType", Default = "batch", HelpText = "export processor type. Supported values: simple and batch", Required = false)] - public string ProcessorType { get; set; } + public string? ProcessorType { 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 +internal sealed class InMemoryOptions { } diff --git a/examples/Console/TestConsoleExporter.cs b/examples/Console/TestConsoleExporter.cs index cf3f803c4fd..fa59c3a9954 100644 --- a/examples/Console/TestConsoleExporter.cs +++ b/examples/Console/TestConsoleExporter.cs @@ -8,49 +8,49 @@ namespace Examples.Console; -internal class TestConsoleExporter +internal sealed 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) + internal static int Run(ConsoleOptions options) { return RunWithActivitySource(); } - private static object RunWithActivitySource() + private static int 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(); + .AddSource("Samples.SampleClient", "Samples.SampleServer") + .ConfigureResource(res => res.AddService("console-test")) +#pragma warning disable CA2000 // Dispose objects before losing scope + .AddProcessor(new MyProcessor()) // This must be added before ConsoleExporter +#pragma warning restore CA2000 // Dispose objects before losing scope + .AddConsoleExporter() + .Build(); // The above line is required only in applications // which decide to use OpenTelemetry. - using (var sample = new InstrumentationWithActivitySource()) - { - sample.Start(); + using var sample = new InstrumentationWithActivitySource(); + sample.Start(); - System.Console.WriteLine("Traces are being created and exported " + - "to Console in the background. " + - "Press ENTER to stop."); - System.Console.ReadLine(); - } + System.Console.WriteLine("Traces are being created and exported " + + "to Console in the background. " + + "Press ENTER to stop."); + System.Console.ReadLine(); - return null; + return 0; } /// /// An example of custom processor which /// can be used to add more tags to an activity. /// - internal class MyProcessor : BaseProcessor + internal sealed class MyProcessor : BaseProcessor { public override void OnStart(Activity activity) { diff --git a/examples/Console/TestGrpcNetClient.cs b/examples/Console/TestGrpcNetClient.cs index d179d93913e..ab2e0378a6b 100644 --- a/examples/Console/TestGrpcNetClient.cs +++ b/examples/Console/TestGrpcNetClient.cs @@ -10,10 +10,12 @@ namespace Examples.Console; -internal class TestGrpcNetClient +internal sealed class TestGrpcNetClient { - internal static object Run() + internal static int Run(GrpcNetClientOptions options) { + Debug.Assert(options != null, "options was null"); + // Prerequisite for running this example. // In a separate console window, start the example // ASP.NET Core gRPC service by running the following command @@ -55,6 +57,6 @@ internal static object Run() System.Console.WriteLine("Press Enter key to exit."); System.Console.ReadLine(); - return null; + return 0; } } diff --git a/examples/Console/TestHttpClient.cs b/examples/Console/TestHttpClient.cs index 27305a50523..5c72029378c 100644 --- a/examples/Console/TestHttpClient.cs +++ b/examples/Console/TestHttpClient.cs @@ -8,15 +8,17 @@ namespace Examples.Console; -internal class TestHttpClient +internal sealed 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() + internal static int Run(HttpClientOptions options) { + Debug.Assert(options != null, "options was null"); + System.Console.WriteLine("Hello World!"); using var tracerProvider = Sdk.CreateTracerProviderBuilder() @@ -30,12 +32,12 @@ internal static object Run() using (var parent = source.StartActivity("incoming request", ActivityKind.Server)) { using var client = new HttpClient(); - client.GetStringAsync("http://bing.com").GetAwaiter().GetResult(); + client.GetStringAsync(new Uri("http://bing.com", UriKind.Absolute)).GetAwaiter().GetResult(); } System.Console.WriteLine("Press Enter key to exit."); System.Console.ReadLine(); - return null; + return 0; } } diff --git a/examples/Console/TestInMemoryExporter.cs b/examples/Console/TestInMemoryExporter.cs index 53d3dd7f49e..d3b30db60ff 100644 --- a/examples/Console/TestInMemoryExporter.cs +++ b/examples/Console/TestInMemoryExporter.cs @@ -8,14 +8,14 @@ namespace Examples.Console; -internal class TestInMemoryExporter +internal sealed 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) + internal static int Run(InMemoryOptions options) { // List that will be populated with the traces by InMemoryExporter var exportedItems = new List(); @@ -28,7 +28,7 @@ internal static object Run(InMemoryOptions options) System.Console.WriteLine($"ActivitySource: {activity.Source.Name} logged the activity {activity.DisplayName}"); } - return null; + return 0; } private static void RunWithActivitySource(ICollection exportedItems) @@ -36,10 +36,10 @@ 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(); + .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. diff --git a/examples/Console/TestLogs.cs b/examples/Console/TestLogs.cs index 4c88753ffd7..b38cc230217 100644 --- a/examples/Console/TestLogs.cs +++ b/examples/Console/TestLogs.cs @@ -7,9 +7,9 @@ namespace Examples.Console; -internal class TestLogs +internal sealed class TestLogs { - internal static object Run(LogsOptions options) + internal static int Run(LogsOptions options) { using var loggerFactory = LoggerFactory.Create(builder => { @@ -17,7 +17,8 @@ internal static object Run(LogsOptions options) { opt.IncludeFormattedMessage = true; opt.IncludeScopes = true; - if (options.UseExporter.Equals("otlp", StringComparison.OrdinalIgnoreCase)) + + if ("otlp".Equals(options.UseExporter, StringComparison.OrdinalIgnoreCase)) { /* * Prerequisite to run this example: @@ -27,10 +28,10 @@ internal static object Run(LogsOptions options) * 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 + * docker run --rm -it -p 4317:4317 -p 4318:4318 -v $(pwd):/cfg otel/opentelemetry-collector:0.123.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 + * docker run --rm -it -p 4317:4317 -p 4318:4318 -v "%cd%":/cfg otel/opentelemetry-collector:0.123.0 --config=/cfg/otlp-collector-example/config.yaml * * Open another terminal window at the examples/Console/ directory and * launch the OTLP example by running: @@ -43,31 +44,46 @@ internal static object Run(LogsOptions options) 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")) + if (!string.IsNullOrEmpty(options.Protocol)) { - protocol = OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf; + switch (options.Protocol.Trim()) + { + case "grpc": + protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc; + break; + case "http/protobuf": + protocol = OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf; + break; + default: + System.Console.WriteLine($"Export protocol {options.Protocol} is not supported. Default protocol 'grpc' will be used."); + break; + } } else { - System.Console.WriteLine($"Export protocol {options.Protocol} is not supported. Default protocol 'grpc' will be used."); + System.Console.WriteLine("Protocol is null or empty. Default protocol 'grpc' will be used."); } var processorType = ExportProcessorType.Batch; - if (options.ProcessorType.Trim().ToLower().Equals("batch")) - { - processorType = ExportProcessorType.Batch; - } - else if (options.ProcessorType.Trim().ToLower().Equals("simple")) + + if (!string.IsNullOrEmpty(options.ProcessorType)) { - processorType = ExportProcessorType.Simple; + switch (options.ProcessorType.Trim()) + { + case "batch": + processorType = ExportProcessorType.Batch; + break; + case "simple": + processorType = ExportProcessorType.Simple; + break; + default: + System.Console.WriteLine($"Export processor type {options.ProcessorType} is not supported. Default processor type 'batch' will be used."); + break; + } } else { - System.Console.WriteLine($"Export processor type {options.ProcessorType} is not supported. Default processor type 'batch' will be used."); + System.Console.WriteLine("Processor type is null or empty. Default processor type 'batch' will be used."); } opt.AddOtlpExporter((exporterOptions, processorOptions) => @@ -96,13 +112,13 @@ internal static object Run(LogsOptions options) }); }); - var logger = loggerFactory.CreateLogger(); - using (logger.BeginScope("{city}", "Seattle")) - using (logger.BeginScope("{storeType}", "Physical")) + var logger = loggerFactory.CreateLogger(); + using (logger.BeginCityScope("Seattle")) + using (logger.BeginStoreTypeScope("Physical")) { - logger.LogInformation("Hello from {name} {price}.", "tomato", 2.99); + logger.HelloFrom("tomato", 2.99); } - return null; + return 0; } } diff --git a/examples/Console/TestLogsExtensions.cs b/examples/Console/TestLogsExtensions.cs new file mode 100644 index 00000000000..f76eb03b3c8 --- /dev/null +++ b/examples/Console/TestLogsExtensions.cs @@ -0,0 +1,19 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.Logging; + +namespace Examples.Console; + +internal static partial class TestLogsExtensions +{ + private static readonly Func CityScope = LoggerMessage.DefineScope("{City}"); + private static readonly Func StoreTypeScope = LoggerMessage.DefineScope("{StoreType}"); + + public static IDisposable? BeginCityScope(this ILogger logger, string city) => CityScope(logger, city); + + public static IDisposable? BeginStoreTypeScope(this ILogger logger, string storeType) => StoreTypeScope(logger, storeType); + + [LoggerMessage(LogLevel.Information, "Hello from {Name} {Price}.")] + public static partial void HelloFrom(this ILogger logger, string name, double price); +} diff --git a/examples/Console/TestMetrics.cs b/examples/Console/TestMetrics.cs index ba943e91775..308acee8d4c 100644 --- a/examples/Console/TestMetrics.cs +++ b/examples/Console/TestMetrics.cs @@ -10,12 +10,12 @@ namespace Examples.Console; -internal class TestMetrics +internal sealed class TestMetrics { - internal static object Run(MetricsOptions options) + internal static int Run(MetricsOptions options) { var meterVersion = "1.0"; - var meterTags = new List> + var meterTags = new List> { new( "MeterTagKey", @@ -27,7 +27,7 @@ internal static object Run(MetricsOptions options) .ConfigureResource(r => r.AddService("myservice")) .AddMeter(meter.Name); // All instruments from this meter are enabled. - if (options.UseExporter.Equals("otlp", StringComparison.OrdinalIgnoreCase)) + if ("otlp".Equals(options.UseExporter, StringComparison.OrdinalIgnoreCase)) { /* * Prerequisite to run this example: @@ -37,10 +37,10 @@ internal static object Run(MetricsOptions options) * 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 + * docker run --rm -it -p 4317:4317 -v $(pwd):/cfg otel/opentelemetry-collector:0.123.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 + * docker run --rm -it -p 4317:4317 -v "%cd%":/cfg otel/opentelemetry-collector:0.123.0 --config=/cfg/otlp-collector-example/config.yaml * * Open another terminal window at the examples/Console/ directory and * launch the OTLP example by running: @@ -79,13 +79,13 @@ internal static object Run(MetricsOptions options) using var provider = providerBuilder.Build(); - Counter counter = null; + Counter? counter = null; if (options.FlagCounter ?? true) { counter = meter.CreateCounter("counter", "things", "A count of things"); } - Histogram histogram = null; + Histogram? histogram = null; if (options.FlagHistogram ?? false) { histogram = meter.CreateHistogram("histogram"); @@ -97,9 +97,9 @@ internal static object Run(MetricsOptions options) { return new List>() { - new Measurement( + new( (int)Process.GetCurrentProcess().PrivateMemorySize64, - new KeyValuePair("tag1", "value1")), + new KeyValuePair("tag1", "value1")), }; }); } @@ -111,45 +111,45 @@ internal static object Run(MetricsOptions options) histogram?.Record( 100, - new KeyValuePair("tag1", "value1")); + new KeyValuePair("tag1", "value1")); histogram?.Record( 200, - new KeyValuePair("tag1", "value2"), - new KeyValuePair("tag2", "value2")); + new KeyValuePair("tag1", "value2"), + new KeyValuePair("tag2", "value2")); histogram?.Record( 100, - new KeyValuePair("tag1", "value1")); + new KeyValuePair("tag1", "value1")); histogram?.Record( 200, - new KeyValuePair("tag2", "value2"), - new KeyValuePair("tag1", "value2")); + new KeyValuePair("tag2", "value2"), + new KeyValuePair("tag1", "value2")); counter?.Add(10); counter?.Add( 100, - new KeyValuePair("tag1", "value1")); + new KeyValuePair("tag1", "value1")); counter?.Add( 200, - new KeyValuePair("tag1", "value2"), - new KeyValuePair("tag2", "value2")); + new KeyValuePair("tag1", "value2"), + new KeyValuePair("tag2", "value2")); counter?.Add( 100, - new KeyValuePair("tag1", "value1")); + new KeyValuePair("tag1", "value1")); counter?.Add( 200, - new KeyValuePair("tag2", "value2"), - new KeyValuePair("tag1", "value2")); + new KeyValuePair("tag2", "value2"), + new KeyValuePair("tag1", "value2")); Task.Delay(500).Wait(); } - return null; + return 0; } } diff --git a/examples/Console/TestOTelShimWithConsoleExporter.cs b/examples/Console/TestOTelShimWithConsoleExporter.cs index 6557357a963..4dbb22ff9db 100644 --- a/examples/Console/TestOTelShimWithConsoleExporter.cs +++ b/examples/Console/TestOTelShimWithConsoleExporter.cs @@ -7,17 +7,17 @@ namespace Examples.Console; -internal class TestOTelShimWithConsoleExporter +internal sealed class TestOTelShimWithConsoleExporter { - internal static object Run(OpenTelemetryShimOptions options) + internal static int 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(); + .AddSource("MyCompany.MyProduct.MyWebServer") + .ConfigureResource(r => r.AddService("MyServiceName")) + .AddConsoleExporter() + .Build(); // The above line is required only in applications // which decide to use OpenTelemetry. @@ -40,6 +40,6 @@ internal static object Run(OpenTelemetryShimOptions options) System.Console.WriteLine("Press Enter key to exit."); System.Console.ReadLine(); - return null; + return 0; } } diff --git a/examples/Console/TestOpenTracingShim.cs b/examples/Console/TestOpenTracingShim.cs index 0de2419b6ef..77ab8d6c7d0 100644 --- a/examples/Console/TestOpenTracingShim.cs +++ b/examples/Console/TestOpenTracingShim.cs @@ -10,17 +10,17 @@ namespace Examples.Console; -internal class TestOpenTracingShim +internal sealed class TestOpenTracingShim { - internal static object Run(OpenTracingShimOptions options) + internal static int 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(); + .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. @@ -53,6 +53,6 @@ internal static object Run(OpenTracingShimOptions options) System.Console.WriteLine("Press Enter key to exit."); System.Console.ReadLine(); - return null; + return 0; } } diff --git a/examples/Console/TestOtlpExporter.cs b/examples/Console/TestOtlpExporter.cs index 94d749eafef..c38ac518a18 100644 --- a/examples/Console/TestOtlpExporter.cs +++ b/examples/Console/TestOtlpExporter.cs @@ -10,7 +10,7 @@ namespace Examples.Console; internal static class TestOtlpExporter { - internal static object Run(string endpoint, string protocol) + internal static int Run(OtlpOptions options) { /* * Prerequisite to run this example: @@ -20,10 +20,10 @@ internal static object Run(string endpoint, string protocol) * 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 + * docker run --rm -it -p 4317:4317 -p 4318:4318 -v $(pwd):/cfg otel/opentelemetry-collector:0.123.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:latest --config=/cfg/otlp-collector-example/config.yaml + * docker run --rm -it -p 4317:4317 -p 4318:4318 -v "%cd%":/cfg otel/opentelemetry-collector:0.123.0 --config=/cfg/otlp-collector-example/config.yaml * * Open another terminal window at the examples/Console/ directory and * launch the OTLP example by running: @@ -36,57 +36,55 @@ internal static object Run(string endpoint, string protocol) * For more information about the OpenTelemetry Collector go to https://github.com/open-telemetry/opentelemetry-collector * */ - return RunWithActivitySource(endpoint, protocol); + return RunWithActivitySource(options); } - private static object RunWithActivitySource(string endpoint, string protocol) + private static int RunWithActivitySource(OtlpOptions options) { - var otlpExportProtocol = ToOtlpExportProtocol(protocol); + var otlpExportProtocol = ToOtlpExportProtocol(options.Protocol); if (!otlpExportProtocol.HasValue) { - System.Console.WriteLine($"Export protocol {protocol} is not supported. Default protocol 'grpc' will be used."); + System.Console.WriteLine($"Export protocol {options.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 => + .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(options.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.Endpoint = new Uri(options.Endpoint); + } - opt.Protocol = otlpExportProtocol.Value; + opt.Protocol = otlpExportProtocol.Value; - System.Console.WriteLine($"OTLP Exporter is using {opt.Protocol} protocol and endpoint {opt.Endpoint}"); - }) - .Build(); + System.Console.WriteLine($"OTLP Exporter is using {opt.Protocol} protocol and endpoint {opt.Endpoint}"); + }) + .Build(); // The above line is required only in Applications // which decide to use OpenTelemetry. - using (var sample = new InstrumentationWithActivitySource()) - { - sample.Start(); + using var sample = new InstrumentationWithActivitySource(); + sample.Start(); - System.Console.WriteLine("Traces are being created and exported " + - "to the OpenTelemetry Collector in the background. " + - "Press ENTER to stop."); - System.Console.ReadLine(); - } + System.Console.WriteLine("Traces are being created and exported " + + "to the OpenTelemetry Collector in the background. " + + "Press ENTER to stop."); + System.Console.ReadLine(); - return null; + return 0; } - private static OtlpExportProtocol? ToOtlpExportProtocol(string protocol) => - protocol.Trim().ToLower() switch + private static OtlpExportProtocol? ToOtlpExportProtocol(string? protocol) => + protocol?.Trim().ToUpperInvariant() switch { - "grpc" => OtlpExportProtocol.Grpc, - "http/protobuf" => OtlpExportProtocol.HttpProtobuf, + "GRPC" => OtlpExportProtocol.Grpc, + "HTTP/PROTOBUF" => OtlpExportProtocol.HttpProtobuf, _ => null, }; } diff --git a/examples/Console/TestPrometheusExporter.cs b/examples/Console/TestPrometheusExporter.cs index 57f38c466df..3dc9ca0d89d 100644 --- a/examples/Console/TestPrometheusExporter.cs +++ b/examples/Console/TestPrometheusExporter.cs @@ -3,21 +3,20 @@ using System.Diagnostics; using System.Diagnostics.Metrics; +using System.Security.Cryptography; using OpenTelemetry; using OpenTelemetry.Metrics; -using OpenTelemetry.Trace; namespace Examples.Console; -internal class TestPrometheusExporter +internal sealed class TestPrometheusExporter { private static readonly Meter MyMeter = new("MyMeter"); private static readonly Meter MyMeter2 = new("MyMeter2"); private static readonly Counter Counter = MyMeter.CreateCounter("myCounter", description: "A counter for demonstration purpose."); private static readonly Histogram MyHistogram = MyMeter.CreateHistogram("myHistogram"); - private static readonly ThreadLocal ThreadLocalRandom = new(() => new Random()); - internal static object Run(int port) + internal static int Run(PrometheusOptions options) { /* prometheus.yml example. Adjust port as per actual. @@ -35,7 +34,7 @@ internal static object Run(int port) .AddMeter(MyMeter.Name) .AddMeter(MyMeter2.Name) .AddPrometheusHttpListener( - options => options.UriPrefixes = new string[] { $"http://localhost:{port}/" }) + o => o.UriPrefixes = [$"http://localhost:{options.Port}/"]) .Build(); var process = Process.GetCurrentProcess(); @@ -57,12 +56,12 @@ internal static object Run(int port) { Counter.Add(9.9, new("name", "apple"), new("color", "red")); Counter.Add(99.9, new("name", "lemon"), new("color", "yellow")); - MyHistogram.Record(ThreadLocalRandom.Value.Next(1, 1500), new("tag1", "value1"), new("tag2", "value2")); + MyHistogram.Record(RandomNumberGenerator.GetInt32(1, 1500), new("tag1", "value1"), new("tag2", "value2")); Task.Delay(10).Wait(); } }); - System.Console.WriteLine($"PrometheusExporter exposes metrics via http://localhost:{port}/metrics/"); + System.Console.WriteLine($"PrometheusExporter exposes metrics via http://localhost:{options.Port}/metrics/"); System.Console.WriteLine($"Press Esc key to exit..."); while (true) { @@ -80,7 +79,7 @@ internal static object Run(int port) Task.Delay(200).Wait(); } - return null; + return 0; } private static IEnumerable> GetThreadCpuTime(Process process) diff --git a/examples/Console/TestZipkinExporter.cs b/examples/Console/TestZipkinExporter.cs index 271826a7c2b..d7d579270f9 100644 --- a/examples/Console/TestZipkinExporter.cs +++ b/examples/Console/TestZipkinExporter.cs @@ -7,9 +7,9 @@ namespace Examples.Console; -internal class TestZipkinExporter +internal sealed class TestZipkinExporter { - internal static object Run(string zipkinUri) + internal static int Run(ZipkinOptions options) { // Prerequisite for running this example. // Setup zipkin inside local docker using following command: @@ -23,25 +23,24 @@ internal static object Run(string zipkinUri) // 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; + .AddSource("Samples.SampleClient", "Samples.SampleServer") + .ConfigureResource(r => r.AddService("zipkin-test")) + .AddZipkinExporter(o => + { + o.Endpoint = new Uri(options.Uri); + }) + .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 0; } } diff --git a/examples/Console/otlp-collector-example/config.yaml b/examples/Console/otlp-collector-example/config.yaml index 8d0584b61fa..57e59398b7c 100644 --- a/examples/Console/otlp-collector-example/config.yaml +++ b/examples/Console/otlp-collector-example/config.yaml @@ -8,7 +8,9 @@ receivers: otlp: protocols: grpc: + endpoint: 0.0.0.0:4317 http: + endpoint: 0.0.0.0:4318 exporters: debug: diff --git a/examples/Directory.Build.props b/examples/Directory.Build.props index 032e1e27ec9..4c709d51681 100644 --- a/examples/Directory.Build.props +++ b/examples/Directory.Build.props @@ -1,3 +1,4 @@ + diff --git a/examples/Directory.Build.targets b/examples/Directory.Build.targets new file mode 100644 index 00000000000..a0db1462028 --- /dev/null +++ b/examples/Directory.Build.targets @@ -0,0 +1,5 @@ + + + + + diff --git a/examples/Directory.Packages.props b/examples/Directory.Packages.props deleted file mode 100644 index 902efc8cc04..00000000000 --- a/examples/Directory.Packages.props +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/examples/GrpcService/Program.cs b/examples/GrpcService/Program.cs index 5ed8ceb4c34..5f80792c640 100644 --- a/examples/GrpcService/Program.cs +++ b/examples/GrpcService/Program.cs @@ -1,22 +1,58 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -namespace Examples.GrpcService; +using Examples.GrpcService; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; -public class Program -{ - public static void Main(string[] args) +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddGrpc(); + +builder.Services.AddOpenTelemetry() + .WithTracing(tracerBuilder => { - 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"); - }); + tracerBuilder.ConfigureResource(r => r.AddService( + builder.Configuration.GetValue("ServiceName", defaultValue: "otel-test"))) + .AddAspNetCoreInstrumentation(); + + var exporter = builder.Configuration.GetValue("UseExporter", defaultValue: "console").ToUpperInvariant(); + switch (exporter) + { + case "OTLP": + tracerBuilder.AddOtlpExporter(otlpOptions => + { + otlpOptions.Endpoint = new Uri(builder.Configuration.GetValue("Otlp:Endpoint", defaultValue: "http://localhost:4317")); + }); + break; + case "ZIPKIN": + tracerBuilder.AddZipkinExporter(zipkinOptions => + { + zipkinOptions.Endpoint = new Uri(builder.Configuration.GetValue("Zipkin:Endpoint", defaultValue: "http://localhost:9411/api/v2/spans")); + }); + break; + default: + tracerBuilder.AddConsoleExporter(); + break; + } + }); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseDeveloperExceptionPage(); } + +app.UseRouting(); + +app.MapGrpcService(); +app.MapGet("/", () => +{ + return Results.Text("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"); +}); + +app.Run(); diff --git a/examples/GrpcService/Services/GreeterService.cs b/examples/GrpcService/Services/GreeterService.cs index 011439970fb..9963b7aeb35 100644 --- a/examples/GrpcService/Services/GreeterService.cs +++ b/examples/GrpcService/Services/GreeterService.cs @@ -5,15 +5,10 @@ namespace Examples.GrpcService; -public class GreeterService : Greeter.GreeterBase +#pragma warning disable CA1812 // Avoid uninstantiated internal classes +internal sealed class GreeterService : Greeter.GreeterBase +#pragma warning restore CA1812 // Avoid uninstantiated internal classes { - private readonly ILogger logger; - - public GreeterService(ILogger logger) - { - this.logger = logger; - } - public override Task SayHello(HelloRequest request, ServerCallContext context) { return Task.FromResult(new HelloReply diff --git a/examples/GrpcService/Startup.cs b/examples/GrpcService/Startup.cs deleted file mode 100644 index d23af73550f..00000000000 --- a/examples/GrpcService/Startup.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using OpenTelemetry.Resources; -using OpenTelemetry.Trace; - -namespace Examples.GrpcService; - -public class Startup -{ - public Startup(IConfiguration configuration) - { - this.Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - public void ConfigureServices(IServiceCollection services) - { - services.AddGrpc(); - - services.AddOpenTelemetry() - .WithTracing(builder => - { - builder - .ConfigureResource(r => r.AddService(this.Configuration.GetValue("ServiceName", defaultValue: "otel-test")!)) - .AddAspNetCoreInstrumentation(); - - // Switch between Otlp/Zipkin/Console by setting UseExporter in appsettings.json. - var exporter = this.Configuration.GetValue("UseExporter", defaultValue: "console")!.ToLowerInvariant(); - switch (exporter) - { - 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; - } - }); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - 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); - }); - }); - } -} diff --git a/examples/MicroserviceExample/Utils/Messaging/MessageReceiver.cs b/examples/MicroserviceExample/Utils/Messaging/MessageReceiver.cs index 09de208df21..e3137d987a4 100644 --- a/examples/MicroserviceExample/Utils/Messaging/MessageReceiver.cs +++ b/examples/MicroserviceExample/Utils/Messaging/MessageReceiver.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Text; using Microsoft.Extensions.Logging; using OpenTelemetry; @@ -11,35 +12,39 @@ namespace Utils.Messaging; -public class MessageReceiver : IDisposable +public sealed class MessageReceiver : IDisposable { 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 IConnection? connection; + private IChannel? channel; 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(); + this.channel?.Dispose(); + this.connection?.Dispose(); } - public void StartConsumer() + public async Task StartConsumerAsync() { - RabbitMqHelper.StartConsumer(this.channel, this.ReceiveMessage); + this.connection = await RabbitMqHelper.CreateConnectionAsync().ConfigureAwait(false); + this.channel = await RabbitMqHelper.CreateModelAndDeclareTestQueueAsync(this.connection).ConfigureAwait(false); + await RabbitMqHelper.StartConsumerAsync(this.channel, this.ReceiveMessageAsync).ConfigureAwait(false); } - public void ReceiveMessage(BasicDeliverEventArgs ea) + public async Task ReceiveMessageAsync(BasicDeliverEventArgs ea) { + this.EnsureStarted(); + + ArgumentNullException.ThrowIfNull(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; @@ -53,7 +58,7 @@ public void ReceiveMessage(BasicDeliverEventArgs ea) { var message = Encoding.UTF8.GetString(ea.Body.Span.ToArray()); - this.logger.LogInformation($"Message received: [{message}]"); + this.logger.MessageReceived(message); activity?.SetTag("message", message); @@ -61,29 +66,38 @@ public void ReceiveMessage(BasicDeliverEventArgs ea) RabbitMqHelper.AddMessagingTags(activity); // Simulate some work - Thread.Sleep(1000); + await Task.Delay(1_000).ConfigureAwait(false); } catch (Exception ex) { - this.logger.LogError(ex, "Message processing failed."); + this.logger.MessageProcessingFailed(ex); } } - private IEnumerable ExtractTraceContextFromBasicProperties(IBasicProperties props, string key) + private IEnumerable ExtractTraceContextFromBasicProperties(IReadOnlyBasicProperties props, string key) { try { - if (props.Headers.TryGetValue(key, out var value)) + if (props.Headers?.TryGetValue(key, out var value) is true) { - var bytes = value as byte[]; - return new[] { Encoding.UTF8.GetString(bytes) }; + var bytes = (byte[])value!; + return [Encoding.UTF8.GetString(bytes)]; } } catch (Exception ex) { - this.logger.LogError(ex, "Failed to extract trace context."); + this.logger.FailedToExtractTraceContext(ex); } - return Enumerable.Empty(); + return []; + } + + [MemberNotNull(nameof(channel), nameof(connection))] + private void EnsureStarted() + { + if (this.channel == null || this.connection == null) + { + throw new InvalidOperationException("The message receiver has not been started."); + } } } diff --git a/examples/MicroserviceExample/Utils/Messaging/MessageReceiverLog.cs b/examples/MicroserviceExample/Utils/Messaging/MessageReceiverLog.cs new file mode 100644 index 00000000000..95a3e69a93a --- /dev/null +++ b/examples/MicroserviceExample/Utils/Messaging/MessageReceiverLog.cs @@ -0,0 +1,18 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.Logging; + +namespace Utils.Messaging; + +internal static partial class MessageReceiverLog +{ + [LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Message received: [{Message}]")] + public static partial void MessageReceived(this ILogger logger, string message); + + [LoggerMessage(EventId = 2, Level = LogLevel.Error, Message = "Message processing failed.")] + public static partial void MessageProcessingFailed(this ILogger logger, Exception exception); + + [LoggerMessage(EventId = 3, Level = LogLevel.Error, Message = "Failed to extract trace context.")] + public static partial void FailedToExtractTraceContext(this ILogger logger, Exception exception); +} diff --git a/examples/MicroserviceExample/Utils/Messaging/MessageSender.cs b/examples/MicroserviceExample/Utils/Messaging/MessageSender.cs index 969cf279cb4..e978cf01f3e 100644 --- a/examples/MicroserviceExample/Utils/Messaging/MessageSender.cs +++ b/examples/MicroserviceExample/Utils/Messaging/MessageSender.cs @@ -10,38 +10,42 @@ namespace Utils.Messaging; -public class MessageSender : IDisposable +public sealed 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; + private IConnection? connection; + private IChannel? channel; public MessageSender(ILogger logger) { this.logger = logger; - this.connection = RabbitMqHelper.CreateConnection(); - this.channel = RabbitMqHelper.CreateModelAndDeclareTestQueue(this.connection); } public void Dispose() { - this.channel.Dispose(); - this.connection.Dispose(); + this.channel?.Dispose(); + this.connection?.Dispose(); } - public string SendMessage() + public async Task SendMessageAsync() { try { + if (this.channel is null) + { + this.connection = await RabbitMqHelper.CreateConnectionAsync().ConfigureAwait(false); + this.channel = await RabbitMqHelper.CreateModelAndDeclareTestQueueAsync(this.connection).ConfigureAwait(false); + } + // 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"; using var activity = ActivitySource.StartActivity(activityName, ActivityKind.Producer); - var props = this.channel.CreateBasicProperties(); + var props = new BasicProperties(); // Depending on Sampling (and whether a listener is registered or not), the // activity above may not be created. @@ -65,19 +69,20 @@ public string SendMessage() RabbitMqHelper.AddMessagingTags(activity); var body = $"Published message: DateTime.Now = {DateTime.Now}."; - this.channel.BasicPublish( + await this.channel.BasicPublishAsync( exchange: RabbitMqHelper.DefaultExchangeName, routingKey: RabbitMqHelper.TestQueueName, + mandatory: false, basicProperties: props, - body: Encoding.UTF8.GetBytes(body)); + body: Encoding.UTF8.GetBytes(body)).ConfigureAwait(false); - this.logger.LogInformation($"Message sent: [{body}]"); + this.logger.MessageSent(body); return body; } catch (Exception ex) { - this.logger.LogError(ex, "Message publishing failed."); + this.logger.MessagePublishingFailed(ex); throw; } } @@ -86,16 +91,13 @@ private void InjectTraceContextIntoBasicProperties(IBasicProperties props, strin { try { - if (props.Headers == null) - { - props.Headers = new Dictionary(); - } + props.Headers ??= new Dictionary(); props.Headers[key] = value; } catch (Exception ex) { - this.logger.LogError(ex, "Failed to inject trace context."); + this.logger.FailedToInjectTraceContext(ex); } } } diff --git a/examples/MicroserviceExample/Utils/Messaging/MessageSenderLog.cs b/examples/MicroserviceExample/Utils/Messaging/MessageSenderLog.cs new file mode 100644 index 00000000000..f33eab31c37 --- /dev/null +++ b/examples/MicroserviceExample/Utils/Messaging/MessageSenderLog.cs @@ -0,0 +1,18 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.Logging; + +namespace Utils.Messaging; + +internal static partial class MessageSenderLog +{ + [LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Message sent: [{Body}]")] + public static partial void MessageSent(this ILogger logger, string body); + + [LoggerMessage(EventId = 2, Level = LogLevel.Error, Message = "Message publishing failed.")] + public static partial void MessagePublishingFailed(this ILogger logger, Exception exception); + + [LoggerMessage(EventId = 3, Level = LogLevel.Error, Message = "Failed to inject trace context.")] + public static partial void FailedToInjectTraceContext(this ILogger logger, Exception exception); +} diff --git a/examples/MicroserviceExample/Utils/Messaging/RabbitMqHelper.cs b/examples/MicroserviceExample/Utils/Messaging/RabbitMqHelper.cs index 476bc26a853..a1a6569abbe 100644 --- a/examples/MicroserviceExample/Utils/Messaging/RabbitMqHelper.cs +++ b/examples/MicroserviceExample/Utils/Messaging/RabbitMqHelper.cs @@ -12,49 +12,44 @@ public static class RabbitMqHelper public const string DefaultExchangeName = ""; public const string TestQueueName = "TestQueue"; - private static readonly ConnectionFactory ConnectionFactory; - - static RabbitMqHelper() + private static readonly ConnectionFactory ConnectionFactory = new() { - 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 async Task CreateConnectionAsync() => + await ConnectionFactory.CreateConnectionAsync().ConfigureAwait(false); - public static IModel CreateModelAndDeclareTestQueue(IConnection connection) + public static async Task CreateModelAndDeclareTestQueueAsync(IConnection connection) { - var channel = connection.CreateModel(); + ArgumentNullException.ThrowIfNull(connection); + + var channel = await connection.CreateChannelAsync().ConfigureAwait(false); - channel.QueueDeclare( + await channel.QueueDeclareAsync( queue: TestQueueName, durable: false, exclusive: false, autoDelete: false, - arguments: null); + arguments: null).ConfigureAwait(false); return channel; } - public static void StartConsumer(IModel channel, Action processMessage) + public static async Task StartConsumerAsync(IChannel channel, Func processMessage) { - var consumer = new EventingBasicConsumer(channel); + var consumer = new AsyncEventingBasicConsumer(channel); - consumer.Received += (bc, ea) => processMessage(ea); + consumer.ReceivedAsync += async (bc, ea) => await processMessage(ea).ConfigureAwait(false); - channel.BasicConsume(queue: TestQueueName, autoAck: true, consumer: consumer); + await channel.BasicConsumeAsync(queue: TestQueueName, autoAck: true, consumer: consumer).ConfigureAwait(false); } - public static void AddMessagingTags(Activity activity) + public static void AddMessagingTags(Activity? activity) { // These tags are added demonstrating the semantic conventions of the OpenTelemetry messaging specification // See: diff --git a/examples/MicroserviceExample/Utils/Utils.csproj b/examples/MicroserviceExample/Utils/Utils.csproj index cc7382d4995..1c95380e4c9 100644 --- a/examples/MicroserviceExample/Utils/Utils.csproj +++ b/examples/MicroserviceExample/Utils/Utils.csproj @@ -1,9 +1,6 @@ - netstandard2.0 - - - disable + $(DefaultTargetFrameworkForExampleApps) diff --git a/examples/MicroserviceExample/WebApi/Controllers/SendMessageController.cs b/examples/MicroserviceExample/WebApi/Controllers/SendMessageController.cs index 6a8a5f9564a..e08009c5f8e 100644 --- a/examples/MicroserviceExample/WebApi/Controllers/SendMessageController.cs +++ b/examples/MicroserviceExample/WebApi/Controllers/SendMessageController.cs @@ -10,18 +10,14 @@ namespace WebApi.Controllers; [Route("[controller]")] public class SendMessageController : ControllerBase { - private readonly ILogger logger; private readonly MessageSender messageSender; - public SendMessageController(ILogger logger, MessageSender messageSender) + public SendMessageController(MessageSender messageSender) { - this.logger = logger; this.messageSender = messageSender; } [HttpGet] - public string Get() - { - return this.messageSender.SendMessage(); - } + public async Task Get() => + await this.messageSender.SendMessageAsync().ConfigureAwait(false); } diff --git a/examples/MicroserviceExample/WebApi/Dockerfile b/examples/MicroserviceExample/WebApi/Dockerfile index d74077a0a87..a11bc474dcf 100644 --- a/examples/MicroserviceExample/WebApi/Dockerfile +++ b/examples/MicroserviceExample/WebApi/Dockerfile @@ -1,12 +1,18 @@ -ARG SDK_VERSION=8.0 -FROM mcr.microsoft.com/dotnet/sdk:${SDK_VERSION} AS build +ARG SDK_VERSION=9.0 +FROM mcr.microsoft.com/dotnet/sdk:8.0.414@sha256:3cef19377b2ef2a0171e930a24627677447c3e41b5f2eab84ff4895f1b15d254 AS dotnet-sdk-8.0 +FROM mcr.microsoft.com/dotnet/sdk:9.0.305@sha256:bb42ae2c058609d1746baf24fe6864ecab0686711dfca1f4b7a99e367ab17162 AS dotnet-sdk-9.0 + +FROM dotnet-sdk-${SDK_VERSION} AS build ARG PUBLISH_CONFIGURATION=Release -ARG PUBLISH_FRAMEWORK=net8.0 +ARG PUBLISH_FRAMEWORK=net9.0 WORKDIR /app COPY . ./ RUN dotnet publish ./examples/MicroserviceExample/WebApi -c "${PUBLISH_CONFIGURATION}" -f "${PUBLISH_FRAMEWORK}" -o /out -p:IntegrationBuild=true -FROM mcr.microsoft.com/dotnet/aspnet:${SDK_VERSION} AS runtime +FROM mcr.microsoft.com/dotnet/aspnet:8.0.20@sha256:e88f90b6d9fd7e9e0d8e231d068fccdbebd3c91892441a85ef35066aea9a4e1e AS dotnet-aspnet-8.0 +FROM mcr.microsoft.com/dotnet/aspnet:9.0.9@sha256:1af4114db9ba87542a3f23dbb5cd9072cad7fcc8505f6e9131d1feb580286a6f AS dotnet-aspnet-9.0 + +FROM dotnet-aspnet-${SDK_VERSION} AS runtime WORKDIR /app COPY --from=build /out ./ ENTRYPOINT ["dotnet", "WebApi.dll"] diff --git a/examples/MicroserviceExample/WebApi/Program.cs b/examples/MicroserviceExample/WebApi/Program.cs index 9ed5a0b9c01..2dbc341f6d4 100644 --- a/examples/MicroserviceExample/WebApi/Program.cs +++ b/examples/MicroserviceExample/WebApi/Program.cs @@ -1,19 +1,36 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -namespace WebApi; +using OpenTelemetry.Trace; +using Utils.Messaging; -public class Program +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); + +builder.Services.AddSingleton(); + +builder.Services.AddOpenTelemetry() + .WithTracing(b => b + .AddAspNetCoreInstrumentation() + .AddSource(nameof(MessageSender)) + .AddZipkinExporter(o => + { + var zipkinHostName = Environment.GetEnvironmentVariable("ZIPKIN_HOSTNAME") ?? "localhost"; + o.Endpoint = new Uri($"http://{zipkinHostName}:9411/api/v2/spans"); + })); + +builder.WebHost.UseUrls("http://*:5000"); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) { - 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(); - }); + app.UseDeveloperExceptionPage(); } + +app.UseRouting(); + +app.MapControllers(); + +app.Run(); diff --git a/examples/MicroserviceExample/WebApi/Startup.cs b/examples/MicroserviceExample/WebApi/Startup.cs deleted file mode 100644 index b77ea597c73..00000000000 --- a/examples/MicroserviceExample/WebApi/Startup.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using OpenTelemetry.Trace; -using Utils.Messaging; - -namespace WebApi; - -public class Startup -{ - public Startup(IConfiguration configuration) - { - this.Configuration = configuration; - } - - 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 Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } -} diff --git a/examples/MicroserviceExample/WebApi/WebApi.csproj b/examples/MicroserviceExample/WebApi/WebApi.csproj index 2d5846e580f..9c7ae7cf936 100644 --- a/examples/MicroserviceExample/WebApi/WebApi.csproj +++ b/examples/MicroserviceExample/WebApi/WebApi.csproj @@ -1,11 +1,11 @@ - + $(DefaultTargetFrameworkForExampleApps) + $(NoWarn);CA1515 - diff --git a/examples/MicroserviceExample/WorkerService/Dockerfile b/examples/MicroserviceExample/WorkerService/Dockerfile index dafc0049b46..07dde87f8b9 100644 --- a/examples/MicroserviceExample/WorkerService/Dockerfile +++ b/examples/MicroserviceExample/WorkerService/Dockerfile @@ -1,12 +1,19 @@ -ARG SDK_VERSION=8.0 -FROM mcr.microsoft.com/dotnet/sdk:${SDK_VERSION} AS build +ARG SDK_VERSION=9.0 + +FROM mcr.microsoft.com/dotnet/sdk:8.0.414@sha256:3cef19377b2ef2a0171e930a24627677447c3e41b5f2eab84ff4895f1b15d254 AS dotnet-sdk-8.0 +FROM mcr.microsoft.com/dotnet/sdk:9.0.305@sha256:bb42ae2c058609d1746baf24fe6864ecab0686711dfca1f4b7a99e367ab17162 AS dotnet-sdk-9.0 + +FROM dotnet-sdk-${SDK_VERSION} AS build ARG PUBLISH_CONFIGURATION=Release -ARG PUBLISH_FRAMEWORK=net8.0 +ARG PUBLISH_FRAMEWORK=net9.0 WORKDIR /app COPY . ./ RUN dotnet publish ./examples/MicroserviceExample/WorkerService -c "${PUBLISH_CONFIGURATION}" -f "${PUBLISH_FRAMEWORK}" -o /out -p:IntegrationBuild=true -FROM mcr.microsoft.com/dotnet/aspnet:${SDK_VERSION} AS runtime +FROM mcr.microsoft.com/dotnet/aspnet:8.0.20@sha256:e88f90b6d9fd7e9e0d8e231d068fccdbebd3c91892441a85ef35066aea9a4e1e AS dotnet-aspnet-8.0 +FROM mcr.microsoft.com/dotnet/aspnet:9.0.9@sha256:1af4114db9ba87542a3f23dbb5cd9072cad7fcc8505f6e9131d1feb580286a6f AS dotnet-aspnet-9.0 + +FROM dotnet-aspnet-${SDK_VERSION} AS runtime WORKDIR /app COPY --from=build /out ./ ENTRYPOINT ["dotnet", "WorkerService.dll"] diff --git a/examples/MicroserviceExample/WorkerService/Program.cs b/examples/MicroserviceExample/WorkerService/Program.cs index 24e03322c12..118484d827b 100644 --- a/examples/MicroserviceExample/WorkerService/Program.cs +++ b/examples/MicroserviceExample/WorkerService/Program.cs @@ -3,31 +3,22 @@ using OpenTelemetry.Trace; using Utils.Messaging; +using WorkerService; -namespace WorkerService; +var builder = Host.CreateApplicationBuilder(args); -public class Program -{ - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } +builder.Services.AddHostedService(); +builder.Services.AddSingleton(); - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureServices((hostContext, services) => - { - services.AddHostedService(); +builder.Services.AddOpenTelemetry() + .WithTracing(b => b + .AddSource(nameof(MessageReceiver)) + .AddZipkinExporter(o => + { + var zipkinHostName = Environment.GetEnvironmentVariable("ZIPKIN_HOSTNAME") ?? "localhost"; + o.Endpoint = new Uri($"http://{zipkinHostName}:9411/api/v2/spans"); + })); - services.AddSingleton(); +var app = builder.Build(); - 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"); - })); - }); -} +app.Run(); diff --git a/examples/MicroserviceExample/WorkerService/Worker.cs b/examples/MicroserviceExample/WorkerService/Worker.cs index 9b3fa484d19..1292076399b 100644 --- a/examples/MicroserviceExample/WorkerService/Worker.cs +++ b/examples/MicroserviceExample/WorkerService/Worker.cs @@ -5,7 +5,7 @@ namespace WorkerService; -public partial class Worker : BackgroundService +internal sealed class Worker : BackgroundService { private readonly MessageReceiver messageReceiver; @@ -14,22 +14,10 @@ public Worker(MessageReceiver messageReceiver) this.messageReceiver = messageReceiver; } - public override Task StartAsync(CancellationToken cancellationToken) - { - return base.StartAsync(cancellationToken); - } - - public override async Task StopAsync(CancellationToken cancellationToken) - { - await base.StopAsync(cancellationToken).ConfigureAwait(false); - } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) { stoppingToken.ThrowIfCancellationRequested(); - this.messageReceiver.StartConsumer(); - - await Task.CompletedTask.ConfigureAwait(false); + await this.messageReceiver.StartConsumerAsync().ConfigureAwait(false); } } diff --git a/examples/MicroserviceExample/WorkerService/WorkerService.csproj b/examples/MicroserviceExample/WorkerService/WorkerService.csproj index b9b1a680772..a1dc2dae9f4 100644 --- a/examples/MicroserviceExample/WorkerService/WorkerService.csproj +++ b/examples/MicroserviceExample/WorkerService/WorkerService.csproj @@ -1,15 +1,14 @@ $(DefaultTargetFrameworkForExampleApps) + $(NoWarn);CA1812 - - diff --git a/examples/MicroserviceExample/docker-compose.yml b/examples/MicroserviceExample/docker-compose.yml index 35385fae591..ebbe2bbf345 100644 --- a/examples/MicroserviceExample/docker-compose.yml +++ b/examples/MicroserviceExample/docker-compose.yml @@ -1,13 +1,11 @@ -version: '3.8' - services: zipkin: - image: openzipkin/zipkin + image: openzipkin/zipkin:3.5.1@sha256:bb570eb45c2994eaf32da783cc098b3d51d1095b73ec92919863d73d0a9eaafb ports: - 9411:9411 rabbitmq: - image: rabbitmq:3-management-alpine + image: rabbitmq:4-management-alpine@sha256:603089229e6060279f1b5db7fb5d844d1f259dd7a73fca8e3b15bb93713ad27c ports: - 5672:5672 - 15672:15672 diff --git a/global.json b/global.json index 0aca8b12938..8c14264b61d 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "8.0.100" + "version": "9.0.305" } } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 93113cdc8e2..dfe83531769 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,3 +1,4 @@ + diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 3ee054532a9..6cef70da6c1 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -1,5 +1,7 @@ + + - - - . /// The supplied for chaining. public static LoggerProviderBuilder AddInstrumentation< -#if NET6_0_OR_GREATER +#if NET [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] #endif T>(this LoggerProviderBuilder loggerProviderBuilder) @@ -36,7 +36,7 @@ public static LoggerProviderBuilder AddInstrumentation< loggerProviderBuilder.ConfigureBuilder((sp, builder) => { - builder.AddInstrumentation(() => sp.GetRequiredService()); + builder.AddInstrumentation(sp.GetRequiredService); }); return loggerProviderBuilder; diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/OpenTelemetryDependencyInjectionMeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/OpenTelemetryDependencyInjectionMeterProviderBuilderExtensions.cs index 457ddc87663..ff52ee37cb3 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/OpenTelemetryDependencyInjectionMeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Metrics/OpenTelemetryDependencyInjectionMeterProviderBuilderExtensions.cs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if NET6_0_OR_GREATER +#if NET using System.Diagnostics.CodeAnalysis; #endif using Microsoft.Extensions.DependencyInjection; @@ -26,7 +26,7 @@ public static class OpenTelemetryDependencyInjectionMeterProviderBuilderExtensio /// . /// The supplied for chaining. public static MeterProviderBuilder AddInstrumentation< -#if NET6_0_OR_GREATER +#if NET [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] #endif T>(this MeterProviderBuilder meterProviderBuilder) @@ -36,7 +36,7 @@ public static MeterProviderBuilder AddInstrumentation< meterProviderBuilder.ConfigureBuilder((sp, builder) => { - builder.AddInstrumentation(() => sp.GetRequiredService()); + builder.AddInstrumentation(sp.GetRequiredService); }); return meterProviderBuilder; diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/OpenTelemetry.Api.ProviderBuilderExtensions.csproj b/src/OpenTelemetry.Api.ProviderBuilderExtensions/OpenTelemetry.Api.ProviderBuilderExtensions.csproj index d057d595f43..8c28b1e5c44 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/OpenTelemetry.Api.ProviderBuilderExtensions.csproj +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/OpenTelemetry.Api.ProviderBuilderExtensions.csproj @@ -5,7 +5,6 @@ Contains extensions to register OpenTelemetry in applications using Microsoft.Extensions.DependencyInjection OpenTelemetry core- - latest-all @@ -16,4 +15,14 @@ + + + + + + + + + + diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/OpenTelemetryDependencyInjectionTracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/OpenTelemetryDependencyInjectionTracerProviderBuilderExtensions.cs index 35088b2a5ef..939fd34b366 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/OpenTelemetryDependencyInjectionTracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/Trace/OpenTelemetryDependencyInjectionTracerProviderBuilderExtensions.cs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if NET6_0_OR_GREATER +#if NET using System.Diagnostics.CodeAnalysis; #endif using Microsoft.Extensions.DependencyInjection; @@ -26,7 +26,7 @@ public static class OpenTelemetryDependencyInjectionTracerProviderBuilderExtensi /// . /// The supplied for chaining. public static TracerProviderBuilder AddInstrumentation< -#if NET6_0_OR_GREATER +#if NET [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] #endif T>(this TracerProviderBuilder tracerProviderBuilder) @@ -36,7 +36,7 @@ public static TracerProviderBuilder AddInstrumentation< tracerProviderBuilder.ConfigureBuilder((sp, builder) => { - builder.AddInstrumentation(() => sp.GetRequiredService()); + builder.AddInstrumentation(sp.GetRequiredService); }); return tracerProviderBuilder; diff --git a/src/OpenTelemetry.Api/.publicApi/Experimental/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Api/.publicApi/Experimental/PublicAPI.Unshipped.txt index 4cb12fd2969..e4f616a9dca 100644 --- a/src/OpenTelemetry.Api/.publicApi/Experimental/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Api/.publicApi/Experimental/PublicAPI.Unshipped.txt @@ -1,73 +1,75 @@ -abstract OpenTelemetry.Logs.Logger.EmitLog(in OpenTelemetry.Logs.LogRecordData data, in OpenTelemetry.Logs.LogRecordAttributeList attributes) -> void -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.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.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 +[OTEL1001]abstract OpenTelemetry.Logs.Logger.EmitLog(in OpenTelemetry.Logs.LogRecordData data, in OpenTelemetry.Logs.LogRecordAttributeList attributes) -> void +[OTEL1001]OpenTelemetry.Logs.Logger +[OTEL1001]OpenTelemetry.Logs.Logger.EmitLog(in OpenTelemetry.Logs.LogRecordData data) -> void +[OTEL1001]OpenTelemetry.Logs.Logger.Logger(string? name) -> void +[OTEL1001]OpenTelemetry.Logs.Logger.Name.get -> string! +[OTEL1001]OpenTelemetry.Logs.Logger.Version.get -> string? +[OTEL1001]OpenTelemetry.Logs.LoggerProvider.GetLogger() -> OpenTelemetry.Logs.Logger! +[OTEL1001]OpenTelemetry.Logs.LoggerProvider.GetLogger(string? name) -> OpenTelemetry.Logs.Logger! +[OTEL1001]OpenTelemetry.Logs.LoggerProvider.GetLogger(string? name, string? version) -> OpenTelemetry.Logs.Logger! +[OTEL1001]OpenTelemetry.Logs.LogRecordAttributeList +[OTEL1001]OpenTelemetry.Logs.LogRecordAttributeList.Add(string! key, object? value) -> void +[OTEL1001]OpenTelemetry.Logs.LogRecordAttributeList.Add(System.Collections.Generic.KeyValuePair attribute) -> void +[OTEL1001]OpenTelemetry.Logs.LogRecordAttributeList.Clear() -> void +[OTEL1001]OpenTelemetry.Logs.LogRecordAttributeList.Count.get -> int +[OTEL1001]OpenTelemetry.Logs.LogRecordAttributeList.Enumerator +[OTEL1001]OpenTelemetry.Logs.LogRecordAttributeList.Enumerator.Current.get -> System.Collections.Generic.KeyValuePair +[OTEL1001]OpenTelemetry.Logs.LogRecordAttributeList.Enumerator.Dispose() -> void +[OTEL1001]OpenTelemetry.Logs.LogRecordAttributeList.Enumerator.Enumerator() -> void +[OTEL1001]OpenTelemetry.Logs.LogRecordAttributeList.Enumerator.MoveNext() -> bool +[OTEL1001]OpenTelemetry.Logs.LogRecordAttributeList.GetEnumerator() -> OpenTelemetry.Logs.LogRecordAttributeList.Enumerator +[OTEL1001]OpenTelemetry.Logs.LogRecordAttributeList.LogRecordAttributeList() -> void +[OTEL1001]OpenTelemetry.Logs.LogRecordAttributeList.RecordException(System.Exception! exception) -> void +[OTEL1001]OpenTelemetry.Logs.LogRecordAttributeList.this[int index].get -> System.Collections.Generic.KeyValuePair +[OTEL1001]OpenTelemetry.Logs.LogRecordAttributeList.this[int index].set -> void +[OTEL1001]OpenTelemetry.Logs.LogRecordAttributeList.this[string! key].set -> void +[OTEL1001]OpenTelemetry.Logs.LogRecordData +[OTEL1001]OpenTelemetry.Logs.LogRecordData.Body.get -> string? +[OTEL1001]OpenTelemetry.Logs.LogRecordData.Body.set -> void +[OTEL1001]OpenTelemetry.Logs.LogRecordData.LogRecordData() -> void +[OTEL1001]OpenTelemetry.Logs.LogRecordData.LogRecordData(in System.Diagnostics.ActivityContext activityContext) -> void +[OTEL1001]OpenTelemetry.Logs.LogRecordData.LogRecordData(System.Diagnostics.Activity? activity) -> void +[OTEL1001]OpenTelemetry.Logs.LogRecordData.Severity.get -> OpenTelemetry.Logs.LogRecordSeverity? +[OTEL1001]OpenTelemetry.Logs.LogRecordData.Severity.set -> void +[OTEL1001]OpenTelemetry.Logs.LogRecordData.SeverityText.get -> string? +[OTEL1001]OpenTelemetry.Logs.LogRecordData.SeverityText.set -> void +[OTEL1001]OpenTelemetry.Logs.LogRecordData.SpanId.get -> System.Diagnostics.ActivitySpanId +[OTEL1001]OpenTelemetry.Logs.LogRecordData.SpanId.set -> void +[OTEL1001]OpenTelemetry.Logs.LogRecordData.Timestamp.get -> System.DateTime +[OTEL1001]OpenTelemetry.Logs.LogRecordData.Timestamp.set -> void +[OTEL1001]OpenTelemetry.Logs.LogRecordData.TraceFlags.get -> System.Diagnostics.ActivityTraceFlags +[OTEL1001]OpenTelemetry.Logs.LogRecordData.TraceFlags.set -> void +[OTEL1001]OpenTelemetry.Logs.LogRecordData.TraceId.get -> System.Diagnostics.ActivityTraceId +[OTEL1001]OpenTelemetry.Logs.LogRecordData.TraceId.set -> void +[OTEL1001]OpenTelemetry.Logs.LogRecordData.EventName.get -> string? +[OTEL1001]OpenTelemetry.Logs.LogRecordData.EventName.set -> void +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Debug = 5 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Debug2 = 6 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Debug3 = 7 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Debug4 = 8 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Error = 17 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Error2 = 18 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Error3 = 19 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Error4 = 20 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Fatal = 21 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Fatal2 = 22 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Fatal3 = 23 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Fatal4 = 24 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Info = 9 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Info2 = 10 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Info3 = 11 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Info4 = 12 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Trace = 1 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Trace2 = 2 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Trace3 = 3 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Trace4 = 4 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Unspecified = 0 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Warn = 13 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Warn2 = 14 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Warn3 = 15 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Warn4 = 16 -> OpenTelemetry.Logs.LogRecordSeverity +[OTEL1001]OpenTelemetry.Logs.LogRecordSeverityExtensions +[OTEL1001]static OpenTelemetry.Logs.LogRecordAttributeList.CreateFromEnumerable(System.Collections.Generic.IEnumerable>! attributes) -> OpenTelemetry.Logs.LogRecordAttributeList +[OTEL1001]static OpenTelemetry.Logs.LogRecordSeverityExtensions.ToShortName(this OpenTelemetry.Logs.LogRecordSeverity logRecordSeverity) -> string! +[OTEL1001]virtual OpenTelemetry.Logs.LoggerProvider.TryCreateLogger(string? name, out OpenTelemetry.Logs.Logger? logger) -> bool diff --git a/src/OpenTelemetry.Api/.publicApi/Stable/PublicAPI.Shipped.txt b/src/OpenTelemetry.Api/.publicApi/Stable/PublicAPI.Shipped.txt index 7b02ca885de..a0742cbc241 100644 --- a/src/OpenTelemetry.Api/.publicApi/Stable/PublicAPI.Shipped.txt +++ b/src/OpenTelemetry.Api/.publicApi/Stable/PublicAPI.Shipped.txt @@ -1,56 +1,8 @@ #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 -~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.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.Logs.LoggerProviderBuilder.AddInstrumentation(System.Func! instrumentationFactory) -> OpenTelemetry.Logs.LoggerProviderBuilder! abstract OpenTelemetry.Metrics.MeterProviderBuilder.AddInstrumentation(System.Func! instrumentationFactory) -> OpenTelemetry.Metrics.MeterProviderBuilder! @@ -64,18 +16,31 @@ 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 @@ -90,7 +55,12 @@ 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.Logs.IDeferredLoggerProviderBuilder OpenTelemetry.Logs.IDeferredLoggerProviderBuilder.Configure(System.Action! configure) -> OpenTelemetry.Logs.LoggerProviderBuilder! OpenTelemetry.Logs.LoggerProvider @@ -156,6 +126,8 @@ OpenTelemetry.Trace.TelemetrySpan.AddEvent(string! name, OpenTelemetry.Trace.Spa OpenTelemetry.Trace.TelemetrySpan.AddEvent(string! name, System.DateTimeOffset timestamp, 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) -> OpenTelemetry.Trace.TelemetrySpan! +OpenTelemetry.Trace.TelemetrySpan.AddLink(OpenTelemetry.Trace.SpanContext spanContext, OpenTelemetry.Trace.SpanAttributes? attributes) -> OpenTelemetry.Trace.TelemetrySpan! +OpenTelemetry.Trace.TelemetrySpan.AddLink(OpenTelemetry.Trace.SpanContext spanContext) -> OpenTelemetry.Trace.TelemetrySpan! OpenTelemetry.Trace.TelemetrySpan.Context.get -> OpenTelemetry.Trace.SpanContext OpenTelemetry.Trace.TelemetrySpan.Dispose() -> void OpenTelemetry.Trace.TelemetrySpan.End() -> void @@ -181,16 +153,31 @@ OpenTelemetry.Trace.Tracer.StartRootSpan(string! name, OpenTelemetry.Trace.SpanK 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, System.Collections.Generic.IEnumerable>? tags = null) -> OpenTelemetry.Trace.Tracer! +OpenTelemetry.Trace.TracerProvider.GetTracer(string! name, string? version) -> 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.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.Get() -> T? override OpenTelemetry.Context.ThreadLocalRuntimeContextSlot.Set(T value) -> void override OpenTelemetry.Trace.Link.Equals(object? obj) -> bool override OpenTelemetry.Trace.Link.GetHashCode() -> int @@ -202,16 +189,32 @@ 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.Trace.ActivityExtensions.GetStatus(this System.Diagnostics.Activity! activity) -> OpenTelemetry.Trace.Status -static OpenTelemetry.Trace.ActivityExtensions.RecordException(this System.Diagnostics.Activity! activity, System.Exception? ex, in System.Diagnostics.TagList tags) -> void -static OpenTelemetry.Trace.ActivityExtensions.RecordException(this System.Diagnostics.Activity! activity, System.Exception? ex) -> void -static OpenTelemetry.Trace.ActivityExtensions.SetStatus(this System.Diagnostics.Activity! activity, OpenTelemetry.Trace.Status status) -> void +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, in System.Diagnostics.TagList tags) -> void +static OpenTelemetry.Trace.ActivityExtensions.RecordException(this System.Diagnostics.Activity? activity, System.Exception? ex) -> 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 diff --git a/src/OpenTelemetry.Api/.publicApi/Stable/net462/PublicAPI.Shipped.txt b/src/OpenTelemetry.Api/.publicApi/Stable/net462/PublicAPI.Shipped.txt index 58383768cd8..378097ec65c 100644 --- a/src/OpenTelemetry.Api/.publicApi/Stable/net462/PublicAPI.Shipped.txt +++ b/src/OpenTelemetry.Api/.publicApi/Stable/net462/PublicAPI.Shipped.txt @@ -1,6 +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 +OpenTelemetry.Context.RemotingRuntimeContextSlot.RemotingRuntimeContextSlot(string! name) -> void +OpenTelemetry.Context.RemotingRuntimeContextSlot.Value.get -> object? +OpenTelemetry.Context.RemotingRuntimeContextSlot.Value.set -> void +override OpenTelemetry.Context.RemotingRuntimeContextSlot.Get() -> T? override OpenTelemetry.Context.RemotingRuntimeContextSlot.Set(T value) -> void diff --git a/src/OpenTelemetry.Api/ActivityContextExtensions.cs b/src/OpenTelemetry.Api/ActivityContextExtensions.cs index f50a9af8fee..fb630ae6613 100644 --- a/src/OpenTelemetry.Api/ActivityContextExtensions.cs +++ b/src/OpenTelemetry.Api/ActivityContextExtensions.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; // The ActivityContext class is in the System.Diagnostics namespace. diff --git a/src/OpenTelemetry.Api/AssemblyInfo.cs b/src/OpenTelemetry.Api/AssemblyInfo.cs deleted file mode 100644 index b1e95ff67e6..00000000000 --- a/src/OpenTelemetry.Api/AssemblyInfo.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("OpenTelemetry" + AssemblyInfo.PublicKey)] -[assembly: InternalsVisibleTo("OpenTelemetry.Api.ProviderBuilderExtensions" + AssemblyInfo.PublicKey)] -[assembly: InternalsVisibleTo("OpenTelemetry.Api.ProviderBuilderExtensions.Tests" + AssemblyInfo.PublicKey)] -[assembly: InternalsVisibleTo("OpenTelemetry.Api.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)] -[assembly: InternalsVisibleTo("OpenTelemetry.Shims.OpenTracing.Tests" + AssemblyInfo.PublicKey)] -[assembly: InternalsVisibleTo("OpenTelemetry.Tests" + AssemblyInfo.PublicKey)] - -#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/Baggage.cs b/src/OpenTelemetry.Api/Baggage.cs index 46cdedbd7d1..d69e0170ca4 100644 --- a/src/OpenTelemetry.Api/Baggage.cs +++ b/src/OpenTelemetry.Api/Baggage.cs @@ -16,7 +16,7 @@ namespace OpenTelemetry; public readonly struct Baggage : IEquatable { private static readonly RuntimeContextSlot RuntimeContextSlot = RuntimeContext.RegisterSlot("otel.baggage"); - private static readonly Dictionary EmptyBaggage = new(); + private static readonly Dictionary EmptyBaggage = []; private readonly Dictionary baggage; @@ -87,7 +87,7 @@ public static Baggage Current /// /// Baggage key/value pairs. /// . - public static Baggage Create(Dictionary baggageItems = null) + public static Baggage Create(Dictionary? baggageItems = null) { if (baggageItems == null) { @@ -133,7 +133,7 @@ public static Dictionary.Enumerator GetEnumerator(Baggage baggag /// 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) + public static string? GetBaggage(string name, Baggage baggage = default) => baggage == default ? Current.GetBaggage(name) : baggage.GetBaggage(name); /// @@ -145,7 +145,7 @@ public static string GetBaggage(string name, Baggage baggage = default) /// 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) + public static Baggage SetBaggage(string name, string? value, Baggage baggage = default) { var baggageHolder = EnsureBaggageHolder(); lock (baggageHolder) @@ -164,7 +164,7 @@ public static Baggage SetBaggage(string name, string value, Baggage baggage = de /// New containing the new key/value pairs. /// 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) + public static Baggage SetBaggage(IEnumerable> baggageItems, Baggage baggage = default) { var baggageHolder = EnsureBaggageHolder(); lock (baggageHolder) @@ -222,11 +222,11 @@ public IReadOnlyDictionary GetBaggage() /// /// Baggage item name. /// Baggage item or if nothing was found. - public string GetBaggage(string name) + public string? GetBaggage(string name) { Guard.ThrowIfNullOrEmpty(name); - return this.baggage != null && this.baggage.TryGetValue(name, out string value) + return this.baggage != null && this.baggage.TryGetValue(name, out string? value) ? value : null; } @@ -237,7 +237,7 @@ public string GetBaggage(string name) /// Baggage item name. /// Baggage item value. /// New containing the key/value pair. - public Baggage SetBaggage(string name, string value) + public Baggage SetBaggage(string name, string? value) { if (string.IsNullOrEmpty(value)) { @@ -247,7 +247,7 @@ public Baggage SetBaggage(string name, string value) return new Baggage( new Dictionary(this.baggage ?? EmptyBaggage, StringComparer.OrdinalIgnoreCase) { - [name] = value, + [name] = value!, }); } @@ -256,15 +256,15 @@ public Baggage SetBaggage(string name, string value) /// /// Baggage key/value pairs. /// New containing the key/value pairs. - public Baggage SetBaggage(params KeyValuePair[] baggageItems) - => this.SetBaggage((IEnumerable>)baggageItems); + public Baggage SetBaggage(params KeyValuePair[] baggageItems) + => this.SetBaggage((IEnumerable>)baggageItems); /// /// Returns a new which contains the new key/value pairs. /// /// Baggage key/value pairs. /// New containing the key/value pairs. - public Baggage SetBaggage(IEnumerable> baggageItems) + public Baggage SetBaggage(IEnumerable> baggageItems) { if (baggageItems?.Any() != true) { @@ -281,7 +281,7 @@ public Baggage SetBaggage(IEnumerable> baggageItems } else { - newBaggage[item.Key] = item.Value; + newBaggage[item.Key] = item.Value!; } } @@ -305,7 +305,9 @@ public Baggage RemoveBaggage(string name) /// Returns a new with all the key/value pairs removed. /// /// New with all the key/value pairs removed. +#pragma warning disable CA1822 // Mark members as static public Baggage ClearBaggage() +#pragma warning restore CA1822 // Mark members as static => default; /// @@ -325,11 +327,11 @@ public bool Equals(Baggage other) return false; } - return baggageIsNullOrEmpty || this.baggage.SequenceEqual(other.baggage); + return baggageIsNullOrEmpty || this.baggage!.SequenceEqual(other.baggage!); } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) => (obj is Baggage baggage) && this.Equals(baggage); /// @@ -343,7 +345,11 @@ public override int GetHashCode() unchecked { hash = (hash * 23) + baggage.Comparer.GetHashCode(item.Key); +#if NET + hash = (hash * 23) + item.Value.GetHashCode(StringComparison.Ordinal); +#else hash = (hash * 23) + item.Value.GetHashCode(); +#endif } } diff --git a/src/OpenTelemetry.Api/BaseProvider.cs b/src/OpenTelemetry.Api/BaseProvider.cs index ce8e30a46b7..117f18c3c6d 100644 --- a/src/OpenTelemetry.Api/BaseProvider.cs +++ b/src/OpenTelemetry.Api/BaseProvider.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - namespace OpenTelemetry; /// diff --git a/src/OpenTelemetry.Api/CHANGELOG.md b/src/OpenTelemetry.Api/CHANGELOG.md index f39d81b108e..961b11de5b0 100644 --- a/src/OpenTelemetry.Api/CHANGELOG.md +++ b/src/OpenTelemetry.Api/CHANGELOG.md @@ -1,7 +1,97 @@ # Changelog +This file contains individual changes for the OpenTelemetry.Api package. For +highlights and announcements covering all components see: [Release +Notes](../../RELEASENOTES.md). + ## Unreleased +## 1.13.0 + +Released 2025-Oct-01 + +* Added `AddLink(SpanContext, SpanAttributes?)` to `TelemetrySpan` to support + linking spans and associating optional attributes for advanced trace relationships. + ([#6305](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6305)) + +* Experimental (only in pre-release versions): Added the `EventName` property + to `LogRecordData` + ([#6306](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6306)) + +## 1.12.0 + +Released 2025-Apr-29 + +* Added a new overload for `TracerProvider.GetTracer` which accepts an optional + `IEnumerable>? tags` parameter, allowing + additional attributes to be associated with the `Tracer`. + ([#6137](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6137)) + +## 1.11.2 + +Released 2025-Mar-04 + +* Revert optimize performance of `TraceContextPropagator.Extract` introduced + in #5749 to resolve [GHSA-8785-wc3w-h8q6](https://github.com/open-telemetry/opentelemetry-dotnet/security/advisories/GHSA-8785-wc3w-h8q6). + ([#6161](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6161)) + +## 1.11.1 + +Released 2025-Jan-22 + +## 1.11.0 + +Released 2025-Jan-15 + +## 1.11.0-rc.1 + +Released 2024-Dec-11 + +## 1.10.0 + +Released 2024-Nov-12 + +* Updated `System.Diagnostics.DiagnosticSource` package version to + `9.0.0`. + ([#5967](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5967)) + +## 1.10.0-rc.1 + +Released 2024-Nov-01 + +## 1.10.0-beta.1 + +Released 2024-Sep-30 + +* **Breaking change:** CompositeTextMapPropagator.Fields now returns a + unioned set of fields from all combined propagators. Previously this always + returned an empty set. + ([#5745](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5745)) + +* Optimize performance of `TraceContextPropagator.Extract`. + ([#5749](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5749)) + +* Obsoleted the `ActivityExtensions.GetStatus` and + `ActivityExtensions.SetStatus` extension methods. Users should migrate to the + `System.Diagnostics.DiagnosticSource` + [Activity.SetStatus](https://learn.microsoft.com/dotnet/api/system.diagnostics.activity.setstatus) + API for setting the status and + [Activity.Status](https://learn.microsoft.com/dotnet/api/system.diagnostics.activity.status) + & + [Activity.StatusDescription](https://learn.microsoft.com/dotnet/api/system.diagnostics.activity.statusdescription) + APIs for reading the status of an `Activity` instance. + ([#5781](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5781)) + +* Updated `System.Diagnostics.DiagnosticSource` package version to + `9.0.0-rc.1.24431.7`. + ([#5853](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5853)) + +* Obsoleted the `ActivityExtensions.RecordException` extension method. Users + should migrate to the `System.Diagnostics.DiagnosticSource` + [Activity.AddException](https://learn.microsoft.com/dotnet/api/system.diagnostics.activity.addexception) + API for adding exceptions on an `Activity` instance. + ([#5841](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5841)) + ## 1.9.0 Released 2024-Jun-14 @@ -15,8 +105,8 @@ Released 2024-Jun-14 Released 2024-Jun-07 * The experimental APIs previously covered by `OTEL1000` (`LoggerProvider`, - `LoggerProviderBuilder`, & `IDeferredLoggerProviderBuilder`) will now be part - of the public API and supported in stable builds. + `LoggerProviderBuilder`, & `IDeferredLoggerProviderBuilder`) are now part of + the public API and supported in stable builds. ([#5648](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5648)) ## 1.9.0-alpha.1 diff --git a/src/OpenTelemetry.Api/Context/AsyncLocalRuntimeContextSlot.cs b/src/OpenTelemetry.Api/Context/AsyncLocalRuntimeContextSlot.cs index 15eb31d08c2..7bafc0e951d 100644 --- a/src/OpenTelemetry.Api/Context/AsyncLocalRuntimeContextSlot.cs +++ b/src/OpenTelemetry.Api/Context/AsyncLocalRuntimeContextSlot.cs @@ -24,15 +24,25 @@ public AsyncLocalRuntimeContextSlot(string name) } /// - public object Value + public object? Value { get => this.slot.Value; - set => this.slot.Value = (T)value; + set + { + if (typeof(T).IsValueType && value is null) + { + this.slot.Value = default!; + } + else + { + this.slot.Value = (T)value!; + } + } } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override T Get() + public override T? Get() { return this.slot.Value; } diff --git a/src/OpenTelemetry.Api/Context/IRuntimeContextSlotValueAccessor.cs b/src/OpenTelemetry.Api/Context/IRuntimeContextSlotValueAccessor.cs index 1e16ea42601..19c02050cd8 100644 --- a/src/OpenTelemetry.Api/Context/IRuntimeContextSlotValueAccessor.cs +++ b/src/OpenTelemetry.Api/Context/IRuntimeContextSlotValueAccessor.cs @@ -11,5 +11,5 @@ 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 10b4aee1d81..56c353f4a28 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/B3Propagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/B3Propagator.cs @@ -27,6 +27,7 @@ public sealed class B3Propagator : TextMapPropagator internal const string UpperTraceId = "0000000000000000"; // Sampled values via the X_B3_SAMPLED header. + internal const char SampledValueChar = '1'; internal const string SampledValue = "1"; // Some old zipkin implementations may send true/false for the sampled header. Only use this for checking incoming values. @@ -35,7 +36,7 @@ public sealed class B3Propagator : TextMapPropagator // "Debug" sampled value. internal const string FlagsValue = "1"; - private static readonly HashSet AllFields = new() { XB3TraceId, XB3SpanId, XB3ParentSpanId, XB3Sampled, XB3Flags }; + private static readonly HashSet AllFields = [XB3TraceId, XB3SpanId, XB3ParentSpanId, XB3Sampled, XB3Flags]; private static readonly HashSet SampledValues = new(StringComparer.Ordinal) { SampledValue, LegacySampledValue }; @@ -66,7 +67,7 @@ public B3Propagator(bool singleHeader) /// [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) + public override PropagationContext Extract(PropagationContext context, T carrier, Func?> getter) #pragma warning restore CS0809 // Obsolete member overrides non-obsolete member { if (context.ActivityContext.IsValid()) @@ -130,7 +131,7 @@ public override void Inject(PropagationContext context, T carrier, Action(PropagationContext context, T carrier, Action(PropagationContext context, T carrier, Func> getter) + private static PropagationContext ExtractFromMultipleHeaders(PropagationContext context, T carrier, Func?> getter) { try { @@ -179,7 +180,8 @@ private static PropagationContext ExtractFromMultipleHeaders(PropagationConte } var traceOptions = ActivityTraceFlags.None; - if (SampledValues.Contains(getter(carrier, XB3Sampled)?.FirstOrDefault()) + var xb3Sampled = getter(carrier, XB3Sampled)?.FirstOrDefault(); + if ((xb3Sampled != null && SampledValues.Contains(xb3Sampled)) || FlagsValue.Equals(getter(carrier, XB3Flags)?.FirstOrDefault(), StringComparison.Ordinal)) { traceOptions |= ActivityTraceFlags.Recorded; @@ -196,7 +198,7 @@ private static PropagationContext ExtractFromMultipleHeaders(PropagationConte } } - private static PropagationContext ExtractFromSingleHeader(PropagationContext context, T carrier, Func> getter) + private static PropagationContext ExtractFromSingleHeader(PropagationContext context, T carrier, Func?> getter) { try { @@ -206,7 +208,7 @@ private static PropagationContext ExtractFromSingleHeader(PropagationContext return context; } - var parts = header.Split(XB3CombinedDelimiter); + var parts = header!.Split(XB3CombinedDelimiter); if (parts.Length < 2 || parts.Length > 4) { return context; diff --git a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs index 57bdb453b8f..3a0dca61219 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs @@ -1,6 +1,9 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +#if NET +using System.Diagnostics.CodeAnalysis; +#endif using System.Net; using System.Text; using OpenTelemetry.Internal; @@ -17,14 +20,14 @@ public class BaggagePropagator : TextMapPropagator 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 = ['=']; + private static readonly char[] CommaSignSeparator = [',']; /// 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) { @@ -46,16 +49,16 @@ public override PropagationContext Extract(PropagationContext context, T carr try { - Dictionary baggage = null; var baggageCollection = getter(carrier, BaggageHeaderName); if (baggageCollection?.Any() ?? false) { - TryExtractBaggage(baggageCollection.ToArray(), out baggage); + if (TryExtractBaggage([.. baggageCollection], out var baggage)) + { + return new PropagationContext(context.ActivityContext, new Baggage(baggage!)); + } } - return new PropagationContext( - context.ActivityContext, - baggage == null ? context.Baggage : new Baggage(baggage)); + return new PropagationContext(context.ActivityContext, context.Baggage); } catch (Exception ex) { @@ -102,11 +105,16 @@ public override void Inject(PropagationContext context, T carrier, Action baggage) + internal static bool TryExtractBaggage( + string[] baggageCollection, +#if NET + [NotNullWhen(true)] +#endif + out Dictionary? baggage) { int baggageLength = -1; bool done = false; - Dictionary baggageDictionary = null; + Dictionary? baggageDictionary = null; foreach (var item in baggageCollection) { @@ -130,7 +138,11 @@ internal static bool TryExtractBaggage(string[] baggageCollection, out Dictionar break; } +#if NET + if (pair.IndexOf('=', StringComparison.Ordinal) < 0) +#else if (pair.IndexOf('=') < 0) +#endif { continue; } @@ -149,10 +161,7 @@ internal static bool TryExtractBaggage(string[] baggageCollection, out Dictionar continue; } - if (baggageDictionary == null) - { - baggageDictionary = new Dictionary(); - } + baggageDictionary ??= []; baggageDictionary[key] = value; } diff --git a/src/OpenTelemetry.Api/Context/Propagation/CompositeTextMapPropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/CompositeTextMapPropagator.cs index a6b12c3cd1b..d0375aadc71 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/CompositeTextMapPropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/CompositeTextMapPropagator.cs @@ -11,8 +11,8 @@ namespace OpenTelemetry.Context.Propagation; /// public class CompositeTextMapPropagator : TextMapPropagator { - private static readonly ISet EmptyFields = new HashSet(); private readonly List propagators; + private readonly ISet allFields; /// /// Initializes a new instance of the class. @@ -22,18 +22,55 @@ public CompositeTextMapPropagator(IEnumerable propagators) { Guard.ThrowIfNull(propagators); - this.propagators = new List(propagators); + var propagatorsList = new List(); + + foreach (var propagator in propagators) + { + if (propagator is not null) + { + propagatorsList.Add(propagator); + } + } + + this.propagators = propagatorsList; + + // For efficiency, we resolve the fields from all propagators only once, as they are + // not expected to change (although the implementation doesn't strictly prevent that). + if (this.propagators.Count == 0) + { + // Use a new empty HashSet for each instance to avoid any potential mutation issues. + this.allFields = new HashSet(); + } + else + { + ISet? fields = this.propagators[0].Fields; + + var output = fields is not null + ? new HashSet(fields) + : []; + + for (int i = 1; i < this.propagators.Count; i++) + { + fields = this.propagators[i].Fields; + if (fields is not null) + { + output.UnionWith(fields); + } + } + + this.allFields = output; + } } /// - public override ISet Fields => EmptyFields; + public override ISet Fields => this.allFields; /// - 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) + for (int i = 0; i < this.propagators.Count; i++) { - context = propagator.Extract(context, carrier, getter); + context = this.propagators[i].Extract(context, carrier, getter); } return context; @@ -42,9 +79,9 @@ public override PropagationContext Extract(PropagationContext context, T carr /// public override void Inject(PropagationContext context, T carrier, Action setter) { - foreach (var propagator in this.propagators) + for (int i = 0; i < this.propagators.Count; i++) { - propagator.Inject(context, carrier, setter); + this.propagators[i].Inject(context, carrier, setter); } } } diff --git a/src/OpenTelemetry.Api/Context/Propagation/NoopTextMapPropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/NoopTextMapPropagator.cs index 03dd45785d4..0721ad52883 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/NoopTextMapPropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/NoopTextMapPropagator.cs @@ -5,11 +5,13 @@ namespace OpenTelemetry.Context.Propagation; internal sealed class NoopTextMapPropagator : TextMapPropagator { +#pragma warning disable CA1805 // Do not initialize unnecessarily private static readonly PropagationContext DefaultPropagationContext = default; +#pragma warning restore CA1805 // Do not initialize unnecessarily - public override ISet Fields => null; + public override ISet? Fields => null; - public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) + public override PropagationContext Extract(PropagationContext context, T carrier, Func?> getter) { return DefaultPropagationContext; } diff --git a/src/OpenTelemetry.Api/Context/Propagation/PropagationContext.cs b/src/OpenTelemetry.Api/Context/Propagation/PropagationContext.cs index 77b6c71e77a..0923ed45d6d 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/PropagationContext.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/PropagationContext.cs @@ -53,7 +53,7 @@ public bool Equals(PropagationContext value) } /// - 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() diff --git a/src/OpenTelemetry.Api/Context/Propagation/TextMapPropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/TextMapPropagator.cs index 1000b317bf5..bb0432378b5 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/TextMapPropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/TextMapPropagator.cs @@ -15,12 +15,12 @@ public abstract class TextMapPropagator /// * 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. + /// Type of 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. @@ -29,10 +29,10 @@ public abstract class TextMapPropagator /// /// Extracts the context from a carrier. /// - /// Type of object to extract context from. Typically HttpRequest or similar. + /// 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); + /// Context from its 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 de711295428..a00f8e8beb1 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs @@ -16,6 +16,12 @@ public class TraceContextPropagator : TextMapPropagator private const string TraceParent = "traceparent"; private const string TraceState = "tracestate"; + // The following length limits are from Trace Context v1 https://www.w3.org/TR/trace-context-1/#key + private const int TraceStateKeyMaxLength = 256; + private const int TraceStateKeyTenantMaxLength = 241; + private const int TraceStateKeyVendorMaxLength = 14; + private const int TraceStateValueMaxLength = 256; + private static readonly int VersionPrefixIdLength = "00-".Length; private static readonly int TraceIdLength = "0af7651916cd43dd8448eb211c80319c".Length; private static readonly int VersionAndTraceIdLength = "00-0af7651916cd43dd8448eb211c80319c-".Length; @@ -24,17 +30,11 @@ public class TraceContextPropagator : TextMapPropagator 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) + public override PropagationContext Extract(PropagationContext context, T carrier, Func?> getter) { if (context.ActivityContext.IsValid()) { @@ -72,11 +72,11 @@ public override PropagationContext Extract(PropagationContext context, T carr return context; } - string tracestate = null; + string? tracestate = null; var tracestateCollection = getter(carrier, TraceState); if (tracestateCollection?.Any() ?? false) { - TryExtractTracestate(tracestateCollection.ToArray(), out tracestate); + TryExtractTracestate([.. tracestateCollection], out tracestate); } return new PropagationContext( @@ -113,7 +113,7 @@ public override void Inject(PropagationContext context, T carrier, Action(PropagationContext context, T carrier, Action 0) { setter(carrier, TraceState, tracestateStr); @@ -173,7 +173,7 @@ internal static bool TryExtractTraceparent(string traceparent, out ActivityTrace try { - traceId = ActivityTraceId.CreateFromString(traceparent.AsSpan().Slice(VersionPrefixIdLength, TraceIdLength)); + traceId = ActivityTraceId.CreateFromString(traceparent.AsSpan(VersionPrefixIdLength, TraceIdLength)); } catch (ArgumentOutOfRangeException) { @@ -189,7 +189,7 @@ internal static bool TryExtractTraceparent(string traceparent, out ActivityTrace byte optionsLowByte; try { - spanId = ActivitySpanId.CreateFromString(traceparent.AsSpan().Slice(VersionAndTraceIdLength, SpanIdLength)); + spanId = ActivitySpanId.CreateFromString(traceparent.AsSpan(VersionAndTraceIdLength, SpanIdLength)); _ = HexCharToByte(traceparent[VersionAndTraceIdAndSpanIdLength]); // to verify if there is no bad chars on options position optionsLowByte = HexCharToByte(traceparent[VersionAndTraceIdAndSpanIdLength + 1]); } @@ -297,7 +297,11 @@ internal static bool TryExtractTracestate(string[] tracestateCollection, out str result.Append(','); } +#if NET + result.Append(listMember); +#else result.Append(listMember.ToString()); +#endif } } @@ -430,7 +434,7 @@ private static bool IsLowerAlphaDigit(char c) return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z'); } -#if NET6_0_OR_GREATER +#if NET private static void WriteTraceParentIntoSpan(Span destination, ActivityContext context) { "00-".CopyTo(destination); diff --git a/src/OpenTelemetry.Api/Context/Propagation/TraceStateUtilsNew.cs b/src/OpenTelemetry.Api/Context/Propagation/TraceStateUtils.cs similarity index 94% rename from src/OpenTelemetry.Api/Context/Propagation/TraceStateUtilsNew.cs rename to src/OpenTelemetry.Api/Context/Propagation/TraceStateUtils.cs index 717e1a94584..d81291b5052 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/TraceStateUtilsNew.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/TraceStateUtils.cs @@ -10,7 +10,7 @@ namespace OpenTelemetry.Context.Propagation; /// /// Extension methods to extract TraceState from string. /// -internal static class TraceStateUtilsNew +internal static class TraceStateUtils { private const int KeyMaxSize = 256; private const int ValueMaxSize = 256; @@ -56,7 +56,7 @@ internal static bool AppendTraceState(string traceStateString, List(keyStr, value.ToString())); + tracestate!.Add(new KeyValuePair(keyStr, value.ToString())); } else { @@ -82,7 +82,7 @@ internal static bool AppendTraceState(string traceStateString, List> traceState) + internal static string GetString(IEnumerable>? traceState) { +#pragma warning disable CA1851 // Possible multiple enumerations of 'IEnumerable' collection if (traceState == null || !traceState.Any()) { return string.Empty; @@ -125,6 +126,7 @@ internal static string GetString(IEnumerable> trace .Append(','); } } +#pragma warning restore CA1851 // Possible multiple enumerations of 'IEnumerable' collection return sb.Remove(sb.Length - 1, 1).ToString(); } @@ -153,7 +155,7 @@ private static bool TryParseKeyValue(ReadOnlySpan pair, out ReadOnlySpan - public object Value + public object? Value { get => this.Get(); - set => this.Set((T)value); + set + { + if (typeof(T).IsValueType && value is null) + { + this.Set(default!); + } + else + { + this.Set((T)value!); + } + } } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override T Get() + public override T? Get() { - if (!(CallContext.LogicalGetData(this.Name) is BitArray wrapper)) + if (CallContext.LogicalGetData(this.Name) is not BitArray wrapper) { return default; } var value = WrapperField.GetValue(wrapper); - return value is T t - ? t - : default; + + if (typeof(T).IsValueType && value is null) + { + return default; + } + + return (T)value; } /// diff --git a/src/OpenTelemetry.Api/Context/RuntimeContext.cs b/src/OpenTelemetry.Api/Context/RuntimeContext.cs index 93c96299d2f..c984147bf49 100644 --- a/src/OpenTelemetry.Api/Context/RuntimeContext.cs +++ b/src/OpenTelemetry.Api/Context/RuntimeContext.cs @@ -56,7 +56,8 @@ public static Type ContextSlotType public static RuntimeContextSlot RegisterSlot(string slotName) { Guard.ThrowIfNullOrEmpty(slotName); - RuntimeContextSlot slot = null; + + RuntimeContextSlot? slot = null; lock (Slots) { @@ -80,6 +81,10 @@ public static RuntimeContextSlot RegisterSlot(string slotName) slot = new RemotingRuntimeContextSlot(slotName); } #endif + else + { + throw new NotSupportedException($"ContextSlotType '{ContextSlotType}' is not supported"); + } Slots[slotName] = slot; return slot; @@ -95,9 +100,10 @@ public static RuntimeContextSlot RegisterSlot(string slotName) public static RuntimeContextSlot GetSlot(string slotName) { Guard.ThrowIfNullOrEmpty(slotName); + var slot = GuardNotFound(slotName); - var contextSlot = Guard.ThrowIfNotOfType>(slot); - return contextSlot; + + return Guard.ThrowIfNotOfType>(slot); } /* @@ -143,7 +149,7 @@ public static void SetValue(string slotName, T value) /// The type of the value. /// The value retrieved from the context slot. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static T GetValue(string slotName) + public static T? GetValue(string slotName) { return GetSlot(slotName).Get(); } @@ -153,12 +159,13 @@ public static T GetValue(string slotName) /// /// The name of the context slot. /// The value to be set. - public static void SetValue(string slotName, object value) + public static void SetValue(string slotName, object? value) { Guard.ThrowIfNullOrEmpty(slotName); + var slot = GuardNotFound(slotName); - var runtimeContextSlotValueAccessor = Guard.ThrowIfNotOfType(slot); - runtimeContextSlotValueAccessor.Value = value; + + Guard.ThrowIfNotOfType(slot).Value = value; } /// @@ -166,12 +173,13 @@ public static void SetValue(string slotName, object value) /// /// The name of the context slot. /// The value retrieved from the context slot. - public static object GetValue(string slotName) + public static object? GetValue(string slotName) { Guard.ThrowIfNullOrEmpty(slotName); + var slot = GuardNotFound(slotName); - var runtimeContextSlotValueAccessor = Guard.ThrowIfNotOfType(slot); - return runtimeContextSlotValueAccessor.Value; + + return Guard.ThrowIfNotOfType(slot).Value; } // For testing purpose diff --git a/src/OpenTelemetry.Api/Context/RuntimeContextSlot.cs b/src/OpenTelemetry.Api/Context/RuntimeContextSlot.cs index 918e4b79424..7502070ffc0 100644 --- a/src/OpenTelemetry.Api/Context/RuntimeContextSlot.cs +++ b/src/OpenTelemetry.Api/Context/RuntimeContextSlot.cs @@ -1,6 +1,8 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using OpenTelemetry.Internal; + namespace OpenTelemetry.Context; /// @@ -15,6 +17,8 @@ public abstract class RuntimeContextSlot : IDisposable /// The name of the context slot. protected RuntimeContextSlot(string name) { + Guard.ThrowIfNullOrEmpty(name); + this.Name = name; } @@ -27,13 +31,17 @@ protected RuntimeContextSlot(string name) /// Get the value from the context slot. /// /// The value retrieved from the context slot. - public abstract T Get(); +#pragma warning disable CA1716 // Identifiers should not match keywords + public abstract T? Get(); +#pragma warning restore CA1716 // Identifiers should not match keywords /// /// Set the value to the context slot. /// /// The value to be set. +#pragma warning disable CA1716 // Identifiers should not match keywords public abstract void Set(T value); +#pragma warning restore CA1716 // Identifiers should not match keywords /// public void Dispose() diff --git a/src/OpenTelemetry.Api/Context/ThreadLocalRuntimeContextSlot.cs b/src/OpenTelemetry.Api/Context/ThreadLocalRuntimeContextSlot.cs index c7724a842b4..360f9df564b 100644 --- a/src/OpenTelemetry.Api/Context/ThreadLocalRuntimeContextSlot.cs +++ b/src/OpenTelemetry.Api/Context/ThreadLocalRuntimeContextSlot.cs @@ -25,15 +25,25 @@ public ThreadLocalRuntimeContextSlot(string name) } /// - public object Value + public object? Value { get => this.slot.Value; - set => this.slot.Value = (T)value; + set + { + if (typeof(T).IsValueType && value is null) + { + this.slot.Value = default!; + } + else + { + this.slot.Value = (T)value!; + } + } } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override T Get() + public override T? Get() { return this.slot.Value; } diff --git a/src/OpenTelemetry.Api/Internal/OpenTelemetryApiEventSource.cs b/src/OpenTelemetry.Api/Internal/OpenTelemetryApiEventSource.cs index ea802a6ed15..c2e9cf9dfb2 100644 --- a/src/OpenTelemetry.Api/Internal/OpenTelemetryApiEventSource.cs +++ b/src/OpenTelemetry.Api/Internal/OpenTelemetryApiEventSource.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics.Tracing; namespace OpenTelemetry.Internal; @@ -14,7 +12,7 @@ namespace OpenTelemetry.Internal; [EventSource(Name = "OpenTelemetry-Api")] internal sealed class OpenTelemetryApiEventSource : EventSource { - public static OpenTelemetryApiEventSource Log = new(); + public static readonly OpenTelemetryApiEventSource Log = new(); [NonEvent] public void ActivityContextExtractException(string format, Exception ex) diff --git a/src/OpenTelemetry.Api/Logs/IDeferredLoggerProviderBuilder.cs b/src/OpenTelemetry.Api/Logs/IDeferredLoggerProviderBuilder.cs index 7421a9df268..11397157cea 100644 --- a/src/OpenTelemetry.Api/Logs/IDeferredLoggerProviderBuilder.cs +++ b/src/OpenTelemetry.Api/Logs/IDeferredLoggerProviderBuilder.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - namespace OpenTelemetry.Logs; /// diff --git a/src/OpenTelemetry.Api/Logs/LogRecordAttributeList.cs b/src/OpenTelemetry.Api/Logs/LogRecordAttributeList.cs index e4367d41c6e..96621ee27ed 100644 --- a/src/OpenTelemetry.Api/Logs/LogRecordAttributeList.cs +++ b/src/OpenTelemetry.Api/Logs/LogRecordAttributeList.cs @@ -1,12 +1,10 @@ // Copyright The OpenTelemetry Authors // 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 +#if EXPOSE_EXPERIMENTAL_FEATURES using System.Diagnostics.CodeAnalysis; #endif using OpenTelemetry.Internal; @@ -19,9 +17,7 @@ namespace OpenTelemetry.Logs; /// Stores attributes to be added to a log message. /// /// -#if NET8_0_OR_GREATER [Experimental(DiagnosticDefinitions.LogsBridgeExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] -#endif public #else /// @@ -29,12 +25,14 @@ namespace OpenTelemetry.Logs; /// internal #endif +#pragma warning disable CA1815 // Override equals and operator equals on value types struct LogRecordAttributeList : IReadOnlyList> +#pragma warning restore CA1815 // Override equals and operator equals on value types { internal const int OverflowMaxCount = 8; internal const int OverflowAdditionalCapacity = 16; internal List>? OverflowAttributes; - private static readonly IReadOnlyList> Empty = Array.Empty>(); + private static readonly IReadOnlyList> Empty = []; private KeyValuePair attribute1; private KeyValuePair attribute2; private KeyValuePair attribute3; @@ -115,7 +113,9 @@ readonly get /// Attribute name. /// Attribute value. [EditorBrowsable(EditorBrowsableState.Never)] +#pragma warning disable CA1044 // Properties should not be write only public object? this[string key] +#pragma warning restore CA1044 // Properties should not be write only { // Note: This only exists to enable collection initializer syntax // like { ["key"] = value }. @@ -132,7 +132,7 @@ public static LogRecordAttributeList CreateFromEnumerable(IEnumerable> Export(ref List>? attributeStorage) { - int count = this.count; - if (count <= 0) + int readonlyCount = this.count; + if (readonlyCount <= 0) { return Empty; } @@ -225,58 +225,55 @@ public readonly Enumerator GetEnumerator() return overflowAttributes; } - Debug.Assert(count <= 8, "Invalid size detected."); + Debug.Assert(readonlyCount <= 8, "Invalid size detected."); attributeStorage ??= new List>(OverflowAdditionalCapacity); // TODO: Perf test this, adjust as needed. - if (count > 0) + attributeStorage.Add(this.attribute1); + if (readonlyCount == 1) { - attributeStorage.Add(this.attribute1); - if (count == 1) - { - return attributeStorage; - } - - attributeStorage.Add(this.attribute2); - if (count == 2) - { - return attributeStorage; - } + return attributeStorage; + } - attributeStorage.Add(this.attribute3); - if (count == 3) - { - return attributeStorage; - } + attributeStorage.Add(this.attribute2); + if (readonlyCount == 2) + { + return attributeStorage; + } - attributeStorage.Add(this.attribute4); - if (count == 4) - { - return attributeStorage; - } + attributeStorage.Add(this.attribute3); + if (readonlyCount == 3) + { + return attributeStorage; + } - attributeStorage.Add(this.attribute5); - if (count == 5) - { - return attributeStorage; - } + attributeStorage.Add(this.attribute4); + if (readonlyCount == 4) + { + return attributeStorage; + } - attributeStorage.Add(this.attribute6); - if (count == 6) - { - return attributeStorage; - } + attributeStorage.Add(this.attribute5); + if (readonlyCount == 5) + { + return attributeStorage; + } - attributeStorage.Add(this.attribute7); - if (count == 7) - { - return attributeStorage; - } + attributeStorage.Add(this.attribute6); + if (readonlyCount == 6) + { + return attributeStorage; + } - attributeStorage.Add(this.attribute8); + attributeStorage.Add(this.attribute7); + if (readonlyCount == 7) + { + return attributeStorage; } + attributeStorage.Add(this.attribute8); + return attributeStorage; } diff --git a/src/OpenTelemetry.Api/Logs/LogRecordData.cs b/src/OpenTelemetry.Api/Logs/LogRecordData.cs index cb3c49292af..374b95d736a 100644 --- a/src/OpenTelemetry.Api/Logs/LogRecordData.cs +++ b/src/OpenTelemetry.Api/Logs/LogRecordData.cs @@ -1,10 +1,8 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; -#if NET8_0_OR_GREATER && EXPOSE_EXPERIMENTAL_FEATURES +#if EXPOSE_EXPERIMENTAL_FEATURES using System.Diagnostics.CodeAnalysis; using OpenTelemetry.Internal; #endif @@ -16,9 +14,7 @@ namespace OpenTelemetry.Logs; /// Stores details about a log message. /// /// -#if NET8_0_OR_GREATER [Experimental(DiagnosticDefinitions.LogsBridgeExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] -#endif public #else /// @@ -26,7 +22,9 @@ namespace OpenTelemetry.Logs; /// internal #endif +#pragma warning disable CA1815 // Override equals and operator equals on value types struct LogRecordData +#pragma warning restore CA1815 // Override equals and operator equals on value types { internal DateTime TimestampBacking = DateTime.UtcNow; @@ -125,6 +123,11 @@ public DateTime Timestamp /// public string? Body { get; set; } = null; + /// + /// Gets or sets the name of the event associated with the log. + /// + public string? EventName { get; set; } = null; + internal static void SetActivityContext(ref LogRecordData data, Activity? activity) { if (activity != null) diff --git a/src/OpenTelemetry.Api/Logs/LogRecordSeverity.cs b/src/OpenTelemetry.Api/Logs/LogRecordSeverity.cs index 9f48e71e854..73d98881d0a 100644 --- a/src/OpenTelemetry.Api/Logs/LogRecordSeverity.cs +++ b/src/OpenTelemetry.Api/Logs/LogRecordSeverity.cs @@ -1,9 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - -#if NET8_0_OR_GREATER && EXPOSE_EXPERIMENTAL_FEATURES +#if EXPOSE_EXPERIMENTAL_FEATURES using System.Diagnostics.CodeAnalysis; using OpenTelemetry.Internal; #endif @@ -15,9 +13,7 @@ namespace OpenTelemetry.Logs; /// Describes the severity level of a log record. /// /// -#if NET8_0_OR_GREATER [Experimental(DiagnosticDefinitions.LogsBridgeExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] -#endif public #else /// diff --git a/src/OpenTelemetry.Api/Logs/LogRecordSeverityExtensions.cs b/src/OpenTelemetry.Api/Logs/LogRecordSeverityExtensions.cs index f171edbc9ca..14e6e846cdd 100644 --- a/src/OpenTelemetry.Api/Logs/LogRecordSeverityExtensions.cs +++ b/src/OpenTelemetry.Api/Logs/LogRecordSeverityExtensions.cs @@ -1,9 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - -#if NET8_0_OR_GREATER && EXPOSE_EXPERIMENTAL_FEATURES +#if EXPOSE_EXPERIMENTAL_FEATURES using System.Diagnostics.CodeAnalysis; using OpenTelemetry.Internal; #endif @@ -15,9 +13,7 @@ namespace OpenTelemetry.Logs; /// Contains extension methods for the enum. /// /// -#if NET8_0_OR_GREATER [Experimental(DiagnosticDefinitions.LogsBridgeExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] -#endif public #else /// @@ -59,8 +55,8 @@ static class LogRecordSeverityExtensions internal const string Fatal3ShortName = FatalShortName + "3"; internal const string Fatal4ShortName = FatalShortName + "4"; - private static readonly string[] LogRecordSeverityShortNames = new string[] - { + private static readonly string[] LogRecordSeverityShortNames = + [ UnspecifiedShortName, TraceShortName, @@ -91,8 +87,8 @@ static class LogRecordSeverityExtensions FatalShortName, Fatal2ShortName, Fatal3ShortName, - Fatal4ShortName, - }; + Fatal4ShortName + ]; /// /// Returns the OpenTelemetry Specification short name for the /// 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 /// diff --git a/src/OpenTelemetry.Api/Logs/LoggerProvider.cs b/src/OpenTelemetry.Api/Logs/LoggerProvider.cs index 01c341c0a5b..c01a9ffd923 100644 --- a/src/OpenTelemetry.Api/Logs/LoggerProvider.cs +++ b/src/OpenTelemetry.Api/Logs/LoggerProvider.cs @@ -1,12 +1,10 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NET || EXPOSE_EXPERIMENTAL_FEATURES using System.Diagnostics.CodeAnalysis; #endif -#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +#if EXPOSE_EXPERIMENTAL_FEATURES using OpenTelemetry.Internal; #endif @@ -32,9 +30,7 @@ protected LoggerProvider() /// /// /// instance. -#if NET8_0_OR_GREATER [Experimental(DiagnosticDefinitions.LogsBridgeExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] -#endif public #else internal @@ -49,9 +45,7 @@ Logger GetLogger() /// /// Optional name identifying the instrumentation library. /// instance. -#if NET8_0_OR_GREATER [Experimental(DiagnosticDefinitions.LogsBridgeExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] -#endif public #else internal @@ -67,9 +61,7 @@ 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 #else internal @@ -94,16 +86,14 @@ 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 #else internal #endif virtual bool TryCreateLogger( string? name, -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NET [NotNullWhen(true)] #endif out Logger? logger) diff --git a/src/OpenTelemetry.Api/Logs/LoggerProviderBuilder.cs b/src/OpenTelemetry.Api/Logs/LoggerProviderBuilder.cs index b0aec6bfca7..2bc69d1d0c6 100644 --- a/src/OpenTelemetry.Api/Logs/LoggerProviderBuilder.cs +++ b/src/OpenTelemetry.Api/Logs/LoggerProviderBuilder.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - namespace OpenTelemetry.Logs; /// diff --git a/src/OpenTelemetry.Api/Logs/NoopLogger.cs b/src/OpenTelemetry.Api/Logs/NoopLogger.cs index f33ec668aca..2c2b61c4185 100644 --- a/src/OpenTelemetry.Api/Logs/NoopLogger.cs +++ b/src/OpenTelemetry.Api/Logs/NoopLogger.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - namespace OpenTelemetry.Logs; internal sealed class NoopLogger : Logger diff --git a/src/OpenTelemetry.Api/Metrics/IDeferredMeterProviderBuilder.cs b/src/OpenTelemetry.Api/Metrics/IDeferredMeterProviderBuilder.cs index c283219c431..5f0dc38d34e 100644 --- a/src/OpenTelemetry.Api/Metrics/IDeferredMeterProviderBuilder.cs +++ b/src/OpenTelemetry.Api/Metrics/IDeferredMeterProviderBuilder.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - namespace OpenTelemetry.Metrics; /// diff --git a/src/OpenTelemetry.Api/Metrics/MeterProvider.cs b/src/OpenTelemetry.Api/Metrics/MeterProvider.cs index a16fd88df93..64fcb5e8e52 100644 --- a/src/OpenTelemetry.Api/Metrics/MeterProvider.cs +++ b/src/OpenTelemetry.Api/Metrics/MeterProvider.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - namespace OpenTelemetry.Metrics; /// diff --git a/src/OpenTelemetry.Api/Metrics/MeterProviderBuilder.cs b/src/OpenTelemetry.Api/Metrics/MeterProviderBuilder.cs index 95075dbe703..4fb6bb007dd 100644 --- a/src/OpenTelemetry.Api/Metrics/MeterProviderBuilder.cs +++ b/src/OpenTelemetry.Api/Metrics/MeterProviderBuilder.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - namespace OpenTelemetry.Metrics; /// diff --git a/src/OpenTelemetry.Api/OpenTelemetry.Api.csproj b/src/OpenTelemetry.Api/OpenTelemetry.Api.csproj index 0a781a94173..a3a2895ee3e 100644 --- a/src/OpenTelemetry.Api/OpenTelemetry.Api.csproj +++ b/src/OpenTelemetry.Api/OpenTelemetry.Api.csproj @@ -4,9 +4,6 @@ OpenTelemetry .NET API OpenTelemetry core- - - - disable @@ -19,7 +16,25 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/OpenTelemetry.Api/README.md b/src/OpenTelemetry.Api/README.md index 5fd769f4461..fcaea931073 100644 --- a/src/OpenTelemetry.Api/README.md +++ b/src/OpenTelemetry.Api/README.md @@ -249,7 +249,7 @@ following sections describes more features. ### Activity creation options Basic usage example above showed how `StartActivity` method can be used to start -an `Activity`. The started activity will automatically becomes the `Current` +an `Activity`. The started activity automatically becomes the `Current` activity. It is important to note that the `StartActivity` returns `null`, if no listeners are interested in the activity to be created. This happens when the final application does not enable OpenTelemetry, or when OpenTelemetry samplers @@ -332,11 +332,13 @@ chose not to sample this activity. 4. Activity Links - Apart from the parent-child relation, activities can be linked using - `ActivityLinks` which represent the OpenTelemetry - [Links](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#links-between-spans). - The linked activities must be provided during the creation time, as shown - below. + In addition to parent-child relationships, activities can also be linked + using `ActivityLinks`, which represent + [Links](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#links-between-spans) + in OpenTelemetry. Providing activity links during creation is recommended, as + this allows samplers to consider them when deciding whether to sample an + activity. However, starting with `System.Diagnostics.DiagnosticSource` 9.0.0, + links can also be added after an activity is created. ```csharp var activityLinks = new List(); @@ -359,12 +361,19 @@ chose not to sample this activity. ActivityKind.Server, default(ActivityContext), initialTags, - activityLinks); + activityLinks); // links provided at creation time. + + // One may add links after activity is created too. + var linkedContext3 = new ActivityContext( + ActivityTraceId.CreateFromString("01260a70a81e1fa3ad5a8acfeaa0f711"), + ActivitySpanId.CreateFromString("34739aa9e2239da1"), + ActivityTraceFlags.None); + activity?.AddLink(linkedContext3); ``` - Note that `Activity` above is created with `default(ActivityContext)` - parent, which makes it child of implicit `Activity.Current` or orphan if - there is no `Current`. + Note that `Activity` above is created with `default(ActivityContext)` + parent, which makes it child of implicit `Activity.Current` or orphan if + there is no `Current`. ### Adding Events @@ -539,4 +548,8 @@ seeing these internal logs. ## References -* [OpenTelemetry Project](https://opentelemetry.io/) +* [OpenTelemetry Baggage API specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/baggage/api.md) +* [OpenTelemetry Logs Bridge API specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/bridge-api.md) +* [OpenTelemetry Metrics API specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md) +* [OpenTelemetry Propagators API specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/api-propagators.md) +* [OpenTelemetry Tracing API specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md) diff --git a/src/OpenTelemetry.Api/Trace/ActivityExtensions.cs b/src/OpenTelemetry.Api/Trace/ActivityExtensions.cs index d8ac769b9c9..b22339267ba 100644 --- a/src/OpenTelemetry.Api/Trace/ActivityExtensions.cs +++ b/src/OpenTelemetry.Api/Trace/ActivityExtensions.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; using System.Runtime.CompilerServices; using OpenTelemetry.Internal; @@ -21,17 +19,34 @@ public static class ActivityExtensions { /// /// 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. /// + /// + /// Note: This method is obsolete. Call the + /// method instead. For more details see: . + /// /// Activity instance. /// Activity execution status. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetStatus(this Activity activity, Status status) + [Obsolete("Call Activity.SetStatus instead this method will be removed in a future version.")] + public static void SetStatus(this Activity? activity, Status status) { if (activity != null) { + switch (status.StatusCode) + { + case StatusCode.Ok: + activity.SetStatus(ActivityStatusCode.Ok); + break; + case StatusCode.Unset: + activity.SetStatus(ActivityStatusCode.Unset); + break; + case StatusCode.Error: + activity.SetStatus(ActivityStatusCode.Error, status.Description); + break; + } + activity.SetTag(SpanAttributeConstants.StatusCodeKey, StatusHelper.GetTagValueForStatusCode(status.StatusCode)); activity.SetTag(SpanAttributeConstants.StatusDescriptionKey, status.Description); } @@ -39,21 +54,37 @@ public static void SetStatus(this Activity activity, Status status) /// /// 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. /// + /// + /// Note: This method is obsolete. Use the and + /// properties instead. For more + /// details see: . + /// /// Activity instance. /// Activity execution status. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Status GetStatus(this Activity activity) + [Obsolete("Use Activity.Status and Activity.StatusDescription instead this method will be removed in a future version.")] + public static Status GetStatus(this Activity? activity) { - if (activity == null - || !activity.TryGetStatus(out var statusCode, out var statusDescription)) + if (activity != null) { - return Status.Unset; + switch (activity.Status) + { + case ActivityStatusCode.Ok: + return Status.Ok; + case ActivityStatusCode.Error: + return new Status(StatusCode.Error, activity.StatusDescription); + } + + if (activity.TryGetStatus(out var statusCode, out var statusDescription)) + { + return new Status(statusCode, statusDescription); + } } - return new Status(statusCode, statusDescription); + return Status.Unset; } /// @@ -61,11 +92,14 @@ public static Status GetStatus(this Activity activity) /// /// Activity instance. /// Exception to be recorded. - /// The exception is recorded as per specification. + /// + /// Note: This method is obsolete. Please use instead. + /// The exception is recorded as per specification. /// "exception.stacktrace" is represented using the value of Exception.ToString. /// + [Obsolete("Call Activity.AddException instead this method will be removed in a future version.")] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void RecordException(this Activity activity, Exception? ex) + public static void RecordException(this Activity? activity, Exception? ex) => RecordException(activity, ex, default); /// @@ -74,33 +108,20 @@ public static void RecordException(this Activity activity, Exception? ex) /// Activity instance. /// Exception to be recorded. /// Additional tags to record on the event. - /// The exception is recorded as per specification. + /// + /// Note: This method is obsolete. Please use instead. + /// The exception is recorded as per specification. /// "exception.stacktrace" is represented using the value of Exception.ToString. /// + [Obsolete("Call Activity.AddException instead this method will be removed in a future version.")] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void RecordException(this Activity activity, Exception? ex, in TagList tags) + public static void RecordException(this Activity? activity, Exception? ex, in TagList tags) { if (ex == null || activity == null) { return; } - var tagsCollection = new ActivityTagsCollection - { - { 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; - } - - activity.AddEvent(new ActivityEvent(SemanticConventions.AttributeExceptionEventName, default, tagsCollection)); + activity.AddException(ex, in tags); } } diff --git a/src/OpenTelemetry.Api/Trace/IDeferredTracerProviderBuilder.cs b/src/OpenTelemetry.Api/Trace/IDeferredTracerProviderBuilder.cs index 8be46ae4432..7b0d3f71232 100644 --- a/src/OpenTelemetry.Api/Trace/IDeferredTracerProviderBuilder.cs +++ b/src/OpenTelemetry.Api/Trace/IDeferredTracerProviderBuilder.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - namespace OpenTelemetry.Trace; /// diff --git a/src/OpenTelemetry.Api/Trace/Link.cs b/src/OpenTelemetry.Api/Trace/Link.cs index 45af8791625..1972a624c25 100644 --- a/src/OpenTelemetry.Api/Trace/Link.cs +++ b/src/OpenTelemetry.Api/Trace/Link.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; namespace OpenTelemetry.Trace; diff --git a/src/OpenTelemetry.Api/Trace/SpanAttributes.cs b/src/OpenTelemetry.Api/Trace/SpanAttributes.cs index 84850ae048d..5975830c98c 100644 --- a/src/OpenTelemetry.Api/Trace/SpanAttributes.cs +++ b/src/OpenTelemetry.Api/Trace/SpanAttributes.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; using OpenTelemetry.Internal; diff --git a/src/OpenTelemetry.Api/Trace/SpanContext.cs b/src/OpenTelemetry.Api/Trace/SpanContext.cs index b94f50cc5f6..9ebab851122 100644 --- a/src/OpenTelemetry.Api/Trace/SpanContext.cs +++ b/src/OpenTelemetry.Api/Trace/SpanContext.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; using OpenTelemetry.Context.Propagation; @@ -36,7 +34,7 @@ public SpanContext( bool isRemote = false, IEnumerable>? traceState = null) { - this.ActivityContext = new ActivityContext(traceId, spanId, traceFlags, TraceStateUtilsNew.GetString(traceState), isRemote); + this.ActivityContext = new ActivityContext(traceId, spanId, traceFlags, TraceStateUtils.GetString(traceState), isRemote); } /// @@ -85,13 +83,14 @@ public IEnumerable> TraceState { get { - if (string.IsNullOrEmpty(this.ActivityContext.TraceState)) + var traceState = this.ActivityContext.TraceState; + if (string.IsNullOrEmpty(traceState)) { - return Enumerable.Empty>(); + return []; } var traceStateResult = new List>(); - TraceStateUtilsNew.AppendTraceState(this.ActivityContext.TraceState, traceStateResult); + TraceStateUtils.AppendTraceState(traceState!, traceStateResult); return traceStateResult; } } @@ -100,7 +99,9 @@ public IEnumerable> TraceState /// Converts a into an . /// /// source. +#pragma warning disable CA2225 // Operator overloads have named alternates public static implicit operator ActivityContext(SpanContext spanContext) +#pragma warning restore CA2225 // Operator overloads have named alternates => spanContext.ActivityContext; /// diff --git a/src/OpenTelemetry.Api/Trace/SpanKind.cs b/src/OpenTelemetry.Api/Trace/SpanKind.cs index f3237a6bd4a..65a6df7af9c 100644 --- a/src/OpenTelemetry.Api/Trace/SpanKind.cs +++ b/src/OpenTelemetry.Api/Trace/SpanKind.cs @@ -1,14 +1,14 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - namespace OpenTelemetry.Trace; /// /// Span kind. /// +#pragma warning disable CA1008 // Enums should have zero value public enum SpanKind +#pragma warning restore CA1008 // Enums should have zero value { /// /// Span kind was not specified. diff --git a/src/OpenTelemetry.Api/Trace/Status.cs b/src/OpenTelemetry.Api/Trace/Status.cs index b7d0eb35c32..679d663f4ef 100644 --- a/src/OpenTelemetry.Api/Trace/Status.cs +++ b/src/OpenTelemetry.Api/Trace/Status.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - namespace OpenTelemetry.Trace; /// @@ -88,7 +86,11 @@ public override int GetHashCode() unchecked { hash = (31 * hash) + this.StatusCode.GetHashCode(); +#if NET + hash = (31 * hash) + (this.Description?.GetHashCode(StringComparison.Ordinal) ?? 0); +#else hash = (31 * hash) + (this.Description?.GetHashCode() ?? 0); +#endif } return hash; diff --git a/src/OpenTelemetry.Api/Trace/StatusCode.cs b/src/OpenTelemetry.Api/Trace/StatusCode.cs index 9332d708d06..28d691c10e6 100644 --- a/src/OpenTelemetry.Api/Trace/StatusCode.cs +++ b/src/OpenTelemetry.Api/Trace/StatusCode.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - namespace OpenTelemetry.Trace; /// diff --git a/src/OpenTelemetry.Api/Trace/TelemetrySpan.cs b/src/OpenTelemetry.Api/Trace/TelemetrySpan.cs index f70598d078b..0b5ce40141f 100644 --- a/src/OpenTelemetry.Api/Trace/TelemetrySpan.cs +++ b/src/OpenTelemetry.Api/Trace/TelemetrySpan.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; using System.Runtime.CompilerServices; using OpenTelemetry.Internal; @@ -47,7 +45,9 @@ public ActivitySpanId ParentSpanId /// Status to be set. public void SetStatus(Status value) { - this.Activity?.SetStatus(value); +#pragma warning disable CS0618 // Type or member is obsolete + this.Activity.SetStatus(value); +#pragma warning restore CS0618 // Type or member is obsolete } /// @@ -226,6 +226,31 @@ public TelemetrySpan AddEvent(string name, DateTimeOffset timestamp, SpanAttribu return this; } + /// + /// Adds a link to another span. + /// + /// Span context to be linked. + /// The instance for chaining. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TelemetrySpan AddLink(SpanContext spanContext) + { + this.AddLinkInternal(spanContext.ActivityContext); + return this; + } + + /// + /// Adds a link to another span. + /// + /// Span context to be linked. + /// Attributes for the link. + /// The instance for chaining. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TelemetrySpan AddLink(SpanContext spanContext, SpanAttributes? attributes) + { + this.AddLinkInternal(spanContext.ActivityContext, attributes?.Attributes); + return this; + } + /// /// End the span. /// @@ -342,4 +367,12 @@ private void AddEventInternal(string name, DateTimeOffset timestamp = default, A this.Activity!.AddEvent(new ActivityEvent(name, timestamp, tags)); } } + + private void AddLinkInternal(ActivityContext context, ActivityTagsCollection? tags = null) + { + if (this.IsRecording) + { + this.Activity!.AddLink(new ActivityLink(context, tags)); + } + } } diff --git a/src/OpenTelemetry.Api/Trace/Tracer.cs b/src/OpenTelemetry.Api/Trace/Tracer.cs index 44bf17e1f5c..d0fb906ec18 100644 --- a/src/OpenTelemetry.Api/Trace/Tracer.cs +++ b/src/OpenTelemetry.Api/Trace/Tracer.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -54,7 +52,7 @@ public static TelemetrySpan CurrentSpan /// /// The span to be made current. /// The supplied span for call chaining. -#if NET6_0_OR_GREATER +#if NET [return: NotNullIfNotNull(nameof(span))] #endif [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/OpenTelemetry.Api/Trace/TracerProvider.cs b/src/OpenTelemetry.Api/Trace/TracerProvider.cs index 1dc0a5f2936..3246b2e4e7b 100644 --- a/src/OpenTelemetry.Api/Trace/TracerProvider.cs +++ b/src/OpenTelemetry.Api/Trace/TracerProvider.cs @@ -1,10 +1,8 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Collections.Concurrent; -#if NET6_0_OR_GREATER +#if NET using System.Diagnostics.CodeAnalysis; #endif @@ -35,12 +33,29 @@ protected TracerProvider() /// Name identifying the instrumentation library. /// Version of the instrumentation library. /// Tracer instance. + // 1.11.1 BACKCOMPAT OVERLOAD -- DO NOT TOUCH + public Tracer GetTracer( +#if NET + [AllowNull] +#endif + string name, + string? version) => + this.GetTracer(name, version, null); + + /// + /// Gets a tracer with given name, version and tags. + /// + /// Name identifying the instrumentation library. + /// Version of the instrumentation library. + /// Tags associated with the tracer. + /// Tracer instance. public Tracer GetTracer( -#if NET6_0_OR_GREATER +#if NET [AllowNull] #endif string name, - string? version = null) + string? version = null, + IEnumerable>? tags = null) { var tracers = this.Tracers; if (tracers == null) @@ -49,7 +64,7 @@ public Tracer GetTracer( return new(activitySource: null); } - var key = new TracerKey(name, version); + var key = new TracerKey(name, version, tags); if (!tracers.TryGetValue(key, out var tracer)) { @@ -62,12 +77,10 @@ public Tracer GetTracer( return new(activitySource: null); } - tracer = new(new(key.Name, key.Version)); -#if DEBUG + tracer = new(new(key.Name, key.Version, key.Tags)); bool result = tracers.TryAdd(key, tracer); +#if DEBUG System.Diagnostics.Debug.Assert(result, "Write into tracers cache failed"); -#else - tracers.TryAdd(key, tracer); #endif } } @@ -80,7 +93,7 @@ protected override void Dispose(bool disposing) { if (disposing) { - var tracers = Interlocked.CompareExchange(ref this.Tracers, null, this.Tracers); + var tracers = Interlocked.Exchange(ref this.Tracers, null); if (tracers != null) { lock (tracers) @@ -105,11 +118,154 @@ internal readonly record struct TracerKey { public readonly string Name; public readonly string? Version; + public readonly KeyValuePair[]? Tags; - public TracerKey(string? name, string? version) + public TracerKey(string? name, string? version, IEnumerable>? tags) { this.Name = name ?? string.Empty; this.Version = version; + this.Tags = GetOrderedTags(tags); + } + + public bool Equals(TracerKey other) + { + if (!string.Equals(this.Name, other.Name, StringComparison.Ordinal) || + !string.Equals(this.Version, other.Version, StringComparison.Ordinal)) + { + return false; + } + + return AreTagsEqual(this.Tags, other.Tags); + } + + public override int GetHashCode() + { + unchecked + { + var hash = 17; +#if NET + hash = (hash * 31) + (this.Name?.GetHashCode(StringComparison.Ordinal) ?? 0); + hash = (hash * 31) + (this.Version?.GetHashCode(StringComparison.Ordinal) ?? 0); +#else + hash = (hash * 31) + (this.Name?.GetHashCode() ?? 0); + hash = (hash * 31) + (this.Version?.GetHashCode() ?? 0); +#endif + + hash = (hash * 31) + GetTagsHashCode(this.Tags); + return hash; + } + } + + private static bool AreTagsEqual( + KeyValuePair[]? tags1, + KeyValuePair[]? tags2) + { + if (tags1 == null && tags2 == null) + { + return true; + } + + if (tags1 == null || tags2 == null || tags1.Length != tags2.Length) + { + return false; + } + + for (int i = 0; i < tags1.Length; i++) + { + var kvp1 = tags1[i]; + var kvp2 = tags2[i]; + + if (!string.Equals(kvp1.Key, kvp2.Key, StringComparison.Ordinal)) + { + return false; + } + + // Compare values + if (kvp1.Value is null) + { + if (kvp2.Value is not null) + { + return false; + } + } + else + { + if (!kvp1.Value.Equals(kvp2.Value)) + { + return false; + } + } + } + + return true; + } + + private static int GetTagsHashCode( + IEnumerable>? tags) + { + if (tags is null) + { + return 0; + } + + var hash = 0; + unchecked + { + foreach (var kvp in tags) + { +#if NET + hash = (hash * 31) + kvp.Key.GetHashCode(StringComparison.Ordinal); +#else + hash = (hash * 31) + kvp.Key.GetHashCode(); +#endif + if (kvp.Value != null) + { + hash = (hash * 31) + kvp.Value.GetHashCode()!; + } + } + } + + return hash; + } + + private static KeyValuePair[]? GetOrderedTags( + IEnumerable>? tags) + { + if (tags is null) + { + return null; + } + + var orderedTagList = new List>(tags); + orderedTagList.Sort((left, right) => + { + // First compare by key + int keyComparison = string.Compare(left.Key, right.Key, StringComparison.Ordinal); + if (keyComparison != 0) + { + return keyComparison; + } + + // If keys are equal, compare by value + if (left.Value == null && right.Value == null) + { + return 0; + } + + if (left.Value == null) + { + return -1; + } + + if (right.Value == null) + { + return 1; + } + + // Both values are non-null, compare as strings + return string.Compare(left.Value.ToString(), right.Value.ToString(), StringComparison.Ordinal); + }); + return [.. orderedTagList]; } } } diff --git a/src/OpenTelemetry.Api/Trace/TracerProviderBuilder.cs b/src/OpenTelemetry.Api/Trace/TracerProviderBuilder.cs index 0a8c3dae3da..ac9b7a2dbe2 100644 --- a/src/OpenTelemetry.Api/Trace/TracerProviderBuilder.cs +++ b/src/OpenTelemetry.Api/Trace/TracerProviderBuilder.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; namespace OpenTelemetry.Trace; diff --git a/src/OpenTelemetry.Exporter.Console/.publicApi/Stable/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.Console/.publicApi/Stable/PublicAPI.Shipped.txt index 0e45009bac8..670e4b8a1c6 100644 --- a/src/OpenTelemetry.Exporter.Console/.publicApi/Stable/PublicAPI.Shipped.txt +++ b/src/OpenTelemetry.Exporter.Console/.publicApi/Stable/PublicAPI.Shipped.txt @@ -1,8 +1,9 @@ +#nullable enable OpenTelemetry.Exporter.ConsoleActivityExporter -OpenTelemetry.Exporter.ConsoleActivityExporter.ConsoleActivityExporter(OpenTelemetry.Exporter.ConsoleExporterOptions options) -> void +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.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 @@ -11,26 +12,26 @@ 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.ConsoleLogRecordExporter.ConsoleLogRecordExporter(OpenTelemetry.Exporter.ConsoleExporterOptions! options) -> void OpenTelemetry.Exporter.ConsoleMetricExporter -OpenTelemetry.Exporter.ConsoleMetricExporter.ConsoleMetricExporter(OpenTelemetry.Exporter.ConsoleExporterOptions options) -> void +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.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.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 -static OpenTelemetry.Logs.ConsoleExporterLoggingExtensions.AddConsoleExporter(this OpenTelemetry.Logs.LoggerProviderBuilder loggerProviderBuilder) -> OpenTelemetry.Logs.LoggerProviderBuilder -static OpenTelemetry.Logs.ConsoleExporterLoggingExtensions.AddConsoleExporter(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions loggerOptions, System.Action configure) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions -static OpenTelemetry.Logs.ConsoleExporterLoggingExtensions.AddConsoleExporter(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions loggerOptions) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions -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.Metrics.ConsoleExporterMetricsExtensions.AddConsoleExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder -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 -static OpenTelemetry.Trace.ConsoleExporterHelperExtensions.AddConsoleExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder +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.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! +static OpenTelemetry.Logs.ConsoleExporterLoggingExtensions.AddConsoleExporter(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder) -> OpenTelemetry.Logs.LoggerProviderBuilder! +static OpenTelemetry.Logs.ConsoleExporterLoggingExtensions.AddConsoleExporter(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions! loggerOptions, System.Action? configure) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! +static OpenTelemetry.Logs.ConsoleExporterLoggingExtensions.AddConsoleExporter(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions! loggerOptions) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! +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.Metrics.ConsoleExporterMetricsExtensions.AddConsoleExporter(this OpenTelemetry.Metrics.MeterProviderBuilder! builder) -> OpenTelemetry.Metrics.MeterProviderBuilder! +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! +static OpenTelemetry.Trace.ConsoleExporterHelperExtensions.AddConsoleExporter(this OpenTelemetry.Trace.TracerProviderBuilder! builder) -> OpenTelemetry.Trace.TracerProviderBuilder! diff --git a/src/OpenTelemetry.Exporter.Console/CHANGELOG.md b/src/OpenTelemetry.Exporter.Console/CHANGELOG.md index 9e94df266e8..8e2ee12d281 100644 --- a/src/OpenTelemetry.Exporter.Console/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Console/CHANGELOG.md @@ -1,7 +1,59 @@ # Changelog +This file contains individual changes for the OpenTelemetry.Exporter.Console +package. For highlights and announcements covering all components see: [Release +Notes](../../RELEASENOTES.md). + ## Unreleased +## 1.13.0 + +Released 2025-Oct-01 + +## 1.12.0 + +Released 2025-Apr-29 + +## 1.11.2 + +Released 2025-Mar-04 + +## 1.11.1 + +Released 2025-Jan-22 + +## 1.11.0 + +Released 2025-Jan-15 + +## 1.11.0-rc.1 + +Released 2024-Dec-11 + +## 1.10.0 + +Released 2024-Nov-12 + +## 1.10.0-rc.1 + +Released 2024-Nov-01 + +* Added direct reference to `System.Text.Json` for the `net8.0` target with + minimum version of `8.0.5` in response to + [CVE-2024-30105](https://github.com/advisories/GHSA-hh2w-p6rv-4g7w) & + [CVE-2024-43485](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-43485). + ([#5874](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5874), + [#5891](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5891)) + +* Added support for Instrumentation Scope Attributes (i.e + [ActivitySource.Tags](https://learn.microsoft.com/dotnet/api/system.diagnostics.activitysource.tags)) + when writing traces to the console. + ([#5935](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5935)) + +## 1.10.0-beta.1 + +Released 2024-Sep-30 + ## 1.9.0 Released 2024-Jun-14 @@ -11,7 +63,7 @@ Released 2024-Jun-14 Released 2024-Jun-07 * The experimental APIs previously covered by `OTEL1000` - (`LoggerProviderBuilder.AddConsoleExporter` extension) will now be part of the + (`LoggerProviderBuilder.AddConsoleExporter` extension) are now part of the public API and supported in stable builds. ([#5648](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5648)) @@ -46,9 +98,9 @@ Released 2023-Dec-08 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 +* Added support for Instrumentation Scope Attributes (i.e + [Meter.Tags](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.meter.tags)), + when writing metrics to the console, fixing issue [#4563](https://github.com/open-telemetry/opentelemetry-dotnet/issues/4563). ([#5089](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5089)) @@ -106,7 +158,8 @@ Released 2023-May-25 ([#4507](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4507)) * 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). + `4.7.2` in response to + [CVE-2021-26701](https://github.com/dotnet/runtime/issues/49377). ([#4390](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4390)) * Updated `LogRecord` console output: `Body` is now shown (if set), diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleActivityExporter.cs b/src/OpenTelemetry.Exporter.Console/ConsoleActivityExporter.cs index 9197cd1287c..c1b707b3e4d 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleActivityExporter.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleActivityExporter.cs @@ -31,12 +31,6 @@ public override ExportResult Export(in Batch batch) this.WriteLine($"Activity.ParentSpanId: {activity.ParentSpanId}"); } - this.WriteLine($"Activity.ActivitySourceName: {activity.Source.Name}"); - if (!string.IsNullOrEmpty(activity.Source.Version)) - { - this.WriteLine($"Activity.ActivitySourceVersion: {activity.Source.Version}"); - } - this.WriteLine($"Activity.DisplayName: {activity.DisplayName}"); this.WriteLine($"Activity.Kind: {activity.Kind}"); this.WriteLine($"Activity.StartTime: {activity.StartTimeUtc:yyyy-MM-ddTHH:mm:ss.fffffffZ}"); @@ -117,13 +111,32 @@ public override ExportResult Export(in Batch batch) } } + this.WriteLine("Instrumentation scope (ActivitySource):"); + this.WriteLine($" Name: {activity.Source.Name}"); + if (!string.IsNullOrEmpty(activity.Source.Version)) + { + this.WriteLine($" Version: {activity.Source.Version}"); + } + + if (activity.Source.Tags?.Any() == true) + { + this.WriteLine(" Tags:"); + foreach (var activitySourceTag in activity.Source.Tags) + { + if (this.TagWriter.TryTransformTag(activitySourceTag, out var result)) + { + this.WriteLine($" {result.Key}: {result.Value}"); + } + } + } + var resource = this.ParentProvider.GetResource(); if (resource != Resource.Empty) { this.WriteLine("Resource associated with Activity:"); foreach (var resourceAttribute in resource.Attributes) { - if (this.TagWriter.TryTransformTag(resourceAttribute, out var result)) + if (this.TagWriter.TryTransformTag(resourceAttribute.Key, resourceAttribute.Value, out var result)) { this.WriteLine($" {result.Key}: {result.Value}"); } diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.Console/ConsoleExporterHelperExtensions.cs index b79aa0d9ed9..68a811669e3 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleExporterHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleExporterHelperExtensions.cs @@ -31,13 +31,13 @@ public static TracerProviderBuilder AddConsoleExporter(this TracerProviderBuilde /// Adds Console exporter to the TracerProvider. /// /// builder to use. - /// Name which is used when retrieving options. - /// Callback action for configuring . + /// Optional name which is used when retrieving options. + /// Optional callback action for configuring . /// The instance of to chain the calls. public static TracerProviderBuilder AddConsoleExporter( this TracerProviderBuilder builder, - string name, - Action configure) + string? name, + Action? configure) { Guard.ThrowIfNull(builder); diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleExporterLoggingExtensions.cs b/src/OpenTelemetry.Exporter.Console/ConsoleExporterLoggingExtensions.cs index 80c767343b7..d8d04913496 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleExporterLoggingExtensions.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleExporterLoggingExtensions.cs @@ -23,16 +23,18 @@ public static OpenTelemetryLoggerOptions AddConsoleExporter(this OpenTelemetryLo /// Adds Console exporter with OpenTelemetryLoggerOptions. /// /// options to use. - /// Callback action for configuring . + /// Optional 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) + public static OpenTelemetryLoggerOptions AddConsoleExporter(this OpenTelemetryLoggerOptions loggerOptions, Action? configure) { Guard.ThrowIfNull(loggerOptions); var options = new ConsoleExporterOptions(); configure?.Invoke(options); +#pragma warning disable CA2000 // Dispose objects before losing scope return loggerOptions.AddProcessor(new SimpleLogRecordExportProcessor(new ConsoleLogRecordExporter(options))); +#pragma warning restore CA2000 // Dispose objects before losing scope } /// @@ -59,13 +61,13 @@ public static LoggerProviderBuilder AddConsoleExporter( /// Adds Console exporter with LoggerProviderBuilder. /// /// . - /// Name which is used when retrieving options. - /// Callback action for configuring . + /// Optional name which is used when retrieving options. + /// Optional callback action for configuring . /// The supplied instance of to chain the calls. public static LoggerProviderBuilder AddConsoleExporter( this LoggerProviderBuilder loggerProviderBuilder, - string name, - Action configure) + string? name, + Action? configure) { Guard.ThrowIfNull(loggerProviderBuilder); diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleExporterMetricsExtensions.cs b/src/OpenTelemetry.Exporter.Console/ConsoleExporterMetricsExtensions.cs index 103593efddd..3d9bf9560f5 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleExporterMetricsExtensions.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleExporterMetricsExtensions.cs @@ -37,13 +37,13 @@ public static MeterProviderBuilder AddConsoleExporter(this MeterProviderBuilder /// Adds to the . /// /// builder to use. - /// Name which is used when retrieving options. - /// Callback action for configuring . + /// Optional name which is used when retrieving options. + /// Optional callback action for configuring . /// The instance of to chain the calls. public static MeterProviderBuilder AddConsoleExporter( this MeterProviderBuilder builder, - string name, - Action configureExporter) + string? name, + Action? configureExporter) { Guard.ThrowIfNull(builder); @@ -72,7 +72,7 @@ public static MeterProviderBuilder AddConsoleExporter( /// The instance of to chain the calls. public static MeterProviderBuilder AddConsoleExporter( this MeterProviderBuilder builder, - Action configureExporterAndMetricReader) + Action? configureExporterAndMetricReader) => AddConsoleExporter(builder, name: null, configureExporterAndMetricReader); /// @@ -86,8 +86,8 @@ public static MeterProviderBuilder AddConsoleExporter( /// The instance of to chain the calls. public static MeterProviderBuilder AddConsoleExporter( this MeterProviderBuilder builder, - string name, - Action configureExporterAndMetricReader) + string? name, + Action? configureExporterAndMetricReader) { Guard.ThrowIfNull(builder); @@ -104,11 +104,13 @@ public static MeterProviderBuilder AddConsoleExporter( }); } - private static MetricReader BuildConsoleExporterMetricReader( + private static PeriodicExportingMetricReader BuildConsoleExporterMetricReader( ConsoleExporterOptions exporterOptions, MetricReaderOptions metricReaderOptions) { +#pragma warning disable CA2000 // Dispose objects before losing scope var metricExporter = new ConsoleMetricExporter(exporterOptions); +#pragma warning restore CA2000 // Dispose objects before losing scope return PeriodicExportingMetricReaderHelper.CreatePeriodicExportingMetricReader( metricExporter, diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleLogRecordExporter.cs b/src/OpenTelemetry.Exporter.Console/ConsoleLogRecordExporter.cs index 3ad90910954..84fd449532b 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleLogRecordExporter.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleLogRecordExporter.cs @@ -10,9 +10,9 @@ namespace OpenTelemetry.Exporter; public class ConsoleLogRecordExporter : ConsoleExporter { private const int RightPaddingLength = 35; - private readonly object syncObject = new(); + private readonly Lock syncObject = new(); private bool disposed; - private string disposedStackTrace; + private string? disposedStackTrace; private bool isDisposeMessageSent; public ConsoleLogRecordExporter(ConsoleExporterOptions options) @@ -38,8 +38,8 @@ public override ExportResult Export(in Batch batch) 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.WriteLine($"{Environment.NewLine}Dispose was called on the following stack trace:"); + this.WriteLine(this.disposedStackTrace!); } return ExportResult.Failure; @@ -89,8 +89,8 @@ public override ExportResult Export(in Batch batch) // 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) + var valueToTransform = logRecord.Attributes[i].Key.Equals("{OriginalFormat}", StringComparison.Ordinal) + ? new KeyValuePair("OriginalFormat (a.k.a Body)", logRecord.Attributes[i].Value) : logRecord.Attributes[i]; if (this.TagWriter.TryTransformTag(valueToTransform, out var result)) @@ -125,7 +125,7 @@ void ProcessScope(LogRecordScope scope, ConsoleLogRecordExporter exporter) exporter.WriteLine("LogRecord.ScopeValues (Key:Value):"); } - foreach (KeyValuePair scopeItem in scope) + foreach (KeyValuePair scopeItem in scope) { if (this.TagWriter.TryTransformTag(scopeItem, out var result)) { @@ -137,10 +137,10 @@ void ProcessScope(LogRecordScope scope, ConsoleLogRecordExporter exporter) var resource = this.ParentProvider.GetResource(); if (resource != Resource.Empty) { - this.WriteLine("\nResource associated with LogRecord:"); + this.WriteLine($"{Environment.NewLine}Resource associated with LogRecord:"); foreach (var resourceAttribute in resource.Attributes) { - if (this.TagWriter.TryTransformTag(resourceAttribute, out var result)) + if (this.TagWriter.TryTransformTag(resourceAttribute.Key, resourceAttribute.Value, out var result)) { this.WriteLine($"{result.Key}: {result.Value}"); } diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs b/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs index 4bd9772b217..c47823b3ad6 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs @@ -10,8 +10,6 @@ namespace OpenTelemetry.Exporter; public class ConsoleMetricExporter : ConsoleExporter { - private Resource resource; - public ConsoleMetricExporter(ConsoleExporterOptions options) : base(options) { @@ -19,54 +17,67 @@ public ConsoleMetricExporter(ConsoleExporterOptions options) public override ExportResult Export(in Batch batch) { - if (this.resource == null) + // Print Resource information once at the beginning of the batch + var resource = this.ParentProvider.GetResource(); + if (resource != Resource.Empty) { - this.resource = this.ParentProvider.GetResource(); - if (this.resource != Resource.Empty) + this.WriteLine("Resource associated with Metrics:"); + foreach (var resourceAttribute in resource.Attributes) { - this.WriteLine("Resource associated with Metric:"); - foreach (var resourceAttribute in this.resource.Attributes) + if (this.TagWriter.TryTransformTag(resourceAttribute.Key, resourceAttribute.Value, out var result)) { - if (this.TagWriter.TryTransformTag(resourceAttribute, out var result)) - { - this.WriteLine($" {result.Key}: {result.Value}"); - } + this.WriteLine($"\t{result.Key}: {result.Value}"); } } } foreach (var metric in batch) { - var msg = new StringBuilder($"\n"); + var msg = new StringBuilder(Environment.NewLine); +#if NET + msg.Append(CultureInfo.InvariantCulture, $"Metric Name: {metric.Name}"); +#else msg.Append($"Metric Name: {metric.Name}"); - if (metric.Description != string.Empty) +#endif + if (!string.IsNullOrEmpty(metric.Description)) { - msg.Append(", "); - msg.Append(metric.Description); +#if NET + msg.Append(CultureInfo.InvariantCulture, $", Description: {metric.Description}"); +#else + msg.Append($", Description: {metric.Description}"); +#endif } - if (metric.Unit != string.Empty) + if (!string.IsNullOrEmpty(metric.Unit)) { +#if NET + msg.Append(CultureInfo.InvariantCulture, $", Unit: {metric.Unit}"); +#else msg.Append($", Unit: {metric.Unit}"); +#endif } - if (!string.IsNullOrEmpty(metric.MeterName)) - { - msg.Append($", Meter: {metric.MeterName}"); - - if (!string.IsNullOrEmpty(metric.MeterVersion)) - { - msg.Append($"/{metric.MeterVersion}"); - } - } +#if NET + msg.Append(CultureInfo.InvariantCulture, $", Metric Type: {metric.MetricType}"); +#else + msg.Append($", Metric Type: {metric.MetricType}"); +#endif this.WriteLine(msg.ToString()); - if (metric.MeterTags != null) + // Print Instrumentation scope (Meter) information once per metric + this.WriteLine("Instrumentation scope (Meter):"); + this.WriteLine($"\tName: {metric.MeterName}"); + if (!string.IsNullOrEmpty(metric.MeterVersion)) + { + this.WriteLine($"\tVersion: {metric.MeterVersion}"); + } + + if (metric.MeterTags?.Any() == true) { + this.WriteLine("\tTags:"); foreach (var meterTag in metric.MeterTags) { - this.WriteLine("\tMeter Tags:"); if (this.TagWriter.TryTransformTag(meterTag, out var result)) { this.WriteLine($"\t\t{result.Key}: {result.Value}"); @@ -82,7 +93,11 @@ public override ExportResult Export(in Batch batch) { if (this.TagWriter.TryTransformTag(tag, out var result)) { +#if NET + tagsBuilder.Append(CultureInfo.InvariantCulture, $"{result.Key}: {result.Value}"); +#else tagsBuilder.Append($"{result.Key}: {result.Value}"); +#endif tagsBuilder.Append(' '); } } @@ -96,10 +111,18 @@ public override ExportResult Export(in Batch batch) var bucketsBuilder = new StringBuilder(); var sum = metricPoint.GetHistogramSum(); var count = metricPoint.GetHistogramCount(); +#if NET + bucketsBuilder.Append(CultureInfo.InvariantCulture, $"Sum: {sum} Count: {count} "); +#else bucketsBuilder.Append($"Sum: {sum} Count: {count} "); +#endif if (metricPoint.TryGetHistogramMinMaxValues(out double min, out double max)) { +#if NET + bucketsBuilder.Append(CultureInfo.InvariantCulture, $"Min: {min} Max: {max} "); +#else bucketsBuilder.Append($"Min: {min} Max: {max} "); +#endif } bucketsBuilder.AppendLine(); @@ -150,7 +173,11 @@ public override ExportResult Export(in Batch batch) if (exponentialHistogramData.ZeroCount != 0) { +#if NET + bucketsBuilder.AppendLine(CultureInfo.InvariantCulture, $"Zero Bucket:{exponentialHistogramData.ZeroCount}"); +#else bucketsBuilder.AppendLine($"Zero Bucket:{exponentialHistogramData.ZeroCount}"); +#endif } var offset = exponentialHistogramData.PositiveBuckets.Offset; @@ -158,7 +185,11 @@ public override ExportResult Export(in Batch batch) { var lowerBound = Base2ExponentialBucketHistogramHelper.CalculateLowerBoundary(offset, scale).ToString(CultureInfo.InvariantCulture); var upperBound = Base2ExponentialBucketHistogramHelper.CalculateLowerBoundary(++offset, scale).ToString(CultureInfo.InvariantCulture); +#if NET + bucketsBuilder.AppendLine(CultureInfo.InvariantCulture, $"({lowerBound}, {upperBound}]:{bucketCount}"); +#else bucketsBuilder.AppendLine($"({lowerBound}, {upperBound}]:{bucketCount}"); +#endif } } @@ -166,25 +197,11 @@ public override ExportResult Export(in Batch batch) } else if (metricType.IsDouble()) { - if (metricType.IsSum()) - { - valueDisplay = metricPoint.GetSumDouble().ToString(CultureInfo.InvariantCulture); - } - else - { - valueDisplay = metricPoint.GetGaugeLastValueDouble().ToString(CultureInfo.InvariantCulture); - } + valueDisplay = metricType.IsSum() ? metricPoint.GetSumDouble().ToString(CultureInfo.InvariantCulture) : metricPoint.GetGaugeLastValueDouble().ToString(CultureInfo.InvariantCulture); } else if (metricType.IsLong()) { - if (metricType.IsSum()) - { - valueDisplay = metricPoint.GetSumLong().ToString(CultureInfo.InvariantCulture); - } - else - { - valueDisplay = metricPoint.GetGaugeLastValueLong().ToString(CultureInfo.InvariantCulture); - } + valueDisplay = metricType.IsSum() ? metricPoint.GetSumLong().ToString(CultureInfo.InvariantCulture) : metricPoint.GetGaugeLastValueLong().ToString(CultureInfo.InvariantCulture); } var exemplarString = new StringBuilder(); @@ -220,11 +237,15 @@ public override ExportResult Export(in Batch batch) { if (!appendedTagString) { - exemplarString.Append(" Filtered Tags : "); + exemplarString.Append(" Filtered Tags: "); appendedTagString = true; } +#if NET + exemplarString.Append(CultureInfo.InvariantCulture, $"{result.Key}: {result.Value}"); +#else exemplarString.Append($"{result.Key}: {result.Value}"); +#endif exemplarString.Append(' '); } } @@ -240,20 +261,23 @@ public override ExportResult Export(in Batch batch) msg.Append(metricPoint.EndTime.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ", CultureInfo.InvariantCulture)); msg.Append("] "); msg.Append(tags); - if (tags != string.Empty) + if (string.IsNullOrEmpty(tags)) { msg.Append(' '); } - msg.Append(metric.MetricType); msg.AppendLine(); +#if NET + msg.Append(CultureInfo.InvariantCulture, $"Value: {valueDisplay}"); +#else msg.Append($"Value: {valueDisplay}"); +#endif if (exemplarString.Length > 0) { msg.AppendLine(); msg.AppendLine("Exemplars"); - msg.Append(exemplarString.ToString()); + msg.Append(exemplarString); } this.WriteLine(msg.ToString()); diff --git a/src/OpenTelemetry.Exporter.Console/Implementation/ConsoleTagWriter.cs b/src/OpenTelemetry.Exporter.Console/Implementation/ConsoleTagWriter.cs index 2f36df00915..4950bd100e5 100644 --- a/src/OpenTelemetry.Exporter.Console/Implementation/ConsoleTagWriter.cs +++ b/src/OpenTelemetry.Exporter.Console/Implementation/ConsoleTagWriter.cs @@ -1,9 +1,8 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; +using System.Globalization; using System.Text; using OpenTelemetry.Internal; @@ -21,9 +20,14 @@ public ConsoleTagWriter(Action onUnsupportedTagDropped) } public bool TryTransformTag(KeyValuePair tag, out KeyValuePair result) + { + return this.TryTransformTag(tag.Key, tag.Value, out result); + } + + public bool TryTransformTag(string key, object? value, out KeyValuePair result) { ConsoleTag consoleTag = default; - if (this.TryWriteTag(ref consoleTag, tag)) + if (this.TryWriteTag(ref consoleTag, key, value)) { result = new KeyValuePair(consoleTag.Key!, consoleTag.Value!); return true; @@ -36,13 +40,13 @@ public bool TryTransformTag(KeyValuePair tag, out KeyValuePair< protected override void WriteIntegralTag(ref ConsoleTag consoleTag, string key, long value) { consoleTag.Key = key; - consoleTag.Value = value.ToString(); + consoleTag.Value = value.ToString(CultureInfo.InvariantCulture); } protected override void WriteFloatingPointTag(ref ConsoleTag consoleTag, string key, double value) { consoleTag.Key = key; - consoleTag.Value = value.ToString(); + consoleTag.Value = value.ToString(CultureInfo.InvariantCulture); } protected override void WriteBooleanTag(ref ConsoleTag consoleTag, string key, bool value) @@ -70,6 +74,13 @@ protected override void OnUnsupportedTagDropped( this.onUnsupportedTagDropped(tagKey, tagValueTypeFullName); } + protected override bool TryWriteEmptyTag(ref ConsoleTag consoleTag, string key, object? value) + { + consoleTag.Key = key; + consoleTag.Value = null; + return true; + } + internal struct ConsoleTag { public string? Key; diff --git a/src/OpenTelemetry.Exporter.Console/OpenTelemetry.Exporter.Console.csproj b/src/OpenTelemetry.Exporter.Console/OpenTelemetry.Exporter.Console.csproj index 3e6d3b58284..2fc8d03e54b 100644 --- a/src/OpenTelemetry.Exporter.Console/OpenTelemetry.Exporter.Console.csproj +++ b/src/OpenTelemetry.Exporter.Console/OpenTelemetry.Exporter.Console.csproj @@ -5,20 +5,13 @@ Console exporter for OpenTelemetry .NET $(PackageTags);Console;distributed-tracing core- - - - disable + true - $(NoWarn),1591 + $(NoWarn),CS1591 - - - - - diff --git a/src/OpenTelemetry.Exporter.Console/README.md b/src/OpenTelemetry.Exporter.Console/README.md index 07a51a29e52..ef7985bf7ef 100644 --- a/src/OpenTelemetry.Exporter.Console/README.md +++ b/src/OpenTelemetry.Exporter.Console/README.md @@ -7,8 +7,11 @@ The console exporter prints data to the Console window. ConsoleExporter supports exporting logs, metrics and traces. > [!WARNING] -> This component is intended to be used while learning how telemetry data is - created and exported. It is not recommended for any production environment. +> This exporter is intended for debugging and learning purposes. It is not + recommended for production use. The output format is not standardized and can + change at any time. + If a standardized format for exporting telemetry to stdout is desired, upvote on + [this feature request](https://github.com/open-telemetry/opentelemetry-dotnet/issues/5920). ## Installation diff --git a/src/OpenTelemetry.Exporter.InMemory/.publicApi/Stable/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.InMemory/.publicApi/Stable/PublicAPI.Shipped.txt index a79ca4f2cf5..be3367f467f 100644 --- a/src/OpenTelemetry.Exporter.InMemory/.publicApi/Stable/PublicAPI.Shipped.txt +++ b/src/OpenTelemetry.Exporter.InMemory/.publicApi/Stable/PublicAPI.Shipped.txt @@ -1,25 +1,26 @@ +#nullable enable OpenTelemetry.Exporter.InMemoryExporter -OpenTelemetry.Exporter.InMemoryExporter.InMemoryExporter(System.Collections.Generic.ICollection exportedItems) -> void +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.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.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.LoggerProviderBuilder loggerProviderBuilder, System.Collections.Generic.ICollection exportedItems) -> OpenTelemetry.Logs.LoggerProviderBuilder -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, 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.Trace.InMemoryExporterHelperExtensions.AddInMemoryExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Collections.Generic.ICollection exportedItems) -> OpenTelemetry.Trace.TracerProviderBuilder +override OpenTelemetry.Exporter.InMemoryExporter.Export(in OpenTelemetry.Batch batch) -> OpenTelemetry.ExportResult +static OpenTelemetry.Logs.InMemoryExporterLoggingExtensions.AddInMemoryExporter(this OpenTelemetry.Logs.LoggerProviderBuilder! loggerProviderBuilder, System.Collections.Generic.ICollection! exportedItems) -> OpenTelemetry.Logs.LoggerProviderBuilder! +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, 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.Trace.InMemoryExporterHelperExtensions.AddInMemoryExporter(this OpenTelemetry.Trace.TracerProviderBuilder! builder, System.Collections.Generic.ICollection! exportedItems) -> OpenTelemetry.Trace.TracerProviderBuilder! diff --git a/src/OpenTelemetry.Exporter.InMemory/AssemblyInfo.cs b/src/OpenTelemetry.Exporter.InMemory/AssemblyInfo.cs deleted file mode 100644 index f7775c1b9b8..00000000000 --- a/src/OpenTelemetry.Exporter.InMemory/AssemblyInfo.cs +++ /dev/null @@ -1,20 +0,0 @@ -// 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 fc2a00afb3c..4521a32b130 100644 --- a/src/OpenTelemetry.Exporter.InMemory/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.InMemory/CHANGELOG.md @@ -1,7 +1,47 @@ # Changelog +This file contains individual changes for the OpenTelemetry.Exporter.InMemory +package. For highlights and announcements covering all components see: [Release +Notes](../../RELEASENOTES.md). + ## Unreleased +## 1.13.0 + +Released 2025-Oct-01 + +## 1.12.0 + +Released 2025-Apr-29 + +## 1.11.2 + +Released 2025-Mar-04 + +## 1.11.1 + +Released 2025-Jan-22 + +## 1.11.0 + +Released 2025-Jan-15 + +## 1.11.0-rc.1 + +Released 2024-Dec-11 + +## 1.10.0 + +Released 2024-Nov-12 + +## 1.10.0-rc.1 + +Released 2024-Nov-01 + +## 1.10.0-beta.1 + +Released 2024-Sep-30 + ## 1.9.0 Released 2024-Jun-14 @@ -11,8 +51,8 @@ Released 2024-Jun-14 Released 2024-Jun-07 * The experimental APIs previously covered by `OTEL1000` - (`LoggerProviderBuilder.AddInMemoryExporter` extension) will now be part of - the public API and supported in stable builds. + (`LoggerProviderBuilder.AddInMemoryExporter` extension) are now part of the + public API and supported in stable builds. ([#5648](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5648)) ## 1.9.0-alpha.1 diff --git a/src/OpenTelemetry.Exporter.InMemory/InMemoryExporter.cs b/src/OpenTelemetry.Exporter.InMemory/InMemoryExporter.cs index dedcf65b37c..8533108ce86 100644 --- a/src/OpenTelemetry.Exporter.InMemory/InMemoryExporter.cs +++ b/src/OpenTelemetry.Exporter.InMemory/InMemoryExporter.cs @@ -6,10 +6,10 @@ namespace OpenTelemetry.Exporter; public class InMemoryExporter : BaseExporter where T : class { - private readonly ICollection exportedItems; + private readonly ICollection? exportedItems; private readonly ExportFunc onExport; private bool disposed; - private string disposedStackTrace; + private string? disposedStackTrace; public InMemoryExporter(ICollection exportedItems) { diff --git a/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterHelperExtensions.cs index 3940a846bc3..e8af683c695 100644 --- a/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterHelperExtensions.cs @@ -20,6 +20,8 @@ public static TracerProviderBuilder AddInMemoryExporter(this TracerProviderBuild Guard.ThrowIfNull(builder); Guard.ThrowIfNull(exportedItems); +#pragma warning disable CA2000 // Dispose objects before losing scope return builder.AddProcessor(new SimpleActivityExportProcessor(new InMemoryExporter(exportedItems))); +#pragma warning restore CA2000 // Dispose objects before losing scope } } diff --git a/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterLoggingExtensions.cs b/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterLoggingExtensions.cs index 19b2079921e..2b665f7a293 100644 --- a/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterLoggingExtensions.cs +++ b/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterLoggingExtensions.cs @@ -6,7 +6,9 @@ namespace OpenTelemetry.Logs; +#pragma warning disable CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup public static class InMemoryExporterLoggingExtensions +#pragma warning restore CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup { /// /// Adds InMemory exporter to the OpenTelemetryLoggerOptions. @@ -22,10 +24,12 @@ public static OpenTelemetryLoggerOptions AddInMemoryExporter( Guard.ThrowIfNull(loggerOptions); Guard.ThrowIfNull(exportedItems); +#pragma warning disable CA2000 // Dispose objects before losing scope var logExporter = BuildExporter(exportedItems); return loggerOptions.AddProcessor( new SimpleLogRecordExportProcessor(logExporter)); +#pragma warning restore CA2000 // Dispose objects before losing scope } /// @@ -41,10 +45,12 @@ public static LoggerProviderBuilder AddInMemoryExporter( Guard.ThrowIfNull(loggerProviderBuilder); Guard.ThrowIfNull(exportedItems); +#pragma warning disable CA2000 // Dispose objects before losing scope var logExporter = BuildExporter(exportedItems); return loggerProviderBuilder.AddProcessor( new SimpleLogRecordExportProcessor(logExporter)); +#pragma warning restore CA2000 // Dispose objects before losing scope } private static InMemoryExporter BuildExporter(ICollection exportedItems) diff --git a/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterMetricsExtensions.cs b/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterMetricsExtensions.cs index e645b67b455..a9cc18eb029 100644 --- a/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterMetricsExtensions.cs +++ b/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterMetricsExtensions.cs @@ -51,15 +51,15 @@ public static MeterProviderBuilder AddInMemoryExporter( /// Be aware that may continue to be updated after export. /// /// builder to use. - /// Name which is used when retrieving options. + /// Optional name which is used when retrieving options. /// Collection which will be populated with the exported . - /// Callback action for configuring . + /// Optional callback action for configuring . /// The instance of to chain the calls. public static MeterProviderBuilder AddInMemoryExporter( this MeterProviderBuilder builder, - string name, + string? name, ICollection exportedItems, - Action configureMetricReader) + Action? configureMetricReader) { Guard.ThrowIfNull(builder); Guard.ThrowIfNull(exportedItems); @@ -119,15 +119,15 @@ public static MeterProviderBuilder AddInMemoryExporter( /// 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. + /// Optional name which is used when retrieving options. /// Collection which will be populated with the exported represented as . - /// Callback action for configuring . + /// Optional callback action for configuring . /// The instance of to chain the calls. public static MeterProviderBuilder AddInMemoryExporter( this MeterProviderBuilder builder, - string name, + string? name, ICollection exportedItems, - Action configureMetricReader) + Action? configureMetricReader) { Guard.ThrowIfNull(builder); Guard.ThrowIfNull(exportedItems); @@ -147,11 +147,13 @@ public static MeterProviderBuilder AddInMemoryExporter( }); } - private static MetricReader BuildInMemoryExporterMetricReader( + private static PeriodicExportingMetricReader BuildInMemoryExporterMetricReader( ICollection exportedItems, MetricReaderOptions metricReaderOptions) { +#pragma warning disable CA2000 // Dispose objects before losing scope var metricExporter = new InMemoryExporter(exportedItems); +#pragma warning restore CA2000 // Dispose objects before losing scope return PeriodicExportingMetricReaderHelper.CreatePeriodicExportingMetricReader( metricExporter, @@ -160,12 +162,14 @@ private static MetricReader BuildInMemoryExporterMetricReader( DefaultExportTimeoutMilliseconds); } - private static MetricReader BuildInMemoryExporterMetricReader( + private static PeriodicExportingMetricReader BuildInMemoryExporterMetricReader( ICollection exportedItems, MetricReaderOptions metricReaderOptions) { +#pragma warning disable CA2000 // Dispose objects before losing scope var metricExporter = new InMemoryExporter( exportFunc: (in Batch metricBatch) => ExportMetricSnapshot(in metricBatch, exportedItems)); +#pragma warning restore CA2000 // Dispose objects before losing scope return PeriodicExportingMetricReaderHelper.CreatePeriodicExportingMetricReader( metricExporter, diff --git a/src/OpenTelemetry.Exporter.InMemory/MetricSnapshot.cs b/src/OpenTelemetry.Exporter.InMemory/MetricSnapshot.cs index 983cf7bb2b8..fb4bcc5c155 100644 --- a/src/OpenTelemetry.Exporter.InMemory/MetricSnapshot.cs +++ b/src/OpenTelemetry.Exporter.InMemory/MetricSnapshot.cs @@ -1,6 +1,8 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using OpenTelemetry.Internal; + namespace OpenTelemetry.Metrics; /// @@ -14,10 +16,11 @@ public class MetricSnapshot public MetricSnapshot(Metric metric) { + Guard.ThrowIfNull(metric); this.instrumentIdentity = metric.InstrumentIdentity; this.MetricType = metric.MetricType; - List metricPoints = new(); + List metricPoints = []; foreach (ref readonly var metricPoint in metric.GetMetricPoints()) { metricPoints.Add(metricPoint.Copy()); diff --git a/src/OpenTelemetry.Exporter.InMemory/OpenTelemetry.Exporter.InMemory.csproj b/src/OpenTelemetry.Exporter.InMemory/OpenTelemetry.Exporter.InMemory.csproj index f27ac138017..5d6f89a7391 100644 --- a/src/OpenTelemetry.Exporter.InMemory/OpenTelemetry.Exporter.InMemory.csproj +++ b/src/OpenTelemetry.Exporter.InMemory/OpenTelemetry.Exporter.InMemory.csproj @@ -1,17 +1,14 @@ - + $(TargetFrameworksForLibraries) In-memory exporter for OpenTelemetry .NET $(PackageTags) core- - - - disable - $(NoWarn),1591 + $(NoWarn),CS1591 @@ -22,4 +19,8 @@ + + + + diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Shipped.txt index 94c328771df..33dc4b523be 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Shipped.txt +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Shipped.txt @@ -1,8 +1,4 @@ #nullable enable -~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.BatchExportProcessorOptions.get -> OpenTelemetry.BatchExportProcessorOptions! OpenTelemetry.Exporter.OtlpExporterOptions.BatchExportProcessorOptions.set -> void @@ -25,13 +21,17 @@ OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf = 1 -> OpenTelemetry.Expo OpenTelemetry.Exporter.OtlpLogExporter OpenTelemetry.Exporter.OtlpLogExporter.OtlpLogExporter(OpenTelemetry.Exporter.OtlpExporterOptions! options) -> void 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.Logs.OtlpLogExporterHelperExtensions OpenTelemetry.Metrics.OtlpMetricExporterExtensions OpenTelemetry.OpenTelemetryBuilderOtlpExporterExtensions OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions override OpenTelemetry.Exporter.OtlpLogExporter.Export(in OpenTelemetry.Batch logRecordBatch) -> OpenTelemetry.ExportResult +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.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! diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/AssemblyInfo.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/AssemblyInfo.cs deleted file mode 100644 index 3f125304deb..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/AssemblyInfo.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Runtime.CompilerServices; - -#if SIGNED -[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] -[assembly: InternalsVisibleTo("Benchmarks, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] -[assembly: InternalsVisibleTo("MockOpenTelemetryCollector, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] -#else -[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests")] -[assembly: InternalsVisibleTo("Benchmarks")] -[assembly: InternalsVisibleTo("MockOpenTelemetryCollector")] -#endif diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/OpenTelemetryBuilderOtlpExporterExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/OpenTelemetryBuilderOtlpExporterExtensions.cs index 80164f0fa5c..baaceb07165 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/OpenTelemetryBuilderOtlpExporterExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/OpenTelemetryBuilderOtlpExporterExtensions.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using Microsoft.Extensions.Configuration; using OpenTelemetry.Exporter; using OpenTelemetry.Internal; @@ -38,7 +36,9 @@ public static class OpenTelemetryBuilderOtlpExporterExtensions /// Supplied for chaining calls. public static IOpenTelemetryBuilder UseOtlpExporter( this IOpenTelemetryBuilder builder) +#pragma warning disable CA1062 // Validate arguments of public methods => UseOtlpExporter(builder, name: null, configuration: null, configure: null); +#pragma warning restore CA1062 // Validate arguments of public methods /// /// @@ -58,7 +58,9 @@ public static IOpenTelemetryBuilder UseOtlpExporter( { Guard.ThrowIfNull(baseUrl); +#pragma warning disable CA1062 // Validate arguments of public methods return UseOtlpExporter(builder, name: null, configuration: null, configure: otlpBuilder => +#pragma warning restore CA1062 // Validate arguments of public methods { otlpBuilder.ConfigureDefaultExporterOptions(o => { @@ -90,8 +92,8 @@ internal static IOpenTelemetryBuilder UseOtlpExporter( /// to bind onto . /// Notes: /// - /// See [TODO:Add doc link] for details on the configuration - /// schema. + /// + /// for details on the configuration schema. /// The instance will be /// named "otlp" by default when calling this method. /// diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/OtlpExporterBuilder.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/OtlpExporterBuilder.cs index 4c3d37cf282..d5a04aa60f2 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/OtlpExporterBuilder.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/OtlpExporterBuilder.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -40,7 +38,7 @@ internal OtlpExporterBuilder( name ??= Options.DefaultName; - RegisterOtlpExporterServices(services!, name!); + RegisterOtlpExporterServices(services!, name); this.name = name; this.Services = services!; @@ -176,9 +174,9 @@ private static void RegisterOtlpExporterServices(IServiceCollection services, st services!.AddOtlpExporterTracingServices(); // Note: UseOtlpExporterRegistration is added to the service collection - // to detect repeated calls to "UseOtlpExporter" and to throw if - // "AddOtlpExporter" extensions are called - services!.AddSingleton(); + // for each invocation to detect repeated calls to "UseOtlpExporter" and + // to throw if "AddOtlpExporter" extensions are called + services!.AddSingleton(UseOtlpExporterRegistration.Instance); services!.RegisterOptionsFactory((sp, configuration, name) => new OtlpExporterBuilderOptions( configuration, diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/OtlpExporterBuilderOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/OtlpExporterBuilderOptions.cs index e3ba3541ffb..7d9786bb040 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/OtlpExporterBuilderOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/OtlpExporterBuilderOptions.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; using Microsoft.Extensions.Configuration; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/UseOtlpExporterRegistration.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/UseOtlpExporterRegistration.cs index ad0ad9fbc90..e2de10663fc 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/UseOtlpExporterRegistration.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Builder/UseOtlpExporterRegistration.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - namespace OpenTelemetry.Exporter; // Note: This class is added to the IServiceCollection when UseOtlpExporter is @@ -10,4 +8,15 @@ namespace OpenTelemetry.Exporter; // calls to signal-specific AddOtlpExporter can throw. internal sealed class UseOtlpExporterRegistration { + public static readonly UseOtlpExporterRegistration Instance = new(); + + private UseOtlpExporterRegistration() + { + // Note: Some dependency injection containers (ex: Unity, Grace) will + // automatically create services if they have a public constructor even + // if the service was never registered into the IServiceCollection. The + // behavior of UseOtlpExporterRegistration requires that it should only + // exist if registered. This private constructor is intended to prevent + // automatic instantiation. + } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md index e8866580e55..79d6d1f3b10 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md @@ -1,7 +1,140 @@ # Changelog +This file contains individual changes for the +OpenTelemetry.Exporter.OpenTelemetryProtocol package. For highlights and +announcements covering all components see: [Release +Notes](../../RELEASENOTES.md). + ## Unreleased +## 1.13.0 + +Released 2025-Oct-01 + +* Fixed an issue in .NET Framework where OTLP export of traces, logs, and + metrics using `OtlpExportProtocol.Grpc` did not correctly set the initial + write position, resulting in gRPC protocol errors. + ([#6280](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6280)) + +* If `EventName` is specified either through `ILogger` or the experimental + log bridge API, it is exported as `EventName` by default instead of + `logrecord.event.name` which was previously behind the + `OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES` feature flag. + Note that exporting `logrecord.event.id` is still behind that same feature + flag. ([#6306](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6306)) + +* gRPC calls to export traces, logs, and metrics using `OtlpExportProtocol.Grpc` + now set the `TE=trailers` HTTP request header to improve interoperability. + ([#6449](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6449)) + +* Improved performance exporting `byte[]` attributes as native binary format + instead of arrays. + ([#6534](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6534)) + +## 1.12.0 + +Released 2025-Apr-29 + +* **Breaking Change**: .NET Framework and .NET Standard builds now default to + exporting over OTLP/HTTP instead of OTLP/gRPC. **This change could result in a + failure to export telemetry unless appropriate measures are taken.** + Additionally, if you explicitly configure the exporter to use OTLP/gRPC it may + result in a `NotSupportedException` without further configuration. Please + carefully review issue + ([#6209](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6209)) + for additional information and workarounds. + ([#6229](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6229)) + +## 1.11.2 + +Released 2025-Mar-04 + +* Fixed a bug in .NET Framework gRPC export client where the default success + export response was incorrectly marked as false, now changed to true, ensuring + exports are correctly marked as successful. + ([#6099](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6099)) + +* Fixed an issues causing trace exports to fail when + `Activity.StatusDescription` exceeds 127 bytes. + ([#6119](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6119)) + +* Fixed incorrect log serialization of attributes with null values, causing + some backends to reject logs. + ([#6149](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6149)) + +## 1.11.1 + +Released 2025-Jan-22 + +* Fixed an issue where the OTLP gRPC exporter did not export logs, metrics, or + traces in .NET Framework projects. + ([#6083](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6083)) + +## 1.11.0 + +Released 2025-Jan-15 + +## 1.11.0-rc.1 + +Released 2024-Dec-11 + +* Removed the following package references: + + * `Google.Protobuf` + * `Grpc` + * `Grpc.Net.Client` + + These changes were made to streamline dependencies and reduce the footprint of + the exporter. + ([#6005](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6005)) + +* Switched from using the `Google.Protobuf` library for serialization to a + custom manual implementation of protobuf serialization. + ([#6005](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6005)) + +* Fixed an issue where a `service.name` was added to the resource if it was + missing. The exporter now respects the resource data provided by the SDK + without modifications. + ([#6015](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6015)) + +* Removed the peer service resolver, which was based on earlier experimental + semantic conventions that are not part of the stable specification. This + change ensures that the exporter no longer modifies or assumes the value of + peer service attributes, aligning it more closely with OpenTelemetry protocol + specifications. + ([#6005](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6005)) + +## 1.10.0 + +Released 2024-Nov-12 + +## 1.10.0-rc.1 + +Released 2024-Nov-01 + +* Added support for exporting instrumentation scope attributes from + `ActivitySource.Tags`. + ([#5897](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5897)) + +## 1.10.0-beta.1 + +Released 2024-Sep-30 + +* **Breaking change**: Non-primitive attribute (logs) and tag (traces) values + converted using `Convert.ToString` will now format using + `CultureInfo.InvariantCulture`. + ([#5700](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5700)) + +* Fixed an issue causing `NotSupportedException`s to be thrown on startup when + `AddOtlpExporter` registration extensions are called while using custom + dependency injection containers which automatically create services (Unity, + Grace, etc.). + ([#5808](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5808)) + + * Fixed `PlatformNotSupportedException`s being thrown during export when running + on mobile platforms which caused telemetry to be dropped silently. + ([#5821](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/5821)) + ## 1.9.0 Released 2024-Jun-14 @@ -11,8 +144,8 @@ Released 2024-Jun-14 Released 2024-Jun-07 * The experimental APIs previously covered by `OTEL1000` - (`LoggerProviderBuilder.AddOtlpExporter` extension) will now be part of the - public API and supported in stable builds. + (`LoggerProviderBuilder.AddOtlpExporter` extension) are now part of the public + API and supported in stable builds. ([#5648](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5648)) ## 1.9.0-alpha.1 diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/IOtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/IOtlpExporterOptions.cs index 4c394759a13..d6402bc85a8 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/IOtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/IOtlpExporterOptions.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - #if NETFRAMEWORK using System.Net.Http; #endif @@ -16,7 +14,7 @@ namespace OpenTelemetry.Exporter; internal interface IOtlpExporterOptions { /// - /// Gets or sets the the OTLP transport protocol. + /// Gets or sets the OTLP transport protocol. /// OtlpExportProtocol Protocol { get; set; } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ActivityExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ActivityExtensions.cs deleted file mode 100644 index d3dacb3a1f9..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ActivityExtensions.cs +++ /dev/null @@ -1,408 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Runtime.CompilerServices; -using Google.Protobuf; -using OpenTelemetry.Internal; -using OpenTelemetry.Proto.Collector.Trace.V1; -using OpenTelemetry.Proto.Common.V1; -using OpenTelemetry.Proto.Resource.V1; -using OpenTelemetry.Proto.Trace.V1; -using OpenTelemetry.Trace; -using OtlpTrace = OpenTelemetry.Proto.Trace.V1; - -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; - -internal static class ActivityExtensions -{ - private static readonly ConcurrentBag SpanListPool = new(); - - internal static void AddBatch( - this ExportTraceServiceRequest request, - SdkLimitOptions sdkLimitOptions, - Resource processResource, - in Batch activityBatch) - { - Dictionary spansByLibrary = new Dictionary(); - ResourceSpans resourceSpans = new ResourceSpans - { - Resource = processResource, - }; - request.ResourceSpans.Add(resourceSpans); - - foreach (var activity in activityBatch) - { - 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); - - spansByLibrary.Add(activitySourceName, spans); - resourceSpans.ScopeSpans.Add(spans); - } - - spans.Spans.Add(span); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void Return(this ExportTraceServiceRequest request) - { - var resourceSpans = request.ResourceSpans.FirstOrDefault(); - if (resourceSpans == null) - { - return; - } - - foreach (var scope in resourceSpans.ScopeSpans) - { - scope.Spans.Clear(); - SpanListPool.Add(scope); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static ScopeSpans GetSpanListFromPool(string name, string version) - { - if (!SpanListPool.TryTake(out var spans)) - { - spans = new ScopeSpans - { - 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; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static Span ToOtlpSpan(this Activity activity, SdkLimitOptions sdkLimitOptions) - { - if (activity.IdFormat != ActivityIdFormat.W3C) - { - // 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]; - - 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 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), - - TraceId = UnsafeByteOperations.UnsafeWrap(traceIdBytes), - SpanId = UnsafeByteOperations.UnsafeWrap(spanIdBytes), - ParentSpanId = parentSpanIdString, - TraceState = activity.TraceStateString ?? string.Empty, - - StartTimeUnixNano = (ulong)startTimeUnixNano, - EndTimeUnixNano = (ulong)(startTimeUnixNano + activity.Duration.ToNanoseconds()), - }; - - 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 (peerServiceName != null && addAsTag) - { - otlpSpan.Attributes.Add( - new KeyValue - { - Key = SemanticConventions.AttributePeerService, - Value = new AnyValue { StringValue = peerServiceName }, - }); - } - } - - otlpSpan.Status = activity.ToOtlpStatus(ref otlpTags); - - 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); - - otlpSpan.Flags = ToOtlpSpanFlags(activity.Context.TraceFlags, activity.HasRemoteParent); - - 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; - } - - OtlpTrace.Status.Types.StatusCode otlpActivityStatusCode = OtlpTrace.Status.Types.StatusCode.Unset; - string otlpStatusDescription = null; - if (activity.Status != ActivityStatusCode.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)) - { - otlpStatusDescription = activity.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) - { - byte[] traceIdBytes = new byte[16]; - byte[] spanIdBytes = new byte[8]; - - activityLink.Context.TraceId.CopyTo(traceIdBytes); - activityLink.Context.SpanId.CopyTo(spanIdBytes); - - var otlpLink = new Span.Types.Link - { - TraceId = UnsafeByteOperations.UnsafeWrap(traceIdBytes), - SpanId = UnsafeByteOperations.UnsafeWrap(spanIdBytes), - }; - - int maxTags = sdkLimitOptions.SpanLinkAttributeCountLimit ?? int.MaxValue; - - var otlpLinkAttributes = otlpLink.Attributes; - - foreach (ref readonly var tag in activityLink.EnumerateTagObjects()) - { - if (otlpLinkAttributes.Count == maxTags) - { - otlpLink.DroppedAttributesCount++; - continue; - } - - OtlpTagWriter.Instance.TryWriteTag(ref otlpLinkAttributes, tag, sdkLimitOptions.AttributeValueLengthLimit); - } - - otlpLink.Flags = ToOtlpSpanFlags(activityLink.Context.TraceFlags, activityLink.Context.IsRemote); - - return otlpLink; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Span.Types.Event ToOtlpEvent(in ActivityEvent activityEvent, SdkLimitOptions sdkLimitOptions) - { - var otlpEvent = new Span.Types.Event - { - Name = activityEvent.Name, - TimeUnixNano = (ulong)activityEvent.Timestamp.ToUnixTimeNanoseconds(), - }; - - int maxTags = sdkLimitOptions.SpanEventAttributeCountLimit ?? int.MaxValue; - - var otlpEventAttributes = otlpEvent.Attributes; - - foreach (ref readonly var tag in activityEvent.EnumerateTagObjects()) - { - if (otlpEventAttributes.Count == maxTags) - { - otlpEvent.DroppedAttributesCount++; - continue; - } - - OtlpTagWriter.Instance.TryWriteTag(ref otlpEventAttributes, tag, sdkLimitOptions.AttributeValueLengthLimit); - } - - return otlpEvent; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint ToOtlpSpanFlags(ActivityTraceFlags activityTraceFlags, bool isRemote) - { - SpanFlags flags = (SpanFlags)activityTraceFlags; - - flags |= SpanFlags.ContextHasIsRemoteMask; - - if (isRemote) - { - flags |= SpanFlags.ContextIsRemoteMask; - } - - return (uint)flags; - } - - private struct TagEnumerationState : PeerServiceResolver.IPeerServiceState - { - public SdkLimitOptions SdkLimitOptions; - - public Span Span; - - public string StatusCode; - - public string StatusDescription; - - 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 void EnumerateTags(Activity activity, int maxTags) - { - var otlpSpanAttributes = this.Span.Attributes; - - foreach (ref readonly var tag in activity.EnumerateTagObjects()) - { - if (tag.Value == null) - { - continue; - } - - var key = tag.Key; - - switch (key) - { - case SpanAttributeConstants.StatusCodeKey: - this.StatusCode = tag.Value as string; - continue; - case SpanAttributeConstants.StatusDescriptionKey: - this.StatusDescription = tag.Value as string; - continue; - } - - if (otlpSpanAttributes.Count == maxTags) - { - this.Span.DroppedAttributesCount++; - } - else - { - OtlpTagWriter.Instance.TryWriteTag(ref otlpSpanAttributes, tag, this.SdkLimitOptions.AttributeValueLengthLimit); - } - - if (tag.Value is string tagStringValue) - { - PeerServiceResolver.InspectTag(ref this, key, tagStringValue); - } - else if (tag.Value is int tagIntValue) - { - PeerServiceResolver.InspectTag(ref this, key, tagIntValue); - } - } - } - } - - private struct EventEnumerationState - { - public SdkLimitOptions SdkLimitOptions; - - public Span Span; - - public void EnumerateEvents(Activity activity, int maxEvents) - { - foreach (ref readonly var @event in activity.EnumerateEvents()) - { - if (this.Span.Events.Count < maxEvents) - { - this.Span.Events.Add(ToOtlpEvent(in @event, this.SdkLimitOptions)); - } - else - { - this.Span.DroppedEventsCount++; - } - } - } - } - - private struct LinkEnumerationState - { - public SdkLimitOptions SdkLimitOptions; - - public Span Span; - - public void EnumerateLinks(Activity activity, int maxLinks) - { - foreach (ref readonly var link in activity.EnumerateLinks()) - { - if (this.Span.Links.Count < maxLinks) - { - 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 index 65d0bf57ee1..0470b2d8a23 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExperimentalOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExperimentalOptions.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using Microsoft.Extensions.Configuration; namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; @@ -11,8 +9,6 @@ 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 const string OtlpRetryEnvVar = "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY"; @@ -31,7 +27,7 @@ public ExperimentalOptions(IConfiguration configuration) this.EmitLogEventAttributes = emitLogEventAttributes; } - if (configuration.TryGetStringValue(OtlpRetryEnvVar, out var retryPolicy) && retryPolicy != null) + if (configuration.TryGetStringValue(OtlpRetryEnvVar, out var retryPolicy)) { if (retryPolicy.Equals("in_memory", StringComparison.OrdinalIgnoreCase)) { @@ -40,7 +36,7 @@ public ExperimentalOptions(IConfiguration configuration) else if (retryPolicy.Equals("disk", StringComparison.OrdinalIgnoreCase)) { this.EnableDiskRetry = true; - if (configuration.TryGetStringValue(OtlpDiskRetryDirectoryPathEnvVar, out var path) && path != null) + if (configuration.TryGetStringValue(OtlpDiskRetryDirectoryPathEnvVar, out var path)) { this.DiskRetryDirectoryPath = path; } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpGrpcExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpGrpcExportClient.cs deleted file mode 100644 index 493d267bc74..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpGrpcExportClient.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using Grpc.Core; -using OpenTelemetry.Internal; -#if NETSTANDARD2_1 || NET6_0_OR_GREATER -using Grpc.Net.Client; -#endif - -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; - -/// 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: default, exception: null); - - protected BaseOtlpGrpcExportClient(OtlpExporterOptions options) - { - Guard.ThrowIfNull(options); - Guard.ThrowIfInvalidTimeout(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; } -#else - internal Channel Channel { get; set; } -#endif - - internal Uri Endpoint { get; } - - internal Metadata Headers { get; } - - internal int TimeoutMilliseconds { get; } - - /// - public abstract ExportClientResponse SendExportRequest(TRequest request, DateTime deadlineUtc, CancellationToken cancellationToken = default); - - /// - public virtual bool Shutdown(int timeoutMilliseconds) - { - if (this.Channel == null) - { - return true; - } - - 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 deleted file mode 100644 index 4fedc6b6176..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpHttpExportClient.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -#if NETFRAMEWORK -using System.Net.Http; -#endif -using OpenTelemetry.Internal; - -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; - -/// 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: default, response: null, exception: null); - - protected BaseOtlpHttpExportClient(OtlpExporterOptions options, HttpClient httpClient, string signalPath) - { - Guard.ThrowIfNull(options); - Guard.ThrowIfNull(httpClient); - Guard.ThrowIfNull(signalPath); - Guard.ThrowIfInvalidTimeout(options.TimeoutMilliseconds); - - Uri exporterEndpoint = options.AppendSignalPathToEndpoint - ? 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 Uri Endpoint { get; set; } - - internal IReadOnlyDictionary Headers { get; } - - /// - public ExportClientResponse SendExportRequest(TRequest request, DateTime deadlineUtc, CancellationToken cancellationToken = default) - { - try - { - using var httpRequest = this.CreateHttpRequest(request); - - using var httpResponse = this.SendHttpRequest(httpRequest, cancellationToken); - - try - { - httpResponse.EnsureSuccessStatusCode(); - } - catch (HttpRequestException ex) - { - return new ExportClientHttpResponse(success: false, deadlineUtc: deadlineUtc, response: httpResponse, ex); - } - - // We do not need to return back response and deadline for successful response so using cached value. - return SuccessExportResponse; - } - catch (HttpRequestException ex) - { - OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(this.Endpoint, ex); - - return new ExportClientHttpResponse(success: false, deadlineUtc: deadlineUtc, response: null, exception: ex); - } - } - - /// - public bool Shutdown(int timeoutMilliseconds) - { - this.HttpClient.CancelPendingRequests(); - return true; - } - - protected abstract HttpContent CreateHttpContent(TRequest exportRequest); - - protected HttpRequestMessage CreateHttpRequest(TRequest exportRequest) - { - var request = new HttpRequestMessage(HttpMethod.Post, this.Endpoint); - foreach (var header in this.Headers) - { - request.Headers.Add(header.Key, header.Value); - } - - request.Content = this.CreateHttpContent(exportRequest); - - return request; - } - - protected HttpResponseMessage SendHttpRequest(HttpRequestMessage request, CancellationToken cancellationToken) - { -#if NET6_0_OR_GREATER - return this.HttpClient.Send(request, cancellationToken); -#else - 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 index f7c95107a7a..339e0ab78ad 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientGrpcResponse.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientGrpcResponse.cs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient.Grpc; namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; @@ -10,8 +10,16 @@ internal sealed class ExportClientGrpcResponse : ExportClientResponse public ExportClientGrpcResponse( bool success, DateTime deadlineUtc, - Exception? exception) + Exception? exception, + Status? status, + string? grpcStatusDetailsHeader) : base(success, deadlineUtc, exception) { + this.Status = status; + this.GrpcStatusDetailsHeader = grpcStatusDetailsHeader; } + + public Status? Status { get; } + + public string? GrpcStatusDetailsHeader { get; } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientHttpResponse.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientHttpResponse.cs index 9d274b0ffed..7e94996e7be 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientHttpResponse.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientHttpResponse.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Net; #if NETFRAMEWORK using System.Net.Http; diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientResponse.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientResponse.cs index 49f8c0eb209..fcccd151920 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientResponse.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExportClientResponse.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; internal abstract class ExportClientResponse diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/Grpc/.editorconfig b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/Grpc/.editorconfig new file mode 100644 index 00000000000..c895abfaeb0 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/Grpc/.editorconfig @@ -0,0 +1,2 @@ +[*.cs] +generated_code = true diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/Grpc/GrpcProtocolHelpers.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/Grpc/GrpcProtocolHelpers.cs new file mode 100644 index 00000000000..599bfc4a091 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/Grpc/GrpcProtocolHelpers.cs @@ -0,0 +1,136 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 NET462 +using System.Net.Http; +#endif +using System.Net.Http.Headers; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient.Grpc; + +internal static class GrpcProtocolHelpers +{ + internal const string StatusTrailer = "grpc-status"; + internal const string MessageTrailer = "grpc-message"; + + public static Status GetResponseStatus(HttpResponseMessage httpResponse, HttpHeaders trailingHeaders) + { + try + { + return trailingHeaders.Any() + ? GetStatusCore(trailingHeaders) + : GetStatusCore(httpResponse.Headers); + } + catch (Exception ex) + { + // Handle error from parsing badly formed status + return new Status(StatusCode.Internal, ex.Message, ex); + } + } + + public static Status GetStatusCore(HttpHeaders headers) + { + var grpcStatus = GetHeaderValue(headers, StatusTrailer); + + // grpc-status is a required trailer + if (grpcStatus == null) + { + return Status.NoReply; + } + + int statusValue; + if (!int.TryParse(grpcStatus, out statusValue)) + { + throw new InvalidOperationException("Unexpected grpc-status value: " + grpcStatus); + } + + // grpc-message is optional + // Always read the gRPC message from the same headers collection as the status + var grpcMessage = GetHeaderValue(headers, MessageTrailer); + + if (!string.IsNullOrEmpty(grpcMessage)) + { + // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#responses + // The value portion of Status-Message is conceptually a Unicode string description of the error, + // physically encoded as UTF-8 followed by percent-encoding. + grpcMessage = Uri.UnescapeDataString(grpcMessage); + } + + return new Status((StatusCode)statusValue, grpcMessage ?? string.Empty); + } + + public static string? GetHeaderValue(HttpHeaders? headers, string name, bool first = false) + { + if (headers == null) + { + return null; + } + +#if NET6_0_OR_GREATER + if (!headers.NonValidated.TryGetValues(name, out var values)) + { + return null; + } + + using (var e = values.GetEnumerator()) + { + if (!e.MoveNext()) + { + return null; + } + + var result = e.Current; + if (!e.MoveNext()) + { + return result; + } + + if (first) + { + return result; + } + } + + throw new InvalidOperationException($"Multiple {name} headers."); +#else + if (!headers.TryGetValues(name, out var values)) + { + return null; + } + + // HttpHeaders appears to always return an array, but fallback to converting values to one just in case + var valuesArray = values as string[] ?? values.ToArray(); + + switch (valuesArray.Length) + { + case 0: + return null; + case 1: + return valuesArray[0]; + default: + if (first) + { + return valuesArray[0]; + } + + throw new InvalidOperationException($"Multiple {name} headers."); + } +#endif + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/Grpc/GrpcStatusDeserializer.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/Grpc/GrpcStatusDeserializer.cs new file mode 100644 index 00000000000..3518a0f658d --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/Grpc/GrpcStatusDeserializer.cs @@ -0,0 +1,314 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Text; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient.Grpc; + +internal static class GrpcStatusDeserializer +{ +#pragma warning disable SA1310 // Field names should not contain underscore + // Wire types in protocol buffers + private const int WIRETYPE_VARINT = 0; + private const int WIRETYPE_FIXED64 = 1; + private const int WIRETYPE_LENGTH_DELIMITED = 2; + private const int WIRETYPE_FIXED32 = 5; +#pragma warning restore SA1310 // Field names should not contain underscore + + internal static TimeSpan? TryGetGrpcRetryDelay(string? grpcStatusDetailsHeader) + { + try + { + var retryInfo = ExtractRetryInfo(grpcStatusDetailsHeader); + if (retryInfo?.RetryDelay != null) + { + return TimeSpan.FromSeconds(retryInfo.Value.RetryDelay.Value.Seconds) + + TimeSpan.FromTicks(retryInfo.Value.RetryDelay.Value.Nanos / 100); // Convert nanos to ticks + } + } + catch (Exception ex) + { + OpenTelemetryProtocolExporterEventSource.Log.GrpcRetryDelayParsingFailed(grpcStatusDetailsHeader, ex); + return null; + } + + return null; + } + + // Marked as internal for test. + internal static Status? DeserializeStatus(string? grpcStatusDetailsBin) + { + if (string.IsNullOrWhiteSpace(grpcStatusDetailsBin)) + { + return null; + } + + var status = new Status(); + byte[] data = Convert.FromBase64String(grpcStatusDetailsBin); + using (var stream = new MemoryStream(data)) + { + while (stream.Position < stream.Length) + { + var tag = DecodeTag(stream); + var fieldNumber = tag >> 3; + var wireType = tag & 0x7; + + switch (fieldNumber) + { + case 1: // code + status.Code = DecodeInt32(stream); + break; + case 2: // message + status.Message = DecodeString(stream); + break; + case 3: // details + status.Details.Add(DecodeAny(stream)); + break; + default: + SkipField(stream, wireType); + break; + } + } + } + + return status; + } + + // Marked as internal for test. + internal static RetryInfo? ExtractRetryInfo(string? grpcStatusDetailsBin) + { + var status = DeserializeStatus(grpcStatusDetailsBin); + if (status == null) + { + return null; + } + + foreach (var detail in status.Value.Details) + { + if (detail.TypeUrl != null && detail.TypeUrl.EndsWith("/google.rpc.RetryInfo")) + { + return DeserializeRetryInfo(detail.Value!); + } + } + + return null; + } + + private static RetryInfo? DeserializeRetryInfo(byte[] data) + { + RetryInfo? retryInfo = null; + using (var stream = new MemoryStream(data)) + { + while (stream.Position < stream.Length) + { + var tag = DecodeTag(stream); + var fieldNumber = tag >> 3; + var wireType = tag & 0x7; + + switch (fieldNumber) + { + case 1: // retry_delay + retryInfo = new RetryInfo(DecodeDuration(stream)); + break; + default: + SkipField(stream, wireType); + break; + } + } + } + + return retryInfo; + } + + private static Duration DecodeDuration(Stream stream) + { + var length = DecodeVarint(stream); + var endPosition = stream.Position + length; + long seconds = 0; + int nanos = 0; + + while (stream.Position < endPosition) + { + var tag = DecodeTag(stream); + var fieldNumber = tag >> 3; + var wireType = tag & 0x7; + + switch (fieldNumber) + { + case 1: // seconds + seconds = DecodeInt64(stream); + break; + case 2: // nanos + nanos = DecodeInt32(stream); + break; + default: + SkipField(stream, wireType); + break; + } + } + + return new Duration(seconds, nanos); + } + + private static Any DecodeAny(Stream stream) + { + var length = DecodeVarint(stream); + var endPosition = stream.Position + length; + + string? typeUrl = null; + byte[]? value = null; + + while (stream.Position < endPosition) + { + var tag = DecodeTag(stream); + var fieldNumber = tag >> 3; + var wireType = tag & 0x7; + + switch (fieldNumber) + { + case 1: // type_url + typeUrl = DecodeString(stream); + break; + case 2: // value + value = DecodeBytes(stream); + break; + default: + SkipField(stream, wireType); + break; + } + } + + return new Any(typeUrl, value); + } + + private static uint DecodeTag(Stream stream) + { + return (uint)DecodeVarint(stream); + } + + private static long DecodeVarint(Stream stream) + { + long result = 0; + int shift = 0; + + while (true) + { + int b = stream.ReadByte(); + if (b == -1) + { + throw new EndOfStreamException(); + } + + result |= (long)(b & 0b_0111_1111) << shift; + if ((b & 0b_1000_0000) == 0) + { + return result; + } + + shift += 7; + if (shift >= 64) + { + throw new InvalidDataException("Invalid varint"); + } + } + } + + private static int DecodeInt32(Stream stream) => (int)DecodeVarint(stream); + + private static long DecodeInt64(Stream stream) => DecodeVarint(stream); + + private static string DecodeString(Stream stream) + { + var bytes = DecodeBytes(stream); + return Encoding.UTF8.GetString(bytes); + } + + private static byte[] DecodeBytes(Stream stream) + { + var length = (int)DecodeVarint(stream); + var buffer = new byte[length]; + int read = stream.Read(buffer, 0, length); + if (read != length) + { + throw new EndOfStreamException(); + } + + return buffer; + } + + private static void SkipField(Stream stream, uint wireType) + { + switch (wireType) + { + case WIRETYPE_VARINT: + DecodeVarint(stream); + break; + case WIRETYPE_FIXED64: + stream.Position += 8; + break; + case WIRETYPE_LENGTH_DELIMITED: + var length = DecodeVarint(stream); + stream.Position += length; + break; + case WIRETYPE_FIXED32: + stream.Position += 4; + break; + default: + throw new InvalidDataException($"Unknown wire type: {wireType}"); + } + } + + internal readonly struct Duration + { + internal Duration(long seconds, int nanos) + { + this.Seconds = seconds; + this.Nanos = nanos; + } + + public long Seconds { get; } + + public int Nanos { get; } + } + + internal readonly struct RetryInfo + { + public RetryInfo(Duration? retryDelay) + { + this.RetryDelay = retryDelay; + } + + public Duration? RetryDelay { get; } + } + + internal readonly struct Any + { + public Any(string? typeUrl, byte[]? value) + { + this.TypeUrl = typeUrl; + this.Value = value; + } + + public string? TypeUrl { get; } + + public byte[]? Value { get; } + } + + internal struct Status + { + public Status() + { + this.Code = 0; + this.Message = null; + this.Details = []; + } + + public int Code { get; set; } + + public string? Message { get; set; } + + public List Details { get; set; } + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/Grpc/Status.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/Grpc/Status.cs new file mode 100644 index 00000000000..3dc5c3a7f2e --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/Grpc/Status.cs @@ -0,0 +1,121 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.Exporter.OpenTelemetryProtocol.Implementation.ExportClient.Grpc; + +/// +/// Represents RPC result, which consists of and an optional detail string. +/// +[DebuggerDisplay("{DebuggerToString(),nq}")] +internal struct Status +{ + public const string NoReplyDetailMessage = "No grpc-status found on response."; + + /// + /// Default result of a successful RPC. StatusCode=OK, empty details message. + /// + public static readonly Status DefaultSuccess = new Status(StatusCode.OK, string.Empty); + + /// + /// Default result of a cancelled RPC. StatusCode=Cancelled, empty details message. + /// + public static readonly Status DefaultCancelled = new Status(StatusCode.Cancelled, string.Empty); + + /// + /// Default result of a cancelled RPC with no grpc-status found on response. + /// + public static readonly Status NoReply = new Status(StatusCode.Internal, NoReplyDetailMessage); + + /// + /// Initializes a new instance of the struct. + /// + /// Status code. + /// Detail. + public Status(StatusCode statusCode, string detail) + : this(statusCode, detail, null) + { + } + + /// + /// Initializes a new instance of the struct. + /// Users should not use this constructor, except for creating instances for testing. + /// The debug error string should only be populated by gRPC internals. + /// Note: experimental API that can change or be removed without any prior notice. + /// + /// Status code. + /// Detail. + /// Optional internal error details. + public Status(StatusCode statusCode, string detail, Exception? debugException) + { + this.StatusCode = statusCode; + this.Detail = detail; + this.DebugException = debugException; + } + + /// + /// Gets the gRPC status code. OK indicates success, all other values indicate an error. + /// + public StatusCode StatusCode { get; } + + /// + /// Gets the detail. + /// + public string Detail { get; } + + /// + /// Gets in case of an error, this field may contain additional error details to help with debugging. + /// This field will be only populated on a client and its value is generated locally, + /// based on the internal state of the gRPC client stack (i.e. the value is never sent over the wire). + /// Note that this field is available only for debugging purposes, the application logic should + /// never rely on values of this field (it should use StatusCode and Detail instead). + /// Example: when a client fails to connect to a server, this field may provide additional details + /// why the connection to the server has failed. + /// Note: experimental API that can change or be removed without any prior notice. + /// + public Exception? DebugException { get; } + + public override string ToString() + { + if (this.DebugException != null) + { + return $"Status(StatusCode=\"{this.StatusCode}\", Detail=\"{this.Detail}\"," + + $" DebugException=\"{this.DebugException.GetType()}: {this.DebugException.Message}\")"; + } + + return $"Status(StatusCode=\"{this.StatusCode}\", Detail=\"{this.Detail}\")"; + } + + private string DebuggerToString() + { + var text = $"StatusCode = {this.StatusCode}"; + if (!string.IsNullOrEmpty(this.Detail)) + { + text += $@", Detail = ""{this.Detail}"""; + } + + if (this.DebugException != null) + { + text += $@", DebugException = ""{this.DebugException.GetType()}: {this.DebugException.Message}"""; + } + + return text; + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/Grpc/StatusCode.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/Grpc/StatusCode.cs new file mode 100644 index 00000000000..4ab8fefa232 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/Grpc/StatusCode.cs @@ -0,0 +1,125 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.OpenTelemetryProtocol.Implementation.ExportClient.Grpc; + +/// +/// Result of a remote procedure call. +/// Based on grpc_status_code from grpc/status.h. +/// +internal enum StatusCode +{ + /// Not an error; returned on success. + 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 can not be identified (use UNAUTHENTICATED + /// instead for those errors). + /// + PermissionDenied = 7, + + /// The request does not have valid authentication credentials for the operation. + Unauthenticated = 16, + + /// + /// 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. + /// + 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. + /// + 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. Note that it is not always safe to retry + /// non-idempotent operations. + /// + Unavailable = 14, + + /// Unrecoverable data loss or corruption. + DataLoss = 15, +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/Grpc/TrailingHeadersHelpers.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/Grpc/TrailingHeadersHelpers.cs new file mode 100644 index 00000000000..a154014d17b --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/Grpc/TrailingHeadersHelpers.cs @@ -0,0 +1,63 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 NET462 +using System.Net.Http; +#endif +using System.Net.Http.Headers; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient.Grpc; + +internal static class TrailingHeadersHelpers +{ + public static readonly string ResponseTrailersKey = "__ResponseTrailers"; + + public static HttpHeaders TrailingHeaders(this HttpResponseMessage responseMessage) + { +#if !NETSTANDARD2_0 && !NET462 + return responseMessage.TrailingHeaders; +#else + 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; +#endif + } + +#if NETSTANDARD2_0 || NET462 + public static void EnsureTrailingHeaders(this HttpResponseMessage responseMessage) + { + if (!responseMessage.RequestMessage.Properties.ContainsKey(ResponseTrailersKey)) + { + responseMessage.RequestMessage.Properties[ResponseTrailersKey] = new ResponseTrailers(); + } + } + + private class ResponseTrailers : HttpHeaders + { + public static readonly ResponseTrailers Empty = new ResponseTrailers(); + } +#endif +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/IExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/IExportClient.cs index 0c44da6ef30..9df60c0e41c 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/IExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/IExportClient.cs @@ -1,22 +1,20 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; /// Export client interface. -/// Type of export request. -internal interface IExportClient +internal interface IExportClient { /// /// Method for sending export request to the server. /// - /// The request to send to the server. + /// The request body to send to the server. + /// length of the content. /// The deadline time in utc for export request to finish. /// An optional token for canceling the call. /// . - ExportClientResponse SendExportRequest(TRequest request, DateTime deadlineUtc, CancellationToken cancellationToken = default); + ExportClientResponse SendExportRequest(byte[] buffer, int contentLength, DateTime deadlineUtc, CancellationToken cancellationToken = default); /// /// Method for shutting down the export client. diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs new file mode 100644 index 00000000000..24fc1551cc9 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs @@ -0,0 +1,106 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NETFRAMEWORK +using System.Net.Http; +#endif +using System.Net.Http.Headers; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; + +internal abstract class OtlpExportClient : IExportClient +{ + private static readonly Version Http2RequestVersion = new(2, 0); + +#if NET + // See: https://github.com/dotnet/runtime/blob/280f2a0c60ce0378b8db49adc0eecc463d00fe5d/src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.AnyMobile.cs#L767 + private static readonly bool SynchronousSendSupportedByCurrentPlatform = !OperatingSystem.IsAndroid() + && !OperatingSystem.IsIOS() + && !OperatingSystem.IsTvOS() + && !OperatingSystem.IsBrowser(); +#endif + + protected OtlpExportClient(OtlpExporterOptions options, HttpClient httpClient, string signalPath) + { + Guard.ThrowIfNull(options); + Guard.ThrowIfNull(httpClient); + Guard.ThrowIfNull(signalPath); + + Uri exporterEndpoint; +#pragma warning disable CS0618 // Suppressing gRPC obsolete warning + if (options.Protocol == OtlpExportProtocol.Grpc) +#pragma warning restore CS0618 // Suppressing gRPC obsolete warning + { + exporterEndpoint = options.Endpoint.AppendPathIfNotPresent(signalPath); + } + else + { + exporterEndpoint = options.AppendSignalPathToEndpoint + ? 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 Uri Endpoint { get; } + + internal IReadOnlyDictionary Headers { get; } + + internal abstract MediaTypeHeaderValue MediaTypeHeader { get; } + + internal virtual bool RequireHttp2 => false; + + public abstract ExportClientResponse SendExportRequest(byte[] buffer, int contentLength, DateTime deadlineUtc, CancellationToken cancellationToken = default); + + /// + public bool Shutdown(int timeoutMilliseconds) + { + this.HttpClient.CancelPendingRequests(); + return true; + } + + protected HttpRequestMessage CreateHttpRequest(byte[] buffer, int contentLength) + { + var request = new HttpRequestMessage(HttpMethod.Post, this.Endpoint); + + if (this.RequireHttp2) + { + request.Version = Http2RequestVersion; + +#if NET6_0_OR_GREATER + request.VersionPolicy = HttpVersionPolicy.RequestVersionExact; +#endif + } + + foreach (var header in this.Headers) + { + request.Headers.Add(header.Key, header.Value); + } + + // TODO: Support compression. + + request.Content = new ByteArrayContent(buffer, 0, contentLength); + request.Content.Headers.ContentType = this.MediaTypeHeader; + + return request; + } + + protected HttpResponseMessage SendHttpRequest(HttpRequestMessage request, CancellationToken cancellationToken) + { +#if NET + // Note: SendAsync must be used with HTTP/2 because synchronous send is + // not supported. + return this.RequireHttp2 || !SynchronousSendSupportedByCurrentPlatform + ? this.HttpClient.SendAsync(request, cancellationToken).GetAwaiter().GetResult() + : this.HttpClient.Send(request, cancellationToken); +#else + return this.HttpClient.SendAsync(request, cancellationToken).GetAwaiter().GetResult(); +#endif + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs new file mode 100644 index 00000000000..31b7d0e9bb2 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs @@ -0,0 +1,169 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NETFRAMEWORK +using System.Net.Http; +#endif +using System.Net.Http.Headers; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient.Grpc; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; + +/// Base class for sending OTLP export request over gRPC. +internal sealed class OtlpGrpcExportClient : OtlpExportClient +{ + public const string GrpcStatusDetailsHeader = "grpc-status-details-bin"; + private static readonly ExportClientHttpResponse SuccessExportResponse = new(success: true, deadlineUtc: default, response: null, exception: null); + private static readonly MediaTypeHeaderValue MediaHeaderValue = new("application/grpc"); + + private static readonly ExportClientGrpcResponse DefaultExceptionExportClientGrpcResponse + = new( + success: false, + deadlineUtc: default, + exception: null, + status: null, + grpcStatusDetailsHeader: null); + + public OtlpGrpcExportClient(OtlpExporterOptions options, HttpClient httpClient, string signalPath) + : base(options, httpClient, signalPath) + { + } + + internal override MediaTypeHeaderValue MediaTypeHeader => MediaHeaderValue; + + internal override bool RequireHttp2 => true; + + /// + public override ExportClientResponse SendExportRequest(byte[] buffer, int contentLength, DateTime deadlineUtc, CancellationToken cancellationToken = default) + { + try + { + using var httpRequest = this.CreateHttpRequest(buffer, contentLength); + + // TE is required by some servers, e.g. C Core. + // A missing TE header results in servers aborting the gRPC call. + httpRequest.Headers.TryAddWithoutValidation("TE", "trailers"); + + using var httpResponse = this.SendHttpRequest(httpRequest, cancellationToken); + + httpResponse.EnsureSuccessStatusCode(); + + var trailingHeaders = httpResponse.TrailingHeaders(); + Status status = GrpcProtocolHelpers.GetResponseStatus(httpResponse, trailingHeaders); + + if (status.Detail.Equals(Status.NoReplyDetailMessage, StringComparison.Ordinal)) + { +#if NET + using var responseStream = httpResponse.Content.ReadAsStream(cancellationToken); +#else + using var responseStream = httpResponse.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); +#endif + int firstByte = responseStream.ReadByte(); + + if (firstByte == -1) + { + if (status.StatusCode == StatusCode.OK) + { + status = new Status(StatusCode.Internal, "Failed to deserialize response message."); + } + + OpenTelemetryProtocolExporterEventSource.Log.ResponseDeserializationFailed(this.Endpoint.ToString()); + + return new ExportClientGrpcResponse( + success: false, + deadlineUtc: deadlineUtc, + exception: null, + status: status, + grpcStatusDetailsHeader: null); + } + + // Note: Trailing headers might not be fully available until the + // response stream is consumed. gRPC often sends critical + // information like error details or final statuses in trailing + // headers which can only be reliably accessed after reading + // the response body. + trailingHeaders = httpResponse.TrailingHeaders(); + status = GrpcProtocolHelpers.GetResponseStatus(httpResponse, trailingHeaders); + } + + if (status.StatusCode == StatusCode.OK) + { + OpenTelemetryProtocolExporterEventSource.Log.ExportSuccess(this.Endpoint.ToString(), "Export completed successfully."); + return SuccessExportResponse; + } + + string? grpcStatusDetailsHeader = null; + if (status.StatusCode == StatusCode.ResourceExhausted || status.StatusCode == StatusCode.Unavailable) + { + grpcStatusDetailsHeader = GrpcProtocolHelpers.GetHeaderValue(trailingHeaders, GrpcStatusDetailsHeader); + } + + OpenTelemetryProtocolExporterEventSource.Log.ExportFailure(this.Endpoint, "Export failed due to unexpected status code.", status); + + return new ExportClientGrpcResponse( + success: false, + deadlineUtc: deadlineUtc, + exception: null, + status: status, + grpcStatusDetailsHeader: grpcStatusDetailsHeader); + } + catch (HttpRequestException ex) when (ex.InnerException is TimeoutException || IsTransientNetworkError(ex)) + { + // Handle transient HTTP errors (retryable) + OpenTelemetryProtocolExporterEventSource.Log.TransientHttpError(this.Endpoint, ex); + return new ExportClientGrpcResponse( + success: false, + deadlineUtc: deadlineUtc, + exception: ex, + status: new Status(StatusCode.Unavailable, "Transient HTTP error - retryable"), + grpcStatusDetailsHeader: null); + } + catch (HttpRequestException ex) + { + // Handle non-retryable HTTP errors. + OpenTelemetryProtocolExporterEventSource.Log.HttpRequestFailed(this.Endpoint, ex); + return new ExportClientGrpcResponse( + success: false, + deadlineUtc: deadlineUtc, + exception: ex, + status: null, + grpcStatusDetailsHeader: null); + } + catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested) + { + // Handle unexpected cancellation. + OpenTelemetryProtocolExporterEventSource.Log.OperationUnexpectedlyCanceled(this.Endpoint, ex); + return new ExportClientGrpcResponse( + success: false, + deadlineUtc: deadlineUtc, + exception: ex, + status: new Status(StatusCode.Cancelled, "Operation was canceled unexpectedly."), + grpcStatusDetailsHeader: null); + } + catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) + { + // Handle TaskCanceledException caused by TimeoutException. + OpenTelemetryProtocolExporterEventSource.Log.RequestTimedOut(this.Endpoint, ex); + return new ExportClientGrpcResponse( + success: false, + deadlineUtc: deadlineUtc, + exception: ex, + status: new Status(StatusCode.DeadlineExceeded, "Request timed out."), + grpcStatusDetailsHeader: null); + } + catch (Exception ex) + { + OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(this.Endpoint, ex); + return DefaultExceptionExportClientGrpcResponse; + } + } + + private static bool IsTransientNetworkError(HttpRequestException ex) + { + return ex.InnerException is System.Net.Sockets.SocketException socketEx + && (socketEx.SocketErrorCode == System.Net.Sockets.SocketError.TimedOut + || socketEx.SocketErrorCode == System.Net.Sockets.SocketError.ConnectionReset + || socketEx.SocketErrorCode == System.Net.Sockets.SocketError.HostUnreachable + || socketEx.SocketErrorCode == System.Net.Sockets.SocketError.ConnectionRefused); + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcLogExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcLogExportClient.cs deleted file mode 100644 index b45837cd820..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcLogExportClient.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using Grpc.Core; -using OtlpCollector = OpenTelemetry.Proto.Collector.Logs.V1; - -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; - -/// Class for sending OTLP Logs export request over gRPC. -internal sealed class OtlpGrpcLogExportClient : BaseOtlpGrpcExportClient -{ - private readonly OtlpCollector.LogsService.LogsServiceClient logsClient; - - public OtlpGrpcLogExportClient(OtlpExporterOptions options, OtlpCollector.LogsService.LogsServiceClient logsServiceClient = null) - : base(options) - { - if (logsServiceClient != null) - { - this.logsClient = logsServiceClient; - } - else - { - this.Channel = options.CreateChannel(); - this.logsClient = new OtlpCollector.LogsService.LogsServiceClient(this.Channel); - } - } - - /// - public override ExportClientResponse SendExportRequest(OtlpCollector.ExportLogsServiceRequest request, DateTime deadlineUtc, CancellationToken cancellationToken = default) - { - try - { - this.logsClient.Export(request, headers: this.Headers, deadline: deadlineUtc, cancellationToken: cancellationToken); - - // 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 new ExportClientGrpcResponse(success: false, deadlineUtc: deadlineUtc, exception: ex); - } - } -} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcMetricsExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcMetricsExportClient.cs deleted file mode 100644 index b156f6c6d02..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcMetricsExportClient.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using Grpc.Core; -using OtlpCollector = OpenTelemetry.Proto.Collector.Metrics.V1; - -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; - -/// Class for sending OTLP metrics export request over gRPC. -internal sealed class OtlpGrpcMetricsExportClient : BaseOtlpGrpcExportClient -{ - private readonly OtlpCollector.MetricsService.MetricsServiceClient metricsClient; - - public OtlpGrpcMetricsExportClient(OtlpExporterOptions options, OtlpCollector.MetricsService.MetricsServiceClient metricsServiceClient = null) - : base(options) - { - if (metricsServiceClient != null) - { - this.metricsClient = metricsServiceClient; - } - else - { - this.Channel = options.CreateChannel(); - this.metricsClient = new OtlpCollector.MetricsService.MetricsServiceClient(this.Channel); - } - } - - /// - public override ExportClientResponse SendExportRequest(OtlpCollector.ExportMetricsServiceRequest request, DateTime deadlineUtc, CancellationToken cancellationToken = default) - { - try - { - this.metricsClient.Export(request, headers: this.Headers, deadline: deadlineUtc, cancellationToken: cancellationToken); - - // 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 new ExportClientGrpcResponse(success: false, deadlineUtc: deadlineUtc, exception: ex); - } - } -} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcTraceExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcTraceExportClient.cs deleted file mode 100644 index b38fbe2e471..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcTraceExportClient.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using Grpc.Core; -using OtlpCollector = OpenTelemetry.Proto.Collector.Trace.V1; - -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; - -/// Class for sending OTLP trace export request over gRPC. -internal sealed class OtlpGrpcTraceExportClient : BaseOtlpGrpcExportClient -{ - private readonly OtlpCollector.TraceService.TraceServiceClient traceClient; - - public OtlpGrpcTraceExportClient(OtlpExporterOptions options, OtlpCollector.TraceService.TraceServiceClient traceServiceClient = null) - : base(options) - { - if (traceServiceClient != null) - { - this.traceClient = traceServiceClient; - } - else - { - this.Channel = options.CreateChannel(); - this.traceClient = new OtlpCollector.TraceService.TraceServiceClient(this.Channel); - } - } - - /// - public override ExportClientResponse SendExportRequest(OtlpCollector.ExportTraceServiceRequest request, DateTime deadlineUtc, CancellationToken cancellationToken = default) - { - try - { - this.traceClient.Export(request, headers: this.Headers, deadline: deadlineUtc, cancellationToken: cancellationToken); - - // 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 new ExportClientGrpcResponse(success: false, deadlineUtc: deadlineUtc, exception: ex); - } - } -} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs new file mode 100644 index 00000000000..0d938a90f89 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs @@ -0,0 +1,50 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NETFRAMEWORK +using System.Net.Http; +#endif +using System.Net.Http.Headers; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; + +/// Class for sending OTLP trace export request over HTTP. +internal sealed class OtlpHttpExportClient : OtlpExportClient +{ + internal static readonly MediaTypeHeaderValue MediaHeaderValue = new("application/x-protobuf"); + private static readonly ExportClientHttpResponse SuccessExportResponse = new(success: true, deadlineUtc: default, response: null, exception: null); + + internal OtlpHttpExportClient(OtlpExporterOptions options, HttpClient httpClient, string signalPath) + : base(options, httpClient, signalPath) + { + } + + internal override MediaTypeHeaderValue MediaTypeHeader => MediaHeaderValue; + + /// + public override ExportClientResponse SendExportRequest(byte[] buffer, int contentLength, DateTime deadlineUtc, CancellationToken cancellationToken = default) + { + try + { + using var httpRequest = this.CreateHttpRequest(buffer, contentLength); + using var httpResponse = this.SendHttpRequest(httpRequest, cancellationToken); + + try + { + httpResponse.EnsureSuccessStatusCode(); + } + catch (HttpRequestException ex) + { + OpenTelemetryProtocolExporterEventSource.Log.HttpRequestFailed(this.Endpoint, ex); + return new ExportClientHttpResponse(success: false, deadlineUtc: deadlineUtc, response: httpResponse, ex); + } + + return SuccessExportResponse; + } + catch (HttpRequestException ex) + { + OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(this.Endpoint, ex); + return new ExportClientHttpResponse(success: false, deadlineUtc: deadlineUtc, response: null, exception: ex); + } + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpLogExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpLogExportClient.cs deleted file mode 100644 index ff872c1073a..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpLogExportClient.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Net; -#if NETFRAMEWORK -using System.Net.Http; -#endif -using System.Net.Http.Headers; -using System.Runtime.CompilerServices; -using Google.Protobuf; -using OtlpCollector = OpenTelemetry.Proto.Collector.Logs.V1; - -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; - -/// 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"; - - public OtlpHttpLogExportClient(OtlpExporterOptions options, HttpClient httpClient) - : base(options, httpClient, LogsExportPath) - { - } - - protected override HttpContent CreateHttpContent(OtlpCollector.ExportLogsServiceRequest exportRequest) - { - return new ExportRequestContent(exportRequest); - } - - internal sealed class ExportRequestContent : HttpContent - { - private static readonly MediaTypeHeaderValue ProtobufMediaTypeHeader = new(MediaContentType); - - private readonly OtlpCollector.ExportLogsServiceRequest exportRequest; - - 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); - } -#endif - - 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) - { - this.exportRequest.WriteTo(stream); - } - } -} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpMetricsExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpMetricsExportClient.cs deleted file mode 100644 index 1a893ddea2f..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpMetricsExportClient.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Net; -#if NETFRAMEWORK -using System.Net.Http; -#endif -using System.Net.Http.Headers; -using System.Runtime.CompilerServices; -using Google.Protobuf; -using OtlpCollector = OpenTelemetry.Proto.Collector.Metrics.V1; - -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; - -/// 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"; - - public OtlpHttpMetricsExportClient(OtlpExporterOptions options, HttpClient httpClient) - : base(options, httpClient, MetricsExportPath) - { - } - - protected override HttpContent CreateHttpContent(OtlpCollector.ExportMetricsServiceRequest exportRequest) - { - return new ExportRequestContent(exportRequest); - } - - internal sealed class ExportRequestContent : HttpContent - { - private static readonly MediaTypeHeaderValue ProtobufMediaTypeHeader = new(MediaContentType); - - private readonly OtlpCollector.ExportMetricsServiceRequest exportRequest; - - 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); - } -#endif - - 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) - { - this.exportRequest.WriteTo(stream); - } - } -} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpTraceExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpTraceExportClient.cs deleted file mode 100644 index 7e49fac8cf6..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpTraceExportClient.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Net; -#if NETFRAMEWORK -using System.Net.Http; -#endif -using System.Net.Http.Headers; -using System.Runtime.CompilerServices; -using Google.Protobuf; -using OtlpCollector = OpenTelemetry.Proto.Collector.Trace.V1; - -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; - -/// 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"; - - public OtlpHttpTraceExportClient(OtlpExporterOptions options, HttpClient httpClient) - : base(options, httpClient, TracesExportPath) - { - } - - protected override HttpContent CreateHttpContent(OtlpCollector.ExportTraceServiceRequest exportRequest) - { - return new ExportRequestContent(exportRequest); - } - - internal sealed class ExportRequestContent : HttpContent - { - private static readonly MediaTypeHeaderValue ProtobufMediaTypeHeader = new(MediaContentType); - - private readonly OtlpCollector.ExportTraceServiceRequest exportRequest; - - 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); - } -#endif - - 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) - { - this.exportRequest.WriteTo(stream); - } - } -} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpRetry.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpRetry.cs index 325236cc2a6..b4617a1fd42 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpRetry.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpRetry.cs @@ -1,14 +1,12 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - -using System.Diagnostics; using System.Net; using System.Net.Http.Headers; -using Google.Rpc; -using Grpc.Core; -using Status = Google.Rpc.Status; +#if NET +using System.Security.Cryptography; +#endif +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient.Grpc; namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; @@ -45,13 +43,12 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClie /// 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(); +#if !NET + private static readonly Random Random = new(); #endif public static bool TryGetHttpRetryResult(ExportClientHttpResponse response, int retryDelayInMilliSeconds, out RetryResult retryResult) @@ -85,12 +82,50 @@ public static bool ShouldHandleHttpRequestException(Exception? exception) public static bool TryGetGrpcRetryResult(ExportClientGrpcResponse response, int retryDelayMilliseconds, out RetryResult retryResult) { - if (response.Exception is RpcException rpcException) + retryResult = default; + + if (response.Status != null) { - return TryGetRetryResult(rpcException.StatusCode, IsGrpcStatusCodeRetryable, response.DeadlineUtc, rpcException.Trailers, TryGetGrpcRetryDelay, retryDelayMilliseconds, out retryResult); + var nextRetryDelayMilliseconds = retryDelayMilliseconds; + + if (IsDeadlineExceeded(response.DeadlineUtc)) + { + return false; + } + + var throttleDelay = GrpcStatusDeserializer.TryGetGrpcRetryDelay(response.GrpcStatusDetailsHeader); + var retryable = IsGrpcStatusCodeRetryable(response.Status.Value.StatusCode, throttleDelay.HasValue); + + if (!retryable) + { + return false; + } + + var delayDuration = throttleDelay ?? TimeSpan.FromMilliseconds(GetRandomNumber(0, nextRetryDelayMilliseconds)); + + if (IsDeadlineExceeded(response.DeadlineUtc + 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; } - retryResult = default; return false; } @@ -123,9 +158,7 @@ private static bool TryGetRetryResult(TStatusCode statusC return false; } - var delayDuration = throttleDelay.HasValue - ? throttleDelay.Value - : TimeSpan.FromMilliseconds(GetRandomNumber(0, nextRetryDelayMilliseconds)); + var delayDuration = throttleDelay ?? TimeSpan.FromMilliseconds(GetRandomNumber(0, nextRetryDelayMilliseconds)); if (deadline.HasValue && IsDeadlineExceeded(deadline + delayDuration)) { @@ -163,35 +196,9 @@ private static int CalculateNextRetryDelay(int nextRetryDelayMilliseconds) 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? responseHeaders) { -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NET return statusCode == HttpStatusCode.TooManyRequests || statusCode == HttpStatusCode.ServiceUnavailable #else return statusCode == (HttpStatusCode)429 || statusCode == HttpStatusCode.ServiceUnavailable @@ -222,7 +229,7 @@ private static bool IsHttpStatusCodeRetryable(HttpStatusCode statusCode, bool ha { switch (statusCode) { -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NET case HttpStatusCode.TooManyRequests: #else case (HttpStatusCode)429: @@ -238,14 +245,16 @@ private static bool IsHttpStatusCodeRetryable(HttpStatusCode statusCode, bool ha private static int GetRandomNumber(int min, int max) { -#if NET6_0_OR_GREATER - return Random.Shared.Next(min, max); +#if NET + return RandomNumberGenerator.GetInt32(min, max); #else // TODO: Implement this better to minimize lock contention. // Consider pulling in Random.Shared implementation. lock (Random) { +#pragma warning disable CA5394 // Do not use insecure randomness return Random.Next(min, max); +#pragma warning restore CA5394 // Do not use insecure randomness } #endif } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs deleted file mode 100644 index 07ed2ddbf64..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs +++ /dev/null @@ -1,442 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Collections.Concurrent; -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; - -internal static class MetricItemExtensions -{ - private static readonly ConcurrentBag MetricListPool = new(); - - internal static void AddMetrics( - this OtlpCollector.ExportMetricsServiceRequest request, - OtlpResource.Resource processResource, - in Batch metrics) - { - var metricsByLibrary = new Dictionary(); - var resourceMetrics = new ResourceMetrics - { - Resource = processResource, - }; - request.ResourceMetrics.Add(resourceMetrics); - - foreach (var metric in metrics) - { - var otlpMetric = metric.ToOtlpMetric(); - - // TODO: Replace null check with exception handling. - if (otlpMetric == null) - { - OpenTelemetryProtocolExporterEventSource.Log.CouldNotTranslateMetric( - nameof(MetricItemExtensions), - nameof(AddMetrics)); - continue; - } - - var meterName = metric.MeterName; - if (!metricsByLibrary.TryGetValue(meterName, out var scopeMetrics)) - { - scopeMetrics = GetMetricListFromPool(meterName, metric.MeterVersion, metric.MeterTags); - - metricsByLibrary.Add(meterName, scopeMetrics); - resourceMetrics.ScopeMetrics.Add(scopeMetrics); - } - - scopeMetrics.Metrics.Add(otlpMetric); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void Return(this OtlpCollector.ExportMetricsServiceRequest request) - { - var resourceMetrics = request.ResourceMetrics.FirstOrDefault(); - if (resourceMetrics == null) - { - return; - } - - foreach (var scopeMetrics in resourceMetrics.ScopeMetrics) - { - scopeMetrics.Metrics.Clear(); - scopeMetrics.Scope.Attributes.Clear(); - MetricListPool.Add(scopeMetrics); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static ScopeMetrics GetMetricListFromPool(string name, string version, IEnumerable> meterTags) - { - if (!MetricListPool.TryTake(out var scopeMetrics)) - { - scopeMetrics = new 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 - }, - }; - - if (meterTags != null) - { - AddScopeAttributes(meterTags, scopeMetrics.Scope.Attributes); - } - } - else - { - scopeMetrics.Scope.Name = name; - scopeMetrics.Scope.Version = version ?? string.Empty; - if (meterTags != null) - { - AddScopeAttributes(meterTags, scopeMetrics.Scope.Attributes); - } - } - - return scopeMetrics; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static OtlpMetrics.Metric ToOtlpMetric(this Metric metric) - { - var otlpMetric = new OtlpMetrics.Metric - { - Name = metric.Name, - }; - - if (metric.Description != null) - { - otlpMetric.Description = metric.Description; - } - - 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; - } - - 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 dataPoint = new NumberDataPoint - { - StartTimeUnixNano = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(), - TimeUnixNano = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(), - }; - - AddAttributes(metricPoint.Tags, dataPoint.Attributes); - - dataPoint.AsInt = metricPoint.GetSumLong(); - - if (metricPoint.TryGetExemplars(out var exemplars)) - { - foreach (ref readonly var exemplar in exemplars) - { - dataPoint.Exemplars.Add( - ToOtlpExemplar(exemplar.LongValue, in exemplar)); - } - } - - sum.DataPoints.Add(dataPoint); - } - - 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 dataPoint = new NumberDataPoint - { - StartTimeUnixNano = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(), - TimeUnixNano = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(), - }; - - AddAttributes(metricPoint.Tags, dataPoint.Attributes); - - dataPoint.AsDouble = metricPoint.GetSumDouble(); - - if (metricPoint.TryGetExemplars(out var exemplars)) - { - foreach (ref readonly var exemplar in exemplars) - { - dataPoint.Exemplars.Add( - ToOtlpExemplar(exemplar.DoubleValue, in exemplar)); - } - } - - sum.DataPoints.Add(dataPoint); - } - - otlpMetric.Sum = sum; - break; - } - - case MetricType.LongGauge: - { - var gauge = new Gauge(); - foreach (ref readonly var metricPoint in metric.GetMetricPoints()) - { - var dataPoint = new NumberDataPoint - { - StartTimeUnixNano = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(), - TimeUnixNano = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(), - }; - - AddAttributes(metricPoint.Tags, dataPoint.Attributes); - - 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)); - } - } - - gauge.DataPoints.Add(dataPoint); - } - - otlpMetric.Gauge = gauge; - break; - } - - case MetricType.DoubleGauge: - { - var gauge = new Gauge(); - foreach (ref readonly var metricPoint in metric.GetMetricPoints()) - { - var dataPoint = new NumberDataPoint - { - StartTimeUnixNano = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(), - TimeUnixNano = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(), - }; - - AddAttributes(metricPoint.Tags, dataPoint.Attributes); - - dataPoint.AsDouble = metricPoint.GetGaugeLastValueDouble(); - - if (metricPoint.TryGetExemplars(out var exemplars)) - { - foreach (ref readonly var exemplar in exemplars) - { - dataPoint.Exemplars.Add( - ToOtlpExemplar(exemplar.DoubleValue, in exemplar)); - } - } - - gauge.DataPoints.Add(dataPoint); - } - - otlpMetric.Gauge = gauge; - break; - } - - case MetricType.Histogram: - { - var histogram = new Histogram - { - AggregationTemporality = temporality, - }; - - foreach (ref readonly var metricPoint in metric.GetMetricPoints()) - { - var dataPoint = new HistogramDataPoint - { - StartTimeUnixNano = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(), - TimeUnixNano = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(), - }; - - 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; - } - - foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets()) - { - dataPoint.BucketCounts.Add((ulong)histogramMeasurement.BucketCount); - if (histogramMeasurement.ExplicitBound != double.PositiveInfinity) - { - dataPoint.ExplicitBounds.Add(histogramMeasurement.ExplicitBound); - } - } - - if (metricPoint.TryGetExemplars(out var exemplars)) - { - foreach (ref readonly var exemplar in exemplars) - { - dataPoint.Exemplars.Add( - ToOtlpExemplar(exemplar.DoubleValue, in exemplar)); - } - } - - histogram.DataPoints.Add(dataPoint); - } - - otlpMetric.Histogram = histogram; - break; - } - - case MetricType.ExponentialHistogram: - { - var histogram = new ExponentialHistogram - { - AggregationTemporality = temporality, - }; - - foreach (ref readonly var metricPoint in metric.GetMetricPoints()) - { - var dataPoint = new 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(); - - 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; - - dataPoint.Positive = new ExponentialHistogramDataPoint.Types.Buckets(); - dataPoint.Positive.Offset = exponentialHistogramData.PositiveBuckets.Offset; - foreach (var bucketCount in exponentialHistogramData.PositiveBuckets) - { - dataPoint.Positive.BucketCounts.Add((ulong)bucketCount); - } - - if (metricPoint.TryGetExemplars(out var exemplars)) - { - foreach (ref readonly var exemplar in exemplars) - { - dataPoint.Exemplars.Add( - ToOtlpExemplar(exemplar.DoubleValue, in exemplar)); - } - } - - histogram.DataPoints.Add(dataPoint); - } - - otlpMetric.ExponentialHistogram = histogram; - break; - } - } - - return otlpMetric; - } - - internal static OtlpMetrics.Exemplar ToOtlpExemplar(T value, in Metrics.Exemplar exemplar) - where T : struct - { - var otlpExemplar = new OtlpMetrics.Exemplar - { - TimeUnixNano = (ulong)exemplar.Timestamp.ToUnixTimeNanoseconds(), - }; - - if (exemplar.TraceId != default) - { - byte[] traceIdBytes = new byte[16]; - exemplar.TraceId.CopyTo(traceIdBytes); - - byte[] spanIdBytes = new byte[8]; - exemplar.SpanId.CopyTo(spanIdBytes); - - otlpExemplar.TraceId = UnsafeByteOperations.UnsafeWrap(traceIdBytes); - otlpExemplar.SpanId = UnsafeByteOperations.UnsafeWrap(spanIdBytes); - } - - 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); - } - - var otlpExemplarFilteredAttributes = otlpExemplar.FilteredAttributes; - - foreach (var tag in exemplar.FilteredTags) - { - OtlpTagWriter.Instance.TryWriteTag(ref otlpExemplarFilteredAttributes, tag); - } - - return otlpExemplar; - } - - private static void AddAttributes(ReadOnlyTagCollection tags, RepeatedField attributes) - { - foreach (var tag in tags) - { - OtlpTagWriter.Instance.TryWriteTag(ref attributes, tag); - } - } - - private static void AddScopeAttributes(IEnumerable> meterTags, RepeatedField attributes) - { - foreach (var tag in meterTags) - { - OtlpTagWriter.Instance.TryWriteTag(ref attributes, tag); - } - } -} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs index 625e8e3ecc5..c4d21d92370 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs @@ -3,6 +3,7 @@ using System.Diagnostics.Tracing; using Microsoft.Extensions.Configuration; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient.Grpc; using OpenTelemetry.Internal; namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; @@ -49,6 +50,60 @@ public void RetryStoredRequestException(Exception ex) } } + [NonEvent] + public void TransientHttpError(Uri endpoint, Exception ex) + { + if (Log.IsEnabled(EventLevel.Warning, EventKeywords.All)) + { + this.TransientHttpError(endpoint.ToString(), ex.ToInvariantString()); + } + } + + [NonEvent] + public void HttpRequestFailed(Uri endpoint, Exception ex) + { + if (Log.IsEnabled(EventLevel.Error, EventKeywords.All)) + { + this.HttpRequestFailed(endpoint.ToString(), ex.ToInvariantString()); + } + } + + [NonEvent] + public void OperationUnexpectedlyCanceled(Uri endpoint, Exception ex) + { + if (Log.IsEnabled(EventLevel.Warning, EventKeywords.All)) + { + this.OperationUnexpectedlyCanceled(endpoint.ToString(), ex.ToInvariantString()); + } + } + + [NonEvent] + public void RequestTimedOut(Uri endpoint, Exception ex) + { + if (Log.IsEnabled(EventLevel.Warning, EventKeywords.All)) + { + this.RequestTimedOut(endpoint.ToString(), ex.ToInvariantString()); + } + } + + [NonEvent] + public void GrpcRetryDelayParsingFailed(string? grpcStatusDetailsHeader, Exception ex) + { + if (Log.IsEnabled(EventLevel.Warning, EventKeywords.All)) + { + this.GrpcRetryDelayParsingFailed(grpcStatusDetailsHeader ?? "null", ex.ToInvariantString()); + } + } + + [NonEvent] + public void ExportFailure(Uri endpoint, string message, Status status) + { + if (Log.IsEnabled(EventLevel.Error, EventKeywords.All)) + { + this.ExportFailure(endpoint.ToString(), message, status.ToString()); + } + } + [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) { @@ -109,6 +164,78 @@ public void RetryStoredRequestException(string ex) this.WriteEvent(13, ex); } + [Event(14, Message = "{0} buffer exceeded the maximum allowed size. Current size: {1} bytes.", Level = EventLevel.Error)] + public void BufferExceededMaxSize(string signalType, int length) + { + this.WriteEvent(14, signalType, length); + } + + [Event(15, Message = "{0} buffer resizing failed due to insufficient memory.", Level = EventLevel.Error)] + public void BufferResizeFailedDueToMemory(string signalType) + { + this.WriteEvent(15, signalType); + } + + [Event(16, Message = "Transient HTTP error occurred when communicating with {0}. Exception: {1}", Level = EventLevel.Warning)] + public void TransientHttpError(string endpoint, string exceptionMessage) + { + this.WriteEvent(16, endpoint, exceptionMessage); + } + + [Event(17, Message = "HTTP request to {0} failed. Exception: {1}", Level = EventLevel.Error)] + public void HttpRequestFailed(string endpoint, string exceptionMessage) + { + this.WriteEvent(17, endpoint, exceptionMessage); + } + + [Event(18, Message = "Operation unexpectedly canceled for endpoint {0}. Exception: {1}", Level = EventLevel.Warning)] + public void OperationUnexpectedlyCanceled(string endpoint, string exceptionMessage) + { + this.WriteEvent(18, endpoint, exceptionMessage); + } + + [Event(19, Message = "Request to endpoint {0} timed out. Exception: {1}", Level = EventLevel.Warning)] + public void RequestTimedOut(string endpoint, string exceptionMessage) + { + this.WriteEvent(19, endpoint, exceptionMessage); + } + + [Event(20, Message = "Failed to deserialize response from {0}.", Level = EventLevel.Error)] + public void ResponseDeserializationFailed(string endpoint) + { + this.WriteEvent(20, endpoint); + } + + [Event(21, Message = "Export succeeded for {0}. Message: {1}", Level = EventLevel.Informational)] + public void ExportSuccess(string endpoint, string message) + { + this.WriteEvent(21, endpoint, message); + } + + [Event(22, Message = "Export encountered GRPC status warning for {0}. Status code: {1}", Level = EventLevel.Warning)] + public void GrpcStatusWarning(string endpoint, string statusCode) + { + this.WriteEvent(22, endpoint, statusCode); + } + + [Event(23, Message = "Export failed for {0}. Message: {1}. {2}.", Level = EventLevel.Error)] + public void ExportFailure(string endpoint, string message, string statusString) + { + this.WriteEvent(23, endpoint, message, statusString); + } + + [Event(24, Message = "Failed to parse gRPC retry delay from header grpcStatusDetailsHeader: '{0}'. Exception: {1}", Level = EventLevel.Warning)] + public void GrpcRetryDelayParsingFailed(string grpcStatusDetailsHeader, string exception) + { + this.WriteEvent(24, grpcStatusDetailsHeader, exception); + } + + [Event(25, Message = "The array tag buffer exceeded the maximum allowed size. The array tag value was replaced with 'TRUNCATED'", Level = EventLevel.Warning)] + public void ArrayBufferExceededMaxSize() + { + this.WriteEvent(25); + } + void IConfigurationExtensionsLogger.LogInvalidConfigurationValue(string key, string value) { this.InvalidConfigurationValue(key, value); diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpExporterOptionsConfigurationType.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpExporterOptionsConfigurationType.cs index d3cedd6915c..0fb01edc253 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpExporterOptionsConfigurationType.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpExporterOptionsConfigurationType.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - namespace OpenTelemetry.Exporter; [Flags] diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpLogRecordTransformer.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpLogRecordTransformer.cs deleted file mode 100644 index e3d70c40181..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpLogRecordTransformer.cs +++ /dev/null @@ -1,281 +0,0 @@ -// 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, attributeCountLimit, attributeValueLengthLimit); - } - } - - if (logRecord.Exception != null) - { - AddStringAttribute(otlpLogRecord, SemanticConventions.AttributeExceptionType, logRecord.Exception.GetType().Name, attributeCountLimit, attributeValueLengthLimit); - AddStringAttribute(otlpLogRecord, SemanticConventions.AttributeExceptionMessage, logRecord.Exception.Message, attributeCountLimit, attributeValueLengthLimit); - AddStringAttribute(otlpLogRecord, SemanticConventions.AttributeExceptionStacktrace, logRecord.Exception.ToInvariantString(), attributeCountLimit, attributeValueLengthLimit); - } - - 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 - { - AddAttribute(otlpLogRecord, attribute, attributeCountLimit, attributeValueLengthLimit); - } - } - - // 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 - { - AddAttribute(otlpLog, scopeItem, attributeCountLimit, attributeValueLengthLimit); - } - } - } - } - catch (Exception ex) - { - OpenTelemetryProtocolExporterEventSource.Log.CouldNotTranslateLogRecord(ex.Message); - } - - return otlpLogRecord; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void AddAttribute(OtlpLogs.LogRecord logRecord, KeyValuePair attribute, int maxAttributeCount, int? maxValueLength) - { - var logRecordAttributes = logRecord.Attributes; - - if (logRecordAttributes.Count == maxAttributeCount) - { - logRecord.DroppedAttributesCount++; - } - else - { - OtlpTagWriter.Instance.TryWriteTag(ref logRecordAttributes, attribute, maxValueLength); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void AddStringAttribute(OtlpLogs.LogRecord logRecord, string key, string value, int maxAttributeCount, int? maxValueLength) - { - var attributeItem = new KeyValuePair(key, value); - - AddAttribute(logRecord, attributeItem, maxAttributeCount, maxValueLength); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void AddIntAttribute(OtlpLogs.LogRecord logRecord, string key, int value, int maxAttributeCount) - { - var attributeItem = new KeyValuePair(key, value); - - AddAttribute(logRecord, attributeItem, maxAttributeCount, maxValueLength: null); - } - - [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/OtlpServiceCollectionExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpServiceCollectionExtensions.cs index 282c84ed271..79c7ddb4764 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpServiceCollectionExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpServiceCollectionExtensions.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpTagWriter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpTagWriter.cs deleted file mode 100644 index 64315fd8e0a..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpTagWriter.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -#nullable enable - -using Google.Protobuf.Collections; -using OpenTelemetry.Internal; -using OtlpCommon = OpenTelemetry.Proto.Common.V1; - -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; - -internal sealed class OtlpTagWriter : TagWriter, OtlpCommon.ArrayValue> -{ - private OtlpTagWriter() - : base(new OtlpArrayTagWriter()) - { - } - - public static OtlpTagWriter Instance { get; } = new(); - - internal static OtlpCommon.AnyValue ToAnyValue(long value) - => new() { IntValue = value }; - - internal static OtlpCommon.AnyValue ToAnyValue(double value) - => new() { DoubleValue = value }; - - internal static OtlpCommon.AnyValue ToAnyValue(bool value) - => new() { BoolValue = value }; - - internal static OtlpCommon.AnyValue ToAnyValue(string value) - => new() { StringValue = value }; - - protected override void WriteIntegralTag(ref RepeatedField tags, string key, long value) - { - tags.Add(new OtlpCommon.KeyValue { Key = key, Value = ToAnyValue(value) }); - } - - protected override void WriteFloatingPointTag(ref RepeatedField tags, string key, double value) - { - tags.Add(new OtlpCommon.KeyValue { Key = key, Value = ToAnyValue(value) }); - } - - protected override void WriteBooleanTag(ref RepeatedField tags, string key, bool value) - { - tags.Add(new OtlpCommon.KeyValue { Key = key, Value = ToAnyValue(value) }); - } - - protected override void WriteStringTag(ref RepeatedField tags, string key, ReadOnlySpan value) - { - tags.Add(new OtlpCommon.KeyValue { Key = key, Value = ToAnyValue(value.ToString()) }); - } - - protected override void WriteArrayTag(ref RepeatedField tags, string key, ref OtlpCommon.ArrayValue value) - { - tags.Add(new OtlpCommon.KeyValue - { - Key = key, - Value = new OtlpCommon.AnyValue - { - ArrayValue = value, - }, - }); - } - - protected override void OnUnsupportedTagDropped( - string tagKey, - string tagValueTypeFullName) - { - OpenTelemetryProtocolExporterEventSource.Log.UnsupportedAttributeType( - tagValueTypeFullName, - tagKey); - } - - private sealed class OtlpArrayTagWriter : ArrayTagWriter - { - public override OtlpCommon.ArrayValue BeginWriteArray() => new(); - - public override void WriteNullValue(ref OtlpCommon.ArrayValue array) - { - array.Values.Add(new OtlpCommon.AnyValue()); - } - - public override void WriteIntegralValue(ref OtlpCommon.ArrayValue array, long value) - { - array.Values.Add(ToAnyValue(value)); - } - - public override void WriteFloatingPointValue(ref OtlpCommon.ArrayValue array, double value) - { - array.Values.Add(ToAnyValue(value)); - } - - public override void WriteBooleanValue(ref OtlpCommon.ArrayValue array, bool value) - { - array.Values.Add(ToAnyValue(value)); - } - - public override void WriteStringValue(ref OtlpCommon.ArrayValue array, ReadOnlySpan value) - { - array.Values.Add(ToAnyValue(value.ToString())); - } - - public override void EndWriteArray(ref OtlpCommon.ArrayValue array) - { - } - } -} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/README.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/README.md deleted file mode 100644 index d70d44456e2..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# OpenTelemetry Protocol Implementation - -`.proto` files under `Implementation\` are copied from the -[`opentelemetry-proto`](https://github.com/open-telemetry/opentelemetry-proto/commit/1a931b4b57c34e7fd8f7dddcaa9b7587840e9c08) -repo. - -Trace proto files marked as stable. diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ResourceExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ResourceExtensions.cs deleted file mode 100644 index 656fa4eea6a..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ResourceExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright The OpenTelemetry Authors -// 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; - -internal static class ResourceExtensions -{ - public static OtlpResource.Resource ToOtlpResource(this Resource resource) - { - var processResource = new OtlpResource.Resource(); - - var processResourceAttributes = processResource.Attributes; - - foreach (KeyValuePair attribute in resource.Attributes) - { - OtlpTagWriter.Instance.TryWriteTag(ref processResourceAttributes, attribute); - } - - 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 - { - Key = ResourceSemanticConventions.AttributeServiceName, - Value = new OtlpCommon.AnyValue { StringValue = serviceName }, - }); - } - - return processResource; - } -} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/.editorconfig b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/.editorconfig new file mode 100644 index 00000000000..822889f9eaa --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/.editorconfig @@ -0,0 +1,4 @@ +[*Constants.cs] +# SA1310: Field names should not contain underscore +# Justification: These names describe the nested names and properties in the .Proto file. +dotnet_diagnostic.SA1310.severity = none diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpCommonFieldNumberConstants.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpCommonFieldNumberConstants.cs new file mode 100644 index 00000000000..4d220d598e8 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpCommonFieldNumberConstants.cs @@ -0,0 +1,32 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; + +/// +/// Defines field number constants for fields defined in +/// . +/// +internal static class ProtobufOtlpCommonFieldNumberConstants +{ + // InstrumentationScope + internal const int InstrumentationScope_Name = 1; + internal const int InstrumentationScope_Version = 2; + internal const int InstrumentationScope_Attributes = 3; + internal const int InstrumentationScope_Dropped_Attributes_Count = 4; + + // KeyValue + internal const int KeyValue_Key = 1; + internal const int KeyValue_Value = 2; + + // AnyValue + internal const int AnyValue_String_Value = 1; + internal const int AnyValue_Bool_Value = 2; + internal const int AnyValue_Int_Value = 3; + internal const int AnyValue_Double_Value = 4; + internal const int AnyValue_Array_Value = 5; + internal const int AnyValue_Kvlist_Value = 6; + internal const int AnyValue_Bytes_Value = 7; + + internal const int ArrayValue_Value = 1; +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpLogFieldNumberConstants.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpLogFieldNumberConstants.cs new file mode 100644 index 00000000000..c5238b2fa6b --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpLogFieldNumberConstants.cs @@ -0,0 +1,72 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; + +/// +/// Defines field number constants for fields defined in +/// . +/// +internal static class ProtobufOtlpLogFieldNumberConstants +{ + // Logs data + internal const int LogsData_Resource_Logs = 1; + + // Resource Logs + internal const int ResourceLogs_Resource = 1; + internal const int ResourceLogs_Scope_Logs = 2; + internal const int ResourceLogs_Schema_Url = 3; + + // Resource + internal const int Resource_Attributes = 1; + + // ScopeLogs + internal const int ScopeLogs_Scope = 1; + internal const int ScopeLogs_Log_Records = 2; + internal const int ScopeLogs_Schema_Url = 3; + + // LogRecord + internal const int LogRecord_Time_Unix_Nano = 1; + internal const int LogRecord_Observed_Time_Unix_Nano = 11; + internal const int LogRecord_Severity_Number = 2; + internal const int LogRecord_Severity_Text = 3; + internal const int LogRecord_Body = 5; + internal const int LogRecord_Attributes = 6; + internal const int LogRecord_Dropped_Attributes_Count = 7; + internal const int LogRecord_Flags = 8; + internal const int LogRecord_Trace_Id = 9; + internal const int LogRecord_Span_Id = 10; + internal const int LogRecord_Event_Name = 12; + + // SeverityNumber + internal const int Severity_Number_Unspecified = 0; + internal const int Severity_Number_Trace = 1; + internal const int Severity_Number_Trace2 = 2; + internal const int Severity_Number_Trace3 = 3; + internal const int Severity_Number_Trace4 = 4; + internal const int Severity_Number_Debug = 5; + internal const int Severity_Number_Debug2 = 6; + internal const int Severity_Number_Debug3 = 7; + internal const int Severity_Number_Debug4 = 8; + internal const int Severity_Number_Info = 9; + internal const int Severity_Number_Info2 = 10; + internal const int Severity_Number_Info3 = 11; + internal const int Severity_Number_Info4 = 12; + internal const int Severity_Number_Warn = 13; + internal const int Severity_Number_Warn2 = 14; + internal const int Severity_Number_Warn3 = 15; + internal const int Severity_Number_Warn4 = 16; + internal const int Severity_Number_Error = 17; + internal const int Severity_Number_Error2 = 18; + internal const int Severity_Number_Error3 = 19; + internal const int Severity_Number_Error4 = 20; + internal const int Severity_Number_Fatal = 21; + internal const int Severity_Number_Fatal2 = 22; + internal const int Severity_Number_Fatal3 = 23; + internal const int Severity_Number_Fatal4 = 24; + + // LogRecordFlags + internal const int LogRecord_Flags_Do_Not_Use = 0; + internal const int LogRecord_Flags_Trace_Flags_Mask = 0x000000FF; +} + diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpLogSerializer.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpLogSerializer.cs new file mode 100644 index 00000000000..c6304b2324e --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpLogSerializer.cs @@ -0,0 +1,353 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using OpenTelemetry.Internal; +using OpenTelemetry.Logs; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; + +internal static class ProtobufOtlpLogSerializer +{ + private const int ReserveSizeForLength = 4; + private const int TraceIdSize = 16; + private const int SpanIdSize = 8; + + [ThreadStatic] + private static Stack>? logsListPool; + [ThreadStatic] + private static Dictionary>? scopeLogsList; + + [ThreadStatic] + private static SerializationState? threadSerializationState; + + internal static int WriteLogsData(ref byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, ExperimentalOptions experimentalOptions, Resources.Resource? resource, in Batch logRecordBatch) + { + logsListPool ??= []; + scopeLogsList ??= []; + + foreach (var logRecord in logRecordBatch) + { + var scopeName = logRecord.Logger.Name; + if (!scopeLogsList.TryGetValue(scopeName, out var logRecords)) + { + logRecords = logsListPool.Count > 0 ? logsListPool.Pop() : []; + scopeLogsList[scopeName] = logRecords; + } + + if (logRecord.Source == LogRecord.LogRecordSource.FromSharedPool) + { + Debug.Assert(logRecord.PoolReferenceCount > 0, "logRecord PoolReferenceCount value was unexpected"); + + // Note: AddReference call here prevents the LogRecord from + // being given back to the pool by Batch. + logRecord.AddReference(); + } + + logRecords.Add(logRecord); + } + + writePosition = TryWriteResourceLogs(ref buffer, writePosition, sdkLimitOptions, experimentalOptions, resource, scopeLogsList); + ReturnLogRecordListToPool(); + + return writePosition; + } + + internal static int TryWriteResourceLogs(ref byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, ExperimentalOptions experimentalOptions, Resources.Resource? resource, Dictionary> scopeLogs) + { + while (true) + { + int entryWritePosition = writePosition; + + try + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpLogFieldNumberConstants.LogsData_Resource_Logs, ProtobufWireType.LEN); + int logsDataLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + writePosition = WriteResourceLogs(buffer, writePosition, sdkLimitOptions, experimentalOptions, resource, scopeLogs); + + ProtobufSerializer.WriteReservedLength(buffer, logsDataLengthPosition, writePosition - (logsDataLengthPosition + ReserveSizeForLength)); + + // Serialization succeeded, return the final write position + return writePosition; + } + catch (Exception ex) when (ex is IndexOutOfRangeException || ex is ArgumentException) + { + // Reset write position and attempt to increase the buffer size + writePosition = entryWritePosition; + + if (!ProtobufSerializer.IncreaseBufferSize(ref buffer, OtlpSignalType.Logs)) + { + throw; + } + + // Continue the loop to retry serialization with the larger buffer + // The loop is limited by the buffer size expansion logic in IncreaseBufferSize, + // which stops at a maximum of 100 MB, ensuring this doesn't become an infinite loop + } + } + } + + internal static void ReturnLogRecordListToPool() + { + if (scopeLogsList?.Count != 0) + { + foreach (var entry in scopeLogsList!) + { + foreach (var logRecord in entry.Value) + { + if (logRecord.Source == LogRecord.LogRecordSource.FromSharedPool) + { + Debug.Assert(logRecord.PoolReferenceCount > 0, "logRecord PoolReferenceCount value was unexpected"); + + // Note: Try to return the LogRecord to the shared pool + // now that work is done. + LogRecordSharedPool.Current.Return(logRecord); + } + } + + entry.Value.Clear(); + logsListPool?.Push(entry.Value); + } + + scopeLogsList.Clear(); + } + } + + internal static int WriteResourceLogs(byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, ExperimentalOptions experimentalOptions, Resources.Resource? resource, Dictionary> scopeLogs) + { + writePosition = ProtobufOtlpResourceSerializer.WriteResource(buffer, writePosition, resource); + writePosition = WriteScopeLogs(buffer, writePosition, sdkLimitOptions, experimentalOptions, scopeLogs); + return writePosition; + } + + internal static int WriteScopeLogs(byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, ExperimentalOptions experimentalOptions, Dictionary> scopeLogs) + { + if (scopeLogs != null) + { + foreach (KeyValuePair> entry in scopeLogs) + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpLogFieldNumberConstants.ResourceLogs_Scope_Logs, ProtobufWireType.LEN); + int resourceLogsScopeLogsLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + writePosition = WriteScopeLog(buffer, writePosition, sdkLimitOptions, experimentalOptions, entry.Value[0].Logger.Name, entry.Value); + ProtobufSerializer.WriteReservedLength(buffer, resourceLogsScopeLogsLengthPosition, writePosition - (resourceLogsScopeLogsLengthPosition + ReserveSizeForLength)); + } + } + + return writePosition; + } + + internal static int WriteScopeLog(byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, ExperimentalOptions experimentalOptions, string loggerName, List logRecords) + { + var value = loggerName.AsSpan(); + var numberOfUtf8CharsInString = ProtobufSerializer.GetNumberOfUtf8CharsInString(value); + var serializedLengthSize = ProtobufSerializer.ComputeVarInt64Size((ulong)numberOfUtf8CharsInString); + + // numberOfUtf8CharsInString + tagSize + length field size. + writePosition = ProtobufSerializer.WriteTagAndLength(buffer, writePosition, numberOfUtf8CharsInString + 1 + serializedLengthSize, ProtobufOtlpLogFieldNumberConstants.ScopeLogs_Scope, ProtobufWireType.LEN); + writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpCommonFieldNumberConstants.InstrumentationScope_Name, numberOfUtf8CharsInString, value); + + for (int i = 0; i < logRecords.Count; i++) + { + writePosition = WriteLogRecord(buffer, writePosition, sdkLimitOptions, experimentalOptions, logRecords[i]); + } + + return writePosition; + } + + internal static int WriteLogRecord(byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, ExperimentalOptions experimentalOptions, LogRecord logRecord) + { + var state = threadSerializationState ??= new(); + + state.AttributeValueLengthLimit = sdkLimitOptions.LogRecordAttributeValueLengthLimit; + state.AttributeCountLimit = sdkLimitOptions.LogRecordAttributeCountLimit ?? int.MaxValue; + state.TagWriterState = new ProtobufOtlpTagWriter.OtlpTagWriterState + { + Buffer = buffer, + WritePosition = writePosition, + TagCount = 0, + DroppedTagCount = 0, + }; + + ref var otlpTagWriterState = ref state.TagWriterState; + + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.ScopeLogs_Log_Records, ProtobufWireType.LEN); + int logRecordLengthPosition = otlpTagWriterState.WritePosition; + otlpTagWriterState.WritePosition += ReserveSizeForLength; + + var timestamp = (ulong)logRecord.Timestamp.ToUnixTimeNanoseconds(); + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteFixed64WithTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.LogRecord_Time_Unix_Nano, timestamp); + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteFixed64WithTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.LogRecord_Observed_Time_Unix_Nano, timestamp); + + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteEnumWithTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.LogRecord_Severity_Number, logRecord.Severity.HasValue ? (int)logRecord.Severity : 0); + + if (!string.IsNullOrWhiteSpace(logRecord.SeverityText)) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteStringWithTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.LogRecord_Severity_Text, logRecord.SeverityText!); + } + else if (logRecord.Severity.HasValue) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteStringWithTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.LogRecord_Severity_Text, logRecord.Severity.Value.ToShortName()); + } + + if (experimentalOptions.EmitLogEventAttributes) + { + if (logRecord.EventId.Id != default) + { + AddLogAttribute(state, ExperimentalOptions.LogRecordEventIdAttribute, logRecord.EventId.Id); + } + } + + if (logRecord.Exception != null) + { + AddLogAttribute(state, SemanticConventions.AttributeExceptionType, logRecord.Exception.GetType().Name); + AddLogAttribute(state, SemanticConventions.AttributeExceptionMessage, logRecord.Exception.Message); + AddLogAttribute(state, SemanticConventions.AttributeExceptionStacktrace, logRecord.Exception.ToInvariantString()); + } + + bool bodyPopulatedFromFormattedMessage = false; + bool isLogRecordBodySet = false; + + if (logRecord.FormattedMessage != null) + { + otlpTagWriterState.WritePosition = WriteLogRecordBody(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, logRecord.FormattedMessage.AsSpan()); + bodyPopulatedFromFormattedMessage = true; + isLogRecordBodySet = 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}", StringComparison.Ordinal) && !bodyPopulatedFromFormattedMessage) + { + otlpTagWriterState.WritePosition = WriteLogRecordBody(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, (attribute.Value as string).AsSpan()); + isLogRecordBodySet = true; + } + else + { + AddLogAttribute(state, attribute); + } + } + + // Supports setting Body directly on LogRecord for the Logs Bridge API. + if (!isLogRecordBodySet && logRecord.Body != null) + { + // If {OriginalFormat} is not present in the attributes, + // use logRecord.Body if it is set. + otlpTagWriterState.WritePosition = WriteLogRecordBody(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, logRecord.Body.AsSpan()); + } + } + + if (logRecord.TraceId != default && logRecord.SpanId != default) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTagAndLength(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, TraceIdSize, ProtobufOtlpLogFieldNumberConstants.LogRecord_Trace_Id, ProtobufWireType.LEN); + otlpTagWriterState.WritePosition = ProtobufOtlpTraceSerializer.WriteTraceId(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, logRecord.TraceId); + + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTagAndLength(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, SpanIdSize, ProtobufOtlpLogFieldNumberConstants.LogRecord_Span_Id, ProtobufWireType.LEN); + otlpTagWriterState.WritePosition = ProtobufOtlpTraceSerializer.WriteSpanId(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, logRecord.SpanId); + + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteFixed32WithTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.LogRecord_Flags, (uint)logRecord.TraceFlags); + } + + if (logRecord.EventId.Name != null) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteStringWithTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.LogRecord_Event_Name, logRecord.EventId.Name!); + } + + logRecord.ForEachScope(ProcessScope, state); + + if (otlpTagWriterState.DroppedTagCount > 0) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTag(buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.LogRecord_Dropped_Attributes_Count, ProtobufWireType.VARINT); + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteVarInt32(buffer, otlpTagWriterState.WritePosition, (uint)otlpTagWriterState.DroppedTagCount); + } + + ProtobufSerializer.WriteReservedLength(otlpTagWriterState.Buffer, logRecordLengthPosition, otlpTagWriterState.WritePosition - (logRecordLengthPosition + ReserveSizeForLength)); + + return otlpTagWriterState.WritePosition; + + static void ProcessScope(LogRecordScope scope, SerializationState state) + { + foreach (var scopeItem in scope) + { + if (scopeItem.Key.Equals("{OriginalFormat}", StringComparison.Ordinal) || 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 + { + AddLogAttribute(state, scopeItem); + } + } + } + } + + private static int WriteLogRecordBody(byte[] buffer, int writePosition, ReadOnlySpan value) + { + var numberOfUtf8CharsInString = ProtobufSerializer.GetNumberOfUtf8CharsInString(value); + var serializedLengthSize = ProtobufSerializer.ComputeVarInt64Size((ulong)numberOfUtf8CharsInString); + + // length = numberOfUtf8CharsInString + tagSize + length field size. + writePosition = ProtobufSerializer.WriteTagAndLength(buffer, writePosition, numberOfUtf8CharsInString + 1 + serializedLengthSize, ProtobufOtlpLogFieldNumberConstants.LogRecord_Body, ProtobufWireType.LEN); + writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpCommonFieldNumberConstants.AnyValue_String_Value, numberOfUtf8CharsInString, value); + return writePosition; + } + + private static void AddLogAttribute(SerializationState state, KeyValuePair attribute) + { + AddLogAttribute(state, attribute.Key, attribute.Value); + } + + private static void AddLogAttribute(SerializationState state, string key, object? value) + { + if (state.TagWriterState.TagCount == state.AttributeCountLimit) + { + state.TagWriterState.DroppedTagCount++; + } + else + { + state.TagWriterState.WritePosition = ProtobufSerializer.WriteTag(state.TagWriterState.Buffer, state.TagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.LogRecord_Attributes, ProtobufWireType.LEN); + int logAttributesLengthPosition = state.TagWriterState.WritePosition; + state.TagWriterState.WritePosition += ReserveSizeForLength; + + ProtobufOtlpTagWriter.Instance.TryWriteTag(ref state.TagWriterState, key, value, state.AttributeValueLengthLimit); + + var logAttributesLength = state.TagWriterState.WritePosition - (logAttributesLengthPosition + ReserveSizeForLength); + ProtobufSerializer.WriteReservedLength(state.TagWriterState.Buffer, logAttributesLengthPosition, logAttributesLength); + state.TagWriterState.TagCount++; + } + } + + private sealed class SerializationState + { + public int? AttributeValueLengthLimit; + public int AttributeCountLimit; + public ProtobufOtlpTagWriter.OtlpTagWriterState TagWriterState; + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpMetricFieldNumberConstants.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpMetricFieldNumberConstants.cs new file mode 100644 index 00000000000..a416f6a985f --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpMetricFieldNumberConstants.cs @@ -0,0 +1,126 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; + +/// +/// Defines field number constants for fields defined in +/// . +/// +internal static class ProtobufOtlpMetricFieldNumberConstants +{ + // Metrics Data + internal const int MetricsData_Resource_Metrics = 1; + + // Resource Metrics + internal const int ResourceMetrics_Resource = 1; + internal const int ResourceMetrics_Scope_Metrics = 2; + internal const int ResourceMetrics_Schema_Url = 3; + + // Scope Metrics + internal const int ScopeMetrics_Scope = 1; + internal const int ScopeMetrics_Metrics = 2; + internal const int ScopeMetrics_Schema_Url = 3; + + // Metric + internal const int Metric_Name = 1; + internal const int Metric_Description = 2; + internal const int Metric_Unit = 3; + internal const int Metric_Data_Gauge = 5; + internal const int Metric_Data_Sum = 7; + internal const int Metric_Data_Histogram = 9; + internal const int Metric_Data_Exponential_Histogram = 10; + internal const int Metric_Data_Summary = 11; + internal const int Metric_Metadata = 12; + + // Gauge + internal const int Gauge_Data_Points = 1; + + // Sum + internal const int Sum_Data_Points = 1; + internal const int Sum_Aggregation_Temporality = 2; + internal const int Sum_Is_Monotonic = 3; + + // Histogram + internal const int Histogram_Data_Points = 1; + internal const int Histogram_Aggregation_Temporality = 2; + + // Exponential Histogram + internal const int ExponentialHistogram_Data_Points = 1; + internal const int ExponentialHistogram_Aggregation_Temporality = 2; + + // Summary + internal const int Summary_Data_Points = 1; + + // Aggregation Temporality (Enum) + internal const int Aggregation_Temporality_Unknown = 0; + internal const int Aggregation_Temporality_Delta = 1; + internal const int Aggregation_Temporality_Cumulative = 2; + + // Data Point Flags (Enum) + internal const int Data_Point_Flags_Do_Not_Use = 0; + internal const int Data_Point_Flags_No_Recorded_Value_Mask = 1; + + // Number Data Point + internal const int NumberDataPoint_Attributes = 7; + internal const int NumberDataPoint_Start_Time_Unix_Nano = 2; + internal const int NumberDataPoint_Time_Unix_Nano = 3; + internal const int NumberDataPoint_Value_As_Double = 4; + internal const int NumberDataPoint_Value_As_Int = 6; + internal const int NumberDataPoint_Exemplars = 5; + internal const int NumberDataPoint_Flags = 8; + + // Histogram Data Point + internal const int HistogramDataPoint_Attributes = 9; + internal const int HistogramDataPoint_Start_Time_Unix_Nano = 2; + internal const int HistogramDataPoint_Time_Unix_Nano = 3; + internal const int HistogramDataPoint_Count = 4; + internal const int HistogramDataPoint_Sum = 5; + internal const int HistogramDataPoint_Bucket_Counts = 6; + internal const int HistogramDataPoint_Explicit_Bounds = 7; + internal const int HistogramDataPoint_Exemplars = 8; + internal const int HistogramDataPoint_Flags = 10; + internal const int HistogramDataPoint_Min = 11; + internal const int HistogramDataPoint_Max = 12; + + // Exponential Histogram Data Point + internal const int ExponentialHistogramDataPoint_Attributes = 1; + internal const int ExponentialHistogramDataPoint_Start_Time_Unix_Nano = 2; + internal const int ExponentialHistogramDataPoint_Time_Unix_Nano = 3; + internal const int ExponentialHistogramDataPoint_Count = 4; + internal const int ExponentialHistogramDataPoint_Sum = 5; + internal const int ExponentialHistogramDataPoint_Scale = 6; + internal const int ExponentialHistogramDataPoint_Zero_Count = 7; + internal const int ExponentialHistogramDataPoint_Positive = 8; + internal const int ExponentialHistogramDataPoint_Negative = 9; + internal const int ExponentialHistogramDataPoint_Flags = 10; + internal const int ExponentialHistogramDataPoint_Exemplars = 11; + internal const int ExponentialHistogramDataPoint_Min = 12; + internal const int ExponentialHistogramDataPoint_Max = 13; + internal const int ExponentialHistogramDataPoint_Zero_Threshold = 14; + + // Exponential Histogram Data Point - Buckets (nested type) + internal const int ExponentialHistogramDataPoint_Buckets_Offset = 1; + internal const int ExponentialHistogramDataPoint_Buckets_Bucket_Counts = 2; + + // Summary Data Point + internal const int SummaryDataPoint_Attributes = 7; + internal const int SummaryDataPoint_Start_Time_Unix_Nano = 2; + internal const int SummaryDataPoint_Time_Unix_Nano = 3; + internal const int SummaryDataPoint_Count = 4; + internal const int SummaryDataPoint_Sum = 5; + internal const int SummaryDataPoint_Quantile_Values = 6; + internal const int SummaryDataPoint_Flags = 8; + + // Summary Data Point - Value At Quantiles (nested type) + internal const int SummaryDataPoint_ValueAtQuantiles_Quantile = 1; + internal const int SummaryDataPoint_ValueAtQuantiles_Value = 2; + + // Exemplar + internal const int Exemplar_Filtered_Attributes = 7; + internal const int Exemplar_Time_Unix_Nano = 2; + internal const int Exemplar_Value_As_Double = 3; + internal const int Exemplar_Value_As_Int = 6; + internal const int Exemplar_Span_Id = 4; + internal const int Exemplar_Trace_Id = 5; +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpMetricSerializer.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpMetricSerializer.cs new file mode 100644 index 00000000000..47873bc4c83 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpMetricSerializer.cs @@ -0,0 +1,516 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using OpenTelemetry.Metrics; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; + +internal static class ProtobufOtlpMetricSerializer +{ + private const int ReserveSizeForLength = 4; + private const int TraceIdSize = 16; + private const int SpanIdSize = 8; + + [ThreadStatic] + private static Stack>? metricListPool; + [ThreadStatic] + private static Dictionary>? scopeMetricsList; + + private delegate int WriteExemplarFunc(byte[] buffer, int writePosition, in Exemplar exemplar); + + internal static int WriteMetricsData(ref byte[] buffer, int writePosition, Resources.Resource? resource, in Batch batch) + { + metricListPool ??= []; + scopeMetricsList ??= []; + + foreach (var metric in batch) + { + var metricName = metric.MeterName; + if (!scopeMetricsList.TryGetValue(metricName, out var metrics)) + { + metrics = metricListPool.Count > 0 ? metricListPool.Pop() : []; + scopeMetricsList[metricName] = metrics; + } + + metrics.Add(metric); + } + + writePosition = TryWriteResourceMetrics(ref buffer, writePosition, resource, scopeMetricsList); + ReturnMetricListToPool(); + + return writePosition; + } + + internal static int TryWriteResourceMetrics(ref byte[] buffer, int writePosition, Resources.Resource? resource, Dictionary> scopeMetrics) + { + while (true) + { + int entryWritePosition = writePosition; + + try + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.MetricsData_Resource_Metrics, ProtobufWireType.LEN); + int mericsDataLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + writePosition = WriteResourceMetrics(buffer, writePosition, resource, scopeMetrics); + + ProtobufSerializer.WriteReservedLength(buffer, mericsDataLengthPosition, writePosition - (mericsDataLengthPosition + ReserveSizeForLength)); + + // Serialization succeeded, return the final write position + return writePosition; + } + catch (Exception ex) when (ex is IndexOutOfRangeException || ex is ArgumentException) + { + // Reset write position and attempt to increase the buffer size + writePosition = entryWritePosition; + + if (!ProtobufSerializer.IncreaseBufferSize(ref buffer, OtlpSignalType.Metrics)) + { + throw; + } + } + } + } + + private static void ReturnMetricListToPool() + { + if (scopeMetricsList?.Count != 0) + { + foreach (var entry in scopeMetricsList!) + { + entry.Value.Clear(); + metricListPool?.Push(entry.Value); + } + + scopeMetricsList.Clear(); + } + } + + private static int WriteResourceMetrics(byte[] buffer, int writePosition, Resources.Resource? resource, Dictionary> scopeMetrics) + { + writePosition = ProtobufOtlpResourceSerializer.WriteResource(buffer, writePosition, resource); + writePosition = WriteScopeMetrics(buffer, writePosition, scopeMetrics); + + return writePosition; + } + + private static int WriteScopeMetrics(byte[] buffer, int writePosition, Dictionary> scopeMetrics) + { + if (scopeMetrics != null) + { + foreach (KeyValuePair> entry in scopeMetrics) + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ResourceMetrics_Scope_Metrics, ProtobufWireType.LEN); + int resourceMetricsScopeMetricsLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + writePosition = WriteScopeMetric(buffer, writePosition, entry.Key, entry.Value); + + ProtobufSerializer.WriteReservedLength(buffer, resourceMetricsScopeMetricsLengthPosition, writePosition - (resourceMetricsScopeMetricsLengthPosition + ReserveSizeForLength)); + } + } + + return writePosition; + } + + private static int WriteScopeMetric(byte[] buffer, int writePosition, string meterName, List metrics) + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ScopeMetrics_Scope, ProtobufWireType.LEN); + int instrumentationScopeLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + Debug.Assert(metrics.Count > 0, "Metrics collection is not expected to be empty."); + var meterVersion = metrics[0].MeterVersion; + var meterTags = metrics[0].MeterTags; + + writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpCommonFieldNumberConstants.InstrumentationScope_Name, meterName); + if (meterVersion != null) + { + writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpCommonFieldNumberConstants.InstrumentationScope_Version, meterVersion); + } + + if (meterTags != null) + { + if (meterTags is IReadOnlyList> readonlyMeterTags) + { + for (int i = 0; i < readonlyMeterTags.Count; i++) + { + writePosition = WriteTag(buffer, writePosition, readonlyMeterTags[i], ProtobufOtlpCommonFieldNumberConstants.InstrumentationScope_Attributes); + } + } + else + { + foreach (var tag in meterTags) + { + writePosition = WriteTag(buffer, writePosition, tag, ProtobufOtlpCommonFieldNumberConstants.InstrumentationScope_Attributes); + } + } + } + + ProtobufSerializer.WriteReservedLength(buffer, instrumentationScopeLengthPosition, writePosition - (instrumentationScopeLengthPosition + ReserveSizeForLength)); + + for (int i = 0; i < metrics.Count; i++) + { + writePosition = WriteMetric(buffer, writePosition, metrics[i]); + } + + return writePosition; + } + + private static int WriteMetric(byte[] buffer, int writePosition, Metric metric) + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ScopeMetrics_Metrics, ProtobufWireType.LEN); + int metricLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Metric_Name, metric.Name); + + if (metric.Description != null) + { + writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Metric_Description, metric.Description); + } + + if (metric.Unit != null) + { + writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Metric_Unit, metric.Unit); + } + + var aggregationValue = metric.Temporality == AggregationTemporality.Cumulative + ? ProtobufOtlpMetricFieldNumberConstants.Aggregation_Temporality_Cumulative + : ProtobufOtlpMetricFieldNumberConstants.Aggregation_Temporality_Delta; + + switch (metric.MetricType) + { + case MetricType.LongSum: + case MetricType.LongSumNonMonotonic: + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Metric_Data_Sum, ProtobufWireType.LEN); + int metricTypeLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + writePosition = ProtobufSerializer.WriteBoolWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Sum_Is_Monotonic, metric.MetricType == MetricType.LongSum); + writePosition = ProtobufSerializer.WriteEnumWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Sum_Aggregation_Temporality, aggregationValue); + + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + { + var sum = metricPoint.GetSumLong(); + writePosition = WriteNumberDataPoint(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Sum_Data_Points, in metricPoint, sum); + } + + ProtobufSerializer.WriteReservedLength(buffer, metricTypeLengthPosition, writePosition - (metricTypeLengthPosition + ReserveSizeForLength)); + break; + } + + case MetricType.DoubleSum: + case MetricType.DoubleSumNonMonotonic: + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Metric_Data_Sum, ProtobufWireType.LEN); + int metricTypeLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + writePosition = ProtobufSerializer.WriteBoolWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Sum_Is_Monotonic, metric.MetricType == MetricType.DoubleSum); + writePosition = ProtobufSerializer.WriteEnumWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Sum_Aggregation_Temporality, aggregationValue); + + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + { + var sum = metricPoint.GetSumDouble(); + writePosition = WriteNumberDataPoint(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Sum_Data_Points, in metricPoint, sum); + } + + ProtobufSerializer.WriteReservedLength(buffer, metricTypeLengthPosition, writePosition - (metricTypeLengthPosition + ReserveSizeForLength)); + break; + } + + case MetricType.LongGauge: + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Metric_Data_Gauge, ProtobufWireType.LEN); + int metricTypeLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + { + var lastValue = metricPoint.GetGaugeLastValueLong(); + writePosition = WriteNumberDataPoint(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Gauge_Data_Points, in metricPoint, lastValue); + } + + ProtobufSerializer.WriteReservedLength(buffer, metricTypeLengthPosition, writePosition - (metricTypeLengthPosition + ReserveSizeForLength)); + break; + } + + case MetricType.DoubleGauge: + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Metric_Data_Gauge, ProtobufWireType.LEN); + int metricTypeLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + { + var lastValue = metricPoint.GetGaugeLastValueDouble(); + writePosition = WriteNumberDataPoint(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Gauge_Data_Points, in metricPoint, lastValue); + } + + ProtobufSerializer.WriteReservedLength(buffer, metricTypeLengthPosition, writePosition - (metricTypeLengthPosition + ReserveSizeForLength)); + break; + } + + case MetricType.Histogram: + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Metric_Data_Histogram, ProtobufWireType.LEN); + int metricTypeLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + writePosition = ProtobufSerializer.WriteEnumWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Histogram_Aggregation_Temporality, aggregationValue); + + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Histogram_Data_Points, ProtobufWireType.LEN); + int dataPointLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + var startTime = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Start_Time_Unix_Nano, startTime); + + var endTime = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Time_Unix_Nano, endTime); + + foreach (var tag in metricPoint.Tags) + { + writePosition = WriteTag(buffer, writePosition, tag, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Attributes); + } + + var count = (ulong)metricPoint.GetHistogramCount(); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Count, count); + + var sum = metricPoint.GetHistogramSum(); + writePosition = ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Sum, sum); + + if (metricPoint.TryGetHistogramMinMaxValues(out double min, out double max)) + { + writePosition = ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Min, min); + writePosition = ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Max, max); + } + + foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets()) + { + var bucketCount = (ulong)histogramMeasurement.BucketCount; + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Bucket_Counts, bucketCount); + + if (histogramMeasurement.ExplicitBound != double.PositiveInfinity) + { + writePosition = ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Explicit_Bounds, histogramMeasurement.ExplicitBound); + } + } + + writePosition = WriteDoubleExemplars(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Exemplars, in metricPoint); + + ProtobufSerializer.WriteReservedLength(buffer, dataPointLengthPosition, writePosition - (dataPointLengthPosition + ReserveSizeForLength)); + } + + ProtobufSerializer.WriteReservedLength(buffer, metricTypeLengthPosition, writePosition - (metricTypeLengthPosition + ReserveSizeForLength)); + break; + } + + case MetricType.ExponentialHistogram: + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Metric_Data_Exponential_Histogram, ProtobufWireType.LEN); + int metricTypeLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + writePosition = ProtobufSerializer.WriteEnumWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogram_Aggregation_Temporality, aggregationValue); + + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogram_Data_Points, ProtobufWireType.LEN); + int dataPointLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + var startTime = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Start_Time_Unix_Nano, startTime); + + var endTime = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Time_Unix_Nano, endTime); + + foreach (var tag in metricPoint.Tags) + { + writePosition = WriteTag(buffer, writePosition, tag, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Attributes); + } + + var sum = metricPoint.GetHistogramSum(); + writePosition = ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Sum, sum); + + var count = (ulong)metricPoint.GetHistogramCount(); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Count, count); + + if (metricPoint.TryGetHistogramMinMaxValues(out double min, out double max)) + { + writePosition = ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Min, min); + writePosition = ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Max, max); + } + + var exponentialHistogramData = metricPoint.GetExponentialHistogramData(); + + writePosition = ProtobufSerializer.WriteSInt32WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Scale, exponentialHistogramData.Scale); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Zero_Count, (ulong)exponentialHistogramData.ZeroCount); + + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Positive, ProtobufWireType.LEN); + int positiveBucketsLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + writePosition = ProtobufSerializer.WriteSInt32WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Buckets_Offset, exponentialHistogramData.PositiveBuckets.Offset); + + foreach (var bucketCount in exponentialHistogramData.PositiveBuckets) + { + writePosition = ProtobufSerializer.WriteInt64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Buckets_Bucket_Counts, (ulong)bucketCount); + } + + ProtobufSerializer.WriteReservedLength(buffer, positiveBucketsLengthPosition, writePosition - (positiveBucketsLengthPosition + ReserveSizeForLength)); + + writePosition = WriteDoubleExemplars(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Exemplars, in metricPoint); + + ProtobufSerializer.WriteReservedLength(buffer, dataPointLengthPosition, writePosition - (dataPointLengthPosition + ReserveSizeForLength)); + } + + ProtobufSerializer.WriteReservedLength(buffer, metricTypeLengthPosition, writePosition - (metricTypeLengthPosition + ReserveSizeForLength)); + break; + } + } + + ProtobufSerializer.WriteReservedLength(buffer, metricLengthPosition, writePosition - (metricLengthPosition + ReserveSizeForLength)); + return writePosition; + } + + private static int WriteNumberDataPoint(byte[] buffer, int writePosition, int fieldNumber, in MetricPoint metricPoint, long value) + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, fieldNumber, ProtobufWireType.LEN); + int dataPointLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + // Casting to ulong is ok here as the bit representation for long versus ulong will be the same + // The difference would in the way the bit representation is interpreted on decoding side (signed versus unsigned) + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Value_As_Int, (ulong)value); + + var startTime = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Start_Time_Unix_Nano, startTime); + + var endTime = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Time_Unix_Nano, endTime); + + foreach (var tag in metricPoint.Tags) + { + writePosition = WriteTag(buffer, writePosition, tag, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Attributes); + } + + if (metricPoint.TryGetExemplars(out var exemplars)) + { + foreach (ref readonly var exemplar in exemplars) + { + writePosition = WriteExemplar( + buffer, + writePosition, + in exemplar, + ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Exemplars, + static (byte[] buffer, int writePosition, in Exemplar exemplar) => ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Exemplar_Value_As_Int, (ulong)exemplar.LongValue)); + } + } + + ProtobufSerializer.WriteReservedLength(buffer, dataPointLengthPosition, writePosition - (dataPointLengthPosition + ReserveSizeForLength)); + return writePosition; + } + + private static int WriteNumberDataPoint(byte[] buffer, int writePosition, int fieldNumber, in MetricPoint metricPoint, double value) + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, fieldNumber, ProtobufWireType.LEN); + int dataPointLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + // Using a func here to avoid boxing/unboxing. + writePosition = ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Value_As_Double, value); + + var startTime = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Start_Time_Unix_Nano, startTime); + + var endTime = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Time_Unix_Nano, endTime); + + foreach (var tag in metricPoint.Tags) + { + writePosition = WriteTag(buffer, writePosition, tag, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Attributes); + } + + writePosition = WriteDoubleExemplars(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Exemplars, in metricPoint); + + ProtobufSerializer.WriteReservedLength(buffer, dataPointLengthPosition, writePosition - (dataPointLengthPosition + ReserveSizeForLength)); + return writePosition; + } + + private static int WriteTag(byte[] buffer, int writePosition, KeyValuePair tag, int fieldNumber) + { + ProtobufOtlpTagWriter.OtlpTagWriterState otlpTagWriterState = new ProtobufOtlpTagWriter.OtlpTagWriterState + { + Buffer = buffer, + WritePosition = writePosition, + }; + + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTag(buffer, otlpTagWriterState.WritePosition, fieldNumber, ProtobufWireType.LEN); + int fieldLengthPosition = otlpTagWriterState.WritePosition; + otlpTagWriterState.WritePosition += ReserveSizeForLength; + + ProtobufOtlpTagWriter.Instance.TryWriteTag(ref otlpTagWriterState, tag.Key, tag.Value); + + ProtobufSerializer.WriteReservedLength(buffer, fieldLengthPosition, otlpTagWriterState.WritePosition - (fieldLengthPosition + ReserveSizeForLength)); + return otlpTagWriterState.WritePosition; + } + + private static int WriteDoubleExemplars(byte[] buffer, int writePosition, int fieldNumber, in MetricPoint metricPoint) + { + if (metricPoint.TryGetExemplars(out var exemplars)) + { + foreach (ref readonly var exemplar in exemplars) + { + writePosition = WriteExemplar( + buffer, + writePosition, + in exemplar, + fieldNumber, + static (byte[] buffer, int writePosition, in Exemplar exemplar) => ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Exemplar_Value_As_Double, exemplar.DoubleValue)); + } + } + + return writePosition; + } + + private static int WriteExemplar(byte[] buffer, int writePosition, in Exemplar exemplar, int fieldNumber, WriteExemplarFunc writeValueFunc) + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, fieldNumber, ProtobufWireType.LEN); + int exemplarLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + foreach (var tag in exemplar.FilteredTags) + { + writePosition = WriteTag(buffer, writePosition, tag, ProtobufOtlpMetricFieldNumberConstants.Exemplar_Filtered_Attributes); + } + + writePosition = writeValueFunc(buffer, writePosition, in exemplar); + + var time = (ulong)exemplar.Timestamp.ToUnixTimeNanoseconds(); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Exemplar_Time_Unix_Nano, time); + + if (exemplar.SpanId != default) + { + writePosition = ProtobufSerializer.WriteTagAndLength(buffer, writePosition, SpanIdSize, ProtobufOtlpMetricFieldNumberConstants.Exemplar_Span_Id, ProtobufWireType.LEN); + var spanIdBytes = new Span(buffer, writePosition, SpanIdSize); + exemplar.SpanId.CopyTo(spanIdBytes); + writePosition += SpanIdSize; + + writePosition = ProtobufSerializer.WriteTagAndLength(buffer, writePosition, TraceIdSize, ProtobufOtlpMetricFieldNumberConstants.Exemplar_Trace_Id, ProtobufWireType.LEN); + var traceIdBytes = new Span(buffer, writePosition, TraceIdSize); + exemplar.TraceId.CopyTo(traceIdBytes); + writePosition += TraceIdSize; + } + + ProtobufSerializer.WriteReservedLength(buffer, exemplarLengthPosition, writePosition - (exemplarLengthPosition + ReserveSizeForLength)); + return writePosition; + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpResourceSerializer.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpResourceSerializer.cs new file mode 100644 index 00000000000..d12099bec02 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpResourceSerializer.cs @@ -0,0 +1,59 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Resources; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; + +internal static class ProtobufOtlpResourceSerializer +{ + private const int ReserveSizeForLength = 4; + + internal static int WriteResource(byte[] buffer, int writePosition, Resource? resource) + { + ProtobufOtlpTagWriter.OtlpTagWriterState otlpTagWriterState = new ProtobufOtlpTagWriter.OtlpTagWriterState + { + Buffer = buffer, + WritePosition = writePosition, + }; + + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpTraceFieldNumberConstants.ResourceSpans_Resource, ProtobufWireType.LEN); + int resourceLengthPosition = otlpTagWriterState.WritePosition; + otlpTagWriterState.WritePosition += ReserveSizeForLength; + + if (resource != null && resource != Resource.Empty) + { + if (resource.Attributes is IReadOnlyList> resourceAttributesList) + { + for (int i = 0; i < resourceAttributesList.Count; i++) + { + ProcessResourceAttribute(ref otlpTagWriterState, resourceAttributesList[i]); + } + } + else + { + foreach (var attribute in resource.Attributes) + { + ProcessResourceAttribute(ref otlpTagWriterState, attribute); + } + } + } + + var resourceLength = otlpTagWriterState.WritePosition - (resourceLengthPosition + ReserveSizeForLength); + ProtobufSerializer.WriteReservedLength(otlpTagWriterState.Buffer, resourceLengthPosition, resourceLength); + + return otlpTagWriterState.WritePosition; + } + + private static void ProcessResourceAttribute(ref ProtobufOtlpTagWriter.OtlpTagWriterState otlpTagWriterState, KeyValuePair attribute) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpTraceFieldNumberConstants.Resource_Attributes, ProtobufWireType.LEN); + int resourceAttributesLengthPosition = otlpTagWriterState.WritePosition; + otlpTagWriterState.WritePosition += ReserveSizeForLength; + + ProtobufOtlpTagWriter.Instance.TryWriteTag(ref otlpTagWriterState, attribute.Key, attribute.Value); + + var resourceAttributesLength = otlpTagWriterState.WritePosition - (resourceAttributesLengthPosition + ReserveSizeForLength); + ProtobufSerializer.WriteReservedLength(otlpTagWriterState.Buffer, resourceAttributesLengthPosition, resourceAttributesLength); + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTagWriter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTagWriter.cs new file mode 100644 index 00000000000..04e60fa0db7 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTagWriter.cs @@ -0,0 +1,199 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; + +internal sealed class ProtobufOtlpTagWriter : TagWriter +{ + private ProtobufOtlpTagWriter() + : base(new OtlpArrayTagWriter()) + { + } + + public static ProtobufOtlpTagWriter Instance { get; } = new(); + + protected override void WriteIntegralTag(ref OtlpTagWriterState state, string key, long value) + { + // Write KeyValue tag + state.WritePosition = ProtobufSerializer.WriteStringWithTag(state.Buffer, state.WritePosition, ProtobufOtlpCommonFieldNumberConstants.KeyValue_Key, key); + + // Write KeyValue.Value tag, length and value. + var size = ProtobufSerializer.ComputeVarInt64Size((ulong)value) + 1; // ComputeVarint64Size(ulong) + TagSize + state.WritePosition = ProtobufSerializer.WriteTagAndLength(state.Buffer, state.WritePosition, size, ProtobufOtlpCommonFieldNumberConstants.KeyValue_Value, ProtobufWireType.LEN); + state.WritePosition = ProtobufSerializer.WriteInt64WithTag(state.Buffer, state.WritePosition, ProtobufOtlpCommonFieldNumberConstants.AnyValue_Int_Value, (ulong)value); + } + + protected override void WriteFloatingPointTag(ref OtlpTagWriterState state, string key, double value) + { + // Write KeyValue tag + state.WritePosition = ProtobufSerializer.WriteStringWithTag(state.Buffer, state.WritePosition, ProtobufOtlpCommonFieldNumberConstants.KeyValue_Key, key); + + // Write KeyValue.Value tag, length and value. + state.WritePosition = ProtobufSerializer.WriteTagAndLength(state.Buffer, state.WritePosition, 9, ProtobufOtlpCommonFieldNumberConstants.KeyValue_Value, ProtobufWireType.LEN); // 8 + TagSize + state.WritePosition = ProtobufSerializer.WriteDoubleWithTag(state.Buffer, state.WritePosition, ProtobufOtlpCommonFieldNumberConstants.AnyValue_Double_Value, value); + } + + protected override void WriteBooleanTag(ref OtlpTagWriterState state, string key, bool value) + { + // Write KeyValue tag + state.WritePosition = ProtobufSerializer.WriteStringWithTag(state.Buffer, state.WritePosition, ProtobufOtlpCommonFieldNumberConstants.KeyValue_Key, key); + + // Write KeyValue.Value tag, length and value. + state.WritePosition = ProtobufSerializer.WriteTagAndLength(state.Buffer, state.WritePosition, 2, ProtobufOtlpCommonFieldNumberConstants.KeyValue_Value, ProtobufWireType.LEN); // 1 + TagSize + state.WritePosition = ProtobufSerializer.WriteBoolWithTag(state.Buffer, state.WritePosition, ProtobufOtlpCommonFieldNumberConstants.AnyValue_Bool_Value, value); + } + + protected override void WriteStringTag(ref OtlpTagWriterState state, string key, ReadOnlySpan value) + { + // Write KeyValue tag + state.WritePosition = ProtobufSerializer.WriteStringWithTag(state.Buffer, state.WritePosition, ProtobufOtlpCommonFieldNumberConstants.KeyValue_Key, key); + + // Write KeyValue.Value tag, length and value. + var numberOfUtf8CharsInString = ProtobufSerializer.GetNumberOfUtf8CharsInString(value); + var serializedLengthSize = ProtobufSerializer.ComputeVarInt64Size((ulong)numberOfUtf8CharsInString); + + // length = numberOfUtf8CharsInString + tagSize + length field size. + state.WritePosition = ProtobufSerializer.WriteTagAndLength(state.Buffer, state.WritePosition, numberOfUtf8CharsInString + 1 + serializedLengthSize, ProtobufOtlpCommonFieldNumberConstants.KeyValue_Value, ProtobufWireType.LEN); + state.WritePosition = ProtobufSerializer.WriteStringWithTag(state.Buffer, state.WritePosition, ProtobufOtlpCommonFieldNumberConstants.AnyValue_String_Value, numberOfUtf8CharsInString, value); + } + + protected override void WriteArrayTag(ref OtlpTagWriterState state, string key, ref OtlpTagWriterArrayState value) + { + // Write KeyValue tag + state.WritePosition = ProtobufSerializer.WriteStringWithTag(state.Buffer, state.WritePosition, ProtobufOtlpCommonFieldNumberConstants.KeyValue_Key, key); + + // Write KeyValue.Value tag and length + var serializedLengthSize = ProtobufSerializer.ComputeVarInt64Size((ulong)value.WritePosition); + state.WritePosition = ProtobufSerializer.WriteTagAndLength(state.Buffer, state.WritePosition, value.WritePosition + 1 + serializedLengthSize, ProtobufOtlpCommonFieldNumberConstants.KeyValue_Value, ProtobufWireType.LEN); // Array content length + Array tag size + length field size + + // Write Array tag and length + state.WritePosition = ProtobufSerializer.WriteTagAndLength(state.Buffer, state.WritePosition, value.WritePosition, ProtobufOtlpCommonFieldNumberConstants.AnyValue_Array_Value, ProtobufWireType.LEN); + Buffer.BlockCopy(value.Buffer, 0, state.Buffer, state.WritePosition, value.WritePosition); + state.WritePosition += value.WritePosition; + } + + protected override void OnUnsupportedTagDropped( + string tagKey, + string tagValueTypeFullName) => OpenTelemetryProtocolExporterEventSource.Log.UnsupportedAttributeType( + tagValueTypeFullName, + tagKey); + + protected override bool TryWriteEmptyTag(ref OtlpTagWriterState state, string key, object? value) + { + state.WritePosition = ProtobufSerializer.WriteStringWithTag(state.Buffer, state.WritePosition, ProtobufOtlpCommonFieldNumberConstants.KeyValue_Key, key); + state.WritePosition = ProtobufSerializer.WriteTagAndLength(state.Buffer, state.WritePosition, 0, ProtobufOtlpCommonFieldNumberConstants.KeyValue_Value, ProtobufWireType.LEN); + return true; + } + + protected override bool TryWriteByteArrayTag(ref OtlpTagWriterState state, string key, ReadOnlySpan value) + { + // Write KeyValue tag + state.WritePosition = ProtobufSerializer.WriteStringWithTag(state.Buffer, state.WritePosition, ProtobufOtlpCommonFieldNumberConstants.KeyValue_Key, key); + + var serializedLengthSize = ProtobufSerializer.ComputeVarInt64Size((ulong)value.Length); + + // length = value.Length + tagSize + length field size. + state.WritePosition = ProtobufSerializer.WriteTagAndLength(state.Buffer, state.WritePosition, value.Length + 1 + serializedLengthSize, ProtobufOtlpCommonFieldNumberConstants.KeyValue_Value, ProtobufWireType.LEN); + state.WritePosition = ProtobufSerializer.WriteByteArrayWithTag(state.Buffer, state.WritePosition, ProtobufOtlpCommonFieldNumberConstants.AnyValue_Bytes_Value, value); + + return true; + } + + internal struct OtlpTagWriterState + { + public byte[] Buffer; + public int DroppedTagCount; + public int TagCount; + public int WritePosition; + } + + internal struct OtlpTagWriterArrayState + { + public byte[] Buffer; + public int WritePosition; + } + + internal sealed class OtlpArrayTagWriter : ArrayTagWriter + { + [ThreadStatic] + internal static byte[]? ThreadBuffer; + private const int MaxBufferSize = 2 * 1024 * 1024; + + public override OtlpTagWriterArrayState BeginWriteArray() + { + ThreadBuffer ??= new byte[2048]; + + return new OtlpTagWriterArrayState + { + Buffer = ThreadBuffer, + WritePosition = 0, + }; + } + + public override void WriteNullValue(ref OtlpTagWriterArrayState state) + { + state.WritePosition = ProtobufSerializer.WriteTagAndLength(state.Buffer, state.WritePosition, 0, ProtobufOtlpCommonFieldNumberConstants.ArrayValue_Value, ProtobufWireType.LEN); + } + + public override void WriteIntegralValue(ref OtlpTagWriterArrayState state, long value) + { + var size = ProtobufSerializer.ComputeVarInt64Size((ulong)value) + 1; + state.WritePosition = ProtobufSerializer.WriteTagAndLength(state.Buffer, state.WritePosition, size, ProtobufOtlpCommonFieldNumberConstants.ArrayValue_Value, ProtobufWireType.LEN); + state.WritePosition = ProtobufSerializer.WriteInt64WithTag(state.Buffer, state.WritePosition, ProtobufOtlpCommonFieldNumberConstants.AnyValue_Int_Value, (ulong)value); + } + + public override void WriteFloatingPointValue(ref OtlpTagWriterArrayState state, double value) + { + state.WritePosition = ProtobufSerializer.WriteTagAndLength(state.Buffer, state.WritePosition, 9, ProtobufOtlpCommonFieldNumberConstants.ArrayValue_Value, ProtobufWireType.LEN); + state.WritePosition = ProtobufSerializer.WriteDoubleWithTag(state.Buffer, state.WritePosition, ProtobufOtlpCommonFieldNumberConstants.AnyValue_Double_Value, value); + } + + public override void WriteBooleanValue(ref OtlpTagWriterArrayState state, bool value) + { + state.WritePosition = ProtobufSerializer.WriteTagAndLength(state.Buffer, state.WritePosition, 2, ProtobufOtlpCommonFieldNumberConstants.ArrayValue_Value, ProtobufWireType.LEN); + state.WritePosition = ProtobufSerializer.WriteBoolWithTag(state.Buffer, state.WritePosition, ProtobufOtlpCommonFieldNumberConstants.AnyValue_Bool_Value, value); + } + + public override void WriteStringValue(ref OtlpTagWriterArrayState state, ReadOnlySpan value) + { + // Write KeyValue.Value tag, length and value. + var numberOfUtf8CharsInString = ProtobufSerializer.GetNumberOfUtf8CharsInString(value); + var serializedLengthSize = ProtobufSerializer.ComputeVarInt64Size((ulong)numberOfUtf8CharsInString); + + // length = numberOfUtf8CharsInString + tagSize + length field size. + state.WritePosition = ProtobufSerializer.WriteTagAndLength(state.Buffer, state.WritePosition, numberOfUtf8CharsInString + 1 + serializedLengthSize, ProtobufOtlpCommonFieldNumberConstants.ArrayValue_Value, ProtobufWireType.LEN); + state.WritePosition = ProtobufSerializer.WriteStringWithTag(state.Buffer, state.WritePosition, ProtobufOtlpCommonFieldNumberConstants.AnyValue_String_Value, numberOfUtf8CharsInString, value); + } + + public override void EndWriteArray(ref OtlpTagWriterArrayState state) + { + } + + public override bool TryResize() + { + var buffer = ThreadBuffer; + + Debug.Assert(buffer != null, "buffer was null"); + + if (buffer!.Length >= MaxBufferSize) + { + OpenTelemetryProtocolExporterEventSource.Log.ArrayBufferExceededMaxSize(); + return false; + } + + try + { + ThreadBuffer = new byte[buffer.Length * 2]; + return true; + } + catch (OutOfMemoryException) + { + OpenTelemetryProtocolExporterEventSource.Log.BufferResizeFailedDueToMemory(nameof(OtlpArrayTagWriter)); + return false; + } + } + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTraceFieldNumberConstants.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTraceFieldNumberConstants.cs new file mode 100644 index 00000000000..67daf09a48c --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTraceFieldNumberConstants.cs @@ -0,0 +1,76 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; + +/// +/// Defines field number constants for fields defined in +/// . +/// +internal static class ProtobufOtlpTraceFieldNumberConstants +{ + // Traces data + internal const int TracesData_Resource_Spans = 1; + + // Resource spans + internal const int ResourceSpans_Resource = 1; + internal const int ResourceSpans_Scope_Spans = 2; + internal const int ResourceSpans_Schema_Url = 3; + + // Resource + internal const int Resource_Attributes = 1; + + // ScopeSpans + internal const int ScopeSpans_Scope = 1; + internal const int ScopeSpans_Span = 2; + internal const int ScopeSpans_Schema_Url = 3; + + // Span + internal const int Span_Trace_Id = 1; + internal const int Span_Span_Id = 2; + internal const int Span_Trace_State = 3; + internal const int Span_Parent_Span_Id = 4; + internal const int Span_Name = 5; + internal const int Span_Kind = 6; + internal const int Span_Start_Time_Unix_Nano = 7; + internal const int Span_End_Time_Unix_Nano = 8; + internal const int Span_Attributes = 9; + internal const int Span_Dropped_Attributes_Count = 10; + internal const int Span_Events = 11; + internal const int Span_Dropped_Events_Count = 12; + internal const int Span_Links = 13; + internal const int Span_Dropped_Links_Count = 14; + internal const int Span_Status = 15; + internal const int Span_Flags = 16; + + // SpanKind + internal const int SpanKind_Internal = 2; + internal const int SpanKind_Server = 3; + internal const int SpanKind_Client = 4; + internal const int SpanKind_Producer = 5; + internal const int SpanKind_Consumer = 6; + + // Events + internal const int Event_Time_Unix_Nano = 1; + internal const int Event_Name = 2; + internal const int Event_Attributes = 3; + internal const int Event_Dropped_Attributes_Count = 4; + + // Links + internal const int Link_Trace_Id = 1; + internal const int Link_Span_Id = 2; + internal const int Link_Trace_State = 3; + internal const int Link_Attributes = 4; + internal const int Link_Dropped_Attributes_Count = 5; + internal const int Link_Flags = 6; + + // Status + internal const int Status_Message = 2; + internal const int Status_Code = 3; + + // StatusCode + internal const int StatusCode_Unset = 0; + internal const int StatusCode_Ok = 1; + internal const int StatusCode_Error = 2; +} + diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTraceSerializer.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTraceSerializer.cs new file mode 100644 index 00000000000..7fb7eb2b5ad --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTraceSerializer.cs @@ -0,0 +1,528 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; + +internal static class ProtobufOtlpTraceSerializer +{ + private const int ReserveSizeForLength = 4; + private const string UnsetStatusCodeTagValue = "UNSET"; + private const string OkStatusCodeTagValue = "OK"; + private const string ErrorStatusCodeTagValue = "ERROR"; + private const int TraceIdSize = 16; + private const int SpanIdSize = 8; + + [ThreadStatic] + private static Stack>? activityListPool; + [ThreadStatic] + private static Dictionary>? scopeTracesList; + + internal static int WriteTraceData(ref byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, Resources.Resource? resource, in Batch batch) + { + activityListPool ??= []; + scopeTracesList ??= []; + + foreach (var activity in batch) + { + var sourceName = activity.Source.Name; + if (!scopeTracesList.TryGetValue(sourceName, out var activities)) + { + activities = activityListPool.Count > 0 ? activityListPool.Pop() : []; + scopeTracesList[sourceName] = activities; + } + + activities.Add(activity); + } + + writePosition = TryWriteResourceSpans(ref buffer, writePosition, sdkLimitOptions, resource); + ReturnActivityListToPool(); + + return writePosition; + } + + internal static int TryWriteResourceSpans(ref byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, Resources.Resource? resource) + { + while (true) + { + int entryWritePosition = writePosition; + + try + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpTraceFieldNumberConstants.TracesData_Resource_Spans, ProtobufWireType.LEN); + int resourceSpansScopeSpansLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + writePosition = WriteResourceSpans(buffer, writePosition, sdkLimitOptions, resource); + + ProtobufSerializer.WriteReservedLength(buffer, resourceSpansScopeSpansLengthPosition, writePosition - (resourceSpansScopeSpansLengthPosition + ReserveSizeForLength)); + + // Serialization succeeded, return the final write position + return writePosition; + } + catch (Exception ex) when (ex is IndexOutOfRangeException || ex is ArgumentException) + { + // Reset write position and attempt to increase the buffer size + writePosition = entryWritePosition; + + if (!ProtobufSerializer.IncreaseBufferSize(ref buffer, OtlpSignalType.Traces)) + { + throw; + } + + // Continue the loop to retry serialization with the larger buffer + // The loop is limited by the buffer size expansion logic in IncreaseBufferSize, + // which stops at a maximum of 100 MB, ensuring this doesn't become an infinite loop + } + } + } + + internal static void ReturnActivityListToPool() + { + if (scopeTracesList?.Count != 0) + { + foreach (var entry in scopeTracesList!) + { + entry.Value.Clear(); + activityListPool?.Push(entry.Value); + } + + scopeTracesList.Clear(); + } + } + + internal static int WriteResourceSpans(byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, Resources.Resource? resource) + { + writePosition = ProtobufOtlpResourceSerializer.WriteResource(buffer, writePosition, resource); + writePosition = WriteScopeSpans(buffer, writePosition, sdkLimitOptions); + + return writePosition; + } + + internal static int WriteScopeSpans(byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions) + { + if (scopeTracesList != null) + { + foreach (KeyValuePair> entry in scopeTracesList) + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpTraceFieldNumberConstants.ResourceSpans_Scope_Spans, ProtobufWireType.LEN); + int resourceSpansScopeSpansLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + writePosition = WriteScopeSpan(buffer, writePosition, sdkLimitOptions, entry.Value[0].Source, entry.Value); + ProtobufSerializer.WriteReservedLength(buffer, resourceSpansScopeSpansLengthPosition, writePosition - (resourceSpansScopeSpansLengthPosition + ReserveSizeForLength)); + } + } + + return writePosition; + } + + internal static int WriteScopeSpan(byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, ActivitySource activitySource, List activities) + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpTraceFieldNumberConstants.ScopeSpans_Scope, ProtobufWireType.LEN); + int instrumentationScopeLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpCommonFieldNumberConstants.InstrumentationScope_Name, activitySource.Name); + if (activitySource.Version != null) + { + writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpCommonFieldNumberConstants.InstrumentationScope_Version, activitySource.Version); + } + + if (activitySource.Tags != null) + { + var maxAttributeCount = sdkLimitOptions.SpanAttributeCountLimit ?? int.MaxValue; + var maxAttributeValueLength = sdkLimitOptions.AttributeValueLengthLimit ?? int.MaxValue; + ProtobufOtlpTagWriter.OtlpTagWriterState otlpTagWriterState = new ProtobufOtlpTagWriter.OtlpTagWriterState + { + Buffer = buffer, + WritePosition = writePosition, + TagCount = 0, + DroppedTagCount = 0, + }; + + if (activitySource.Tags is IReadOnlyList> activitySourceTagsList) + { + for (int i = 0; i < activitySourceTagsList.Count; i++) + { + if (otlpTagWriterState.TagCount < maxAttributeCount) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpCommonFieldNumberConstants.InstrumentationScope_Attributes, ProtobufWireType.LEN); + int instrumentationScopeAttributesLengthPosition = otlpTagWriterState.WritePosition; + otlpTagWriterState.WritePosition += ReserveSizeForLength; + + ProtobufOtlpTagWriter.Instance.TryWriteTag(ref otlpTagWriterState, activitySourceTagsList[i].Key, activitySourceTagsList[i].Value, maxAttributeValueLength); + + var instrumentationScopeAttributesLength = otlpTagWriterState.WritePosition - (instrumentationScopeAttributesLengthPosition + ReserveSizeForLength); + ProtobufSerializer.WriteReservedLength(otlpTagWriterState.Buffer, instrumentationScopeAttributesLengthPosition, instrumentationScopeAttributesLength); + otlpTagWriterState.TagCount++; + } + else + { + otlpTagWriterState.DroppedTagCount++; + } + } + } + else + { + foreach (var tag in activitySource.Tags) + { + if (otlpTagWriterState.TagCount < maxAttributeCount) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpCommonFieldNumberConstants.InstrumentationScope_Attributes, ProtobufWireType.LEN); + int instrumentationScopeAttributesLengthPosition = otlpTagWriterState.WritePosition; + otlpTagWriterState.WritePosition += ReserveSizeForLength; + + ProtobufOtlpTagWriter.Instance.TryWriteTag(ref otlpTagWriterState, tag.Key, tag.Value, maxAttributeValueLength); + + var instrumentationScopeAttributesLength = otlpTagWriterState.WritePosition - (instrumentationScopeAttributesLengthPosition + ReserveSizeForLength); + ProtobufSerializer.WriteReservedLength(otlpTagWriterState.Buffer, instrumentationScopeAttributesLengthPosition, instrumentationScopeAttributesLength); + otlpTagWriterState.TagCount++; + } + else + { + otlpTagWriterState.DroppedTagCount++; + } + } + } + + if (otlpTagWriterState.DroppedTagCount > 0) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTag(buffer, otlpTagWriterState.WritePosition, ProtobufOtlpCommonFieldNumberConstants.InstrumentationScope_Dropped_Attributes_Count, ProtobufWireType.VARINT); + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteVarInt32(buffer, otlpTagWriterState.WritePosition, (uint)otlpTagWriterState.DroppedTagCount); + } + + writePosition = otlpTagWriterState.WritePosition; + } + + ProtobufSerializer.WriteReservedLength(buffer, instrumentationScopeLengthPosition, writePosition - (instrumentationScopeLengthPosition + ReserveSizeForLength)); + + for (int i = 0; i < activities.Count; i++) + { + writePosition = WriteSpan(buffer, writePosition, sdkLimitOptions, activities[i]); + } + + return writePosition; + } + + internal static int WriteSpan(byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, Activity activity) + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpTraceFieldNumberConstants.ScopeSpans_Span, ProtobufWireType.LEN); + int spanLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + writePosition = ProtobufSerializer.WriteTagAndLength(buffer, writePosition, TraceIdSize, ProtobufOtlpTraceFieldNumberConstants.Span_Trace_Id, ProtobufWireType.LEN); + writePosition = WriteTraceId(buffer, writePosition, activity.TraceId); + + writePosition = ProtobufSerializer.WriteTagAndLength(buffer, writePosition, SpanIdSize, ProtobufOtlpTraceFieldNumberConstants.Span_Span_Id, ProtobufWireType.LEN); + writePosition = WriteSpanId(buffer, writePosition, activity.SpanId); + + if (activity.TraceStateString != null) + { + writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpTraceFieldNumberConstants.Span_Trace_State, activity.TraceStateString); + } + + if (activity.ParentSpanId != default) + { + writePosition = ProtobufSerializer.WriteTagAndLength(buffer, writePosition, SpanIdSize, ProtobufOtlpTraceFieldNumberConstants.Span_Parent_Span_Id, ProtobufWireType.LEN); + writePosition = WriteSpanId(buffer, writePosition, activity.ParentSpanId); + } + + writePosition = WriteTraceFlags(buffer, writePosition, activity.ActivityTraceFlags, activity.HasRemoteParent, ProtobufOtlpTraceFieldNumberConstants.Span_Flags); + writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpTraceFieldNumberConstants.Span_Name, activity.DisplayName); + writePosition = ProtobufSerializer.WriteEnumWithTag(buffer, writePosition, ProtobufOtlpTraceFieldNumberConstants.Span_Kind, (int)activity.Kind + 1); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpTraceFieldNumberConstants.Span_Start_Time_Unix_Nano, (ulong)activity.StartTimeUtc.ToUnixTimeNanoseconds()); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpTraceFieldNumberConstants.Span_End_Time_Unix_Nano, (ulong)(activity.StartTimeUtc.ToUnixTimeNanoseconds() + activity.Duration.ToNanoseconds())); + + (writePosition, StatusCode? statusCode, string? statusMessage) = WriteActivityTags(buffer, writePosition, sdkLimitOptions, activity); + writePosition = WriteSpanEvents(buffer, writePosition, sdkLimitOptions, activity); + writePosition = WriteSpanLinks(buffer, writePosition, sdkLimitOptions, activity); + writePosition = WriteSpanStatus(buffer, writePosition, activity, statusCode, statusMessage); + ProtobufSerializer.WriteReservedLength(buffer, spanLengthPosition, writePosition - (spanLengthPosition + ReserveSizeForLength)); + + return writePosition; + } + + internal static int WriteTraceId(byte[] buffer, int position, ActivityTraceId activityTraceId) + { + var traceBytes = new Span(buffer, position, TraceIdSize); + activityTraceId.CopyTo(traceBytes); + return position + TraceIdSize; + } + + internal static int WriteSpanId(byte[] buffer, int position, ActivitySpanId activitySpanId) + { + var spanIdBytes = new Span(buffer, position, SpanIdSize); + activitySpanId.CopyTo(spanIdBytes); + return position + SpanIdSize; + } + + internal static int WriteTraceFlags(byte[] buffer, int position, ActivityTraceFlags activityTraceFlags, bool hasRemoteParent, int fieldNumber) + { + uint spanFlags = (uint)activityTraceFlags & (byte)0x000000FF; + + spanFlags |= 0x00000100; + if (hasRemoteParent) + { + spanFlags |= 0x00000200; + } + + position = ProtobufSerializer.WriteFixed32WithTag(buffer, position, fieldNumber, spanFlags); + + return position; + } + + internal static (int Position, StatusCode? StatusCode, string? StatusMessage) WriteActivityTags(byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, Activity activity) + { + StatusCode? statusCode = null; + string? statusMessage = null; + int maxAttributeCount = sdkLimitOptions.SpanAttributeCountLimit ?? int.MaxValue; + int maxAttributeValueLength = sdkLimitOptions.AttributeValueLengthLimit ?? int.MaxValue; + ProtobufOtlpTagWriter.OtlpTagWriterState otlpTagWriterState = new ProtobufOtlpTagWriter.OtlpTagWriterState + { + Buffer = buffer, + WritePosition = writePosition, + TagCount = 0, + DroppedTagCount = 0, + }; + + foreach (ref readonly var tag in activity.EnumerateTagObjects()) + { + switch (tag.Key) + { + case "otel.status_code": + + statusCode = tag.Value 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(tag.Value as string, StringComparison.OrdinalIgnoreCase) => StatusCode.Unset, + not null when ErrorStatusCodeTagValue.Equals(tag.Value as string, StringComparison.OrdinalIgnoreCase) => StatusCode.Error, + not null when OkStatusCodeTagValue.Equals(tag.Value as string, StringComparison.OrdinalIgnoreCase) => StatusCode.Ok, + _ => null, + }; + continue; + case "otel.status_description": + statusMessage = tag.Value as string; + continue; + } + + if (otlpTagWriterState.TagCount < maxAttributeCount) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpTraceFieldNumberConstants.Span_Attributes, ProtobufWireType.LEN); + int spanAttributesLengthPosition = otlpTagWriterState.WritePosition; + otlpTagWriterState.WritePosition += ReserveSizeForLength; + + ProtobufOtlpTagWriter.Instance.TryWriteTag(ref otlpTagWriterState, tag.Key, tag.Value, maxAttributeValueLength); + + ProtobufSerializer.WriteReservedLength(buffer, spanAttributesLengthPosition, otlpTagWriterState.WritePosition - (spanAttributesLengthPosition + 4)); + otlpTagWriterState.TagCount++; + } + else + { + otlpTagWriterState.DroppedTagCount++; + } + } + + if (otlpTagWriterState.DroppedTagCount > 0) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTag(buffer, otlpTagWriterState.WritePosition, ProtobufOtlpTraceFieldNumberConstants.Span_Dropped_Attributes_Count, ProtobufWireType.VARINT); + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteVarInt32(buffer, otlpTagWriterState.WritePosition, (uint)otlpTagWriterState.DroppedTagCount); + } + + return (otlpTagWriterState.WritePosition, statusCode, statusMessage); + } + + internal static int WriteSpanEvents(byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, Activity activity) + { + int maxEventCountLimit = sdkLimitOptions.SpanEventCountLimit ?? int.MaxValue; + int eventCount = 0; + int droppedEventCount = 0; + foreach (ref readonly var evnt in activity.EnumerateEvents()) + { + if (eventCount < maxEventCountLimit) + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpTraceFieldNumberConstants.Span_Events, ProtobufWireType.LEN); + int spanEventsLengthPosition = writePosition; + writePosition += ReserveSizeForLength; // Reserve 4 bytes for length + + writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpTraceFieldNumberConstants.Event_Name, evnt.Name); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpTraceFieldNumberConstants.Event_Time_Unix_Nano, (ulong)evnt.Timestamp.ToUnixTimeNanoseconds()); + writePosition = WriteEventAttributes(ref buffer, writePosition, sdkLimitOptions, evnt); + + ProtobufSerializer.WriteReservedLength(buffer, spanEventsLengthPosition, writePosition - (spanEventsLengthPosition + ReserveSizeForLength)); + eventCount++; + } + else + { + droppedEventCount++; + } + } + + if (droppedEventCount > 0) + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpTraceFieldNumberConstants.Span_Dropped_Events_Count, ProtobufWireType.VARINT); + writePosition = ProtobufSerializer.WriteVarInt32(buffer, writePosition, (uint)droppedEventCount); + } + + return writePosition; + } + + internal static int WriteEventAttributes(ref byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, ActivityEvent evnt) + { + int maxAttributeCount = sdkLimitOptions.SpanEventAttributeCountLimit ?? int.MaxValue; + int maxAttributeValueLength = sdkLimitOptions.AttributeValueLengthLimit ?? int.MaxValue; + + ProtobufOtlpTagWriter.OtlpTagWriterState otlpTagWriterState = new ProtobufOtlpTagWriter.OtlpTagWriterState + { + Buffer = buffer, + WritePosition = writePosition, + TagCount = 0, + DroppedTagCount = 0, + }; + + foreach (ref readonly var tag in evnt.EnumerateTagObjects()) + { + if (otlpTagWriterState.TagCount < maxAttributeCount) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpTraceFieldNumberConstants.Event_Attributes, ProtobufWireType.LEN); + int eventAttributesLengthPosition = otlpTagWriterState.WritePosition; + otlpTagWriterState.WritePosition += ReserveSizeForLength; + ProtobufOtlpTagWriter.Instance.TryWriteTag(ref otlpTagWriterState, tag.Key, tag.Value, maxAttributeValueLength); + ProtobufSerializer.WriteReservedLength(buffer, eventAttributesLengthPosition, otlpTagWriterState.WritePosition - (eventAttributesLengthPosition + ReserveSizeForLength)); + otlpTagWriterState.TagCount++; + } + else + { + otlpTagWriterState.DroppedTagCount++; + } + } + + if (otlpTagWriterState.DroppedTagCount > 0) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTag(buffer, otlpTagWriterState.WritePosition, ProtobufOtlpTraceFieldNumberConstants.Event_Dropped_Attributes_Count, ProtobufWireType.VARINT); + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteVarInt32(buffer, otlpTagWriterState.WritePosition, (uint)otlpTagWriterState.DroppedTagCount); + } + + return otlpTagWriterState.WritePosition; + } + + internal static int WriteSpanLinks(byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, Activity activity) + { + int maxLinksCount = sdkLimitOptions.SpanLinkCountLimit ?? int.MaxValue; + int linkCount = 0; + int droppedLinkCount = 0; + + foreach (ref readonly var link in activity.EnumerateLinks()) + { + if (linkCount < maxLinksCount) + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpTraceFieldNumberConstants.Span_Links, ProtobufWireType.LEN); + int spanLinksLengthPosition = writePosition; + writePosition += ReserveSizeForLength; // Reserve 4 bytes for length + + writePosition = ProtobufSerializer.WriteTagAndLength(buffer, writePosition, TraceIdSize, ProtobufOtlpTraceFieldNumberConstants.Link_Trace_Id, ProtobufWireType.LEN); + writePosition = WriteTraceId(buffer, writePosition, link.Context.TraceId); + writePosition = ProtobufSerializer.WriteTagAndLength(buffer, writePosition, SpanIdSize, ProtobufOtlpTraceFieldNumberConstants.Link_Span_Id, ProtobufWireType.LEN); + writePosition = WriteSpanId(buffer, writePosition, link.Context.SpanId); + if (link.Context.TraceState != null) + { + writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpTraceFieldNumberConstants.Span_Trace_State, link.Context.TraceState); + } + + writePosition = WriteLinkAttributes(buffer, writePosition, sdkLimitOptions, link); + writePosition = WriteTraceFlags(buffer, writePosition, link.Context.TraceFlags, link.Context.IsRemote, ProtobufOtlpTraceFieldNumberConstants.Link_Flags); + + ProtobufSerializer.WriteReservedLength(buffer, spanLinksLengthPosition, writePosition - (spanLinksLengthPosition + ReserveSizeForLength)); + linkCount++; + } + else + { + droppedLinkCount++; + } + } + + if (droppedLinkCount > 0) + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpTraceFieldNumberConstants.Span_Dropped_Links_Count, ProtobufWireType.VARINT); + writePosition = ProtobufSerializer.WriteVarInt32(buffer, writePosition, (uint)droppedLinkCount); + } + + return writePosition; + } + + internal static int WriteLinkAttributes(byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, ActivityLink link) + { + int maxAttributeCount = sdkLimitOptions.SpanLinkAttributeCountLimit ?? int.MaxValue; + int maxAttributeValueLength = sdkLimitOptions.AttributeValueLengthLimit ?? int.MaxValue; + ProtobufOtlpTagWriter.OtlpTagWriterState otlpTagWriterState = new ProtobufOtlpTagWriter.OtlpTagWriterState + { + Buffer = buffer, + WritePosition = writePosition, + TagCount = 0, + DroppedTagCount = 0, + }; + + foreach (ref readonly var tag in link.EnumerateTagObjects()) + { + if (otlpTagWriterState.TagCount < maxAttributeCount) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpTraceFieldNumberConstants.Link_Attributes, ProtobufWireType.LEN); + int linkAttributesLengthPosition = otlpTagWriterState.WritePosition; + otlpTagWriterState.WritePosition += ReserveSizeForLength; + ProtobufOtlpTagWriter.Instance.TryWriteTag(ref otlpTagWriterState, tag.Key, tag.Value, maxAttributeValueLength); + ProtobufSerializer.WriteReservedLength(buffer, linkAttributesLengthPosition, otlpTagWriterState.WritePosition - (linkAttributesLengthPosition + ReserveSizeForLength)); + otlpTagWriterState.TagCount++; + } + else + { + otlpTagWriterState.DroppedTagCount++; + } + } + + if (otlpTagWriterState.DroppedTagCount > 0) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTag(buffer, otlpTagWriterState.WritePosition, ProtobufOtlpTraceFieldNumberConstants.Link_Dropped_Attributes_Count, ProtobufWireType.VARINT); + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteVarInt32(buffer, otlpTagWriterState.WritePosition, (uint)otlpTagWriterState.DroppedTagCount); + } + + return otlpTagWriterState.WritePosition; + } + + internal static int WriteSpanStatus(byte[] buffer, int position, Activity activity, StatusCode? statusCode, string? statusMessage) + { + if (activity.Status == ActivityStatusCode.Unset && statusCode == null) + { + return position; + } + + var useActivity = activity.Status != ActivityStatusCode.Unset; + var isError = useActivity ? activity.Status == ActivityStatusCode.Error : statusCode == StatusCode.Error; + var description = useActivity ? activity.StatusDescription : statusMessage; + + if (isError && description != null) + { + var descriptionSpan = description.AsSpan(); + var numberOfUtf8CharsInString = ProtobufSerializer.GetNumberOfUtf8CharsInString(descriptionSpan); + var serializedLengthSize = ProtobufSerializer.ComputeVarInt64Size((ulong)numberOfUtf8CharsInString); + + // length = numberOfUtf8CharsInString + Status_Message tag size + serializedLengthSize field size + Span_Status tag size + Span_Status length size. + position = ProtobufSerializer.WriteTagAndLength(buffer, position, numberOfUtf8CharsInString + 1 + serializedLengthSize + 2, ProtobufOtlpTraceFieldNumberConstants.Span_Status, ProtobufWireType.LEN); + position = ProtobufSerializer.WriteStringWithTag(buffer, position, ProtobufOtlpTraceFieldNumberConstants.Status_Message, numberOfUtf8CharsInString, descriptionSpan); + } + else + { + position = ProtobufSerializer.WriteTagAndLength(buffer, position, 2, ProtobufOtlpTraceFieldNumberConstants.Span_Status, ProtobufWireType.LEN); + } + + var finalStatusCode = useActivity ? (int)activity.Status : (statusCode != null && statusCode != StatusCode.Unset) ? (int)statusCode! : (int)StatusCode.Unset; + position = ProtobufSerializer.WriteEnumWithTag(buffer, position, ProtobufOtlpTraceFieldNumberConstants.Status_Code, finalStatusCode); + + return position; + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufSerializer.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufSerializer.cs new file mode 100644 index 00000000000..deee79e77db --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufSerializer.cs @@ -0,0 +1,340 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Buffers.Binary; +using System.Diagnostics; +using System.Runtime.CompilerServices; +#if NETFRAMEWORK || NETSTANDARD2_0 +using System.Runtime.InteropServices; +#endif +using System.Text; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; + +internal static class ProtobufSerializer +{ + private const int MaxBufferSize = 100 * 1024 * 1024; + private const uint UInt128 = 0x80; + private const ulong ULong128 = 0x80; + private const int Fixed32Size = 4; + private const int Fixed64Size = 8; + private const int MaskBitsLow = 0b_0111_1111; + private const int MaskBitHigh = 0b_1000_0000; + + private static readonly Encoding Utf8Encoding = Encoding.UTF8; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static uint GetTagValue(int fieldNumber, ProtobufWireType wireType) => ((uint)(fieldNumber << 3)) | (uint)wireType; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int WriteTag(byte[] buffer, int writePosition, int fieldNumber, ProtobufWireType type) => WriteVarInt32(buffer, writePosition, GetTagValue(fieldNumber, type)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int WriteLength(byte[] buffer, int writePosition, int length) => WriteVarInt32(buffer, writePosition, (uint)length); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int WriteTagAndLength(byte[] buffer, int writePosition, int contentLength, int fieldNumber, ProtobufWireType type) + { + writePosition = WriteTag(buffer, writePosition, fieldNumber, type); + writePosition = WriteLength(buffer, writePosition, contentLength); + + return writePosition; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void WriteReservedLength(byte[] buffer, int writePosition, int length) + { + var slice = buffer.AsSpan(writePosition, 4); + slice[0] = (byte)((length & MaskBitsLow) | MaskBitHigh); + slice[1] = (byte)(((length >> 7) & MaskBitsLow) | MaskBitHigh); + slice[2] = (byte)(((length >> 14) & MaskBitsLow) | MaskBitHigh); + slice[3] = (byte)((length >> 21) & MaskBitsLow); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int WriteBoolWithTag(byte[] buffer, int writePosition, int fieldNumber, bool value) + { + writePosition = WriteTag(buffer, writePosition, fieldNumber, ProtobufWireType.VARINT); + buffer[writePosition++] = value ? (byte)1 : (byte)0; + return writePosition; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int WriteEnumWithTag(byte[] buffer, int writePosition, int fieldNumber, int value) + { + writePosition = WriteTag(buffer, writePosition, fieldNumber, ProtobufWireType.VARINT); + buffer[writePosition++] = (byte)value; + return writePosition; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int WriteFixed32LittleEndianFormat(byte[] buffer, int writePosition, uint value) + { + Span span = new(buffer, writePosition, Fixed32Size); + BinaryPrimitives.WriteUInt32LittleEndian(span, value); + writePosition += Fixed32Size; + + return writePosition; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int WriteFixed64LittleEndianFormat(byte[] buffer, int writePosition, ulong value) + { + Span span = new(buffer, writePosition, Fixed64Size); + BinaryPrimitives.WriteUInt64LittleEndian(span, value); + writePosition += Fixed64Size; + + return writePosition; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int WriteFixed32WithTag(byte[] buffer, int writePosition, int fieldNumber, uint value) + { + writePosition = WriteTag(buffer, writePosition, fieldNumber, ProtobufWireType.I32); + writePosition = WriteFixed32LittleEndianFormat(buffer, writePosition, value); + + return writePosition; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int WriteFixed64WithTag(byte[] buffer, int writePosition, int fieldNumber, ulong value) + { + writePosition = WriteTag(buffer, writePosition, fieldNumber, ProtobufWireType.I64); + writePosition = WriteFixed64LittleEndianFormat(buffer, writePosition, value); + + return writePosition; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int WriteSInt32WithTag(byte[] buffer, int writePosition, int fieldNumber, int value) + { + writePosition = WriteTag(buffer, writePosition, fieldNumber, ProtobufWireType.VARINT); + + // https://protobuf.dev/programming-guides/encoding/#signed-ints + writePosition = WriteVarInt32(buffer, writePosition, (uint)((value << 1) ^ (value >> 31))); + + return writePosition; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int WriteVarInt32(byte[] buffer, int writePosition, uint value) + { + while (value >= UInt128) + { + buffer[writePosition++] = (byte)(MaskBitHigh | (value & MaskBitsLow)); + value >>= 7; + } + + buffer[writePosition++] = (byte)value; + return writePosition; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int WriteVarInt64(byte[] buffer, int writePosition, ulong value) + { + while (value >= ULong128) + { + buffer[writePosition++] = (byte)(MaskBitHigh | (value & MaskBitsLow)); + value >>= 7; + } + + buffer[writePosition++] = (byte)value; + return writePosition; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int WriteInt64WithTag(byte[] buffer, int writePosition, int fieldNumber, ulong value) + { + writePosition = WriteTag(buffer, writePosition, fieldNumber, ProtobufWireType.VARINT); + writePosition = WriteVarInt64(buffer, writePosition, value); + + return writePosition; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int WriteDoubleWithTag(byte[] buffer, int writePosition, int fieldNumber, double value) + { + writePosition = WriteTag(buffer, writePosition, fieldNumber, ProtobufWireType.I64); + writePosition = WriteFixed64LittleEndianFormat(buffer, writePosition, (ulong)BitConverter.DoubleToInt64Bits(value)); + + return writePosition; + } + + /// + /// Computes the number of bytes required to encode a 64-bit unsigned integer in Protocol Buffers' varint format. + /// + /// + /// Protocol Buffers uses variable-length encoding (varint) to serialize integers efficiently: + /// - Each byte uses 7 bits to encode the number and 1 bit (MSB) to indicate if more bytes follow + /// - The algorithm checks how many significant bits the number contains by shifting and masking + /// - Numbers are encoded in groups of 7 bits, from least to most significant + /// - Each group requires one byte, so the method returns the number of 7-bit groups needed + /// + /// Examples: + /// - Values 0-127 (7 bits) require 1 byte + /// - Values 128-16383 (14 bits) require 2 bytes + /// - Values 16384-2097151 (21 bits) require 3 bytes + /// And so on... + /// + /// For more details, see: + /// - Protocol Buffers encoding reference: https://developers.google.com/protocol-buffers/docs/encoding#varints. + /// + /// The unsigned 64-bit integer to be encoded. + /// Number of bytes needed to encode the value. + internal static int ComputeVarInt64Size(ulong value) + { + if ((value & (0xffffffffffffffffL << 7)) == 0) + { + return 1; + } + + if ((value & (0xffffffffffffffffL << 14)) == 0) + { + return 2; + } + + if ((value & (0xffffffffffffffffL << 21)) == 0) + { + return 3; + } + + if ((value & (0xffffffffffffffffL << 28)) == 0) + { + return 4; + } + + if ((value & (0xffffffffffffffffL << 35)) == 0) + { + return 5; + } + + if ((value & (0xffffffffffffffffL << 42)) == 0) + { + return 6; + } + + if ((value & (0xffffffffffffffffL << 49)) == 0) + { + return 7; + } + + if ((value & (0xffffffffffffffffL << 56)) == 0) + { + return 8; + } + + if ((value & (0xffffffffffffffffL << 63)) == 0) + { + return 9; + } + + return 10; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int WriteByteArrayWithTag(byte[] buffer, int writePosition, int fieldNumber, ReadOnlySpan value) + { + writePosition = WriteTag(buffer, writePosition, fieldNumber, ProtobufWireType.LEN); + writePosition = WriteLength(buffer, writePosition, value.Length); + value.CopyTo(buffer.AsSpan(writePosition)); + + writePosition += value.Length; + return writePosition; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int WriteStringWithTag(byte[] buffer, int writePosition, int fieldNumber, string value) + { + Debug.Assert(value != null, "value was null"); + + return WriteStringWithTag(buffer, writePosition, fieldNumber, value.AsSpan()); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int GetNumberOfUtf8CharsInString(ReadOnlySpan value) + { +#if NETFRAMEWORK || NETSTANDARD2_0 + int numberOfUtf8CharsInString; + unsafe + { + fixed (char* strPtr = &GetNonNullPinnableReference(value)) + { + numberOfUtf8CharsInString = Utf8Encoding.GetByteCount(strPtr, value.Length); + } + } +#else + int numberOfUtf8CharsInString = Utf8Encoding.GetByteCount(value); +#endif + return numberOfUtf8CharsInString; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int WriteStringWithTag(byte[] buffer, int writePosition, int fieldNumber, ReadOnlySpan value) + { + var numberOfUtf8CharsInString = GetNumberOfUtf8CharsInString(value); + return WriteStringWithTag(buffer, writePosition, fieldNumber, numberOfUtf8CharsInString, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int WriteStringWithTag(byte[] buffer, int writePosition, int fieldNumber, int numberOfUtf8CharsInString, ReadOnlySpan value) + { + writePosition = WriteTag(buffer, writePosition, fieldNumber, ProtobufWireType.LEN); + writePosition = WriteLength(buffer, writePosition, numberOfUtf8CharsInString); + +#if NETFRAMEWORK || NETSTANDARD2_0 + if (buffer.Length - writePosition < numberOfUtf8CharsInString) + { + // Note: Validate there is enough space in the buffer to hold the + // string otherwise throw to trigger a resize of the buffer. +#pragma warning disable CA2201 // Do not raise reserved exception types + throw new IndexOutOfRangeException(); +#pragma warning restore CA2201 // Do not raise reserved exception types + } + + unsafe + { + fixed (char* strPtr = &GetNonNullPinnableReference(value)) + { + fixed (byte* bufferPtr = buffer) + { + var bytesWritten = Utf8Encoding.GetBytes(strPtr, value.Length, bufferPtr + writePosition, numberOfUtf8CharsInString); + Debug.Assert(bytesWritten == numberOfUtf8CharsInString, "bytesWritten did not match numberOfUtf8CharsInString"); + } + } + } +#else + var bytesWritten = Utf8Encoding.GetBytes(value, buffer.AsSpan(writePosition)); + Debug.Assert(bytesWritten == numberOfUtf8CharsInString, "bytesWritten did not match numberOfUtf8CharsInString"); +#endif + + writePosition += numberOfUtf8CharsInString; + return writePosition; + } + + internal static bool IncreaseBufferSize(ref byte[] buffer, OtlpSignalType otlpSignalType) + { + if (buffer.Length >= MaxBufferSize) + { + OpenTelemetryProtocolExporterEventSource.Log.BufferExceededMaxSize(otlpSignalType.ToString(), buffer.Length); + return false; + } + + try + { + var newBufferSize = buffer.Length * 2; + buffer = new byte[newBufferSize]; + return true; + } + catch (OutOfMemoryException) + { + OpenTelemetryProtocolExporterEventSource.Log.BufferResizeFailedDueToMemory(otlpSignalType.ToString()); + return false; + } + } + +#if NETFRAMEWORK || NETSTANDARD2_0 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe ref T GetNonNullPinnableReference(ReadOnlySpan span) + => ref (span.Length != 0) ? ref Unsafe.AsRef(in MemoryMarshal.GetReference(span)) : ref Unsafe.AsRef((void*)1); +#endif +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufWireType.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufWireType.cs new file mode 100644 index 00000000000..b3b5fe40319 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufWireType.cs @@ -0,0 +1,47 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; + +/// +/// Wire types within protobuf encoding. +/// https://protobuf.dev/programming-guides/encoding/#structure. +/// +internal enum ProtobufWireType : uint +{ + /// + /// Variable-length integer. + /// Used for int32, int64, uint32, uint64, sint32, sint64, bool, enum. + /// + VARINT = 0, + + /// + /// A fixed-length 64-bit value. + /// Used for fixed64, sfixed64, double. + /// + I64 = 1, + + /// + /// A length-delimited value. + /// Used for string, bytes, embedded messages, packed repeated fields. + /// + LEN = 2, + + /// + /// Group Start value. + /// (Deprecated). + /// + SGROUP = 3, + + /// + /// Group End value. + /// (Deprecated). + /// + EGROUP = 4, + + /// + /// A fixed-length 32-bit value. + /// Used for fixed32, sfixed32, float. + /// + I32 = 5, +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterPersistentStorageTransmissionHandler.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterPersistentStorageTransmissionHandler.cs index e8ed3da211d..066ecb02d02 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterPersistentStorageTransmissionHandler.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterPersistentStorageTransmissionHandler.cs @@ -1,20 +1,14 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; -using Google.Protobuf; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; using OpenTelemetry.PersistentStorage.Abstractions; using OpenTelemetry.PersistentStorage.FileSystem; -using OpenTelemetry.Proto.Collector.Logs.V1; -using OpenTelemetry.Proto.Collector.Metrics.V1; -using OpenTelemetry.Proto.Collector.Trace.V1; namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; -internal sealed class OtlpExporterPersistentStorageTransmissionHandler : OtlpExporterTransmissionHandler, IDisposable +internal sealed class OtlpExporterPersistentStorageTransmissionHandler : OtlpExporterTransmissionHandler, IDisposable { private const int RetryIntervalInMilliseconds = 60000; private readonly ManualResetEvent shutdownEvent = new(false); @@ -22,26 +16,24 @@ internal sealed class OtlpExporterPersistentStorageTransmissionHandler private readonly AutoResetEvent exportEvent = new(false); private readonly Thread thread; private readonly PersistentBlobProvider persistentBlobProvider; - private readonly Func requestFactory; private bool disposed; - public OtlpExporterPersistentStorageTransmissionHandler(IExportClient exportClient, double timeoutMilliseconds, Func requestFactory, string storagePath) - : this(new FileBlobProvider(storagePath), exportClient, timeoutMilliseconds, requestFactory) + public OtlpExporterPersistentStorageTransmissionHandler(IExportClient exportClient, double timeoutMilliseconds, string storagePath) +#pragma warning disable CA2000 // Dispose objects before losing scope + : this(new FileBlobProvider(storagePath), exportClient, timeoutMilliseconds) +#pragma warning restore CA2000 // Dispose objects before losing scope { } - internal OtlpExporterPersistentStorageTransmissionHandler(PersistentBlobProvider persistentBlobProvider, IExportClient exportClient, double timeoutMilliseconds, Func requestFactory) + internal OtlpExporterPersistentStorageTransmissionHandler(PersistentBlobProvider persistentBlobProvider, IExportClient exportClient, double timeoutMilliseconds) : base(exportClient, timeoutMilliseconds) { Debug.Assert(persistentBlobProvider != null, "persistentBlobProvider was null"); - Debug.Assert(requestFactory != null, "requestFactory was null"); - this.persistentBlobProvider = persistentBlobProvider!; - this.requestFactory = requestFactory!; this.thread = new Thread(this.RetryStoredRequests) { - Name = $"OtlpExporter Persistent Retry Storage - {typeof(TRequest)}", + Name = "OtlpExporter Persistent Retry Storage", IsBackground = true, }; @@ -56,36 +48,10 @@ internal bool InitiateAndWaitForRetryProcess(int timeOutMilliseconds) return this.dataExportNotification.WaitOne(timeOutMilliseconds); } - protected override bool OnSubmitRequestFailure(TRequest request, ExportClientResponse response) + protected override bool OnSubmitRequestFailure(byte[] request, int contentLength, ExportClientResponse response) { - if (RetryHelper.ShouldRetryRequest(request, response, OtlpRetry.InitialBackoffMilliseconds, out _)) - { - byte[]? data = null; - if (request is ExportTraceServiceRequest traceRequest) - { - data = traceRequest.ToByteArray(); - } - else if (request is ExportMetricsServiceRequest metricsRequest) - { - data = metricsRequest.ToByteArray(); - } - else if (request is ExportLogsServiceRequest logsRequest) - { - data = logsRequest.ToByteArray(); - } - else - { - Debug.Fail("Unexpected request type encountered"); - data = null; - } - - if (data != null) - { - return this.persistentBlobProvider.TryCreateBlob(data, out _); - } - } - - return false; + Debug.Assert(request != null, "request was null"); + return RetryHelper.ShouldRetryRequest(response, OtlpRetry.InitialBackoffMilliseconds, out _) && this.persistentBlobProvider.TryCreateBlob(request!, out _); } protected override void OnShutdown(int timeoutMilliseconds) @@ -129,6 +95,8 @@ protected override void Dispose(bool disposing) this.disposed = true; } + + base.Dispose(disposing); } private void RetryStoredRequests() @@ -159,8 +127,7 @@ private void RetryStoredRequests() if (blob.TryLease((int)this.TimeoutMilliseconds) && blob.TryRead(out var data)) { var deadlineUtc = DateTime.UtcNow.AddMilliseconds(this.TimeoutMilliseconds); - var request = this.requestFactory.Invoke(data); - if (this.TryRetryRequest(request, deadlineUtc, out var response) || !RetryHelper.ShouldRetryRequest(request, response, OtlpRetry.InitialBackoffMilliseconds, out var retryInfo)) + if (this.TryRetryRequest(data, data.Length, deadlineUtc, out var response) || !RetryHelper.ShouldRetryRequest(response, OtlpRetry.InitialBackoffMilliseconds, out var retryInfo)) { blob.TryDelete(); } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterRetryTransmissionHandler.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterRetryTransmissionHandler.cs index d4be5c9d640..6830288525e 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterRetryTransmissionHandler.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterRetryTransmissionHandler.cs @@ -1,30 +1,28 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; -internal sealed class OtlpExporterRetryTransmissionHandler : OtlpExporterTransmissionHandler +internal sealed class OtlpExporterRetryTransmissionHandler : OtlpExporterTransmissionHandler { - internal OtlpExporterRetryTransmissionHandler(IExportClient exportClient, double timeoutMilliseconds) + internal OtlpExporterRetryTransmissionHandler(IExportClient exportClient, double timeoutMilliseconds) : base(exportClient, timeoutMilliseconds) { } - protected override bool OnSubmitRequestFailure(TRequest request, ExportClientResponse response) + protected override bool OnSubmitRequestFailure(byte[] request, int contentLength, ExportClientResponse response) { var nextRetryDelayMilliseconds = OtlpRetry.InitialBackoffMilliseconds; - while (RetryHelper.ShouldRetryRequest(request, response, nextRetryDelayMilliseconds, out var retryResult)) + while (RetryHelper.ShouldRetryRequest(response, nextRetryDelayMilliseconds, out var retryResult)) { // Note: This delay cannot exceed the configured timeout period for otlp exporter. // If the backend responds with `RetryAfter` duration that would result in exceeding the configured timeout period // we would fail fast and drop the data. Thread.Sleep(retryResult.RetryDelay); - if (this.TryRetryRequest(request, response.DeadlineUtc, out response)) + if (this.TryRetryRequest(request, contentLength, response.DeadlineUtc, out response)) { return true; } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterTransmissionHandler.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterTransmissionHandler.cs index 2b56e16dd7e..b63175ee502 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterTransmissionHandler.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/OtlpExporterTransmissionHandler.cs @@ -1,17 +1,15 @@ // 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 : IDisposable +internal class OtlpExporterTransmissionHandler : IDisposable { - public OtlpExporterTransmissionHandler(IExportClient exportClient, double timeoutMilliseconds) + public OtlpExporterTransmissionHandler(IExportClient exportClient, double timeoutMilliseconds) { Guard.ThrowIfNull(exportClient); @@ -19,7 +17,7 @@ public OtlpExporterTransmissionHandler(IExportClient exportClient, dou this.TimeoutMilliseconds = timeoutMilliseconds; } - internal IExportClient ExportClient { get; } + internal IExportClient ExportClient { get; } internal double TimeoutMilliseconds { get; } @@ -27,21 +25,22 @@ public OtlpExporterTransmissionHandler(IExportClient exportClient, dou /// Attempts to send an export request to the server. /// /// The request to send to the server. + /// length of content. /// if the request is sent successfully; otherwise, . /// - public bool TrySubmitRequest(TRequest request) + public bool TrySubmitRequest(byte[] request, int contentLength) { try { var deadlineUtc = DateTime.UtcNow.AddMilliseconds(this.TimeoutMilliseconds); - var response = this.ExportClient.SendExportRequest(request, deadlineUtc); + var response = this.ExportClient.SendExportRequest(request, contentLength, deadlineUtc); if (response.Success) { return true; } - return this.OnSubmitRequestFailure(request, response); + return this.OnSubmitRequestFailure(request, contentLength, response); } catch (Exception ex) { @@ -102,32 +101,25 @@ protected virtual void OnShutdown(int timeoutMilliseconds) /// Fired when a request could not be submitted. /// /// The request that was attempted to send to the server. + /// Length of content. /// . /// If the request is resubmitted and succeeds; otherwise, . - protected virtual bool OnSubmitRequestFailure(TRequest request, ExportClientResponse response) - { - return false; - } + protected virtual bool OnSubmitRequestFailure(byte[] request, int contentLength, ExportClientResponse response) => false; /// /// Fired when resending a request to the server. /// /// The request to be resent to the server. + /// Length of content. /// The deadline time in utc for export request to finish. /// . /// If the retry succeeds; otherwise, . - protected bool TryRetryRequest(TRequest request, DateTime deadlineUtc, out ExportClientResponse response) + protected bool TryRetryRequest(byte[] request, int contentLength, DateTime deadlineUtc, out ExportClientResponse response) { - response = this.ExportClient.SendExportRequest(request, deadlineUtc); - if (!response.Success) - { - OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(response.Exception, isRetry: true); - return false; - } - - return true; + response = this.ExportClient.SendExportRequest(request, contentLength, deadlineUtc); + return response.Success; } /// diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/RetryHelper.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/RetryHelper.cs index cf663a5f792..79438dd77da 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/RetryHelper.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Transmission/RetryHelper.cs @@ -1,15 +1,13 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; internal static class RetryHelper { - internal static bool ShouldRetryRequest(TRequest request, ExportClientResponse response, int retryDelayMilliseconds, out OtlpRetry.RetryResult retryResult) + internal static bool ShouldRetryRequest(ExportClientResponse response, int retryDelayMilliseconds, out OtlpRetry.RetryResult retryResult) { if (response is ExportClientGrpcResponse grpcResponse) { diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/profiles/v1experimental/profiles.proto b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/profiles/v1experimental/profiles.proto deleted file mode 100644 index bbc2b2931da..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/profiles/v1experimental/profiles.proto +++ /dev/null @@ -1,191 +0,0 @@ -// Copyright 2023, OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT 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 opentelemetry.proto.profiles.v1experimental; - -import "opentelemetry/proto/common/v1/common.proto"; -import "opentelemetry/proto/resource/v1/resource.proto"; -import "opentelemetry/proto/profiles/v1experimental/pprofextended.proto"; - -option csharp_namespace = "OpenTelemetry.Proto.Profiles.V1Experimental"; -option java_multiple_files = true; -option java_package = "io.opentelemetry.proto.profiles.v1experimental"; -option java_outer_classname = "ProfilesProto"; -option go_package = "go.opentelemetry.io/proto/otlp/profiles/v1experimental"; - -// Relationships Diagram -// -// ┌──────────────────┐ LEGEND -// │ ProfilesData │ -// └──────────────────┘ ─────▶ embedded -// │ -// │ 1-n ─────▷ referenced by index -// ▼ -// ┌──────────────────┐ -// │ ResourceProfiles │ -// └──────────────────┘ -// │ -// │ 1-n -// ▼ -// ┌──────────────────┐ -// │ ScopeProfiles │ -// └──────────────────┘ -// │ -// │ 1-n -// ▼ -// ┌──────────────────┐ -// │ ProfileContainer │ -// └──────────────────┘ -// │ -// │ 1-1 -// ▼ -// ┌──────────────────┐ -// │ Profile │ -// └──────────────────┘ -// │ 1-n -// │ 1-n ┌───────────────────────────────────────┐ -// ▼ │ ▽ -// ┌──────────────────┐ 1-n ┌──────────────┐ ┌──────────┐ -// │ Sample │ ──────▷ │ KeyValue │ │ Link │ -// └──────────────────┘ └──────────────┘ └──────────┘ -// │ 1-n △ △ -// │ 1-n ┌─────────────────┘ │ 1-n -// ▽ │ │ -// ┌──────────────────┐ n-1 ┌──────────────┐ -// │ Location │ ──────▷ │ Mapping │ -// └──────────────────┘ └──────────────┘ -// │ -// │ 1-n -// ▼ -// ┌──────────────────┐ -// │ Line │ -// └──────────────────┘ -// │ -// │ 1-1 -// ▽ -// ┌──────────────────┐ -// │ Function │ -// └──────────────────┘ -// - -// ProfilesData represents the profiles data that can be stored in persistent storage, -// OR can be embedded by other protocols that transfer OTLP profiles data but do not -// implement the OTLP protocol. -// -// The main difference between this message and collector protocol is that -// in this message there will not be any "control" or "metadata" specific to -// OTLP protocol. -// -// When new fields are added into this message, the OTLP request MUST be updated -// as well. -message ProfilesData { - // An array of ResourceProfiles. - // For data coming from a single resource this array will typically contain - // one element. Intermediary nodes that receive data from multiple origins - // typically batch the data before forwarding further and in that case this - // array will contain multiple elements. - repeated ResourceProfiles resource_profiles = 1; -} - - -// A collection of ScopeProfiles from a Resource. -message ResourceProfiles { - reserved 1000; - - // The resource for the profiles in this message. - // If this field is not set then no resource info is known. - opentelemetry.proto.resource.v1.Resource resource = 1; - - // A list of ScopeProfiles that originate from a resource. - repeated ScopeProfiles scope_profiles = 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_profiles" field which have their own schema_url field. - string schema_url = 3; -} - -// A collection of ProfileContainers produced by an InstrumentationScope. -message ScopeProfiles { - // The instrumentation scope information for the profiles in this message. - // Semantically when InstrumentationScope isn't set, it is equivalent with - // an empty instrumentation scope name (unknown). - opentelemetry.proto.common.v1.InstrumentationScope scope = 1; - - // A list of ProfileContainers that originate from an instrumentation scope. - repeated ProfileContainer profiles = 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 profiles in the "profiles" field. - string schema_url = 3; -} - -// A ProfileContainer represents a single profile. It wraps pprof profile with OpenTelemetry specific metadata. -message ProfileContainer { - // A globally unique identifier for a profile. The ID is a 16-byte array. An ID with - // all zeroes is considered invalid. - // - // This field is required. - bytes profile_id = 1; - - // start_time_unix_nano is the start time of the profile. - // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. - // - // This field is semantically required and it is expected that end_time >= start_time. - fixed64 start_time_unix_nano = 2; - - // end_time_unix_nano is the end time of the profile. - // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. - // - // This field is semantically required and it is expected that end_time >= start_time. - fixed64 end_time_unix_nano = 3; - - // attributes is a collection of key/value pairs. Note, global attributes - // like server name can be set using the resource API. Examples of attributes: - // - // "/http/user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" - // "/http/server_latency": 300 - // "abc.com/myattribute": true - // "abc.com/score": 10.239 - // - // The OpenTelemetry API specification further restricts the allowed value types: - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/README.md#attribute - // Attribute keys MUST be unique (it is not allowed to have more than one - // attribute with the same key). - repeated opentelemetry.proto.common.v1.KeyValue attributes = 4; - - // dropped_attributes_count is the number of attributes that were discarded. Attributes - // can be discarded because their keys are too long or because there are too many - // attributes. If this value is 0, then no attributes were dropped. - uint32 dropped_attributes_count = 5; - - // Specifies format of the original payload. Common values are defined in semantic conventions. [required if original_payload is present] - string original_payload_format = 6; - - // Original payload can be stored in this field. This can be useful for users who want to get the original payload. - // Formats such as JFR are highly extensible and can contain more information than what is defined in this spec. - // Inclusion of original payload should be configurable by the user. Default behavior should be to not include the original payload. - // If the original payload is in pprof format, it SHOULD not be included in this field. - // The field is optional, however if it is present `profile` MUST be present and contain the same profiling information. - bytes original_payload = 7; - - // This is a reference to a pprof profile. Required, even when original_payload is present. - opentelemetry.proto.profiles.v1experimental.Profile profile = 8; -} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj index 0af6642d8ac..8470c6df958 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj @@ -5,8 +5,6 @@ $(PackageTags);OTLP core- - - disable BUILDING_INTERNAL_PERSISTENT_STORAGE;$(DefineConstants) true @@ -18,35 +16,27 @@ https://github.com/open-telemetry/opentelemetry-dotnet/pull/5520#discussion_r1556221048 and https://github.com/dotnet/runtime/issues/92509 --> $(NoWarn);SYSLIB1100;SYSLIB1101 + true - - - - - - - - - - - Implementation - + + + diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportProtocol.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportProtocol.cs index 10375e1dfc6..0a01afbd6ef 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportProtocol.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportProtocol.cs @@ -6,11 +6,16 @@ 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. /// +#pragma warning disable CA1028 // Enum storage should be Int32 public enum OtlpExportProtocol : byte +#pragma warning restore CA1028 // Enum storage should be Int32 { /// /// OTLP over gRPC (corresponds to 'grpc' Protocol configuration option). Used as default. /// +#if NET462_OR_GREATER || NETSTANDARD2_0 + [Obsolete("CAUTION: OTLP/gRPC is no longer supported for .NET Framework or .NET Standard targets without supplying a properly configured HttpClientFactory. It is strongly encouraged that you migrate to using OTLP/HTTPPROTOBUF.")] +#endif Grpc = 0, /// diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportProtocolParser.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportProtocolParser.cs index e5e788c5617..563c6b8a6b8 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportProtocolParser.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportProtocolParser.cs @@ -10,7 +10,9 @@ public static bool TryParse(string value, out OtlpExportProtocol result) switch (value?.Trim()) { case "grpc": +#pragma warning disable CS0618 // Suppressing gRPC obsolete warning result = OtlpExportProtocol.Grpc; +#pragma warning restore CS0618 // Suppressing gRPC obsolete warning return true; case "http/protobuf": result = OtlpExportProtocol.HttpProtobuf; diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index 7c14310d17f..91ebfdbd3e1 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; #if NETFRAMEWORK using System.Net.Http; @@ -28,11 +26,15 @@ public class OtlpExporterOptions : IOtlpExporterOptions { internal const string DefaultGrpcEndpoint = "http://localhost:4317"; internal const string DefaultHttpEndpoint = "http://localhost:4318"; +#if NET462_OR_GREATER || NETSTANDARD2_0 + internal const OtlpExportProtocol DefaultOtlpExportProtocol = OtlpExportProtocol.HttpProtobuf; +#else internal const OtlpExportProtocol DefaultOtlpExportProtocol = OtlpExportProtocol.Grpc; +#endif internal static readonly KeyValuePair[] StandardHeaders = new KeyValuePair[] { - new KeyValuePair("User-Agent", GetUserAgentString()), + new("User-Agent", GetUserAgentString()), }; internal readonly Func DefaultHttpClientFactory; @@ -86,7 +88,9 @@ public Uri Endpoint { if (this.endpoint == null) { +#pragma warning disable CS0618 // Suppressing gRPC obsolete warning return this.Protocol == OtlpExportProtocol.Grpc +#pragma warning restore CS0618 // Suppressing gRPC obsolete warning ? new Uri(DefaultGrpcEndpoint) : new Uri(DefaultHttpEndpoint); } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs index ba68de13261..218b4721caa 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs @@ -4,56 +4,23 @@ #if NETFRAMEWORK using System.Net.Http; #endif +using System.Diagnostics; using System.Reflection; -using Grpc.Core; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; -#if NETSTANDARD2_1 || NET6_0_OR_GREATER -using Grpc.Net.Client; -#endif -using System.Diagnostics; -using Google.Protobuf; 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; internal static class OtlpExporterOptionsExtensions { -#if NETSTANDARD2_1 || NET6_0_OR_GREATER - public static GrpcChannel CreateChannel(this OtlpExporterOptions options) -#else - public static Channel CreateChannel(this OtlpExporterOptions options) -#endif - { - 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."); - } + private const string TraceGrpcServicePath = "opentelemetry.proto.collector.trace.v1.TraceService/Export"; + private const string MetricsGrpcServicePath = "opentelemetry.proto.collector.metrics.v1.MetricsService/Export"; + private const string LogsGrpcServicePath = "opentelemetry.proto.collector.logs.v1.LogsService/Export"; -#if NETSTANDARD2_1 || NET6_0_OR_GREATER - 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 - } - - public static Metadata GetMetadataFromHeaders(this OtlpExporterOptions options) - { - return options.GetHeaders((m, k, v) => m.Add(k, v)); - } + private const string TraceHttpServicePath = "v1/traces"; + private const string MetricsHttpServicePath = "v1/metrics"; + private const string LogsHttpServicePath = "v1/logs"; public static THeaders GetHeaders(this OtlpExporterOptions options, Action addHeader) where THeaders : new() @@ -64,23 +31,33 @@ public static THeaders GetHeaders(this OtlpExporterOptions options, Ac { // According to the specification, URL-encoded headers must be supported. optionHeaders = Uri.UnescapeDataString(optionHeaders); + ReadOnlySpan headersSpan = optionHeaders.AsSpan(); - Array.ForEach( - optionHeaders.Split(','), - (pair) => + while (!headersSpan.IsEmpty) + { + int commaIndex = headersSpan.IndexOf(','); + ReadOnlySpan pair; + if (commaIndex == -1) { - // 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."); - } + pair = headersSpan; + headersSpan = []; + } + else + { + pair = headersSpan.Slice(0, commaIndex); + headersSpan = headersSpan.Slice(commaIndex + 1); + } + + int equalIndex = pair.IndexOf('='); + if (equalIndex == -1) + { + throw new ArgumentException("Headers provided in an invalid format."); + } - var key = keyValueData[0].Trim(); - var value = keyValueData[1].Trim(); - addHeader(headers, key, value); - }); + var key = pair.Slice(0, equalIndex).Trim().ToString(); + var value = pair.Slice(equalIndex + 1).Trim().ToString(); + addHeader(headers, key, value); + } } foreach (var header in OtlpExporterOptions.StandardHeaders) @@ -91,139 +68,64 @@ public static THeaders GetHeaders(this OtlpExporterOptions options, Ac return headers; } - public static OtlpExporterTransmissionHandler GetTraceExportTransmissionHandler(this OtlpExporterOptions options, ExperimentalOptions experimentalOptions) + public static OtlpExporterTransmissionHandler GetExportTransmissionHandler(this OtlpExporterOptions options, ExperimentalOptions experimentalOptions, OtlpSignalType otlpSignalType) { - var exportClient = GetTraceExportClient(options); + var exportClient = GetExportClient(options, otlpSignalType); // `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. - double timeoutMilliseconds = exportClient is OtlpHttpTraceExportClient httpTraceExportClient + double timeoutMilliseconds = exportClient is OtlpHttpExportClient httpTraceExportClient ? httpTraceExportClient.HttpClient.Timeout.TotalMilliseconds : options.TimeoutMilliseconds; if (experimentalOptions.EnableInMemoryRetry) { - return new OtlpExporterRetryTransmissionHandler(exportClient, timeoutMilliseconds); + return new OtlpExporterRetryTransmissionHandler(exportClient, timeoutMilliseconds); } else if (experimentalOptions.EnableDiskRetry) { Debug.Assert(!string.IsNullOrEmpty(experimentalOptions.DiskRetryDirectoryPath), $"{nameof(experimentalOptions.DiskRetryDirectoryPath)} is null or empty"); - return new OtlpExporterPersistentStorageTransmissionHandler( + return new OtlpExporterPersistentStorageTransmissionHandler( exportClient, timeoutMilliseconds, - (byte[] data) => - { - var request = new TraceOtlpCollector.ExportTraceServiceRequest(); - request.MergeFrom(data); - return request; - }, Path.Combine(experimentalOptions.DiskRetryDirectoryPath, "traces")); } else { - return new OtlpExporterTransmissionHandler(exportClient, timeoutMilliseconds); - } - } - - public static OtlpExporterTransmissionHandler GetMetricsExportTransmissionHandler(this OtlpExporterOptions options, ExperimentalOptions experimentalOptions) - { - var exportClient = GetMetricsExportClient(options); - - // `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. - double timeoutMilliseconds = exportClient is OtlpHttpMetricsExportClient httpMetricsExportClient - ? httpMetricsExportClient.HttpClient.Timeout.TotalMilliseconds - : options.TimeoutMilliseconds; - - if (experimentalOptions.EnableInMemoryRetry) - { - return new OtlpExporterRetryTransmissionHandler(exportClient, timeoutMilliseconds); - } - else if (experimentalOptions.EnableDiskRetry) - { - Debug.Assert(!string.IsNullOrEmpty(experimentalOptions.DiskRetryDirectoryPath), $"{nameof(experimentalOptions.DiskRetryDirectoryPath)} is null or empty"); - - return new OtlpExporterPersistentStorageTransmissionHandler( - exportClient, - timeoutMilliseconds, - (byte[] data) => - { - var request = new MetricsOtlpCollector.ExportMetricsServiceRequest(); - request.MergeFrom(data); - return request; - }, - Path.Combine(experimentalOptions.DiskRetryDirectoryPath, "metrics")); - } - else - { - return new OtlpExporterTransmissionHandler(exportClient, timeoutMilliseconds); + return new OtlpExporterTransmissionHandler(exportClient, timeoutMilliseconds); } } - public static OtlpExporterTransmissionHandler GetLogsExportTransmissionHandler(this OtlpExporterOptions options, ExperimentalOptions experimentalOptions) + public static IExportClient GetExportClient(this OtlpExporterOptions options, OtlpSignalType otlpSignalType) { - var exportClient = GetLogExportClient(options); - double timeoutMilliseconds = exportClient is OtlpHttpLogExportClient httpLogExportClient - ? httpLogExportClient.HttpClient.Timeout.TotalMilliseconds - : options.TimeoutMilliseconds; + var httpClient = options.HttpClientFactory?.Invoke() ?? throw new InvalidOperationException("OtlpExporterOptions was missing HttpClientFactory or it returned null."); - if (experimentalOptions.EnableInMemoryRetry) +#pragma warning disable CS0618 // Suppressing gRPC obsolete warning + if (options.Protocol != OtlpExportProtocol.Grpc && options.Protocol != OtlpExportProtocol.HttpProtobuf) { - return new OtlpExporterRetryTransmissionHandler(exportClient, timeoutMilliseconds); + throw new NotSupportedException($"Protocol {options.Protocol} is not supported."); } - else if (experimentalOptions.EnableDiskRetry) - { - Debug.Assert(!string.IsNullOrEmpty(experimentalOptions.DiskRetryDirectoryPath), $"{nameof(experimentalOptions.DiskRetryDirectoryPath)} is null or empty"); - return new OtlpExporterPersistentStorageTransmissionHandler( - exportClient, - timeoutMilliseconds, - (byte[] data) => - { - var request = new LogOtlpCollector.ExportLogsServiceRequest(); - request.MergeFrom(data); - return request; - }, - Path.Combine(experimentalOptions.DiskRetryDirectoryPath, "logs")); - } - else + return otlpSignalType switch { - return new OtlpExporterTransmissionHandler(exportClient, timeoutMilliseconds); - } - } + OtlpSignalType.Traces => options.Protocol == OtlpExportProtocol.Grpc + ? new OtlpGrpcExportClient(options, httpClient, TraceGrpcServicePath) + : new OtlpHttpExportClient(options, httpClient, TraceHttpServicePath), - 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."), - }; + OtlpSignalType.Metrics => options.Protocol == OtlpExportProtocol.Grpc + ? new OtlpGrpcExportClient(options, httpClient, MetricsGrpcServicePath) + : new OtlpHttpExportClient(options, httpClient, MetricsHttpServicePath), - 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."), - }; + OtlpSignalType.Logs => options.Protocol == OtlpExportProtocol.Grpc + ? new OtlpGrpcExportClient(options, httpClient, LogsGrpcServicePath) + : new OtlpHttpExportClient(options, httpClient, LogsHttpServicePath), - 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."), + _ => throw new NotSupportedException($"OtlpSignalType {otlpSignalType} is not supported."), }; +#pragma warning restore CS0618 // Suppressing gRPC obsolete warning + } public static void TryEnableIHttpClientFactoryIntegration(this OtlpExporterOptions options, IServiceProvider serviceProvider, string httpClientName) { @@ -233,25 +135,28 @@ public static void TryEnableIHttpClientFactoryIntegration(this OtlpExporterOptio { options.HttpClientFactory = () => { - Type httpClientFactoryType = Type.GetType("System.Net.Http.IHttpClientFactory, Microsoft.Extensions.Http", throwOnError: false); + Type? httpClientFactoryType = Type.GetType("System.Net.Http.IHttpClientFactory, Microsoft.Extensions.Http", throwOnError: false); if (httpClientFactoryType != null) { - object httpClientFactory = serviceProvider.GetService(httpClientFactoryType); + object? httpClientFactory = serviceProvider.GetService(httpClientFactoryType); if (httpClientFactory != null) { - MethodInfo createClientMethod = httpClientFactoryType.GetMethod( + MethodInfo? createClientMethod = httpClientFactoryType.GetMethod( "CreateClient", BindingFlags.Public | BindingFlags.Instance, binder: null, - new Type[] { typeof(string) }, + [typeof(string)], modifiers: null); if (createClientMethod != null) { - HttpClient client = (HttpClient)createClientMethod.Invoke(httpClientFactory, new object[] { httpClientName }); + HttpClient? client = (HttpClient?)createClientMethod.Invoke(httpClientFactory, [httpClientName]); - client.Timeout = TimeSpan.FromMilliseconds(options.TimeoutMilliseconds); + if (client != null) + { + client.Timeout = TimeSpan.FromMilliseconds(options.TimeoutMilliseconds); - return client; + return client; + } } } } @@ -266,7 +171,11 @@ internal static Uri AppendPathIfNotPresent(this Uri uri, string path) var absoluteUri = uri.AbsoluteUri; var separator = string.Empty; - if (absoluteUri.EndsWith("/")) +#if NET || NETSTANDARD2_1_OR_GREATER + if (absoluteUri.EndsWith('/')) +#else + if (absoluteUri.EndsWith("/", StringComparison.Ordinal)) +#endif { // Endpoint already ends with 'path/' if (absoluteUri.EndsWith(string.Concat(path, "/"), StringComparison.OrdinalIgnoreCase)) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs index b3781c4c346..dbec08c771b 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs @@ -1,14 +1,13 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - +using System.Buffers.Binary; using System.Diagnostics; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; using OpenTelemetry.Logs; -using OtlpCollector = OpenTelemetry.Proto.Collector.Logs.V1; -using OtlpResource = OpenTelemetry.Proto.Resource.V1; +using OpenTelemetry.Resources; namespace OpenTelemetry.Exporter; @@ -18,10 +17,18 @@ namespace OpenTelemetry.Exporter; /// public sealed class OtlpLogExporter : BaseExporter { - private readonly OtlpExporterTransmissionHandler transmissionHandler; - private readonly OtlpLogRecordTransformer otlpLogRecordTransformer; + private const int GrpcStartWritePosition = 5; + private readonly SdkLimitOptions sdkLimitOptions; + private readonly ExperimentalOptions experimentalOptions; + private readonly OtlpExporterTransmissionHandler transmissionHandler; + private readonly int startWritePosition; + + private Resource? resource; - private OtlpResource.Resource? processResource; + // Initial buffer size set to ~732KB. + // This choice allows us to gradually grow the buffer while targeting a final capacity of around 100 MB, + // by the 7th doubling to maintain efficient allocation without frequent resizing. + private byte[] buffer = new byte[750000]; /// /// Initializes a new instance of the class. @@ -38,38 +45,51 @@ public OtlpLogExporter(OtlpExporterOptions options) /// . /// . /// . - /// . + /// . internal OtlpLogExporter( OtlpExporterOptions exporterOptions, SdkLimitOptions sdkLimitOptions, ExperimentalOptions experimentalOptions, - OtlpExporterTransmissionHandler? transmissionHandler = null) + OtlpExporterTransmissionHandler? transmissionHandler = null) { Debug.Assert(exporterOptions != null, "exporterOptions was null"); Debug.Assert(sdkLimitOptions != null, "sdkLimitOptions was null"); Debug.Assert(experimentalOptions != null, "experimentalOptions was null"); - this.transmissionHandler = transmissionHandler ?? exporterOptions.GetLogsExportTransmissionHandler(experimentalOptions!); - - this.otlpLogRecordTransformer = new OtlpLogRecordTransformer(sdkLimitOptions!, experimentalOptions!); + this.experimentalOptions = experimentalOptions!; + this.sdkLimitOptions = sdkLimitOptions!; +#pragma warning disable CS0618 // Suppressing gRPC obsolete warning + this.startWritePosition = exporterOptions!.Protocol == OtlpExportProtocol.Grpc ? GrpcStartWritePosition : 0; +#pragma warning restore CS0618 // Suppressing gRPC obsolete warning + this.transmissionHandler = transmissionHandler ?? exporterOptions!.GetExportTransmissionHandler(experimentalOptions!, OtlpSignalType.Logs); } - internal OtlpResource.Resource ProcessResource - => this.processResource ??= this.ParentProvider.GetResource().ToOtlpResource(); + internal Resource Resource => this.resource ??= this.ParentProvider.GetResource(); /// +#pragma warning disable CA1725 // Parameter names should match base declaration public override ExportResult Export(in Batch logRecordBatch) +#pragma warning restore CA1725 // Parameter names should match base declaration { // Prevents the exporter's gRPC and HTTP operations from being instrumented. using var scope = SuppressInstrumentationScope.Begin(); - OtlpCollector.ExportLogsServiceRequest? request = null; - try { - request = this.otlpLogRecordTransformer.BuildExportRequest(this.ProcessResource, logRecordBatch); + int writePosition = ProtobufOtlpLogSerializer.WriteLogsData(ref this.buffer, this.startWritePosition, this.sdkLimitOptions, this.experimentalOptions, this.Resource, logRecordBatch); + + if (this.startWritePosition == GrpcStartWritePosition) + { + // Grpc payload consists of 3 parts + // byte 0 - Specifying if the payload is compressed. + // 1-4 byte - Specifies the length of payload in big endian format. + // 5 and above - Protobuf serialized data. + Span data = new Span(this.buffer, 1, 4); + var dataLength = writePosition - GrpcStartWritePosition; + BinaryPrimitives.WriteUInt32BigEndian(data, (uint)dataLength); + } - if (!this.transmissionHandler.TrySubmitRequest(request)) + if (!this.transmissionHandler.TrySubmitRequest(this.buffer, writePosition)) { return ExportResult.Failure; } @@ -79,20 +99,10 @@ public override ExportResult Export(in Batch logRecordBatch) 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; - } + protected override bool OnShutdown(int timeoutMilliseconds) => this.transmissionHandler?.Shutdown(timeoutMilliseconds) ?? true; } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporterHelperExtensions.cs index 3a994c8e772..851a7729286 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporterHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporterHelperExtensions.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -58,7 +56,9 @@ public static OpenTelemetryLoggerOptions AddOtlpExporter( var finalOptionsName = name ?? Options.DefaultName; +#pragma warning disable CA1062 // Validate arguments of public methods return loggerOptions.AddProcessor(sp => +#pragma warning restore CA1062 // Validate arguments of public methods { var exporterOptions = GetOptions(sp, name, finalOptionsName, OtlpExporterOptions.CreateOtlpExporterOptions); @@ -104,7 +104,9 @@ public static OpenTelemetryLoggerOptions AddOtlpExporter( var finalOptionsName = name ?? Options.DefaultName; +#pragma warning disable CA1062 // Validate arguments of public methods return loggerOptions.AddProcessor(sp => +#pragma warning restore CA1062 // Validate arguments of public methods { var exporterOptions = GetOptions(sp, name, finalOptionsName, OtlpExporterOptions.CreateOtlpExporterOptions); @@ -286,9 +288,20 @@ internal static BaseProcessor BuildOtlpLogExporter( Debug.Assert(sdkLimitOptions != null, "sdkLimitOptions was null"); Debug.Assert(experimentalOptions != null, "experimentalOptions was null"); +#if NET462_OR_GREATER || NETSTANDARD2_0 +#pragma warning disable CS0618 // Suppressing gRPC obsolete warning + if (exporterOptions!.Protocol == OtlpExportProtocol.Grpc && + ReferenceEquals(exporterOptions.HttpClientFactory, exporterOptions.DefaultHttpClientFactory)) +#pragma warning restore CS0618 // Suppressing gRPC obsolete warning + { + throw new NotSupportedException("OtlpExportProtocol.Grpc with the default HTTP client factory is not supported on .NET Framework or .NET Standard 2.0." + + "Please switch to OtlpExportProtocol.HttpProtobuf or provide a custom HttpClientFactory."); + } +#endif + if (!skipUseOtlpExporterRegistrationCheck) { - serviceProvider.EnsureNoUseOtlpExporterRegistrations(); + serviceProvider!.EnsureNoUseOtlpExporterRegistrations(); } /* @@ -307,30 +320,40 @@ internal static BaseProcessor BuildOtlpLogExporter( * "OtlpLogExporter"); */ +#pragma warning disable CA2000 // Dispose objects before losing scope BaseExporter otlpExporter = new OtlpLogExporter( exporterOptions!, sdkLimitOptions!, experimentalOptions!); +#pragma warning restore CA2000 // Dispose objects before losing scope - if (configureExporterInstance != null) + try { - otlpExporter = configureExporterInstance(otlpExporter); - } + if (configureExporterInstance != null) + { + otlpExporter = configureExporterInstance(otlpExporter); + } - if (processorOptions!.ExportProcessorType == ExportProcessorType.Simple) - { - return new SimpleLogRecordExportProcessor(otlpExporter); + if (processorOptions!.ExportProcessorType == ExportProcessorType.Simple) + { + return new SimpleLogRecordExportProcessor(otlpExporter); + } + else + { + var batchOptions = processorOptions.BatchExportProcessorOptions; + + return new BatchLogRecordExportProcessor( + otlpExporter, + batchOptions.MaxQueueSize, + batchOptions.ScheduledDelayMilliseconds, + batchOptions.ExporterTimeoutMilliseconds, + batchOptions.MaxExportBatchSize); + } } - else + catch { - var batchOptions = processorOptions.BatchExportProcessorOptions; - - return new BatchLogRecordExportProcessor( - otlpExporter, - batchOptions.MaxQueueSize, - batchOptions.ScheduledDelayMilliseconds, - batchOptions.ExporterTimeoutMilliseconds, - batchOptions.MaxExportBatchSize); + otlpExporter.Dispose(); + throw; } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporter.cs index b2b0c3e5766..1ff7dcc5ae5 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporter.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporter.cs @@ -1,12 +1,13 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Buffers.Binary; using System.Diagnostics; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; using OpenTelemetry.Metrics; -using OtlpCollector = OpenTelemetry.Proto.Collector.Metrics.V1; -using OtlpResource = OpenTelemetry.Proto.Resource.V1; +using OpenTelemetry.Resources; namespace OpenTelemetry.Exporter; @@ -16,9 +17,16 @@ namespace OpenTelemetry.Exporter; /// public class OtlpMetricExporter : BaseExporter { - private readonly OtlpExporterTransmissionHandler transmissionHandler; + private const int GrpcStartWritePosition = 5; + private readonly OtlpExporterTransmissionHandler transmissionHandler; + private readonly int startWritePosition; - private OtlpResource.Resource processResource; + private Resource? resource; + + // Initial buffer size set to ~732KB. + // This choice allows us to gradually grow the buffer while targeting a final capacity of around 100 MB, + // by the 7th doubling to maintain efficient allocation without frequent resizing. + private byte[] buffer = new byte[750000]; /// /// Initializes a new instance of the class. @@ -34,33 +42,47 @@ public OtlpMetricExporter(OtlpExporterOptions options) /// /// . /// . - /// . + /// . internal OtlpMetricExporter( OtlpExporterOptions exporterOptions, ExperimentalOptions experimentalOptions, - OtlpExporterTransmissionHandler transmissionHandler = null) + OtlpExporterTransmissionHandler? transmissionHandler = null) { Debug.Assert(exporterOptions != null, "exporterOptions was null"); Debug.Assert(experimentalOptions != null, "experimentalOptions was null"); - this.transmissionHandler = transmissionHandler ?? exporterOptions.GetMetricsExportTransmissionHandler(experimentalOptions); +#pragma warning disable CS0618 // Suppressing gRPC obsolete warning + this.startWritePosition = exporterOptions!.Protocol == OtlpExportProtocol.Grpc ? GrpcStartWritePosition : 0; +#pragma warning restore CS0618 // Suppressing gRPC obsolete warning + this.transmissionHandler = transmissionHandler ?? exporterOptions!.GetExportTransmissionHandler(experimentalOptions!, OtlpSignalType.Metrics); } - internal OtlpResource.Resource ProcessResource => this.processResource ??= this.ParentProvider.GetResource().ToOtlpResource(); + internal Resource Resource => this.resource ??= this.ParentProvider.GetResource(); /// +#pragma warning disable CA1725 // Parameter names should match base declaration public override ExportResult Export(in Batch metrics) +#pragma warning restore CA1725 // Parameter names should match base declaration { // Prevents the exporter's gRPC and HTTP operations from being instrumented. using var scope = SuppressInstrumentationScope.Begin(); - var request = new OtlpCollector.ExportMetricsServiceRequest(); - try { - request.AddMetrics(this.ProcessResource, metrics); + int writePosition = ProtobufOtlpMetricSerializer.WriteMetricsData(ref this.buffer, this.startWritePosition, this.Resource, metrics); + + if (this.startWritePosition == GrpcStartWritePosition) + { + // Grpc payload consists of 3 parts + // byte 0 - Specifying if the payload is compressed. + // 1-4 byte - Specifies the length of payload in big endian format. + // 5 and above - Protobuf serialized data. + Span data = new Span(this.buffer, 1, 4); + var dataLength = writePosition - GrpcStartWritePosition; + BinaryPrimitives.WriteUInt32BigEndian(data, (uint)dataLength); + } - if (!this.transmissionHandler.TrySubmitRequest(request)) + if (!this.transmissionHandler.TrySubmitRequest(this.buffer, writePosition)) { return ExportResult.Failure; } @@ -70,17 +92,10 @@ public override ExportResult Export(in Batch metrics) OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(ex); return ExportResult.Failure; } - finally - { - request.Return(); - } return ExportResult.Success; } /// - protected override bool OnShutdown(int timeoutMilliseconds) - { - return this.transmissionHandler.Shutdown(timeoutMilliseconds); - } + protected override bool OnShutdown(int timeoutMilliseconds) => this.transmissionHandler.Shutdown(timeoutMilliseconds); } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterExtensions.cs index 57ad3dd47ec..4efbc2c8b18 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterExtensions.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -170,14 +168,27 @@ internal static MetricReader BuildOtlpExporterMetricReader( Debug.Assert(metricReaderOptions != null, "metricReaderOptions was null"); Debug.Assert(experimentalOptions != null, "experimentalOptions was null"); +#if NET462_OR_GREATER || NETSTANDARD2_0 +#pragma warning disable CS0618 // Suppressing gRPC obsolete warning + if (exporterOptions!.Protocol == OtlpExportProtocol.Grpc && + ReferenceEquals(exporterOptions.HttpClientFactory, exporterOptions.DefaultHttpClientFactory)) +#pragma warning restore CS0618 // Suppressing gRPC obsolete warning + { + throw new NotSupportedException("OtlpExportProtocol.Grpc with the default HTTP client factory is not supported on .NET Framework or .NET Standard 2.0." + + "Please switch to OtlpExportProtocol.HttpProtobuf or provide a custom HttpClientFactory."); + } +#endif + if (!skipUseOtlpExporterRegistrationCheck) { - serviceProvider.EnsureNoUseOtlpExporterRegistrations(); + serviceProvider!.EnsureNoUseOtlpExporterRegistrations(); } - exporterOptions.TryEnableIHttpClientFactoryIntegration(serviceProvider, "OtlpMetricExporter"); + exporterOptions!.TryEnableIHttpClientFactoryIntegration(serviceProvider!, "OtlpMetricExporter"); - BaseExporter metricExporter = new OtlpMetricExporter(exporterOptions, experimentalOptions); +#pragma warning disable CA2000 // Dispose objects before losing scope + BaseExporter metricExporter = new OtlpMetricExporter(exporterOptions!, experimentalOptions!); +#pragma warning restore CA2000 // Dispose objects before losing scope if (configureExporterInstance != null) { diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpSignalType.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpSignalType.cs new file mode 100644 index 00000000000..da317f04b32 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpSignalType.cs @@ -0,0 +1,25 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Exporter; + +/// +/// Represents the different types of signals that can be exported. +/// +internal enum OtlpSignalType +{ + /// + /// Represents trace signals. + /// + Traces = 0, + + /// + /// Represents metric signals. + /// + Metrics = 1, + + /// + /// Represents log signals. + /// + Logs = 2, +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs index b02d348bd8a..6f10f51e7b2 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs @@ -1,11 +1,12 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Buffers.Binary; using System.Diagnostics; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; -using OtlpCollector = OpenTelemetry.Proto.Collector.Trace.V1; -using OtlpResource = OpenTelemetry.Proto.Resource.V1; +using OpenTelemetry.Resources; namespace OpenTelemetry.Exporter; @@ -15,10 +16,17 @@ namespace OpenTelemetry.Exporter; /// public class OtlpTraceExporter : BaseExporter { + private const int GrpcStartWritePosition = 5; private readonly SdkLimitOptions sdkLimitOptions; - private readonly OtlpExporterTransmissionHandler transmissionHandler; + private readonly OtlpExporterTransmissionHandler transmissionHandler; + private readonly int startWritePosition; - private OtlpResource.Resource processResource; + private Resource? resource; + + // Initial buffer size set to ~732KB. + // This choice allows us to gradually grow the buffer while targeting a final capacity of around 100 MB, + // by the 7th doubling to maintain efficient allocation without frequent resizing. + private byte[] buffer = new byte[750000]; /// /// Initializes a new instance of the class. @@ -35,36 +43,49 @@ public OtlpTraceExporter(OtlpExporterOptions options) /// . /// . /// . - /// . + /// . internal OtlpTraceExporter( OtlpExporterOptions exporterOptions, SdkLimitOptions sdkLimitOptions, ExperimentalOptions experimentalOptions, - OtlpExporterTransmissionHandler transmissionHandler = null) + OtlpExporterTransmissionHandler? transmissionHandler = null) { Debug.Assert(exporterOptions != null, "exporterOptions was null"); Debug.Assert(sdkLimitOptions != null, "sdkLimitOptions was null"); - this.sdkLimitOptions = sdkLimitOptions; - - this.transmissionHandler = transmissionHandler ?? exporterOptions.GetTraceExportTransmissionHandler(experimentalOptions); + this.sdkLimitOptions = sdkLimitOptions!; +#pragma warning disable CS0618 // Suppressing gRPC obsolete warning + this.startWritePosition = exporterOptions!.Protocol == OtlpExportProtocol.Grpc ? GrpcStartWritePosition : 0; +#pragma warning restore CS0618 // Suppressing gRPC obsolete warning + this.transmissionHandler = transmissionHandler ?? exporterOptions!.GetExportTransmissionHandler(experimentalOptions, OtlpSignalType.Traces); } - internal OtlpResource.Resource ProcessResource => this.processResource ??= this.ParentProvider.GetResource().ToOtlpResource(); + internal Resource Resource => this.resource ??= this.ParentProvider.GetResource(); /// +#pragma warning disable CA1725 // Parameter names should match base declaration public override ExportResult Export(in Batch activityBatch) +#pragma warning restore CA1725 // Parameter names should match base declaration { // 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); + int writePosition = ProtobufOtlpTraceSerializer.WriteTraceData(ref this.buffer, this.startWritePosition, this.sdkLimitOptions, this.Resource, activityBatch); + + if (this.startWritePosition == GrpcStartWritePosition) + { + // Grpc payload consists of 3 parts + // byte 0 - Specifying if the payload is compressed. + // 1-4 byte - Specifies the length of payload in big endian format. + // 5 and above - Protobuf serialized data. + Span data = new Span(this.buffer, 1, 4); + var dataLength = writePosition - GrpcStartWritePosition; + BinaryPrimitives.WriteUInt32BigEndian(data, (uint)dataLength); + } - if (!this.transmissionHandler.TrySubmitRequest(request)) + if (!this.transmissionHandler.TrySubmitRequest(this.buffer, writePosition)) { return ExportResult.Failure; } @@ -74,17 +95,10 @@ public override ExportResult Export(in Batch activityBatch) OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(ex); return ExportResult.Failure; } - finally - { - request.Return(); - } return ExportResult.Success; } /// - protected override bool OnShutdown(int timeoutMilliseconds) - { - return this.transmissionHandler.Shutdown(timeoutMilliseconds); - } + protected override bool OnShutdown(int timeoutMilliseconds) => this.transmissionHandler.Shutdown(timeoutMilliseconds); } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporterHelperExtensions.cs index 036e9a90dd3..831a1380bb6 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporterHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporterHelperExtensions.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -131,32 +129,53 @@ internal static BaseProcessor BuildOtlpExporterProcessor( Debug.Assert(experimentalOptions != null, "experimentalOptions was null"); Debug.Assert(batchExportProcessorOptions != null, "batchExportProcessorOptions was null"); +#if NET462_OR_GREATER || NETSTANDARD2_0 +#pragma warning disable CS0618 // Suppressing gRPC obsolete warning + if (exporterOptions!.Protocol == OtlpExportProtocol.Grpc && + ReferenceEquals(exporterOptions.HttpClientFactory, exporterOptions.DefaultHttpClientFactory)) +#pragma warning restore CS0618 // Suppressing gRPC obsolete warning + { + throw new NotSupportedException("OtlpExportProtocol.Grpc with the default HTTP client factory is not supported on .NET Framework or .NET Standard 2.0." + + "Please switch to OtlpExportProtocol.HttpProtobuf or provide a custom HttpClientFactory."); + } +#endif + if (!skipUseOtlpExporterRegistrationCheck) { - serviceProvider.EnsureNoUseOtlpExporterRegistrations(); + serviceProvider!.EnsureNoUseOtlpExporterRegistrations(); } - exporterOptions.TryEnableIHttpClientFactoryIntegration(serviceProvider, "OtlpTraceExporter"); + exporterOptions!.TryEnableIHttpClientFactoryIntegration(serviceProvider!, "OtlpTraceExporter"); - BaseExporter otlpExporter = new OtlpTraceExporter(exporterOptions, sdkLimitOptions, experimentalOptions); +#pragma warning disable CA2000 // Dispose objects before losing scope + BaseExporter otlpExporter = new OtlpTraceExporter(exporterOptions!, sdkLimitOptions!, experimentalOptions!); +#pragma warning restore CA2000 // Dispose objects before losing scope - if (configureExporterInstance != null) + try { - otlpExporter = configureExporterInstance(otlpExporter); - } + if (configureExporterInstance != null) + { + otlpExporter = configureExporterInstance(otlpExporter); + } - if (exportProcessorType == ExportProcessorType.Simple) - { - return new SimpleActivityExportProcessor(otlpExporter); + if (exportProcessorType == ExportProcessorType.Simple) + { + return new SimpleActivityExportProcessor(otlpExporter); + } + else + { + return new BatchActivityExportProcessor( + otlpExporter, + batchExportProcessorOptions!.MaxQueueSize, + batchExportProcessorOptions.ScheduledDelayMilliseconds, + batchExportProcessorOptions.ExporterTimeoutMilliseconds, + batchExportProcessorOptions.MaxExportBatchSize); + } } - else + catch { - return new BatchActivityExportProcessor( - otlpExporter, - batchExportProcessorOptions!.MaxQueueSize, - batchExportProcessorOptions.ScheduledDelayMilliseconds, - batchExportProcessorOptions.ExporterTimeoutMilliseconds, - batchExportProcessorOptions.MaxExportBatchSize); + otlpExporter.Dispose(); + throw; } } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/DirectorySizeTracker.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/DirectorySizeTracker.cs index a60715d185b..d251885f81d 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/DirectorySizeTracker.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/DirectorySizeTracker.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - namespace OpenTelemetry.PersistentStorage.FileSystem; /// @@ -58,16 +56,16 @@ internal static long CalculateFolderSize(string path) long directorySize = 0; try { - foreach (string file in Directory.EnumerateFiles(path)) + foreach (var file in Directory.EnumerateFiles(path)) { if (File.Exists(file)) { - FileInfo fileInfo = new FileInfo(file); + var fileInfo = new FileInfo(file); directorySize += fileInfo.Length; } } - foreach (string dir in Directory.GetDirectories(path)) + foreach (var dir in Directory.GetDirectories(path)) { directorySize += CalculateFolderSize(dir); } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/FileBlob.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/FileBlob.cs index 337c3226243..72f121b9508 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/FileBlob.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/FileBlob.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics.CodeAnalysis; using OpenTelemetry.Internal; using OpenTelemetry.PersistentStorage.Abstractions; @@ -60,7 +58,7 @@ protected override bool OnTryWrite(byte[] buffer, int leasePeriodMilliseconds = { Guard.ThrowIfNull(buffer); - string path = this.FullPath + ".tmp"; + var path = this.FullPath + ".tmp"; try { diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/FileBlobProvider.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/FileBlobProvider.cs index c9afdf34ce8..9e79fb28736 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/FileBlobProvider.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/FileBlobProvider.cs @@ -1,13 +1,10 @@ // 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; @@ -26,7 +23,7 @@ public class FileBlobProvider : PersistentBlobProvider, IDisposable private readonly DirectorySizeTracker directorySizeTracker; private readonly long retentionPeriodInMilliseconds; private readonly int writeTimeoutInMilliseconds; - private readonly Timer maintenanceTimer; + private readonly System.Timers.Timer maintenanceTimer; private bool disposedValue; /// @@ -63,7 +60,7 @@ public class FileBlobProvider : PersistentBlobProvider, IDisposable /// path exceeds system defined maximum length. /// /// - /// insufficient priviledges for provided path. + /// insufficient privileges for provided path. /// /// /// path contains a colon character (:) that is not part of a drive label ("C:\"). @@ -89,7 +86,7 @@ public FileBlobProvider( this.retentionPeriodInMilliseconds = retentionPeriodInMilliseconds; this.writeTimeoutInMilliseconds = writeTimeoutInMilliseconds; - this.maintenanceTimer = new Timer(maintenancePeriodInMilliseconds); + this.maintenanceTimer = new System.Timers.Timer(maintenancePeriodInMilliseconds); this.maintenanceTimer.Elapsed += this.OnMaintenanceEvent; this.maintenanceTimer.AutoReset = true; this.maintenanceTimer.Enabled = true; @@ -107,7 +104,7 @@ protected override IEnumerable OnGetBlobs() foreach (var file in Directory.EnumerateFiles(this.DirectoryPath, "*.blob", SearchOption.TopDirectoryOnly).OrderByDescending(f => f)) { - DateTime fileDateTime = PersistentStorageHelper.GetDateTimeFromBlobName(file); + var fileDateTime = PersistentStorageHelper.GetDateTimeFromBlobName(file); if (fileDateTime > retentionDeadline) { yield return new FileBlob(file, this.directorySizeTracker); @@ -176,7 +173,7 @@ private void OnMaintenanceEvent(object? source, ElapsedEventArgs e) private bool CheckStorageSize() { - if (!this.directorySizeTracker.IsSpaceAvailable(out long size)) + if (!this.directorySizeTracker.IsSpaceAvailable(out var size)) { // TODO: check accuracy of size reporting. PersistentStorageEventSource.Log.PersistentStorageWarning( @@ -188,7 +185,7 @@ private bool CheckStorageSize() return true; } - private PersistentBlob? CreateFileBlob(byte[] buffer, int leasePeriodMilliseconds = 0) + private FileBlob? CreateFileBlob(byte[] buffer, int leasePeriodMilliseconds = 0) { if (!this.CheckStorageSize()) { @@ -200,14 +197,7 @@ private bool CheckStorageSize() 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; - } + return blob.TryWrite(buffer, leasePeriodMilliseconds) ? blob : null; } catch (Exception ex) { diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentBlob.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentBlob.cs index 2f0912feb75..aca1491ba29 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentBlob.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentBlob.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics.CodeAnalysis; namespace OpenTelemetry.PersistentStorage.Abstractions; diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentBlobProvider.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentBlobProvider.cs index 5d4895f8d46..465af27746d 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentBlobProvider.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentBlobProvider.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics.CodeAnalysis; namespace OpenTelemetry.PersistentStorage.Abstractions; @@ -104,12 +102,12 @@ public IEnumerable GetBlobs() { try { - return this.OnGetBlobs() ?? Enumerable.Empty(); + return this.OnGetBlobs() ?? []; } catch (Exception ex) { PersistentStorageAbstractionsEventSource.Log.PersistentStorageAbstractionsException(nameof(PersistentBlobProvider), "Failed to get all the blobs", ex); - return Enumerable.Empty(); + return []; } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentStorageHelper.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentStorageHelper.cs index 538597dc18f..6d2fd821cbb 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentStorageHelper.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/PersistentStorageHelper.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Globalization; using System.Runtime.CompilerServices; @@ -14,7 +12,7 @@ internal static void RemoveExpiredBlob(DateTime retentionDeadline, string filePa { if (filePath.EndsWith(".blob", StringComparison.OrdinalIgnoreCase)) { - DateTime fileDateTime = GetDateTimeFromBlobName(filePath); + var fileDateTime = GetDateTimeFromBlobName(filePath); if (fileDateTime < retentionDeadline) { try @@ -32,11 +30,11 @@ internal static void RemoveExpiredBlob(DateTime retentionDeadline, string filePa internal static bool RemoveExpiredLease(DateTime leaseDeadline, string filePath) { - bool success = false; + var success = false; if (filePath.EndsWith(".lock", StringComparison.OrdinalIgnoreCase)) { - DateTime fileDateTime = GetDateTimeFromLeaseName(filePath); + var fileDateTime = GetDateTimeFromLeaseName(filePath); if (fileDateTime < leaseDeadline) { var newFilePath = filePath.Substring(0, filePath.LastIndexOf('@')); @@ -57,11 +55,11 @@ internal static bool RemoveExpiredLease(DateTime leaseDeadline, string filePath) internal static bool RemoveTimedOutTmpFiles(DateTime timeoutDeadline, string filePath) { - bool success = false; + var success = false; if (filePath.EndsWith(".tmp", StringComparison.OrdinalIgnoreCase)) { - DateTime fileDateTime = GetDateTimeFromBlobName(filePath); + var fileDateTime = GetDateTimeFromBlobName(filePath); if (fileDateTime < timeoutDeadline) { try @@ -146,7 +144,7 @@ internal static DateTime GetDateTimeFromLeaseName(string filePath) { var fileName = Path.GetFileNameWithoutExtension(filePath); var startIndex = fileName.LastIndexOf('@') + 1; - var time = fileName.Substring(startIndex, fileName.Length - startIndex); + var time = fileName.Substring(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 index 018aef5b4a8..353cd8dffbc 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/README.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/PersistentStorage/README.md @@ -1,9 +1,9 @@ # 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) +[OpenTelemetry.PersistentStorage.Abstractions](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/tree/1be4157075ad09e13aa54b8e5845d2310bd82673/src/OpenTelemetry.PersistentStorage.Abstractions) and -[OpenTelemetry.PersistentStorage.FileSystem](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/tree/58607b7cdeb15207027a6fa4ca56e7fac897bda4/src/OpenTelemetry.PersistentStorage.FileSystem). +[OpenTelemetry.PersistentStorage.FileSystem](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/tree/1be4157075ad09e13aa54b8e5845d2310bd82673/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: diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Shipped.txt index e69de29bb2d..7dc5c58110b 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Shipped.txt +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Unshipped.txt index 99d1e9e4939..92eaa1e4111 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/.publicApi/PublicAPI.Unshipped.txt @@ -4,18 +4,18 @@ 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.get -> string? OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.ScrapeEndpointPath.set -> void OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.ScrapeResponseCacheDurationMilliseconds.get -> int OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.ScrapeResponseCacheDurationMilliseconds.set -> void OpenTelemetry.Metrics.PrometheusExporterMeterProviderBuilderExtensions -static Microsoft.AspNetCore.Builder.PrometheusExporterApplicationBuilderExtensions.UseOpenTelemetryPrometheusScrapingEndpoint(this Microsoft.AspNetCore.Builder.IApplicationBuilder app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder -static Microsoft.AspNetCore.Builder.PrometheusExporterApplicationBuilderExtensions.UseOpenTelemetryPrometheusScrapingEndpoint(this Microsoft.AspNetCore.Builder.IApplicationBuilder app, OpenTelemetry.Metrics.MeterProvider meterProvider, System.Func predicate, string path, System.Action configureBranchedPipeline, string optionsName) -> Microsoft.AspNetCore.Builder.IApplicationBuilder -static Microsoft.AspNetCore.Builder.PrometheusExporterApplicationBuilderExtensions.UseOpenTelemetryPrometheusScrapingEndpoint(this Microsoft.AspNetCore.Builder.IApplicationBuilder app, string path) -> Microsoft.AspNetCore.Builder.IApplicationBuilder -static Microsoft.AspNetCore.Builder.PrometheusExporterApplicationBuilderExtensions.UseOpenTelemetryPrometheusScrapingEndpoint(this Microsoft.AspNetCore.Builder.IApplicationBuilder app, System.Func predicate) -> Microsoft.AspNetCore.Builder.IApplicationBuilder -static Microsoft.AspNetCore.Builder.PrometheusExporterEndpointRouteBuilderExtensions.MapPrometheusScrapingEndpoint(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder -static Microsoft.AspNetCore.Builder.PrometheusExporterEndpointRouteBuilderExtensions.MapPrometheusScrapingEndpoint(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string path) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder -static Microsoft.AspNetCore.Builder.PrometheusExporterEndpointRouteBuilderExtensions.MapPrometheusScrapingEndpoint(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string path, OpenTelemetry.Metrics.MeterProvider meterProvider, System.Action configureBranchedPipeline, string optionsName) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder -static OpenTelemetry.Metrics.PrometheusExporterMeterProviderBuilderExtensions.AddPrometheusExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.PrometheusExporterMeterProviderBuilderExtensions.AddPrometheusExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, string name, System.Action configure) -> OpenTelemetry.Metrics.MeterProviderBuilder -static OpenTelemetry.Metrics.PrometheusExporterMeterProviderBuilderExtensions.AddPrometheusExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action configure) -> OpenTelemetry.Metrics.MeterProviderBuilder +static Microsoft.AspNetCore.Builder.PrometheusExporterApplicationBuilderExtensions.UseOpenTelemetryPrometheusScrapingEndpoint(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! +static Microsoft.AspNetCore.Builder.PrometheusExporterApplicationBuilderExtensions.UseOpenTelemetryPrometheusScrapingEndpoint(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, OpenTelemetry.Metrics.MeterProvider? meterProvider, System.Func? predicate, string? path, System.Action? configureBranchedPipeline, string? optionsName) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! +static Microsoft.AspNetCore.Builder.PrometheusExporterApplicationBuilderExtensions.UseOpenTelemetryPrometheusScrapingEndpoint(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, string! path) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! +static Microsoft.AspNetCore.Builder.PrometheusExporterApplicationBuilderExtensions.UseOpenTelemetryPrometheusScrapingEndpoint(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, System.Func! predicate) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! +static Microsoft.AspNetCore.Builder.PrometheusExporterEndpointRouteBuilderExtensions.MapPrometheusScrapingEndpoint(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! +static Microsoft.AspNetCore.Builder.PrometheusExporterEndpointRouteBuilderExtensions.MapPrometheusScrapingEndpoint(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! path) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! +static Microsoft.AspNetCore.Builder.PrometheusExporterEndpointRouteBuilderExtensions.MapPrometheusScrapingEndpoint(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string? path, OpenTelemetry.Metrics.MeterProvider? meterProvider, System.Action? configureBranchedPipeline, string? optionsName) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! +static OpenTelemetry.Metrics.PrometheusExporterMeterProviderBuilderExtensions.AddPrometheusExporter(this OpenTelemetry.Metrics.MeterProviderBuilder! builder) -> OpenTelemetry.Metrics.MeterProviderBuilder! +static OpenTelemetry.Metrics.PrometheusExporterMeterProviderBuilderExtensions.AddPrometheusExporter(this OpenTelemetry.Metrics.MeterProviderBuilder! builder, string? name, System.Action? configure) -> OpenTelemetry.Metrics.MeterProviderBuilder! +static OpenTelemetry.Metrics.PrometheusExporterMeterProviderBuilderExtensions.AddPrometheusExporter(this OpenTelemetry.Metrics.MeterProviderBuilder! builder, System.Action! configure) -> OpenTelemetry.Metrics.MeterProviderBuilder! diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/AssemblyInfo.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/AssemblyInfo.cs deleted file mode 100644 index 59d05ae59c9..00000000000 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/AssemblyInfo.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Runtime.CompilerServices; - -#if SIGNED -[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] -[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Prometheus.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] -#else -[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests")] -[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Prometheus.Tests")] -#endif diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md index fa76f44a318..768eb691981 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md @@ -1,7 +1,55 @@ # Changelog +This file contains individual changes for the +OpenTelemetry.Exporter.Prometheus.AspNetCore package. For highlights and +announcements covering all components see: [Release +Notes](../../RELEASENOTES.md). + ## Unreleased +## 1.12.0-beta.1 + +Released 2025-May-06 + +* Updated OpenTelemetry core component version(s) to `1.12.0`. + ([#6269](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6269)) + +## 1.11.2-beta.1 + +Released 2025-Mar-05 + +* Updated OpenTelemetry core component version(s) to `1.11.2`. + ([#6169](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6169)) + +## 1.11.0-beta.1 + +Released 2025-Jan-16 + +* Updated OpenTelemetry core component version(s) to `1.11.0`. + ([#6064](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6064)) + +## 1.10.0-beta.1 + +Released 2024-Nov-12 + +* Added meter-level tags to Prometheus exporter + ([#5837](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5837)) + +* Updated OpenTelemetry core component version(s) to `1.10.0`. + ([#5970](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5970)) + +## 1.9.0-beta.2 + +Released 2024-Jun-24 + +* Fixed a bug which lead to empty responses when the internal buffer is resized + processing a collection request + ([#5676](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5676)) + +## 1.9.0-beta.1 + +Released 2024-Jun-14 + ## 1.9.0-alpha.2 Released 2024-May-29 @@ -113,14 +161,17 @@ Released 2022-Sep-29 * Bug fix for Prometheus Exporter reporting StatusCode 204 instead of 200, when no metrics are collected ([#3643](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3643)) + * Added overloads which accept a name to the `MeterProviderBuilder` `AddPrometheusExporter` extension to allow for more fine-grained options management ([#3648](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3648)) + * Added support for OpenMetrics UNIT metadata ([#3651](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3651)) + * Added `"# EOF\n"` ending following the [OpenMetrics - specification](https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md) + specification](https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md) ([#3654](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3654)) ## 1.4.0-alpha.2 @@ -133,6 +184,7 @@ Released 2022-Aug-18 ([#3430](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3430) [#3503](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3503) [#3507](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3507)) + * Added `IEndpointRouteBuilder` extension methods to help with Prometheus middleware configuration on ASP.NET Core ([#3295](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3295)) @@ -152,8 +204,10 @@ Released 2022-Apr-15 * Added `IApplicationBuilder` extension methods to help with Prometheus middleware configuration on ASP.NET Core ([#3029](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3029)) + * Changed Prometheus exporter to return 204 No Content and log a warning event if there are no metrics to collect. + * 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)) 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 a143d39ac12..1979137998b 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj @@ -7,9 +7,6 @@ $(PackageTags);prometheus;metrics coreunstable- $(DefineConstants);PROMETHEUS_ASPNETCORE - - - disable @@ -25,7 +22,6 @@ - @@ -36,6 +32,13 @@ + + + + + + + diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusAspNetCoreOptions.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusAspNetCoreOptions.cs index 75acb565175..ed8186ec88d 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusAspNetCoreOptions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusAspNetCoreOptions.cs @@ -15,7 +15,7 @@ public class PrometheusAspNetCoreOptions /// /// Gets or sets the path to use for the scraping endpoint. Default value: "/metrics". /// - public string ScrapeEndpointPath { get; set; } = DefaultScrapeEndpointPath; + public string? ScrapeEndpointPath { get; set; } = DefaultScrapeEndpointPath; /// /// Gets or sets a value indicating whether addition of _total suffix for counter metric names is disabled. Default value: . diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterApplicationBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterApplicationBuilderExtensions.cs index 946f07d05aa..92bef6f1b59 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterApplicationBuilderExtensions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterApplicationBuilderExtensions.cs @@ -93,12 +93,14 @@ public static IApplicationBuilder UseOpenTelemetryPrometheusScrapingEndpoint(thi /// cref="IApplicationBuilder"/> for chaining calls. public static IApplicationBuilder UseOpenTelemetryPrometheusScrapingEndpoint( this IApplicationBuilder app, - MeterProvider meterProvider, - Func predicate, - string path, - Action configureBranchedPipeline, - string optionsName) + MeterProvider? meterProvider, + Func? predicate, + string? path, + Action? configureBranchedPipeline, + string? optionsName) { + Guard.ThrowIfNull(app); + // Note: Order is important here. MeterProvider is accessed before // GetOptions so that any changes made to // PrometheusAspNetCoreOptions in deferred AddPrometheusExporter @@ -114,7 +116,7 @@ public static IApplicationBuilder UseOpenTelemetryPrometheusScrapingEndpoint( path = options.ScrapeEndpointPath ?? PrometheusAspNetCoreOptions.DefaultScrapeEndpointPath; } - if (!path.StartsWith("/")) + if (!path.StartsWith('/')) { path = $"/{path}"; } diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterEndpointRouteBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterEndpointRouteBuilderExtensions.cs index 4e70f2f76c9..6604935ebf0 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterEndpointRouteBuilderExtensions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterEndpointRouteBuilderExtensions.cs @@ -64,11 +64,13 @@ public static IEndpointConventionBuilder MapPrometheusScrapingEndpoint(this IEnd /// A convention routes for the Prometheus scraping endpoint. public static IEndpointConventionBuilder MapPrometheusScrapingEndpoint( this IEndpointRouteBuilder endpoints, - string path, - MeterProvider meterProvider, - Action configureBranchedPipeline, - string optionsName) + string? path, + MeterProvider? meterProvider, + Action? configureBranchedPipeline, + string? optionsName) { + Guard.ThrowIfNull(endpoints); + var builder = endpoints.CreateApplicationBuilder(); // Note: Order is important here. MeterProvider is accessed before @@ -84,7 +86,7 @@ public static IEndpointConventionBuilder MapPrometheusScrapingEndpoint( path = options.ScrapeEndpointPath ?? PrometheusAspNetCoreOptions.DefaultScrapeEndpointPath; } - if (!path.StartsWith("/")) + if (!path.StartsWith('/')) { path = $"/{path}"; } diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMeterProviderBuilderExtensions.cs index 6a075fc1b99..e3567d4ad63 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMeterProviderBuilderExtensions.cs @@ -37,13 +37,13 @@ public static MeterProviderBuilder AddPrometheusExporter( /// Adds to the . /// /// builder to use. - /// Name which is used when retrieving options. - /// Callback action for configuring . + /// Optional name which is used when retrieving options. + /// Optional callback action for configuring . /// The instance of to chain the calls. public static MeterProviderBuilder AddPrometheusExporter( this MeterProviderBuilder builder, - string name, - Action configure) + string? name, + Action? configure) { Guard.ThrowIfNull(builder); @@ -62,9 +62,11 @@ public static MeterProviderBuilder AddPrometheusExporter( }); } - private static MetricReader BuildPrometheusExporterMetricReader(PrometheusAspNetCoreOptions options) + private static BaseExportingMetricReader BuildPrometheusExporterMetricReader(PrometheusAspNetCoreOptions options) { +#pragma warning disable CA2000 // Dispose objects before losing scope var exporter = new PrometheusExporter(options.ExporterOptions); +#pragma warning restore CA2000 // Dispose objects before losing scope return new BaseExportingMetricReader(exporter) { diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs index fdfcd2b7113..863695b975f 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs @@ -25,8 +25,9 @@ internal sealed class PrometheusExporterMiddleware public PrometheusExporterMiddleware(MeterProvider meterProvider, RequestDelegate next) { Guard.ThrowIfNull(meterProvider); + Guard.ThrowIfNull(next); - if (!meterProvider.TryFindExporter(out PrometheusExporter exporter)) + if (!meterProvider.TryFindExporter(out PrometheusExporter? exporter)) { throw new ArgumentException("A PrometheusExporter could not be found configured on the provided MeterProvider."); } @@ -36,6 +37,8 @@ public PrometheusExporterMiddleware(MeterProvider meterProvider, RequestDelegate internal PrometheusExporterMiddleware(PrometheusExporter exporter) { + Debug.Assert(exporter != null, "exporter was null"); + this.exporter = exporter; } @@ -62,7 +65,7 @@ public async Task InvokeAsync(HttpContext httpContext) if (dataView.Count > 0) { response.StatusCode = 200; -#if NET8_0_OR_GREATER +#if NET response.Headers.Append("Last-Modified", collectionResponse.GeneratedAtUtc.ToString("R")); #else response.Headers.Add("Last-Modified", collectionResponse.GeneratedAtUtc.ToString("R")); @@ -71,7 +74,7 @@ public async Task InvokeAsync(HttpContext httpContext) ? "application/openmetrics-text; version=1.0.0; charset=utf-8" : "text/plain; charset=utf-8; version=0.0.4"; - await response.Body.WriteAsync(dataView.Array, 0, dataView.Count).ConfigureAwait(false); + await response.Body.WriteAsync(dataView.Array.AsMemory(0, dataView.Count)).ConfigureAwait(false); } else { @@ -93,8 +96,6 @@ public async Task InvokeAsync(HttpContext httpContext) response.StatusCode = 500; } } - - this.exporter.OnExport = null; } private static bool AcceptsOpenMetrics(HttpRequest request) diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Shipped.txt index e69de29bb2d..7dc5c58110b 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Shipped.txt +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Unshipped.txt index d05f12424ea..6caa1a77cb1 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/.publicApi/PublicAPI.Unshipped.txt @@ -1,12 +1,12 @@ 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.get -> System.Collections.Generic.IReadOnlyCollection! OpenTelemetry.Exporter.PrometheusHttpListenerOptions.UriPrefixes.set -> void OpenTelemetry.Exporter.PrometheusHttpListenerOptions.PrometheusHttpListenerOptions() -> void -OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ScrapeEndpointPath.get -> string +OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ScrapeEndpointPath.get -> string? OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ScrapeEndpointPath.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 +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 deleted file mode 100644 index 7d6b4b08289..00000000000 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/AssemblyInfo.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Runtime.CompilerServices; - -#if SIGNED -[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Prometheus.HttpListener.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] -[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Prometheus.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] -[assembly: InternalsVisibleTo("Benchmarks, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] -#else -[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Prometheus.HttpListener.Tests")] -[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Prometheus.Tests")] -[assembly: InternalsVisibleTo("Benchmarks")] -#endif diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md index 07ac849c6d7..7ab44c75ca1 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md @@ -1,7 +1,55 @@ # Changelog +This file contains individual changes for the +OpenTelemetry.Exporter.Prometheus.HttpListener package. For highlights and +announcements covering all components see: [Release +Notes](../../RELEASENOTES.md). + ## Unreleased +## 1.12.0-beta.1 + +Released 2025-May-06 + +* Updated OpenTelemetry core component version(s) to `1.12.0`. + ([#6269](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6269)) + +## 1.11.2-beta.1 + +Released 2025-Mar-05 + +* Updated OpenTelemetry core component version(s) to `1.11.2`. + ([#6169](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6169)) + +## 1.11.0-beta.1 + +Released 2025-Jan-16 + +* Updated OpenTelemetry core component version(s) to `1.11.0`. + ([#6064](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6064)) + +## 1.10.0-beta.1 + +Released 2024-Nov-12 + +* Added meter-level tags to Prometheus exporter + ([#5837](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5837)) + +* Updated OpenTelemetry core component version(s) to `1.10.0`. + ([#5970](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5970)) + +## 1.9.0-beta.2 + +Released 2024-Jun-24 + +* Fixed a bug which lead to empty responses when the internal buffer is resized + processing a collection request + ([#5676](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5676)) + +## 1.9.0-beta.1 + +Released 2024-Jun-14 + ## 1.9.0-alpha.2 Released 2024-May-29 @@ -110,14 +158,17 @@ Released 2022-Sep-29 * Bug fix for Prometheus Exporter reporting StatusCode 204 instead of 200, when no metrics are collected ([#3643](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3643)) + * Added overloads which accept a name to the `MeterProviderBuilder` `AddPrometheusHttpListener` extension to allow for more fine-grained options management ([#3648](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3648)) + * Added support for OpenMetrics UNIT metadata ([#3651](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3651)) + * Added `"# EOF\n"` ending following the [OpenMetrics - specification](https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md) + specification](https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md) ([#3654](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3654)) ## 1.4.0-alpha.2 @@ -130,6 +181,7 @@ Released 2022-Aug-18 ([#3430](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3430) [#3503](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3503) [#3507](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3507)) + * Fixed bug [#2840](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2840) by allowing `+` and `*` to be used in the URI prefixes (e.g. `"http://*:9184"`). @@ -150,8 +202,10 @@ Released 2022-Apr-15 * Added `IApplicationBuilder` extension methods to help with Prometheus middleware configuration on ASP.NET Core ([#3029](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3029)) + * Changed Prometheus exporter to return 204 No Content and log a warning event if there are no metrics to collect. + * 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)) diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs index b93812176fb..ce4c5384485 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs @@ -1,6 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics; using System.Runtime.CompilerServices; using OpenTelemetry.Metrics; @@ -12,7 +13,7 @@ internal sealed class PrometheusCollectionManager private readonly PrometheusExporter exporter; private readonly int scrapeResponseCacheDurationMilliseconds; - private readonly Func, ExportResult> onCollectRef; + private readonly PrometheusExporter.ExportFunc onCollectRef; private readonly Dictionary metricsCache; private readonly HashSet scopes; private int metricsCacheCount; @@ -26,18 +27,18 @@ internal sealed class PrometheusCollectionManager private DateTime? previousOpenMetricsDataViewGeneratedAtUtc; private int readerCount; private bool collectionRunning; - private TaskCompletionSource collectionTcs; + private TaskCompletionSource? collectionTcs; public PrometheusCollectionManager(PrometheusExporter exporter) { this.exporter = exporter; this.scrapeResponseCacheDurationMilliseconds = this.exporter.ScrapeResponseCacheDurationMilliseconds; this.onCollectRef = this.OnCollect; - this.metricsCache = new Dictionary(); - this.scopes = new HashSet(); + this.metricsCache = []; + this.scopes = []; } -#if NET6_0_OR_GREATER +#if NET public ValueTask EnterCollect(bool openMetricsRequested) #else public Task EnterCollect(bool openMetricsRequested) @@ -57,7 +58,7 @@ public Task EnterCollect(bool openMetricsRequested) { Interlocked.Increment(ref this.readerCount); this.ExitGlobalLock(); -#if NET6_0_OR_GREATER +#if NET return new ValueTask(new CollectionResponse(this.previousOpenMetricsDataView, this.previousPlainTextDataView, previousDataViewGeneratedAtUtc.Value, fromCache: true)); #else return Task.FromResult(new CollectionResponse(this.previousOpenMetricsDataView, this.previousPlainTextDataView, previousDataViewGeneratedAtUtc.Value, fromCache: true)); @@ -67,14 +68,11 @@ public Task EnterCollect(bool openMetricsRequested) // If a collection is already running, return a task to wait on the result. if (this.collectionRunning) { - if (this.collectionTcs == null) - { - this.collectionTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - } + this.collectionTcs ??= new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); Interlocked.Increment(ref this.readerCount); this.ExitGlobalLock(); -#if NET6_0_OR_GREATER +#if NET return new ValueTask(this.collectionTcs.Task); #else return this.collectionTcs.Task; @@ -115,7 +113,7 @@ public Task EnterCollect(bool openMetricsRequested) ? this.previousOpenMetricsDataViewGeneratedAtUtc : this.previousPlainTextDataViewGeneratedAtUtc; - response = new CollectionResponse(this.previousOpenMetricsDataView, this.previousPlainTextDataView, previousDataViewGeneratedAtUtc.Value, fromCache: false); + response = new CollectionResponse(this.previousOpenMetricsDataView, this.previousPlainTextDataView, previousDataViewGeneratedAtUtc!.Value, fromCache: false); } else { @@ -134,7 +132,7 @@ public Task EnterCollect(bool openMetricsRequested) this.ExitGlobalLock(); -#if NET6_0_OR_GREATER +#if NET return new ValueTask(response); #else return Task.FromResult(response); @@ -147,6 +145,22 @@ public void ExitCollect() Interlocked.Decrement(ref this.readerCount); } + private static bool IncreaseBufferSize(ref byte[] buffer) + { + var newBufferSize = buffer.Length * 2; + + if (newBufferSize > 100 * 1024 * 1024) + { + return false; + } + + var newBuffer = new byte[newBufferSize]; + buffer.CopyTo(newBuffer, 0); + buffer = newBuffer; + + return true; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void EnterGlobalLock() { @@ -188,23 +202,25 @@ private void WaitForReadersToComplete() [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool ExecuteCollect(bool openMetricsRequested) { + Debug.Assert(this.exporter.Collect != null, "this.exporter.Collect was null"); + this.exporter.OnExport = this.onCollectRef; this.exporter.OpenMetricsRequested = openMetricsRequested; - var result = this.exporter.Collect(Timeout.Infinite); + var result = this.exporter.Collect!(Timeout.Infinite); this.exporter.OnExport = null; return result; } - private ExportResult OnCollect(Batch metrics) + private ExportResult OnCollect(in Batch metrics) { var cursor = 0; - var buffer = this.exporter.OpenMetricsRequested ? this.openMetricsBuffer : this.plainTextBuffer; + ref byte[] buffer = ref (this.exporter.OpenMetricsRequested ? ref this.openMetricsBuffer : ref this.plainTextBuffer); try { if (this.exporter.OpenMetricsRequested) { - cursor = this.WriteTargetInfo(); + cursor = this.WriteTargetInfo(ref buffer); this.scopes.Clear(); @@ -227,7 +243,7 @@ private ExportResult OnCollect(Batch metrics) } catch (IndexOutOfRangeException) { - if (!this.IncreaseBufferSize(ref buffer)) + if (!IncreaseBufferSize(ref buffer)) { // 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 @@ -265,7 +281,7 @@ private ExportResult OnCollect(Batch metrics) } catch (IndexOutOfRangeException) { - if (!this.IncreaseBufferSize(ref buffer)) + if (!IncreaseBufferSize(ref buffer)) { throw; } @@ -282,7 +298,7 @@ private ExportResult OnCollect(Batch metrics) } catch (IndexOutOfRangeException) { - if (!this.IncreaseBufferSize(ref buffer)) + if (!IncreaseBufferSize(ref buffer)) { throw; } @@ -291,11 +307,11 @@ private ExportResult OnCollect(Batch metrics) if (this.exporter.OpenMetricsRequested) { - this.previousOpenMetricsDataView = new ArraySegment(this.openMetricsBuffer, 0, cursor); + this.previousOpenMetricsDataView = new ArraySegment(buffer, 0, cursor); } else { - this.previousPlainTextDataView = new ArraySegment(this.plainTextBuffer, 0, cursor); + this.previousPlainTextDataView = new ArraySegment(buffer, 0, cursor); } return ExportResult.Success; @@ -304,18 +320,18 @@ private ExportResult OnCollect(Batch metrics) { if (this.exporter.OpenMetricsRequested) { - this.previousOpenMetricsDataView = new ArraySegment(Array.Empty(), 0, 0); + this.previousOpenMetricsDataView = new ArraySegment([], 0, 0); } else { - this.previousPlainTextDataView = new ArraySegment(Array.Empty(), 0, 0); + this.previousPlainTextDataView = new ArraySegment([], 0, 0); } return ExportResult.Failure; } } - private int WriteTargetInfo() + private int WriteTargetInfo(ref byte[] buffer) { if (this.targetInfoBufferLength < 0) { @@ -323,13 +339,13 @@ private int WriteTargetInfo() { try { - this.targetInfoBufferLength = PrometheusSerializer.WriteTargetInfo(this.openMetricsBuffer, 0, this.exporter.Resource); + this.targetInfoBufferLength = PrometheusSerializer.WriteTargetInfo(buffer, 0, this.exporter.Resource); break; } catch (IndexOutOfRangeException) { - if (!this.IncreaseBufferSize(ref this.openMetricsBuffer)) + if (!IncreaseBufferSize(ref buffer)) { throw; } @@ -340,22 +356,6 @@ private int WriteTargetInfo() return this.targetInfoBufferLength; } - private bool IncreaseBufferSize(ref byte[] buffer) - { - var newBufferSize = buffer.Length * 2; - - if (newBufferSize > 100 * 1024 * 1024) - { - return false; - } - - var newBuffer = new byte[newBufferSize]; - buffer.CopyTo(newBuffer, 0); - buffer = newBuffer; - - return true; - } - private PrometheusMetric GetPrometheusMetric(Metric metric) { // Optimize writing metrics with bounded cache that has pre-calculated Prometheus names. diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs index 292b0aa7c31..2d9ea4c27d1 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporter.cs @@ -1,6 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics; using OpenTelemetry.Internal; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; @@ -13,9 +14,7 @@ namespace OpenTelemetry.Exporter.Prometheus; [ExportModes(ExportModes.Pull)] internal sealed class PrometheusExporter : BaseExporter, IPullMetricExporter { - private Func funcCollect; - private Func, ExportResult> funcExport; - private Resource resource; + private Resource? resource; private bool disposed; /// @@ -32,22 +31,16 @@ public PrometheusExporter(PrometheusExporterOptions options) this.CollectionManager = new PrometheusCollectionManager(this); } + public delegate ExportResult ExportFunc(in Batch batch); + /// /// Gets or sets the Collect delegate. /// - public Func Collect - { - get => this.funcCollect; - set => this.funcCollect = value; - } + public Func? Collect { get; set; } - internal Func, ExportResult> OnExport - { - get => this.funcExport; - set => this.funcExport = value; - } + internal ExportFunc? OnExport { get; set; } - internal Action OnDispose { get; set; } + internal Action? OnDispose { get; set; } internal PrometheusCollectionManager CollectionManager { get; } @@ -62,7 +55,9 @@ internal Func, ExportResult> OnExport /// public override ExportResult Export(in Batch metrics) { - return this.OnExport(metrics); + Debug.Assert(this.OnExport != null, "this.OnExport was null"); + + return this.OnExport!(in metrics); } /// diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusHeadersParser.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusHeadersParser.cs index 81576b723dc..2c2c8f5b7d6 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusHeadersParser.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusHeadersParser.cs @@ -7,7 +7,7 @@ internal static class PrometheusHeadersParser { private const string OpenMetricsMediaType = "application/openmetrics-text"; - internal static bool AcceptsOpenMetrics(string contentType) + internal static bool AcceptsOpenMetrics(string? contentType) { var value = contentType.AsSpan(); diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs index 6f18f06a536..9f80d6dc5d3 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs @@ -1,6 +1,8 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Text; using OpenTelemetry.Metrics; @@ -14,10 +16,10 @@ 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[] - { + private static readonly PrometheusType[] MetricTypes = + [ 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) { @@ -29,16 +31,16 @@ public PrometheusMetric(string name, string unit, PrometheusType type, bool disa var sanitizedName = SanitizeMetricName(name); var openMetricsName = SanitizeOpenMetricsName(sanitizedName); - string sanitizedUnit = null; + 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) + // [OpenMetrics UNIT metadata](https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#metricfamily) // and as a suffix to the metric name. The unit suffix comes before any type-specific suffixes. // https://github.com/open-telemetry/opentelemetry-specification/blob/3dfb383fe583e3b74a2365c5a1d90256b273ee76/specification/compatibility/prometheus_and_openmetrics.md#metric-metadata-1 - if (!sanitizedName.EndsWith(sanitizedUnit)) + if (!sanitizedName.EndsWith(sanitizedUnit, StringComparison.Ordinal)) { sanitizedName += $"_{sanitizedUnit}"; openMetricsName += $"_{sanitizedUnit}"; @@ -49,20 +51,20 @@ public PrometheusMetric(string name, string unit, PrometheusType type, bool disa // 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 // Note that we no longer append '_ratio' for units that are '1', see: https://github.com/open-telemetry/opentelemetry-specification/issues/4058 - if (type == PrometheusType.Counter && !sanitizedName.EndsWith("_total") && !disableTotalNameSuffixForCounters) + if (type == PrometheusType.Counter && !sanitizedName.EndsWith("_total", StringComparison.Ordinal) && !disableTotalNameSuffixForCounters) { sanitizedName += "_total"; } // For counters requested using OpenMetrics format, the MetricFamily name MUST be suffixed with '_total', regardless of the setting to disable the 'total' suffix. - // https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#counter-1 - if (type == PrometheusType.Counter && !openMetricsName.EndsWith("_total")) + // https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#counter-1 + if (type == PrometheusType.Counter && !openMetricsName.EndsWith("_total", StringComparison.Ordinal)) { openMetricsName += "_total"; } // In OpenMetrics format, the UNIT, TYPE and HELP metadata must be suffixed with the unit (handled above), and not the '_total' suffix, as in the case for counters. - // https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#unit + // https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#unit var openMetricsMetadataName = type == PrometheusType.Counter ? SanitizeOpenMetricsName(openMetricsName) : sanitizedName; @@ -80,7 +82,7 @@ public PrometheusMetric(string name, string unit, PrometheusType type, bool disa public string OpenMetricsMetadataName { get; } - public string Unit { get; } + public string? Unit { get; } public PrometheusType Type { get; } @@ -91,7 +93,7 @@ public static PrometheusMetric Create(Metric metric, bool disableTotalNameSuffix internal static string SanitizeMetricName(string metricName) { - StringBuilder sb = null; + StringBuilder? sb = null; var lastCharUnderscore = false; for (var i = 0; i < metricName.Length; i++) @@ -125,7 +127,7 @@ internal static string SanitizeMetricName(string metricName) return sb?.ToString() ?? metricName; - static StringBuilder CreateStringBuilder(string name) => new StringBuilder(name.Length); + static StringBuilder CreateStringBuilder(string name) => new(name.Length); } internal static string RemoveAnnotations(string unit) @@ -134,7 +136,7 @@ internal static string RemoveAnnotations(string unit) // 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; + StringBuilder? sb = null; var hasOpenBrace = false; var startOpenBraceIndex = 0; @@ -168,13 +170,16 @@ internal static string RemoveAnnotations(string unit) return unit; } - sb.Append(unit, lastWriteIndex, unit.Length - lastWriteIndex); + Debug.Assert(sb != null, "sb was null"); + + sb!.Append(unit, lastWriteIndex, unit.Length - lastWriteIndex); + return sb.ToString(); } private static string SanitizeOpenMetricsName(string metricName) { - if (metricName.EndsWith("_total")) + if (metricName.EndsWith("_total", StringComparison.Ordinal)) { return metricName.Substring(0, metricName.Length - 6); } @@ -204,7 +209,7 @@ private static string GetUnit(string unit) return updatedUnit; } - private static bool TryProcessRateUnits(string updatedUnit, out string updatedPerUnit) + private static bool TryProcessRateUnits(string updatedUnit, [NotNullWhen(true)] out string? updatedPerUnit) { updatedPerUnit = null; @@ -236,7 +241,7 @@ private static PrometheusType GetPrometheusType(Metric metric) // 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 + // OpenMetrics specification for units: https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#units-and-base-units private static string MapUnit(ReadOnlySpan unit) { return unit switch diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs index 3c142d91233..c199a6695c5 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs @@ -17,8 +17,6 @@ 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` #pragma warning restore SA1310 // Field name should not contain an underscore @@ -28,7 +26,7 @@ public static int WriteDouble(byte[] buffer, int cursor, double value) { if (MathHelper.IsFinite(value)) { -#if NET6_0_OR_GREATER +#if NET Span span = stackalloc char[128]; var result = value.TryFormat(span, out var cchWritten, "G", CultureInfo.InvariantCulture); @@ -62,7 +60,7 @@ public static int WriteDouble(byte[] buffer, int cursor, double value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int WriteLong(byte[] buffer, int cursor, long value) { -#if NET6_0_OR_GREATER +#if NET Span span = stackalloc char[20]; var result = value.TryFormat(span, out var cchWritten, "G", CultureInfo.InvariantCulture); @@ -102,16 +100,13 @@ public static int WriteUnicodeNoEscape(byte[] buffer, int cursor, ushort ordinal buffer[cursor++] = unchecked((byte)(0b_1100_0000 | (ordinal >> 6))); buffer[cursor++] = unchecked((byte)(0b_1000_0000 | (ordinal & 0b_0011_1111))); } - else if (ordinal <= 0xFFFF) + else { + // all other <= 0xFFFF which is ushort.MaxValue 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; } @@ -177,7 +172,7 @@ 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) @@ -204,7 +199,7 @@ public static int WriteLabelValue(byte[] buffer, int cursor, string value) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteLabel(byte[] buffer, int cursor, string labelKey, object labelValue) + public static int WriteLabel(byte[] buffer, int cursor, string labelKey, object? labelValue) { cursor = WriteLabelKey(buffer, cursor, labelKey); buffer[cursor++] = unchecked((byte)'='); @@ -216,7 +211,7 @@ public static int WriteLabel(byte[] buffer, int cursor, string labelKey, object return cursor; - static string GetLabelValueString(object labelValue) + 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 @@ -235,6 +230,8 @@ public static int WriteMetricName(byte[] buffer, int cursor, PrometheusMetric me // Metric name has already been escaped. var name = openMetricsRequested ? metric.OpenMetricsName : metric.Name; + Debug.Assert(!string.IsNullOrWhiteSpace(name), "name was null or whitespace"); + for (int i = 0; i < name.Length; i++) { var ordinal = (ushort)name[i]; @@ -250,6 +247,8 @@ public static int WriteMetricMetadataName(byte[] buffer, int cursor, PrometheusM // Metric name has already been escaped. var name = openMetricsRequested ? metric.OpenMetricsMetadataName : metric.Name; + Debug.Assert(!string.IsNullOrWhiteSpace(name), "name was null or whitespace"); + for (int i = 0; i < name.Length; i++) { var ordinal = (ushort)name[i]; @@ -321,7 +320,7 @@ public static int WriteUnitMetadata(byte[] buffer, int cursor, PrometheusMetric buffer[cursor++] = unchecked((byte)' '); // Unit name has already been escaped. - for (int i = 0; i < metric.Unit.Length; i++) + for (int i = 0; i < metric.Unit!.Length; i++) { var ordinal = (ushort)metric.Unit[i]; buffer[cursor++] = unchecked((byte)ordinal); @@ -400,6 +399,15 @@ public static int WriteTags(byte[] buffer, int cursor, Metric metric, ReadOnlyTa buffer[cursor++] = unchecked((byte)','); } + if (metric.MeterTags != null) + { + foreach (var tag in metric.MeterTags) + { + cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value); + buffer[cursor++] = unchecked((byte)','); + } + } + foreach (var tag in tags) { cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value); 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 3766816a7e1..f69ce360219 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/OpenTelemetry.Exporter.Prometheus.HttpListener.csproj +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/OpenTelemetry.Exporter.Prometheus.HttpListener.csproj @@ -5,9 +5,6 @@ Stand-alone HttpListener for hosting OpenTelemetry .NET Prometheus Exporter $(PackageTags);prometheus;metrics coreunstable- - - - disable @@ -19,9 +16,17 @@ + - + + + + + + + + diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs index 8576873f520..b3bdf286f34 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs @@ -11,10 +11,10 @@ internal sealed class PrometheusHttpListener : IDisposable { private readonly PrometheusExporter exporter; private readonly HttpListener httpListener = new(); - private readonly object syncObject = new(); + private readonly Lock syncObject = new(); - private CancellationTokenSource tokenSource; - private Task workerThread; + private CancellationTokenSource? tokenSource; + private Task? workerThread; /// /// Initializes a new instance of the class. @@ -28,14 +28,22 @@ public PrometheusHttpListener(PrometheusExporter exporter, PrometheusHttpListene this.exporter = exporter; - string path = options.ScrapeEndpointPath; + string path = options.ScrapeEndpointPath ?? PrometheusHttpListenerOptions.DefaultScrapeEndpointPath; - if (!path.StartsWith("/")) +#if NET + if (!path.StartsWith('/')) +#else + if (!path.StartsWith("/", StringComparison.Ordinal)) +#endif { path = $"/{path}"; } - if (!path.EndsWith("/")) +#if NET + if (!path.EndsWith('/')) +#else + if (!path.EndsWith("/", StringComparison.Ordinal)) +#endif { path = $"{path}/"; } @@ -83,7 +91,7 @@ public void Stop() } this.tokenSource.Cancel(); - this.workerThread.Wait(); + this.workerThread!.Wait(); this.tokenSource = null; } } @@ -116,7 +124,7 @@ private void WorkerProc() try { using var scope = SuppressInstrumentationScope.Begin(); - while (!this.tokenSource.IsCancellationRequested) + while (!this.tokenSource!.IsCancellationRequested) { var ctxTask = this.httpListener.GetContextAsync(); ctxTask.Wait(this.tokenSource.Token); @@ -164,7 +172,11 @@ private async Task ProcessRequestAsync(HttpListenerContext context) ? "application/openmetrics-text; version=1.0.0; charset=utf-8" : "text/plain; charset=utf-8; version=0.0.4"; +#if NET + await context.Response.OutputStream.WriteAsync(dataView.Array.AsMemory(0, dataView.Count)).ConfigureAwait(false); +#else await context.Response.OutputStream.WriteAsync(dataView.Array, 0, dataView.Count).ConfigureAwait(false); +#endif } else { diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerMeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerMeterProviderBuilderExtensions.cs index 929774a11f9..94eea0f5742 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerMeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerMeterProviderBuilderExtensions.cs @@ -37,13 +37,13 @@ public static MeterProviderBuilder AddPrometheusHttpListener( /// Adds PrometheusHttpListener to MeterProviderBuilder. /// /// builder to use. - /// Name which is used when retrieving options. - /// Callback action for configuring . + /// Optional name which is used when retrieving options. + /// Optional callback action for configuring . /// The instance of to chain calls. public static MeterProviderBuilder AddPrometheusHttpListener( this MeterProviderBuilder builder, - string name, - Action configure) + string? name, + Action? configure) { Guard.ThrowIfNull(builder); @@ -62,7 +62,7 @@ public static MeterProviderBuilder AddPrometheusHttpListener( }); } - private static MetricReader BuildPrometheusHttpListenerMetricReader( + private static BaseExportingMetricReader BuildPrometheusHttpListenerMetricReader( PrometheusHttpListenerOptions options) { var exporter = new PrometheusExporter(new PrometheusExporterOptions @@ -78,8 +78,10 @@ private static MetricReader BuildPrometheusHttpListenerMetricReader( try { +#pragma warning disable CA2000 // Dispose objects before losing scope var listener = new PrometheusHttpListener(exporter, options); - exporter.OnDispose = () => listener.Dispose(); +#pragma warning restore CA2000 // Dispose objects before losing scope + exporter.OnDispose = listener.Dispose; listener.Start(); } catch (Exception ex) diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerOptions.cs b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerOptions.cs index d0c6bd2edf0..8909a3bd82e 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerOptions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListenerOptions.cs @@ -10,12 +10,14 @@ namespace OpenTelemetry.Exporter; /// public class PrometheusHttpListenerOptions { - private IReadOnlyCollection uriPrefixes = new[] { "http://localhost:9464/" }; + internal const string DefaultScrapeEndpointPath = "/metrics"; + + private IReadOnlyCollection uriPrefixes = ["http://localhost:9464/"]; /// /// Gets or sets the path to use for the scraping endpoint. Default value: "/metrics". /// - public string ScrapeEndpointPath { get; set; } = "/metrics"; + public string? ScrapeEndpointPath { get; set; } = DefaultScrapeEndpointPath; /// /// Gets or sets a value indicating whether addition of _total suffix for counter metric names is disabled. Default value: . diff --git a/src/OpenTelemetry.Exporter.Zipkin/.publicApi/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.Zipkin/.publicApi/PublicAPI.Shipped.txt index 5d94b912d2d..182efe4b8a8 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/.publicApi/PublicAPI.Shipped.txt +++ b/src/OpenTelemetry.Exporter.Zipkin/.publicApi/PublicAPI.Shipped.txt @@ -1,13 +1,14 @@ +#nullable enable OpenTelemetry.Exporter.ZipkinExporter -OpenTelemetry.Exporter.ZipkinExporter.ZipkinExporter(OpenTelemetry.Exporter.ZipkinExporterOptions options, System.Net.Http.HttpClient client = null) -> void +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.get -> OpenTelemetry.BatchExportProcessorOptions! OpenTelemetry.Exporter.ZipkinExporterOptions.BatchExportProcessorOptions.set -> void -OpenTelemetry.Exporter.ZipkinExporterOptions.Endpoint.get -> System.Uri +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.get -> System.Func! OpenTelemetry.Exporter.ZipkinExporterOptions.HttpClientFactory.set -> void OpenTelemetry.Exporter.ZipkinExporterOptions.MaxPayloadSizeInBytes.get -> int? OpenTelemetry.Exporter.ZipkinExporterOptions.MaxPayloadSizeInBytes.set -> void @@ -15,7 +16,7 @@ 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, string name, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.ZipkinExporterHelperExtensions.AddZipkinExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configure) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.ZipkinExporterHelperExtensions.AddZipkinExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder +override OpenTelemetry.Exporter.ZipkinExporter.Export(in OpenTelemetry.Batch batch) -> OpenTelemetry.ExportResult +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! +static OpenTelemetry.Trace.ZipkinExporterHelperExtensions.AddZipkinExporter(this OpenTelemetry.Trace.TracerProviderBuilder! builder) -> OpenTelemetry.Trace.TracerProviderBuilder! diff --git a/src/OpenTelemetry.Exporter.Zipkin/AssemblyInfo.cs b/src/OpenTelemetry.Exporter.Zipkin/AssemblyInfo.cs deleted file mode 100644 index 7ce3243549d..00000000000 --- a/src/OpenTelemetry.Exporter.Zipkin/AssemblyInfo.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Runtime.CompilerServices; - -#if SIGNED -[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Zipkin.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] -[assembly: InternalsVisibleTo("Benchmarks, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] -#else -[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Zipkin.Tests")] -[assembly: InternalsVisibleTo("Benchmarks")] -#endif diff --git a/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md b/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md index 28869770365..f22f4de85d9 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md @@ -1,7 +1,71 @@ # Changelog +This file contains individual changes for the OpenTelemetry.Exporter.Zipkin +package. For highlights and announcements covering all components see: [Release +Notes](../../RELEASENOTES.md). + ## Unreleased +## 1.13.0 + +Released 2025-Oct-01 + +* Removed the peer service resolver, which was based on earlier experimental + semantic conventions that are not part of the stable specification. This + change ensures that the exporter no longer modifies or assumes the value of + peer service attributes. + ([#6191](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6191)) + +* Extended remote endpoint calculation to align with the [opentelemetry-specification](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.40.0/specification/trace/sdk_exporters/zipkin.md#otlp---zipkin). + ([#6191](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6191)) + +## 1.12.0 + +Released 2025-Apr-29 + +## 1.11.2 + +Released 2025-Mar-04 + +## 1.11.1 + +Released 2025-Jan-22 + +## 1.11.0 + +Released 2025-Jan-15 + +## 1.11.0-rc.1 + +Released 2024-Dec-11 + +## 1.10.0 + +Released 2024-Nov-12 + +## 1.10.0-rc.1 + +Released 2024-Nov-01 + +* Added direct reference to `System.Text.Json` for the `net8.0` target with + minimum version of `8.0.5` in response to + [CVE-2024-30105](https://github.com/advisories/GHSA-hh2w-p6rv-4g7w) & + [CVE-2024-43485](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-43485). + ([#5874](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5874), + [#5891](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5891)) + +## 1.10.0-beta.1 + +Released 2024-Sep-30 + +* **Breaking change**: Non-primitive tag values converted using + `Convert.ToString` will now format using `CultureInfo.InvariantCulture`. + ([#5700](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5700)) + +* Fixed `PlatformNotSupportedException`s being thrown during export when running + on mobile platforms which caused telemetry to be dropped silently. + ([#5821](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/5821)) + ## 1.9.0 Released 2024-Jun-14 @@ -166,7 +230,7 @@ Released 2022-June-1 ([#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. + array-valued attributes are serialized to a JSON-array representation. ([#3281](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3281)) ## 1.3.0-beta.2 diff --git a/src/Shared/PooledList.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/PooledList.cs similarity index 88% rename from src/Shared/PooledList.cs rename to src/OpenTelemetry.Exporter.Zipkin/Implementation/PooledList.cs index 043cd0a4525..7027dc4e4b1 100644 --- a/src/Shared/PooledList.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/PooledList.cs @@ -3,12 +3,13 @@ using System.Buffers; using System.Collections; +using System.Diagnostics.CodeAnalysis; namespace OpenTelemetry.Internal; internal readonly struct PooledList : IEnumerable, ICollection { - private static int lastAllocatedSize = 64; + public static int LastAllocatedSize = 64; private readonly T[] buffer; @@ -33,7 +34,7 @@ public ref T this[int index] public static PooledList Create() { - return new PooledList(ArrayPool.Shared.Rent(lastAllocatedSize), 0); + return new PooledList(ArrayPool.Shared.Rent(LastAllocatedSize), 0); } public static void Add(ref PooledList list, T item) @@ -44,10 +45,10 @@ public static void Add(ref PooledList list, T item) if (list.Count >= buffer.Length) { - lastAllocatedSize = buffer.Length * 2; + LastAllocatedSize = buffer.Length * 2; var previousBuffer = buffer; - buffer = ArrayPool.Shared.Rent(lastAllocatedSize); + buffer = ArrayPool.Shared.Rent(LastAllocatedSize); var span = previousBuffer.AsSpan(); span.CopyTo(buffer); @@ -97,6 +98,7 @@ public struct Enumerator : IEnumerator, IEnumerator private readonly T[] buffer; private readonly int count; private int index; + [AllowNull] private T current; public Enumerator(in PooledList list) @@ -107,9 +109,9 @@ public Enumerator(in PooledList list) this.current = default; } - public T Current { get => this.current; } + public T Current => this.current; - object IEnumerator.Current { get => this.Current; } + object? IEnumerator.Current => this.Current; public void Dispose() { diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinActivityConversionExtensions.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinActivityConversionExtensions.cs index 802591d052a..247cd87463b 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinActivityConversionExtensions.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinActivityConversionExtensions.cs @@ -20,103 +20,15 @@ internal static class ZipkinActivityConversionExtensions internal static ZipkinSpan ToZipkinSpan(this Activity activity, ZipkinEndpoint localEndpoint, bool useShortTraceIds = false) { var context = activity.Context; + string? parentId = activity.ParentSpanId == default ? null : EncodeSpanId(activity.ParentSpanId); - string parentId = activity.ParentSpanId == default ? - null - : EncodeSpanId(activity.ParentSpanId); + var tags = PooledList>.Create(); + ExtractActivityTags(activity, ref tags); + ExtractActivityStatus(activity, ref tags); + ExtractActivitySource(activity, ref tags); - var tagState = new TagEnumerationState - { - Tags = PooledList>.Create(), - }; - - tagState.EnumerateTags(activity); - - // 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) - { - PooledList>.Add( - ref tagState.Tags, - new KeyValuePair( - SpanAttributeConstants.StatusCodeKey, - "OK")); - } - - // 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)); - } - } - - // 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( - ZipkinErrorFlagTagName, - tagState.StatusDescription ?? string.Empty)); - } - } - - var activitySource = activity.Source; - if (!string.IsNullOrEmpty(activitySource.Name)) - { - PooledList>.Add(ref tagState.Tags, new KeyValuePair("otel.scope.name", activitySource.Name)); - - // otel.library.name is deprecated, but has to be propagated according to https://github.com/open-telemetry/opentelemetry-specification/blob/v1.31.0/specification/common/mapping-to-non-otlp.md#instrumentationscope - 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.scope.version", activitySource.Version)); - - // otel.library.version is deprecated, but has to be propagated according to https://github.com/open-telemetry/opentelemetry-specification/blob/v1.31.0/specification/common/mapping-to-non-otlp.md#instrumentationscope - 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); - - if (peerServiceName != null) - { - remoteEndpoint = RemoteEndpointCache.GetOrAdd((peerServiceName, default), ZipkinEndpoint.Create); - if (addAsTag) - { - PooledList>.Add(ref tagState.Tags, new KeyValuePair(SemanticConventions.AttributePeerService, peerServiceName)); - } - } - } - - EventEnumerationState eventState = default; - eventState.EnumerateEvents(activity); + ZipkinEndpoint? remoteEndpoint = ExtractRemoteEndpoint(activity); + var annotations = ExtractActivityEvents(activity); return new ZipkinSpan( EncodeTraceId(context.TraceId, useShortTraceIds), @@ -128,8 +40,8 @@ internal static ZipkinSpan ToZipkinSpan(this Activity activity, ZipkinEndpoint l duration: activity.Duration.ToEpochMicroseconds(), localEndpoint, remoteEndpoint, - eventState.Annotations, - tagState.Tags, + annotations, + tags, null, null); } @@ -172,7 +84,7 @@ private static string EncodeTraceId(ActivityTraceId traceId, bool useShortTraceI return id; } - private static string ToActivityKind(Activity activity) + private static string? ToActivityKind(Activity activity) { return activity.Kind switch { @@ -184,89 +96,235 @@ private static string ToActivityKind(Activity activity) }; } - internal struct TagEnumerationState : PeerServiceResolver.IPeerServiceState + private static string ExtractStatusDescription(Activity activity) { - public PooledList> Tags; - - public string PeerService { get; set; } - - public int? PeerServicePriority { get; set; } - - public string HostName { get; set; } - - public string IpAddress { get; set; } + return activity.StatusDescription + ?? activity.GetTagItem(SpanAttributeConstants.StatusDescriptionKey) as string + ?? activity.GetTagItem(ZipkinErrorFlagTagName) as string + ?? string.Empty; + } - public long Port { get; set; } + private static void ExtractActivityTags(Activity activity, ref PooledList> tags) + { + foreach (ref readonly var tag in activity.EnumerateTagObjects()) + { + if (tag.Key != ZipkinErrorFlagTagName && tag.Key != SpanAttributeConstants.StatusCodeKey) + { + PooledList>.Add(ref tags, tag); + } + } + } - public StatusCode? StatusCode { get; set; } + private static void ExtractActivityStatus(Activity activity, ref PooledList> tags) + { + // 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) + { + PooledList>.Add( + ref tags, + new KeyValuePair( + SpanAttributeConstants.StatusCodeKey, + "OK")); + } - public string StatusDescription { get; set; } + // activity.Status is Error + else + { + string statusDescription = ExtractStatusDescription(activity); + PooledList>.Add( + ref tags, + new KeyValuePair( + SpanAttributeConstants.StatusCodeKey, + "ERROR")); - public void EnumerateTags(Activity activity) + // Error flag rule from https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk_exporters/zipkin.md#status + PooledList>.Add( + ref tags, + new KeyValuePair( + ZipkinErrorFlagTagName, + statusDescription)); + } + } + else { - foreach (ref readonly var tag in activity.EnumerateTagObjects()) + if (activity.GetTagItem(SpanAttributeConstants.StatusCodeKey) is string status) { - if (tag.Value == null) + if (status == "OK") { - continue; - } - - string key = tag.Key; + activity.SetStatus(ActivityStatusCode.Ok); - if (tag.Value is string strVal) - { - 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; - } + PooledList>.Add( + ref tags, + new KeyValuePair( + SpanAttributeConstants.StatusCodeKey, + "OK")); } - else if (tag.Value is int intVal && tag.Key == SemanticConventions.AttributeNetPeerPort) + else if (status == "ERROR") { - PeerServiceResolver.InspectTag(ref this, key, intVal); - } + string statusDescription = ExtractStatusDescription(activity); + + activity.SetStatus(ActivityStatusCode.Error); + + PooledList>.Add( + ref tags, + new KeyValuePair( + SpanAttributeConstants.StatusCodeKey, + "ERROR")); - PooledList>.Add(ref this.Tags, tag); + PooledList>.Add( + ref tags, + new KeyValuePair( + ZipkinErrorFlagTagName, + statusDescription)); + } } } } - private struct EventEnumerationState + private static void ExtractActivitySource(Activity activity, ref PooledList> tags) { - public bool Created; + var source = activity.Source; + if (!string.IsNullOrEmpty(source.Name)) + { + PooledList>.Add(ref tags, new KeyValuePair("otel.scope.name", source.Name)); + + // otel.library.name is deprecated, but has to be propagated according to https://github.com/open-telemetry/opentelemetry-specification/blob/v1.31.0/specification/common/mapping-to-non-otlp.md#instrumentationscope + PooledList>.Add(ref tags, new KeyValuePair("otel.library.name", source.Name)); + + if (!string.IsNullOrEmpty(source.Version)) + { + PooledList>.Add(ref tags, new KeyValuePair("otel.scope.version", source.Version)); - public PooledList Annotations; + // otel.library.version is deprecated, but has to be propagated according to https://github.com/open-telemetry/opentelemetry-specification/blob/v1.31.0/specification/common/mapping-to-non-otlp.md#instrumentationscope + PooledList>.Add(ref tags, new KeyValuePair("otel.library.version", source.Version)); + } + } + } - public void EnumerateEvents(Activity activity) + private static ZipkinEndpoint? ExtractRemoteEndpoint(Activity activity) + { + if (activity.Kind != ActivityKind.Client && activity.Kind != ActivityKind.Producer) { - var enumerator = activity.EnumerateEvents(); + return null; + } - if (enumerator.MoveNext()) + static ZipkinEndpoint? TryCreateEndpoint(string? remoteEndpoint) + { + if (remoteEndpoint != null) { - this.Annotations = PooledList.Create(); - this.Created = true; + var endpoint = RemoteEndpointCache.GetOrAdd((remoteEndpoint, default), ZipkinEndpoint.Create); + return endpoint; + } - do - { - ref readonly var @event = ref enumerator.Current; + return null; + } - PooledList.Add(ref this.Annotations, new ZipkinAnnotation(@event.Timestamp.ToEpochMicroseconds(), @event.Name)); - } - while (enumerator.MoveNext()); - } + string? remoteEndpoint = activity.GetTagItem(SemanticConventions.AttributePeerService) as string; + var endpoint = TryCreateEndpoint(remoteEndpoint); + if (endpoint != null) + { + return endpoint; + } + + remoteEndpoint = activity.GetTagItem(SemanticConventions.AttributeServerAddress) as string; + endpoint = TryCreateEndpoint(remoteEndpoint); + if (endpoint != null) + { + return endpoint; + } + + remoteEndpoint = activity.GetTagItem(SemanticConventions.AttributeNetPeerName) as string; + endpoint = TryCreateEndpoint(remoteEndpoint); + if (endpoint != null) + { + return endpoint; + } + + var peerAddress = activity.GetTagItem(SemanticConventions.AttributeNetworkPeerAddress) as string; + var peerPort = activity.GetTagItem(SemanticConventions.AttributeNetworkPeerPort) as string; + remoteEndpoint = peerPort != null ? $"{peerAddress}:{peerPort}" : peerAddress; + endpoint = TryCreateEndpoint(remoteEndpoint); + if (endpoint != null) + { + return endpoint; + } + + remoteEndpoint = activity.GetTagItem(SemanticConventions.AttributeServerSocketDomain) as string; + endpoint = TryCreateEndpoint(remoteEndpoint); + if (endpoint != null) + { + return endpoint; + } + + var serverAddress = activity.GetTagItem(SemanticConventions.AttributeServerSocketAddress) as string; + var serverPort = activity.GetTagItem(SemanticConventions.AttributeServerSocketPort) as string; + remoteEndpoint = serverPort != null ? $"{serverAddress}:{serverPort}" : serverAddress; + endpoint = TryCreateEndpoint(remoteEndpoint); + if (endpoint != null) + { + return endpoint; + } + + remoteEndpoint = activity.GetTagItem(SemanticConventions.AttributeNetSockPeerName) as string; + endpoint = TryCreateEndpoint(remoteEndpoint); + if (endpoint != null) + { + return endpoint; + } + + var socketAddress = activity.GetTagItem(SemanticConventions.AttributeNetSockPeerAddr) as string; + var socketPort = activity.GetTagItem(SemanticConventions.AttributeNetSockPeerPort) as string; + remoteEndpoint = socketPort != null ? $"{socketAddress}:{socketPort}" : socketAddress; + endpoint = TryCreateEndpoint(remoteEndpoint); + if (endpoint != null) + { + return endpoint; + } + + remoteEndpoint = activity.GetTagItem(SemanticConventions.AttributePeerHostname) as string; + endpoint = TryCreateEndpoint(remoteEndpoint); + if (endpoint != null) + { + return endpoint; + } + + remoteEndpoint = activity.GetTagItem(SemanticConventions.AttributePeerAddress) as string; + endpoint = TryCreateEndpoint(remoteEndpoint); + if (endpoint != null) + { + return endpoint; + } + + remoteEndpoint = activity.GetTagItem(SemanticConventions.AttributeDbName) as string; + endpoint = TryCreateEndpoint(remoteEndpoint); + if (endpoint != null) + { + return endpoint; + } + + return null; + } + + private static PooledList ExtractActivityEvents(Activity activity) + { + var enumerator = activity.EnumerateEvents().GetEnumerator(); + if (!enumerator.MoveNext()) + { + return default; } + + var annotations = PooledList.Create(); + + do + { + var @event = enumerator.Current; + PooledList.Add(ref annotations, new ZipkinAnnotation(@event.Timestamp.ToEpochMicroseconds(), @event.Name)); + } + while (enumerator.MoveNext()); + + return annotations; } } diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinEndpoint.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinEndpoint.cs index c6c9908b38e..cec7cc7156c 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinEndpoint.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinEndpoint.cs @@ -13,11 +13,11 @@ public ZipkinEndpoint(string serviceName) } public ZipkinEndpoint( - string serviceName, - string ipv4, - string ipv6, + string? serviceName, + string? ipv4, + string? ipv6, int? port, - Dictionary tags) + Dictionary? tags) { this.ServiceName = serviceName; this.Ipv4 = ipv4; @@ -26,15 +26,15 @@ public ZipkinEndpoint( 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 Dictionary Tags { get; } + public Dictionary? Tags { get; } public static ZipkinEndpoint Create(string serviceName) { diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpan.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpan.cs index 8038492a0be..a6ea86f6cea 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpan.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpan.cs @@ -11,16 +11,16 @@ internal readonly struct ZipkinSpan { public ZipkinSpan( string traceId, - string parentId, + string? parentId, string id, - string kind, + string? kind, string name, long? timestamp, long? duration, ZipkinEndpoint localEndpoint, - ZipkinEndpoint remoteEndpoint, + ZipkinEndpoint? remoteEndpoint, in PooledList annotations, - in PooledList> tags, + in PooledList> tags, bool? debug, bool? shared) { @@ -44,11 +44,11 @@ public ZipkinSpan( public string TraceId { get; } - public string ParentId { get; } + public string? ParentId { get; } public string Id { get; } - public string Kind { get; } + public string? Kind { get; } public string Name { get; } @@ -58,11 +58,11 @@ public ZipkinSpan( public ZipkinEndpoint LocalEndpoint { get; } - public ZipkinEndpoint RemoteEndpoint { get; } + public ZipkinEndpoint? RemoteEndpoint { get; } public PooledList Annotations { get; } - public PooledList> Tags { get; } + public PooledList> Tags { get; } public bool? Debug { get; } @@ -148,7 +148,7 @@ public void Write(Utf8JsonWriter writer) writer.WriteEndArray(); } - if (!this.Tags.IsEmpty || this.LocalEndpoint.Tags != null) + if (!this.Tags.IsEmpty || this.LocalEndpoint!.Tags != null) { writer.WritePropertyName(ZipkinSpanJsonHelper.TagsPropertyName); writer.WriteStartObject(); @@ -161,7 +161,7 @@ public void Write(Utf8JsonWriter writer) try { - foreach (var tag in this.LocalEndpoint.Tags ?? Enumerable.Empty>()) + foreach (var tag in this.LocalEndpoint!.Tags! ?? Enumerable.Empty>()) { ZipkinTagWriter.Instance.TryWriteTag(ref writer, tag); } diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinTagWriter.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinTagWriter.cs index d40d126b92e..3e1c42c69ec 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinTagWriter.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinTagWriter.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Buffers.Text; using System.Globalization; using System.Text.Json; @@ -66,4 +64,6 @@ protected override void OnUnsupportedTagDropped( tagValueTypeFullName, tagKey); } + + protected override bool TryWriteEmptyTag(ref Utf8JsonWriter state, string key, object? value) => false; } diff --git a/src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj b/src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj index 22b6537a7dd..a9f9e7df834 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj +++ b/src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj @@ -1,24 +1,21 @@ + $(TargetFrameworksForLibraries) Zipkin exporter for OpenTelemetry .NET $(PackageTags);Zipkin;distributed-tracing core- - - - disable + true - - @@ -30,13 +27,13 @@ - - - + + - + + diff --git a/src/OpenTelemetry.Exporter.Zipkin/README.md b/src/OpenTelemetry.Exporter.Zipkin/README.md index aa277b7fdc2..ededcf9b07d 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/README.md +++ b/src/OpenTelemetry.Exporter.Zipkin/README.md @@ -15,7 +15,7 @@ dotnet add package OpenTelemetry.Exporter.Zipkin ## Enable/Add Zipkin as a tracing exporter -You can enable the the `ZipkinExporter` with the `AddZipkinExporter()` extension +You can enable the `ZipkinExporter` with the `AddZipkinExporter()` extension method on `TracerProviderBuilder`. ## Configuration diff --git a/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporter.cs b/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporter.cs index e7a9e0ebccb..103cacbcea5 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporter.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporter.cs @@ -24,22 +24,35 @@ public class ZipkinExporter : BaseExporter private readonly ZipkinExporterOptions options; private readonly int maxPayloadSizeInBytes; private readonly HttpClient httpClient; +#if NET + private readonly bool synchronousSendSupportedByCurrentPlatform; +#endif /// /// Initializes a new instance of the class. /// /// Configuration options. /// Http client to use to upload telemetry. - public ZipkinExporter(ZipkinExporterOptions options, HttpClient client = null) + public ZipkinExporter(ZipkinExporterOptions options, HttpClient? client = null) { Guard.ThrowIfNull(options); this.options = options; - this.maxPayloadSizeInBytes = (!options.MaxPayloadSizeInBytes.HasValue || options.MaxPayloadSizeInBytes <= 0) ? ZipkinExporterOptions.DefaultMaxPayloadSizeInBytes : options.MaxPayloadSizeInBytes.Value; + 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."); + +#if NET + // See: https://github.com/dotnet/runtime/blob/280f2a0c60ce0378b8db49adc0eecc463d00fe5d/src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.AnyMobile.cs#L767 + this.synchronousSendSupportedByCurrentPlatform = !OperatingSystem.IsAndroid() + && !OperatingSystem.IsIOS() + && !OperatingSystem.IsTvOS() + && !OperatingSystem.IsBrowser(); +#endif } - internal ZipkinEndpoint LocalEndpoint { get; private set; } + internal ZipkinEndpoint? LocalEndpoint { get; private set; } /// public override ExportResult Export(in Batch batch) @@ -61,8 +74,10 @@ public override ExportResult Export(in Batch batch) Content = new JsonContent(this, batch), }; -#if NET6_0_OR_GREATER - using var response = this.httpClient.Send(request, CancellationToken.None); +#if NET + using var response = this.synchronousSendSupportedByCurrentPlatform + ? this.httpClient.Send(request, CancellationToken.None) + : this.httpClient.SendAsync(request, CancellationToken.None).GetAwaiter().GetResult(); #else using var response = this.httpClient.SendAsync(request, CancellationToken.None).GetAwaiter().GetResult(); #endif @@ -83,15 +98,15 @@ internal void SetLocalEndpointFromResource(Resource resource) { var hostName = ResolveHostName(); - string ipv4 = null; - string ipv6 = null; + string? ipv4 = null; + string? ipv6 = null; if (!string.IsNullOrEmpty(hostName)) { - ipv4 = ResolveHostAddress(hostName, AddressFamily.InterNetwork); - ipv6 = ResolveHostAddress(hostName, AddressFamily.InterNetworkV6); + ipv4 = ResolveHostAddress(hostName!, AddressFamily.InterNetwork); + ipv6 = ResolveHostAddress(hostName!, AddressFamily.InterNetworkV6); } - string serviceName = null; + string? serviceName = null; foreach (var label in resource.Attributes) { if (label.Key == ResourceSemanticConventions.AttributeServiceName) @@ -115,9 +130,9 @@ internal void SetLocalEndpointFromResource(Resource resource) tags: null); } - private static string ResolveHostAddress(string hostName, AddressFamily family) + private static string? ResolveHostAddress(string hostName, AddressFamily family) { - string result = null; + string? result = null; try { @@ -145,9 +160,9 @@ private static string ResolveHostAddress(string hostName, AddressFamily family) return result; } - private static string ResolveHostName() + private static string? ResolveHostName() { - string result = null; + string? result = null; try { @@ -180,7 +195,7 @@ private sealed class JsonContent : HttpContent private readonly ZipkinExporter exporter; private readonly Batch batch; - private Utf8JsonWriter writer; + private Utf8JsonWriter? writer; public JsonContent(ZipkinExporter exporter, in Batch batch) { @@ -190,14 +205,14 @@ public JsonContent(ZipkinExporter exporter, in Batch batch) this.Headers.ContentType = JsonHeader; } -#if NET6_0_OR_GREATER - protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken) +#if NET + 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; @@ -210,6 +225,17 @@ protected override bool TryComputeLength(out long length) return false; } + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.writer?.Dispose(); + this.writer = null; + } + + base.Dispose(disposing); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SerializeToStreamInternal(Stream stream) { @@ -226,7 +252,7 @@ private void SerializeToStreamInternal(Stream stream) foreach (var activity in this.batch) { - var zipkinSpan = activity.ToZipkinSpan(this.exporter.LocalEndpoint, this.exporter.options.UseShortTraceIds); + var zipkinSpan = activity.ToZipkinSpan(this.exporter.LocalEndpoint!, this.exporter.options.UseShortTraceIds); zipkinSpan.Write(this.writer); diff --git a/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterHelperExtensions.cs index ca858c649ab..869202b0f8d 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterHelperExtensions.cs @@ -39,13 +39,13 @@ public static TracerProviderBuilder AddZipkinExporter(this TracerProviderBuilder /// Adds Zipkin exporter to the TracerProvider. /// /// builder to use. - /// Name which is used when retrieving options. - /// Callback action for configuring . + /// Optional name which is used when retrieving options. + /// Optional callback action for configuring . /// The instance of to chain the calls. public static TracerProviderBuilder AddZipkinExporter( this TracerProviderBuilder builder, - string name, - Action configure) + string? name, + Action? configure) { Guard.ThrowIfNull(builder); @@ -68,12 +68,11 @@ public static TracerProviderBuilder AddZipkinExporter( { var options = sp.GetRequiredService>().Get(name); - return BuildZipkinExporterProcessor(builder, options, sp); + return BuildZipkinExporterProcessor(options, sp); }); } private static BaseProcessor BuildZipkinExporterProcessor( - TracerProviderBuilder builder, ZipkinExporterOptions options, IServiceProvider serviceProvider) { @@ -81,21 +80,23 @@ private static BaseProcessor BuildZipkinExporterProcessor( { options.HttpClientFactory = () => { - Type httpClientFactoryType = Type.GetType("System.Net.Http.IHttpClientFactory, Microsoft.Extensions.Http", throwOnError: false); + Type? httpClientFactoryType = Type.GetType("System.Net.Http.IHttpClientFactory, Microsoft.Extensions.Http", throwOnError: false); if (httpClientFactoryType != null) { - object httpClientFactory = serviceProvider.GetService(httpClientFactoryType); + object? httpClientFactory = serviceProvider.GetService(httpClientFactoryType); if (httpClientFactory != null) { - MethodInfo createClientMethod = httpClientFactoryType.GetMethod( + MethodInfo? createClientMethod = httpClientFactoryType.GetMethod( "CreateClient", BindingFlags.Public | BindingFlags.Instance, binder: null, - new Type[] { typeof(string) }, + [typeof(string)], modifiers: null); if (createClientMethod != null) { - return (HttpClient)createClientMethod.Invoke(httpClientFactory, new object[] { "ZipkinExporter" }); + var parameters = new object[] { "ZipkinExporter" }; + var client = (HttpClient?)createClientMethod.Invoke(httpClientFactory, parameters); + return client ?? new HttpClient(); } } } @@ -104,20 +105,17 @@ private static BaseProcessor BuildZipkinExporterProcessor( }; } +#pragma warning disable CA2000 // Dispose objects before losing scope var zipkinExporter = new ZipkinExporter(options); +#pragma warning restore CA2000 // Dispose objects before losing scope - if (options.ExportProcessorType == ExportProcessorType.Simple) - { - return new SimpleActivityExportProcessor(zipkinExporter); - } - else - { - return new BatchActivityExportProcessor( + return options.ExportProcessorType == ExportProcessorType.Simple + ? new SimpleActivityExportProcessor(zipkinExporter) + : 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 bc94307351d..161ab174b2f 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterOptions.cs @@ -40,12 +40,12 @@ internal ZipkinExporterOptions( Debug.Assert(configuration != null, "configuration was null"); Debug.Assert(defaultBatchOptions != null, "defaultBatchOptions was null"); - if (configuration.TryGetUriValue(ZipkinExporterEventSource.Log, ZipkinEndpointEnvVar, out var endpoint)) + if (configuration!.TryGetUriValue(ZipkinExporterEventSource.Log, ZipkinEndpointEnvVar, out var endpoint)) { this.Endpoint = endpoint; } - this.BatchExportProcessorOptions = defaultBatchOptions; + this.BatchExportProcessorOptions = defaultBatchOptions!; } /// diff --git a/src/OpenTelemetry.Extensions.Hosting/AssemblyInfo.cs b/src/OpenTelemetry.Extensions.Hosting/AssemblyInfo.cs deleted file mode 100644 index d36465c0505..00000000000 --- a/src/OpenTelemetry.Extensions.Hosting/AssemblyInfo.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("OpenTelemetry.Extensions.Hosting.Tests" + AssemblyInfo.PublicKey)] - -#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.Extensions.Hosting/CHANGELOG.md b/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md index 0b9b1a1c9fe..3201a20f9f3 100644 --- a/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md +++ b/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md @@ -1,7 +1,55 @@ # Changelog +This file contains individual changes for the OpenTelemetry.Extensions.Hosting +package. For highlights and announcements covering all components see: [Release +Notes](../../RELEASENOTES.md). + ## Unreleased +## 1.13.0 + +Released 2025-Oct-01 + +## 1.12.0 + +Released 2025-Apr-29 + +## 1.11.2 + +Released 2025-Mar-04 + +## 1.11.1 + +Released 2025-Jan-22 + +## 1.11.0 + +Released 2025-Jan-15 + +## 1.11.0-rc.1 + +Released 2024-Dec-11 + +## 1.10.0 + +Released 2024-Nov-12 + +* Updated `Microsoft.Extensions.Hosting.Abstractions` package + version to `9.0.0`. + ([#5967](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5967)) + +## 1.10.0-rc.1 + +Released 2024-Nov-01 + +## 1.10.0-beta.1 + +Released 2024-Sep-30 + +* Updated `Microsoft.Extensions.Hosting.Abstractions` package + version to `9.0.0-rc.1.24431.7`. + ([#5853](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5853)) + ## 1.9.0 Released 2024-Jun-14 @@ -11,7 +59,7 @@ Released 2024-Jun-14 Released 2024-Jun-07 * The experimental APIs previously covered by `OTEL1000` - (`OpenTelemetryBuilder.WithLogging` method) will now be part of the public API + (`OpenTelemetryBuilder.WithLogging` method) are now be part of the public API and supported in stable builds. ([#5648](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5648)) diff --git a/src/OpenTelemetry.Extensions.Hosting/Implementation/HostingExtensionsEventSource.cs b/src/OpenTelemetry.Extensions.Hosting/Implementation/HostingExtensionsEventSource.cs index 1d31393b0fa..af4bb57a7f2 100644 --- a/src/OpenTelemetry.Extensions.Hosting/Implementation/HostingExtensionsEventSource.cs +++ b/src/OpenTelemetry.Extensions.Hosting/Implementation/HostingExtensionsEventSource.cs @@ -11,7 +11,7 @@ namespace OpenTelemetry.Extensions.Hosting.Implementation; [EventSource(Name = "OpenTelemetry-Extensions-Hosting")] internal sealed class HostingExtensionsEventSource : EventSource { - public static HostingExtensionsEventSource Log = new(); + public static readonly HostingExtensionsEventSource Log = new(); [Event(1, Message = "OpenTelemetry TracerProvider was not found in application services. Tracing will remain disabled.", Level = EventLevel.Warning)] public void TracerProviderNotRegistered() diff --git a/src/OpenTelemetry.Extensions.Hosting/OpenTelemetry.Extensions.Hosting.csproj b/src/OpenTelemetry.Extensions.Hosting/OpenTelemetry.Extensions.Hosting.csproj index ee51a4f8543..22d961e6a88 100644 --- a/src/OpenTelemetry.Extensions.Hosting/OpenTelemetry.Extensions.Hosting.csproj +++ b/src/OpenTelemetry.Extensions.Hosting/OpenTelemetry.Extensions.Hosting.csproj @@ -5,7 +5,6 @@ Contains extensions to start OpenTelemetry in applications using Microsoft.Extensions.Hosting OpenTelemetry core- - latest-all @@ -16,4 +15,8 @@ + + + + diff --git a/src/OpenTelemetry.Extensions.Propagators/.publicApi/PublicAPI.Shipped.txt b/src/OpenTelemetry.Extensions.Propagators/.publicApi/PublicAPI.Shipped.txt index eadd932c939..3379641fda2 100644 --- a/src/OpenTelemetry.Extensions.Propagators/.publicApi/PublicAPI.Shipped.txt +++ b/src/OpenTelemetry.Extensions.Propagators/.publicApi/PublicAPI.Shipped.txt @@ -1,11 +1,12 @@ +#nullable enable 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 +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/AssemblyInfo.cs b/src/OpenTelemetry.Extensions.Propagators/AssemblyInfo.cs deleted file mode 100644 index a06279f44f2..00000000000 --- a/src/OpenTelemetry.Extensions.Propagators/AssemblyInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Runtime.CompilerServices; - -#if SIGNED -[assembly: InternalsVisibleTo("OpenTelemetry.Extensions.Propagators.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051C1562A090FB0C9F391012A32198B5E5D9A60E9B80FA2D7B434C9E5CCB7259BD606E66F9660676AFC6692B8CDC6793D190904551D2103B7B22FA636DCBB8208839785BA402EA08FC00C8F1500CCEF28BBF599AA64FFB1E1D5DC1BF3420A3777BADFE697856E9D52070A50C3EA5821C80BEF17CA3ACFFA28F89DD413F096F898")] -#else -[assembly: InternalsVisibleTo("OpenTelemetry.Extensions.Propagators.Tests")] -#endif diff --git a/src/OpenTelemetry.Extensions.Propagators/B3Propagator.cs b/src/OpenTelemetry.Extensions.Propagators/B3Propagator.cs index 37ced1218e8..a7ede4f0a6a 100644 --- a/src/OpenTelemetry.Extensions.Propagators/B3Propagator.cs +++ b/src/OpenTelemetry.Extensions.Propagators/B3Propagator.cs @@ -27,6 +27,7 @@ public sealed class B3Propagator : TextMapPropagator internal const string UpperTraceId = "0000000000000000"; // Sampled values via the X_B3_SAMPLED header. + internal const char SampledValueChar = '1'; internal const string SampledValue = "1"; // Some old zipkin implementations may send true/false for the sampled header. Only use this for checking incoming values. @@ -35,7 +36,7 @@ public sealed class B3Propagator : TextMapPropagator // "Debug" sampled value. internal const string FlagsValue = "1"; - private static readonly HashSet AllFields = new() { XB3TraceId, XB3SpanId, XB3ParentSpanId, XB3Sampled, XB3Flags }; + private static readonly HashSet AllFields = [XB3TraceId, XB3SpanId, XB3ParentSpanId, XB3Sampled, XB3Flags]; private static readonly HashSet SampledValues = new(StringComparer.Ordinal) { SampledValue, LegacySampledValue }; @@ -62,7 +63,7 @@ public B3Propagator(bool singleHeader) public override ISet Fields => AllFields; /// - public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) + public override PropagationContext Extract(PropagationContext context, T carrier, Func?> getter) { if (context.ActivityContext.IsValid()) { @@ -82,14 +83,7 @@ public override PropagationContext Extract(PropagationContext context, T carr return context; } - if (this.singleHeader) - { - return ExtractFromSingleHeader(context, carrier, getter); - } - else - { - return ExtractFromMultipleHeaders(context, carrier, getter); - } + return this.singleHeader ? ExtractFromSingleHeader(context, carrier, getter) : ExtractFromMultipleHeaders(context, carrier, getter); } /// @@ -122,7 +116,7 @@ public override void Inject(PropagationContext context, T carrier, Action(PropagationContext context, T carrier, Action(PropagationContext context, T carrier, Func> getter) + private static PropagationContext ExtractFromMultipleHeaders(PropagationContext context, T carrier, Func?> getter) { try { @@ -171,7 +165,8 @@ private static PropagationContext ExtractFromMultipleHeaders(PropagationConte } var traceOptions = ActivityTraceFlags.None; - if (SampledValues.Contains(getter(carrier, XB3Sampled)?.FirstOrDefault()) + var xb3Sampled = getter(carrier, XB3Sampled)?.FirstOrDefault(); + if ((xb3Sampled != null && SampledValues.Contains(xb3Sampled)) || FlagsValue.Equals(getter(carrier, XB3Flags)?.FirstOrDefault(), StringComparison.Ordinal)) { traceOptions |= ActivityTraceFlags.Recorded; @@ -188,11 +183,18 @@ private static PropagationContext ExtractFromMultipleHeaders(PropagationConte } } - private static PropagationContext ExtractFromSingleHeader(PropagationContext context, T carrier, Func> getter) + private static PropagationContext ExtractFromSingleHeader(PropagationContext context, T carrier, Func?> getter) { try { - var header = getter(carrier, XB3Combined)?.FirstOrDefault(); + var headers = getter(carrier, XB3Combined); + if (headers == null) + { + return context; + } + + var header = headers.FirstOrDefault(); + if (string.IsNullOrWhiteSpace(header)) { return context; diff --git a/src/OpenTelemetry.Extensions.Propagators/CHANGELOG.md b/src/OpenTelemetry.Extensions.Propagators/CHANGELOG.md index 10b287b0dc3..2170045f1d7 100644 --- a/src/OpenTelemetry.Extensions.Propagators/CHANGELOG.md +++ b/src/OpenTelemetry.Extensions.Propagators/CHANGELOG.md @@ -1,7 +1,47 @@ # Changelog +This file contains individual changes for the +OpenTelemetry.Extensions.Propagators package. For highlights and announcements +covering all components see: [Release Notes](../../RELEASENOTES.md). + ## Unreleased +## 1.13.0 + +Released 2025-Oct-01 + +## 1.12.0 + +Released 2025-Apr-29 + +## 1.11.2 + +Released 2025-Mar-04 + +## 1.11.1 + +Released 2025-Jan-22 + +## 1.11.0 + +Released 2025-Jan-15 + +## 1.11.0-rc.1 + +Released 2024-Dec-11 + +## 1.10.0 + +Released 2024-Nov-12 + +## 1.10.0-rc.1 + +Released 2024-Nov-01 + +## 1.10.0-beta.1 + +Released 2024-Sep-30 + ## 1.9.0 Released 2024-Jun-14 diff --git a/src/OpenTelemetry.Extensions.Propagators/JaegerPropagator.cs b/src/OpenTelemetry.Extensions.Propagators/JaegerPropagator.cs index 472b719ded0..d11d8d67912 100644 --- a/src/OpenTelemetry.Extensions.Propagators/JaegerPropagator.cs +++ b/src/OpenTelemetry.Extensions.Propagators/JaegerPropagator.cs @@ -17,7 +17,7 @@ public class JaegerPropagator : TextMapPropagator internal const string JaegerDelimiterEncoded = "%3A"; // while the spec defines the delimiter as a ':', some clients will url encode headers. internal const string SampledValue = "1"; - internal static readonly string[] JaegerDelimiters = { JaegerDelimiter, JaegerDelimiterEncoded }; + internal static readonly string[] JaegerDelimiters = [JaegerDelimiter, JaegerDelimiterEncoded]; private static readonly int TraceId128BitLength = "0af7651916cd43dd8448eb211c80319c".Length; private static readonly int SpanIdLength = "00f067aa0ba902b7".Length; @@ -26,7 +26,7 @@ public class JaegerPropagator : TextMapPropagator public override ISet Fields => new HashSet { JaegerHeader }; /// - public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) + public override PropagationContext Extract(PropagationContext context, T carrier, Func?> getter) { if (context.ActivityContext.IsValid()) { @@ -49,7 +49,12 @@ public override PropagationContext Extract(PropagationContext context, T carr try { var jaegerHeaderCollection = getter(carrier, JaegerHeader); - var jaegerHeader = jaegerHeaderCollection?.First(); + if (jaegerHeaderCollection == null) + { + return context; + } + + var jaegerHeader = jaegerHeaderCollection.First(); if (string.IsNullOrWhiteSpace(jaegerHeader)) { @@ -154,7 +159,7 @@ internal static bool TryExtractTraceContext(string jaegerHeader, out ActivityTra spanId = ActivitySpanId.CreateFromString(spanIdStr.AsSpan()); var traceFlagsStr = traceComponents[3]; - if (SampledValue.Equals(traceFlagsStr)) + if (SampledValue.Equals(traceFlagsStr, StringComparison.Ordinal)) { traceOptions |= ActivityTraceFlags.Recorded; } diff --git a/src/OpenTelemetry.Extensions.Propagators/OpenTelemetry.Extensions.Propagators.csproj b/src/OpenTelemetry.Extensions.Propagators/OpenTelemetry.Extensions.Propagators.csproj index c099b609a25..34f57c8043e 100644 --- a/src/OpenTelemetry.Extensions.Propagators/OpenTelemetry.Extensions.Propagators.csproj +++ b/src/OpenTelemetry.Extensions.Propagators/OpenTelemetry.Extensions.Propagators.csproj @@ -5,9 +5,6 @@ $(PackageTags);distributed-tracing;AspNet;AspNetCore;B3 core- true - - - disable @@ -15,7 +12,11 @@ - $(NoWarn),1591 + $(NoWarn),CS1591 + + + + diff --git a/src/OpenTelemetry.Shims.OpenTracing/.publicApi/PublicAPI.Shipped.txt b/src/OpenTelemetry.Shims.OpenTracing/.publicApi/PublicAPI.Shipped.txt index e69de29bb2d..7dc5c58110b 100644 --- a/src/OpenTelemetry.Shims.OpenTracing/.publicApi/PublicAPI.Shipped.txt +++ b/src/OpenTelemetry.Shims.OpenTracing/.publicApi/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/OpenTelemetry.Shims.OpenTracing/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Shims.OpenTracing/.publicApi/PublicAPI.Unshipped.txt index 90f671f8a12..c5d387112fb 100644 --- a/src/OpenTelemetry.Shims.OpenTracing/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Shims.OpenTracing/.publicApi/PublicAPI.Unshipped.txt @@ -1,8 +1,8 @@ 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.TracerProvider tracerProvider) -> void -OpenTelemetry.Shims.OpenTracing.TracerShim.TracerShim(OpenTelemetry.Trace.TracerProvider tracerProvider, OpenTelemetry.Context.Propagation.TextMapPropagator textFormat) -> void +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.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/AssemblyInfo.cs b/src/OpenTelemetry.Shims.OpenTracing/AssemblyInfo.cs deleted file mode 100644 index ae3704ebf27..00000000000 --- a/src/OpenTelemetry.Shims.OpenTracing/AssemblyInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Runtime.CompilerServices; - -#if SIGNED -[assembly: InternalsVisibleTo("OpenTelemetry.Shims.OpenTracing.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] -#else -[assembly: InternalsVisibleTo("OpenTelemetry.Shims.OpenTracing.Tests")] -#endif diff --git a/src/OpenTelemetry.Shims.OpenTracing/CHANGELOG.md b/src/OpenTelemetry.Shims.OpenTracing/CHANGELOG.md index 1dbb969e79c..67954363842 100644 --- a/src/OpenTelemetry.Shims.OpenTracing/CHANGELOG.md +++ b/src/OpenTelemetry.Shims.OpenTracing/CHANGELOG.md @@ -1,7 +1,51 @@ # Changelog +This file contains individual changes for the OpenTelemetry.Shims.OpenTracing +package. For highlights and announcements covering all components see: [Release +Notes](../../RELEASENOTES.md). + ## Unreleased +## 1.12.0-beta.1 + +Released 2025-May-06 + +* Updated OpenTelemetry core component version(s) to `1.12.0`. + ([#6269](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6269)) + +## 1.11.2-beta.1 + +Released 2025-Mar-05 + +* Updated OpenTelemetry core component version(s) to `1.11.2`. + ([#6169](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6169)) + +## 1.11.0-beta.1 + +Released 2025-Jan-16 + +* Updated OpenTelemetry core component version(s) to `1.11.0`. + ([#6064](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6064)) + +## 1.10.0-beta.1 + +Released 2024-Nov-12 + +* Fixed an issue causing all tag values added via the `ISpanBuilder` API to be + converted to strings on the `ISpan` started from the builder. + ([#5797](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5797)) + +* Updated OpenTelemetry core component version(s) to `1.10.0`. + ([#5970](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5970)) + +## 1.9.0-beta.2 + +Released 2024-Jun-24 + +## 1.9.0-beta.1 + +Released 2024-Jun-14 + ## 1.9.0-alpha.2 Released 2024-May-29 @@ -26,6 +70,7 @@ 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. diff --git a/src/OpenTelemetry.Shims.OpenTracing/OpenTelemetry.Shims.OpenTracing.csproj b/src/OpenTelemetry.Shims.OpenTracing/OpenTelemetry.Shims.OpenTracing.csproj index 645c9e49e59..4262f637d14 100644 --- a/src/OpenTelemetry.Shims.OpenTracing/OpenTelemetry.Shims.OpenTracing.csproj +++ b/src/OpenTelemetry.Shims.OpenTracing/OpenTelemetry.Shims.OpenTracing.csproj @@ -4,9 +4,6 @@ OpenTracing shim for OpenTelemetry .NET $(PackageTags);distributed-tracing;OpenTracing coreunstable- - - - disable @@ -22,7 +19,13 @@ + + + + + + diff --git a/src/OpenTelemetry.Shims.OpenTracing/ScopeManagerShim.cs b/src/OpenTelemetry.Shims.OpenTracing/ScopeManagerShim.cs index f9464d966a1..6b2fb783355 100644 --- a/src/OpenTelemetry.Shims.OpenTracing/ScopeManagerShim.cs +++ b/src/OpenTelemetry.Shims.OpenTracing/ScopeManagerShim.cs @@ -19,7 +19,7 @@ internal sealed class ScopeManagerShim : IScopeManager #endif /// - public IScope Active + public IScope? Active { get { @@ -56,7 +56,7 @@ public IScope Activate(ISpan span, bool finishSpanOnDispose) Interlocked.Decrement(ref this.spanScopeTableCount); } #endif - scope.Dispose(); + scope!.Dispose(); }); SpanScopeTable.Add(shim.Span, instrumentation); @@ -69,9 +69,9 @@ public IScope Activate(ISpan span, bool finishSpanOnDispose) private sealed class ScopeInstrumentation : IScope { - private readonly Action disposeAction; + private readonly Action? disposeAction; - public ScopeInstrumentation(TelemetrySpan span, Action disposeAction = null) + public ScopeInstrumentation(TelemetrySpan span, Action? disposeAction = null) { this.Span = new SpanShim(span); this.disposeAction = disposeAction; diff --git a/src/OpenTelemetry.Shims.OpenTracing/SpanBuilderShim.cs b/src/OpenTelemetry.Shims.OpenTracing/SpanBuilderShim.cs index 0359762ac3e..8f256920127 100644 --- a/src/OpenTelemetry.Shims.OpenTracing/SpanBuilderShim.cs +++ b/src/OpenTelemetry.Shims.OpenTracing/SpanBuilderShim.cs @@ -27,17 +27,17 @@ internal sealed class SpanBuilderShim : ISpanBuilder /// /// The OpenTelemetry links. These correspond loosely to OpenTracing references. /// - private readonly List links = new(); + private readonly List links = []; /// /// The OpenTelemetry attributes. These correspond to OpenTracing Tags. /// - private readonly List> attributes = new(); + private readonly SpanAttributes attributes = new(); /// /// The parent as an TelemetrySpan, if any. /// - private TelemetrySpan parentSpan; + private TelemetrySpan? parentSpan; /// /// The parent as an SpanContext, if any. @@ -65,12 +65,12 @@ public SpanBuilderShim(Tracer tracer, string spanName) this.ScopeManager = new ScopeManagerShim(); } - private IScopeManager ScopeManager { get; } + private ScopeManagerShim ScopeManager { get; } private bool ParentSet => this.parentSpan != null || this.parentSpanContext.IsValid; /// - public ISpanBuilder AsChildOf(ISpanContext parent) + public ISpanBuilder AsChildOf(ISpanContext? parent) { if (parent == null) { @@ -81,7 +81,7 @@ public ISpanBuilder AsChildOf(ISpanContext parent) } /// - public ISpanBuilder AsChildOf(ISpan parent) + public ISpanBuilder AsChildOf(ISpan? parent) { if (parent == null) { @@ -98,7 +98,7 @@ public ISpanBuilder AsChildOf(ISpan parent) } /// - public ISpanBuilder AddReference(string referenceType, ISpanContext referencedContext) + public ISpanBuilder AddReference(string referenceType, ISpanContext? referencedContext) { if (referencedContext == null) { @@ -132,31 +132,23 @@ public ISpanBuilder IgnoreActiveSpan() /// public ISpan Start() { - TelemetrySpan span = null; + 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); + span = this.tracer.StartRootSpan(this.spanName, this.spanKind, this.attributes, 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); + span = this.tracer.StartSpan(this.spanName, this.spanKind, this.parentSpan, this.attributes, 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); + span = this.tracer.StartSpan(this.spanName, this.spanKind, this.parentSpanContext, this.attributes, this.links, this.explicitStartTime ?? default); } - if (span == null) - { - span = this.tracer.StartSpan(this.spanName, this.spanKind, default(SpanContext), default, null, this.explicitStartTime ?? default); - } - - foreach (var kvp in this.attributes) - { - span.SetAttribute(kvp.Key, kvp.Value.ToString()); - } + span ??= this.tracer.StartSpan(this.spanName, this.spanKind, default(SpanContext), this.attributes, null, this.explicitStartTime ?? default); if (this.error) { @@ -184,8 +176,13 @@ public ISpanBuilder WithStartTimestamp(DateTimeOffset timestamp) } /// - public ISpanBuilder WithTag(string key, string value) + public ISpanBuilder WithTag(string key, string? value) { + if (key == null) + { + return this; + } + // see https://opentracing.io/specification/conventions/ for special key handling. if (global::OpenTracing.Tag.Tags.SpanKind.Key.Equals(key, StringComparison.Ordinal)) { @@ -204,12 +201,7 @@ public ISpanBuilder WithTag(string key, string value) } else { - // Keys must be non-null. - // Null values => string.Empty. - if (key != null) - { - this.attributes.Add(new KeyValuePair(key, value ?? string.Empty)); - } + this.attributes.Add(key, value); } return this; @@ -224,7 +216,7 @@ public ISpanBuilder WithTag(string key, bool value) } else { - this.attributes.Add(new KeyValuePair(key, value)); + this.attributes.Add(key, value); } return this; @@ -233,31 +225,31 @@ public ISpanBuilder WithTag(string key, bool value) /// public ISpanBuilder WithTag(string key, int value) { - this.attributes.Add(new KeyValuePair(key, value)); + this.attributes.Add(key, value); return this; } /// public ISpanBuilder WithTag(string key, double value) { - this.attributes.Add(new KeyValuePair(key, value)); + this.attributes.Add(key, value); return this; } /// public ISpanBuilder WithTag(global::OpenTracing.Tag.BooleanTag tag, bool value) { - Guard.ThrowIfNull(tag?.Key); + Guard.ThrowIfNull(tag); return this.WithTag(tag.Key, value); } /// - public ISpanBuilder WithTag(global::OpenTracing.Tag.IntOrStringTag tag, string value) + public ISpanBuilder WithTag(global::OpenTracing.Tag.IntOrStringTag tag, string? value) { - Guard.ThrowIfNull(tag?.Key); + Guard.ThrowIfNull(tag); - if (int.TryParse(value, out var result)) + if (value != null && int.TryParse(value, out var result)) { return this.WithTag(tag.Key, result); } @@ -268,15 +260,15 @@ public ISpanBuilder WithTag(global::OpenTracing.Tag.IntOrStringTag tag, string v /// public ISpanBuilder WithTag(global::OpenTracing.Tag.IntTag tag, int value) { - Guard.ThrowIfNull(tag?.Key); + Guard.ThrowIfNull(tag); return this.WithTag(tag.Key, value); } /// - public ISpanBuilder WithTag(global::OpenTracing.Tag.StringTag tag, string value) + public ISpanBuilder WithTag(global::OpenTracing.Tag.StringTag tag, string? value) { - Guard.ThrowIfNull(tag?.Key); + Guard.ThrowIfNull(tag); return this.WithTag(tag.Key, value); } diff --git a/src/OpenTelemetry.Shims.OpenTracing/SpanShim.cs b/src/OpenTelemetry.Shims.OpenTracing/SpanShim.cs index 2ab6fee4fe8..f8a5d981207 100644 --- a/src/OpenTelemetry.Shims.OpenTracing/SpanShim.cs +++ b/src/OpenTelemetry.Shims.OpenTracing/SpanShim.cs @@ -14,8 +14,8 @@ internal sealed class SpanShim : ISpan /// public const string DefaultEventName = "log"; - private static readonly IReadOnlyCollection OpenTelemetrySupportedAttributeValueTypes = new List - { + private static readonly IReadOnlyCollection OpenTelemetrySupportedAttributeValueTypes = + [ typeof(string), typeof(bool), typeof(byte), @@ -24,7 +24,7 @@ internal sealed class SpanShim : ISpan typeof(long), typeof(float), typeof(double), - }; + ]; private readonly SpanContextShim spanContextShim; @@ -39,7 +39,7 @@ public SpanShim(TelemetrySpan span) /// public ISpanContext Context => this.spanContextShim; - public TelemetrySpan Span { get; private set; } + public TelemetrySpan Span { get; } /// public void Finish() @@ -54,7 +54,7 @@ public void Finish(DateTimeOffset finishTimestamp) } /// - public string GetBaggageItem(string key) + public string? GetBaggageItem(string key) => Baggage.GetBaggage(key); /// @@ -137,7 +137,7 @@ public ISpan Log(DateTimeOffset timestamp, string @event) } /// - public ISpan SetBaggageItem(string key, string value) + public ISpan SetBaggageItem(string key, string? value) { Baggage.SetBaggage(key, value); return this; @@ -153,7 +153,7 @@ public ISpan SetOperationName(string operationName) } /// - public ISpan SetTag(string key, string value) + public ISpan SetTag(string key, string? value) { Guard.ThrowIfNull(key); @@ -201,30 +201,38 @@ public ISpan SetTag(string key, double value) /// public ISpan SetTag(global::OpenTracing.Tag.BooleanTag tag, bool value) { - return this.SetTag(tag?.Key, value); + Guard.ThrowIfNull(tag); + + return this.SetTag(tag.Key, value); } /// - public ISpan SetTag(global::OpenTracing.Tag.IntOrStringTag tag, string value) + public ISpan SetTag(global::OpenTracing.Tag.IntOrStringTag tag, string? value) { - if (int.TryParse(value, out var result)) + Guard.ThrowIfNull(tag); + + if (value != null && int.TryParse(value, out var result)) { - return this.SetTag(tag?.Key, 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); + Guard.ThrowIfNull(tag); + + return this.SetTag(tag.Key, value); } /// - public ISpan SetTag(global::OpenTracing.Tag.StringTag tag, string value) + public ISpan SetTag(global::OpenTracing.Tag.StringTag tag, string? value) { - return this.SetTag(tag?.Key, value); + Guard.ThrowIfNull(tag); + + return this.SetTag(tag.Key, value); } /// @@ -234,7 +242,7 @@ public ISpan SetTag(global::OpenTracing.Tag.StringTag tag, string value) /// A 2-Tuple containing the event name and payload information. private static Tuple> ConvertToEventPayload(IEnumerable> fields) { - string eventName = null; + string? eventName = null; var attributes = new Dictionary(); foreach (var field in fields) @@ -268,7 +276,7 @@ private static Tuple> ConvertToEventPayload( else { // TODO should we completely ignore unsupported types? - attributes.Add(field.Key, field.Value.ToString()); + attributes.Add(field.Key, field.Value.ToString()!); } } diff --git a/src/OpenTelemetry.Shims.OpenTracing/TracerShim.cs b/src/OpenTelemetry.Shims.OpenTracing/TracerShim.cs index 3ed448da945..b67dfd9a82a 100644 --- a/src/OpenTelemetry.Shims.OpenTracing/TracerShim.cs +++ b/src/OpenTelemetry.Shims.OpenTracing/TracerShim.cs @@ -14,7 +14,7 @@ namespace OpenTelemetry.Shims.OpenTracing; public class TracerShim : global::OpenTracing.ITracer { private readonly Trace.Tracer tracer; - private readonly TextMapPropagator definedPropagator; + private readonly TextMapPropagator? definedPropagator; /// /// Initializes a new instance of the class. @@ -30,7 +30,7 @@ public TracerShim(Trace.TracerProvider tracerProvider) /// /// . /// . - public TracerShim(Trace.TracerProvider tracerProvider, TextMapPropagator textFormat) + public TracerShim(Trace.TracerProvider tracerProvider, TextMapPropagator? textFormat) { Guard.ThrowIfNull(tracerProvider); @@ -46,15 +46,9 @@ public TracerShim(Trace.TracerProvider tracerProvider, TextMapPropagator textFor public global::OpenTracing.IScopeManager ScopeManager { get; } /// - public global::OpenTracing.ISpan ActiveSpan => this.ScopeManager.Active?.Span; + public global::OpenTracing.ISpan? ActiveSpan => this.ScopeManager.Active?.Span; - private TextMapPropagator Propagator - { - get - { - return this.definedPropagator ?? Propagators.DefaultTextMapPropagator; - } - } + private TextMapPropagator Propagator => this.definedPropagator ?? Propagators.DefaultTextMapPropagator; /// public global::OpenTracing.ISpanBuilder BuildSpan(string operationName) @@ -63,7 +57,7 @@ private TextMapPropagator Propagator } /// - public global::OpenTracing.ISpanContext Extract(IFormat format, TCarrier carrier) + public global::OpenTracing.ISpanContext? Extract(IFormat format, TCarrier carrier) { Guard.ThrowIfNull(format); Guard.ThrowIfNull(carrier); @@ -76,10 +70,10 @@ private TextMapPropagator Propagator foreach (var entry in textMapCarrier) { - carrierMap.Add(entry.Key, new[] { entry.Value }); + carrierMap.Add(entry.Key, [entry.Value]); } - static IEnumerable GetCarrierKeyValue(Dictionary> source, string key) + static IEnumerable? GetCarrierKeyValue(Dictionary> source, string key) { if (key == null || !source.TryGetValue(key, out var value)) { diff --git a/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt index a6594de1a9c..b9507b58b38 100644 --- a/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt @@ -1,29 +1,27 @@ -abstract OpenTelemetry.Metrics.ExemplarReservoir.Collect() -> OpenTelemetry.Metrics.ReadOnlyExemplarCollection -abstract OpenTelemetry.Metrics.ExemplarReservoir.Offer(in OpenTelemetry.Metrics.ExemplarMeasurement measurement) -> void -abstract OpenTelemetry.Metrics.ExemplarReservoir.Offer(in OpenTelemetry.Metrics.ExemplarMeasurement measurement) -> void +[OTEL1004]abstract OpenTelemetry.Metrics.ExemplarReservoir.Collect() -> OpenTelemetry.Metrics.ReadOnlyExemplarCollection +[OTEL1004]abstract OpenTelemetry.Metrics.ExemplarReservoir.Offer(in OpenTelemetry.Metrics.ExemplarMeasurement measurement) -> void +[OTEL1004]abstract OpenTelemetry.Metrics.ExemplarReservoir.Offer(in OpenTelemetry.Metrics.ExemplarMeasurement measurement) -> void 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.ExemplarMeasurement -OpenTelemetry.Metrics.ExemplarMeasurement.ExemplarMeasurement() -> void -OpenTelemetry.Metrics.ExemplarMeasurement.Tags.get -> System.ReadOnlySpan> -OpenTelemetry.Metrics.ExemplarMeasurement.Value.get -> T -OpenTelemetry.Metrics.ExemplarReservoir -OpenTelemetry.Metrics.ExemplarReservoir.ResetOnCollect.get -> bool -OpenTelemetry.Metrics.FixedSizeExemplarReservoir -OpenTelemetry.Metrics.FixedSizeExemplarReservoir.Capacity.get -> int -OpenTelemetry.Metrics.FixedSizeExemplarReservoir.FixedSizeExemplarReservoir(int capacity) -> void -OpenTelemetry.Metrics.FixedSizeExemplarReservoir.UpdateExemplar(int exemplarIndex, in OpenTelemetry.Metrics.ExemplarMeasurement measurement) -> void -OpenTelemetry.Metrics.FixedSizeExemplarReservoir.UpdateExemplar(int exemplarIndex, in OpenTelemetry.Metrics.ExemplarMeasurement measurement) -> void -OpenTelemetry.Metrics.MetricStreamConfiguration.CardinalityLimit.get -> int? -OpenTelemetry.Metrics.MetricStreamConfiguration.CardinalityLimit.set -> void +[OTEL1004]OpenTelemetry.Metrics.ExemplarMeasurement +[OTEL1004]OpenTelemetry.Metrics.ExemplarMeasurement.ExemplarMeasurement() -> void +[OTEL1004]OpenTelemetry.Metrics.ExemplarMeasurement.Tags.get -> System.ReadOnlySpan> +[OTEL1004]OpenTelemetry.Metrics.ExemplarMeasurement.Value.get -> T +[OTEL1004]OpenTelemetry.Metrics.ExemplarReservoir +[OTEL1004]OpenTelemetry.Metrics.ExemplarReservoir.ResetOnCollect.get -> bool +[OTEL1004]OpenTelemetry.Metrics.FixedSizeExemplarReservoir +[OTEL1004]OpenTelemetry.Metrics.FixedSizeExemplarReservoir.Capacity.get -> int +[OTEL1004]OpenTelemetry.Metrics.FixedSizeExemplarReservoir.FixedSizeExemplarReservoir(int capacity) -> void +[OTEL1004]OpenTelemetry.Metrics.FixedSizeExemplarReservoir.UpdateExemplar(int exemplarIndex, in OpenTelemetry.Metrics.ExemplarMeasurement measurement) -> void +[OTEL1004]OpenTelemetry.Metrics.FixedSizeExemplarReservoir.UpdateExemplar(int exemplarIndex, in OpenTelemetry.Metrics.ExemplarMeasurement measurement) -> void OpenTelemetry.Metrics.MetricStreamConfiguration.ExemplarReservoirFactory.get -> System.Func? OpenTelemetry.Metrics.MetricStreamConfiguration.ExemplarReservoirFactory.set -> void -override sealed OpenTelemetry.Metrics.FixedSizeExemplarReservoir.Collect() -> OpenTelemetry.Metrics.ReadOnlyExemplarCollection -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! -static OpenTelemetry.Sdk.CreateLoggerProviderBuilder() -> OpenTelemetry.Logs.LoggerProviderBuilder! -virtual OpenTelemetry.Metrics.FixedSizeExemplarReservoir.OnCollected() -> void +[OTEL1004]override sealed OpenTelemetry.Metrics.FixedSizeExemplarReservoir.Collect() -> OpenTelemetry.Metrics.ReadOnlyExemplarCollection +[OTEL1000]static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.UseOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder) -> Microsoft.Extensions.Logging.ILoggingBuilder! +[OTEL1000]static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.UseOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, System.Action! configure) -> Microsoft.Extensions.Logging.ILoggingBuilder! +[OTEL1000]static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.UseOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, System.Action? configureBuilder, System.Action? configureOptions) -> Microsoft.Extensions.Logging.ILoggingBuilder! +[OTEL1001]static OpenTelemetry.Sdk.CreateLoggerProviderBuilder() -> OpenTelemetry.Logs.LoggerProviderBuilder! +[OTEL1004]virtual OpenTelemetry.Metrics.FixedSizeExemplarReservoir.OnCollected() -> void diff --git a/src/OpenTelemetry/.publicApi/Stable/PublicAPI.Shipped.txt b/src/OpenTelemetry/.publicApi/Stable/PublicAPI.Shipped.txt index 43e084ad586..43b209cd7e6 100644 --- a/src/OpenTelemetry/.publicApi/Stable/PublicAPI.Shipped.txt +++ b/src/OpenTelemetry/.publicApi/Stable/PublicAPI.Shipped.txt @@ -20,6 +20,7 @@ OpenTelemetry.BaseProcessor.ParentProvider.get -> OpenTelemetry.BaseProvider? OpenTelemetry.BaseProcessor.Shutdown(int timeoutMilliseconds = -1) -> bool OpenTelemetry.Batch OpenTelemetry.Batch.Batch() -> void +OpenTelemetry.Batch.Batch(T! item) -> void OpenTelemetry.Batch.Batch(T![]! items, int count) -> void OpenTelemetry.Batch.Count.get -> long OpenTelemetry.Batch.Dispose() -> void @@ -235,6 +236,8 @@ 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.CardinalityLimit.get -> int? +OpenTelemetry.Metrics.MetricStreamConfiguration.CardinalityLimit.set -> void OpenTelemetry.Metrics.MetricStreamConfiguration.Description.get -> string? OpenTelemetry.Metrics.MetricStreamConfiguration.Description.set -> void OpenTelemetry.Metrics.MetricStreamConfiguration.MetricStreamConfiguration() -> void @@ -268,6 +271,12 @@ OpenTelemetry.Metrics.ReadOnlyExemplarCollection.Enumerator.MoveNext() -> bool OpenTelemetry.Metrics.ReadOnlyExemplarCollection.GetEnumerator() -> OpenTelemetry.Metrics.ReadOnlyExemplarCollection.Enumerator OpenTelemetry.Metrics.ReadOnlyExemplarCollection.ReadOnlyExemplarCollection() -> void OpenTelemetry.OpenTelemetryBuilderSdkExtensions +OpenTelemetry.OpenTelemetrySdk +OpenTelemetry.OpenTelemetrySdk.Dispose() -> void +OpenTelemetry.OpenTelemetrySdk.LoggerProvider.get -> OpenTelemetry.Logs.LoggerProvider! +OpenTelemetry.OpenTelemetrySdk.MeterProvider.get -> OpenTelemetry.Metrics.MeterProvider! +OpenTelemetry.OpenTelemetrySdk.TracerProvider.get -> OpenTelemetry.Trace.TracerProvider! +OpenTelemetry.OpenTelemetrySdkExtensions OpenTelemetry.ProviderExtensions OpenTelemetry.ReadOnlyFilteredTagCollection OpenTelemetry.ReadOnlyFilteredTagCollection.Enumerator @@ -434,6 +443,8 @@ static OpenTelemetry.OpenTelemetryBuilderSdkExtensions.WithMetrics(this OpenTele static OpenTelemetry.OpenTelemetryBuilderSdkExtensions.WithMetrics(this OpenTelemetry.IOpenTelemetryBuilder! builder) -> OpenTelemetry.IOpenTelemetryBuilder! static OpenTelemetry.OpenTelemetryBuilderSdkExtensions.WithTracing(this OpenTelemetry.IOpenTelemetryBuilder! builder, System.Action! configure) -> OpenTelemetry.IOpenTelemetryBuilder! static OpenTelemetry.OpenTelemetryBuilderSdkExtensions.WithTracing(this OpenTelemetry.IOpenTelemetryBuilder! builder) -> OpenTelemetry.IOpenTelemetryBuilder! +static OpenTelemetry.OpenTelemetrySdk.Create(System.Action! configure) -> OpenTelemetry.OpenTelemetrySdk! +static OpenTelemetry.OpenTelemetrySdkExtensions.GetLoggerFactory(this OpenTelemetry.OpenTelemetrySdk! sdk) -> Microsoft.Extensions.Logging.ILoggerFactory! 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! diff --git a/src/OpenTelemetry/AssemblyInfo.cs b/src/OpenTelemetry/AssemblyInfo.cs deleted file mode 100644 index 46c323088a6..00000000000 --- a/src/OpenTelemetry/AssemblyInfo.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Runtime.CompilerServices; - -[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.Exporter.Prometheus.AspNetCore" + AssemblyInfo.PublicKey)] -[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests" + AssemblyInfo.PublicKey)] -[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("OpenTelemetry.Tests" + AssemblyInfo.PublicKey)] -[assembly: InternalsVisibleTo("OpenTelemetry.Tests.Stress.Metrics" + AssemblyInfo.PublicKey)] -[assembly: InternalsVisibleTo("Benchmarks" + AssemblyInfo.PublicKey)] - -#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 2e9fe7619c2..bafa4c117ca 100644 --- a/src/OpenTelemetry/BaseExportProcessor.cs +++ b/src/OpenTelemetry/BaseExportProcessor.cs @@ -29,13 +29,17 @@ public enum ExportProcessorType /// Implements processor that exports telemetry objects. /// /// The type of telemetry object to be exported. +#pragma warning disable CA1708 // Identifiers should differ by more than case public abstract class BaseExportProcessor : BaseProcessor +#pragma warning restore CA1708 // Identifiers should differ by more than case where T : class { /// /// Gets the exporter used by the processor. /// +#pragma warning disable CA1051 // Do not declare visible instance fields protected readonly BaseExporter exporter; +#pragma warning restore CA1051 // Do not declare visible instance fields private readonly string friendlyTypeName; private bool disposed; @@ -48,7 +52,9 @@ protected BaseExportProcessor(BaseExporter exporter) { Guard.ThrowIfNull(exporter); +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 this.friendlyTypeName = $"{this.GetType().Name}{{{exporter.GetType().Name}}}"; +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 this.exporter = exporter; } diff --git a/src/OpenTelemetry/BaseProcessor.cs b/src/OpenTelemetry/BaseProcessor.cs index daa7468c2d7..b75cc24d86e 100644 --- a/src/OpenTelemetry/BaseProcessor.cs +++ b/src/OpenTelemetry/BaseProcessor.cs @@ -9,7 +9,9 @@ namespace OpenTelemetry; /// Base processor base class. /// /// The type of object to be processed. +#pragma warning disable CA1012 // Abstract types should not have public constructors public abstract class BaseProcessor : IDisposable +#pragma warning restore CA1012 // Abstract types should not have public constructors { private readonly string typeName; private int shutdownCount; diff --git a/src/OpenTelemetry/Batch.cs b/src/OpenTelemetry/Batch.cs index b4ddd08cf17..4f0e0608dcf 100644 --- a/src/OpenTelemetry/Batch.cs +++ b/src/OpenTelemetry/Batch.cs @@ -29,15 +29,21 @@ namespace OpenTelemetry; public Batch(T[] items, int count) { Guard.ThrowIfNull(items); +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 Guard.ThrowIfOutOfRange(count, min: 0, max: items.Length); +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 this.items = items; this.Count = this.targetCount = count; } - internal Batch(T item) + /// + /// Initializes a new instance of the struct. + /// + /// The item to store in the batch. + public Batch(T item) { - Debug.Assert(item != null, $"{nameof(item)} was null."); + Guard.ThrowIfNull(item); this.item = item; this.Count = this.targetCount = 1; diff --git a/src/OpenTelemetry/BatchExportProcessor.cs b/src/OpenTelemetry/BatchExportProcessor.cs index b377d5e89e5..46a64b8e042 100644 --- a/src/OpenTelemetry/BatchExportProcessor.cs +++ b/src/OpenTelemetry/BatchExportProcessor.cs @@ -60,7 +60,9 @@ protected BatchExportProcessor( this.exporterThread = new Thread(this.ExporterProc) { IsBackground = true, +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 Name = $"OpenTelemetry-{nameof(BatchExportProcessor)}-{exporter.GetType().Name}", +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 }; this.exporterThread.Start(); } diff --git a/src/OpenTelemetry/BatchExportProcessorOptions.cs b/src/OpenTelemetry/BatchExportProcessorOptions.cs index 67345e9715c..6695c0c6a56 100644 --- a/src/OpenTelemetry/BatchExportProcessorOptions.cs +++ b/src/OpenTelemetry/BatchExportProcessorOptions.cs @@ -26,7 +26,7 @@ public class BatchExportProcessorOptions 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. + /// Gets or sets the maximum batch size of every export. It must be smaller or equal to . 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 ca4d195698d..a11f9108e92 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -1,7 +1,167 @@ # Changelog +This file contains individual changes for the OpenTelemetry package. For +highlights and announcements covering all components see: [Release +Notes](../../RELEASENOTES.md). + ## Unreleased +## 1.13.0 + +Released 2025-Oct-01 + +* Added a verification to ensure that a `MetricReader` can only be registered + to a single `MeterProvider`, as required by the OpenTelemetry specification. + ([#6458](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6458)) + +* Added `FormatMessage` configuration option to self-diagnostics feature. When + set to `true` (default is false), log messages will be formatted by replacing + placeholders with actual parameter values for improved readability. + + Example `OTEL_DIAGNOSTICS.json`: + + ```json + { + "LogDirectory": ".", + "FileSize": 32768, + "LogLevel": "Warning", + "FormatMessage": true + } + ``` + +* Fixed parsing of `OTEL_TRACES_SAMPLER_ARG` decimal values to always use `.` + as the delimiter when using the `traceidratio` sampler, preventing + locale-specific parsing issues. + ([#6444](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6444)) + +## 1.12.0 + +Released 2025-Apr-29 + +## 1.11.2 + +Released 2025-Mar-04 + +## 1.11.1 + +Released 2025-Jan-22 + +## 1.11.0 + +Released 2025-Jan-15 + +* [Meter.Tags](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.metrics.meter.tags?view=net-9.0) + will now be considered when resolving the SDK metric to update when + measurements are recorded. Meters with the same name and different tags will + now lead to unique metrics. + ([#5982](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5982)) + +* Fixed a bug in tracing where `TraceState` set by a custom `Sampler` is not + applied when creating propagation-only spans. + ([#6058](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6058)) + +## 1.11.0-rc.1 + +Released 2024-Dec-11 + +## 1.10.0 + +Released 2024-Nov-12 + +* Promoted the MetricPoint reclaim feature for Delta aggregation temporality + from experimental to stable. + ([#5956](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5956)) + + **Previous Behavior:** + The SDK maintained a fixed set of MetricPoints which were assigned on a + first-come basis based on the tags. MetricPoint reclaim was an experimental + feature users could opt-into setting the environment variable + `OTEL_DOTNET_EXPERIMENTAL_METRICS_RECLAIM_UNUSED_METRIC_POINTS=true`. + + **New Behavior:** + MetricPoint reclaim is now enabled by default when Delta aggregation + temporality is used without the need to set an environment variable. Unused + MetricPoints will automatically be reclaimed and reused for future + measurements. There is NO ability to revert to the old behavior. + +* Updated the `Microsoft.Extensions.Logging.Configuration` and + `Microsoft.Extensions.Diagnostics.Abstractions` package versions to + `9.0.0`. + ([#5967](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5967)) + +## 1.10.0-rc.1 + +Released 2024-Nov-01 + +* The experimental APIs previously covered by `OTEL1003` + (`MetricStreamConfiguration.CardinalityLimit`) are now part of the public API + and supported in stable builds. + ([#5926](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5926)) + +* Promoted overflow attribute from experimental to stable and removed the + `OTEL_DOTNET_EXPERIMENTAL_METRICS_EMIT_OVERFLOW_ATTRIBUTE` environment + variable. + ([#5909](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5909)) + + **Previous Behavior:** + By default, when the cardinality limit was reached, measurements were dropped, + and an internal log was emitted the first time this occurred. Users could + opt-into experimental overflow attribute feature with + `OTEL_DOTNET_EXPERIMENTAL_METRICS_EMIT_OVERFLOW_ATTRIBUTE=true`. With this + setting, the SDK would use an overflow attribute (`otel.metric.overflow = + true`) to aggregate measurements instead of dropping measurements. No internal + log was emitted in this case. + + **New Behavior:** + The SDK now always uses the overflow attribute (`otel.metric.overflow = true`) + to aggregate measurements when the cardinality limit is reached. The previous + approach of dropping measurements has been removed. No internal logs are + emitted when the limit is hit. + + The default cardinality limit remains 2000 per metric. To set the cardinality + limit for an individual metric, use the [changing cardinality limit for a + Metric](../../docs/metrics/customizing-the-sdk/README.md#changing-the-cardinality-limit-for-a-metric). + + There is NO ability to revert to old behavior. + +* Exposed a `public` constructor on `Batch` which accepts a single instance + of `T` to be contained in the batch. + ([#5642](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5642)) + +## 1.10.0-beta.1 + +Released 2024-Sep-30 + +* Added `OpenTelemetrySdk.Create` API for configuring OpenTelemetry .NET signals + (logging, tracing, and metrics) via a single builder. This new API simplifies + bootstrap and teardown, and supports cross-cutting extensions targeting + `IOpenTelemetryBuilder`. + ([#5325](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5325)) + +* Updated the `Microsoft.Extensions.Logging.Configuration` and + `Microsoft.Extensions.Diagnostics.Abstractions` packages version to + `9.0.0-rc.1.24431.7`. + ([#5853](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5853)) + +* Added support in metrics for histogram bucket boundaries set via the .NET 9 + [InstrumentAdvice<T>](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.instrumentadvice-1) + API. + + Note: With this change explicit bucket histogram boundary resolution will + apply in the following order: + + 1. View API + 2. Advice API + 3. SDK defaults + + See [#5854](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5854) + for details. + +* Added support for collecting metrics emitted via the .NET 9 + [Gauge<T>](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.gauge-1) + API. + ([#5867](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5867)) + ## 1.9.0 Released 2024-Jun-14 @@ -12,7 +172,7 @@ Released 2024-Jun-07 * The experimental APIs previously covered by `OTEL1000` (`LoggerProviderBuilder` `AddProcessor` & `ConfigureResource` extensions, and - `LoggerProvider` `ForceFlush` & `Shutdown` extensions) will now be part of the + `LoggerProvider` `ForceFlush` & `Shutdown` extensions) are now part of the public API and supported in stable builds. ([#5648](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5648)) @@ -39,8 +199,8 @@ Released 2024-May-20 * The experimental APIs previously covered by `OTEL1002` (`Exemplar`, `ExemplarFilterType`, `MeterProviderBuilder.SetExemplarFilter`, `ReadOnlyExemplarCollection`, `ReadOnlyFilteredTagCollection`, & - `MetricPoint.TryGetExemplars`) will now be part of the public API and - supported in stable builds. + `MetricPoint.TryGetExemplars`) are now part of the public API and supported in + stable builds. ([#5607](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5607)) * Fixed the nullable annotations for the `SamplingResult` constructors diff --git a/src/OpenTelemetry/CompositeProcessor.cs b/src/OpenTelemetry/CompositeProcessor.cs index d59936ee87e..df484950e55 100644 --- a/src/OpenTelemetry/CompositeProcessor.cs +++ b/src/OpenTelemetry/CompositeProcessor.cs @@ -24,10 +24,12 @@ public CompositeProcessor(IEnumerable> processors) { Guard.ThrowIfNull(processors); +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 using var iter = processors.GetEnumerator(); +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 if (!iter.MoveNext()) { - throw new ArgumentException($"'{iter}' is null or empty", nameof(iter)); + throw new ArgumentException($"'{iter}' is null or empty", nameof(processors)); } this.Head = new DoublyLinkedListNode(iter.Current); diff --git a/src/OpenTelemetry/Internal/CircularBuffer.cs b/src/OpenTelemetry/Internal/CircularBuffer.cs index 74c3c8a6c5d..7e6a6a822d6 100644 --- a/src/OpenTelemetry/Internal/CircularBuffer.cs +++ b/src/OpenTelemetry/Internal/CircularBuffer.cs @@ -1,6 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics; using System.Runtime.CompilerServices; namespace OpenTelemetry.Internal; @@ -65,7 +66,7 @@ public int Count /// public bool Add(T value) { - Guard.ThrowIfNull(value); + Debug.Assert(value != null, "value was null"); while (true) { @@ -104,7 +105,7 @@ public bool TryAdd(T value, int maxSpinCount) return this.Add(value); } - Guard.ThrowIfNull(value); + Debug.Assert(value != null, "value was null"); var spinCountDown = maxSpinCount; diff --git a/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs b/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs index b62f4b10093..164b1450e5f 100644 --- a/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs +++ b/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -#if NET6_0_OR_GREATER +#if NET using System.Diagnostics.CodeAnalysis; #endif using System.Diagnostics.Tracing; @@ -16,7 +16,7 @@ namespace OpenTelemetry.Internal; [EventSource(Name = "OpenTelemetry-Sdk")] internal sealed class OpenTelemetrySdkEventSource : EventSource, IConfigurationExtensionsLogger { - public static OpenTelemetrySdkEventSource Log = new(); + public static readonly OpenTelemetrySdkEventSource Log = new(); #if DEBUG public static OpenTelemetryEventListener Listener = new(); #endif @@ -215,7 +215,7 @@ public void NoDroppedExportProcessorItems(string exportProcessorName, string exp this.WriteEvent(31, exportProcessorName, exporterName); } -#if NET6_0_OR_GREATER +#if NET [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)] @@ -224,7 +224,7 @@ public void ExistsDroppedExportProcessorItems(string exportProcessorName, string this.WriteEvent(32, exportProcessorName, exporterName, droppedCount); } -#if NET6_0_OR_GREATER +#if NET [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)] @@ -257,7 +257,7 @@ public void ProviderDisposed(string providerName) this.WriteEvent(37, providerName); } -#if NET6_0_OR_GREATER +#if NET [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)] @@ -278,7 +278,7 @@ public void MetricReaderEvent(string message) this.WriteEvent(40, message); } -#if NET6_0_OR_GREATER +#if NET [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)] @@ -287,7 +287,7 @@ public void MetricViewIgnored(string instrumentName, string meterName, string re this.WriteEvent(41, instrumentName, meterName, reason, fix); } -#if NET6_0_OR_GREATER +#if NET [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)] @@ -416,7 +416,7 @@ protected override void OnEventWritten(EventWrittenEventArgs e) string? message; if (e.Message != null && e.Payload != null && e.Payload.Count > 0) { - message = string.Format(e.Message, e.Payload.ToArray()); + message = string.Format(System.Globalization.CultureInfo.CurrentCulture, e.Message, e.Payload.ToArray()); } else { diff --git a/src/OpenTelemetry/Internal/SelfDiagnostics.cs b/src/OpenTelemetry/Internal/SelfDiagnostics.cs index a3f946fa05e..02aa30fb608 100644 --- a/src/OpenTelemetry/Internal/SelfDiagnostics.cs +++ b/src/OpenTelemetry/Internal/SelfDiagnostics.cs @@ -39,17 +39,6 @@ public static void EnsureInitialized() } /// - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) - { - if (disposing) - { - this.configRefresher.Dispose(); - } - } + public void Dispose() => + this.configRefresher.Dispose(); } diff --git a/src/OpenTelemetry/Internal/SelfDiagnosticsConfigParser.cs b/src/OpenTelemetry/Internal/SelfDiagnosticsConfigParser.cs index 3216c050f7c..13ba5fc4df7 100644 --- a/src/OpenTelemetry/Internal/SelfDiagnosticsConfigParser.cs +++ b/src/OpenTelemetry/Internal/SelfDiagnosticsConfigParser.cs @@ -28,6 +28,9 @@ internal sealed class SelfDiagnosticsConfigParser private static readonly Regex LogLevelRegex = new( @"""LogLevel""\s*:\s*""(?.*?)""", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex FormatMessageRegex = new( + @"""FormatMessage""\s*:\s*(?:""(?.*?)""|(?true|false))", 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. @@ -36,11 +39,13 @@ internal sealed class SelfDiagnosticsConfigParser public bool TryGetConfiguration( [NotNullWhen(true)] out string? logDirectory, out int fileSizeInKB, - out EventLevel logLevel) + out EventLevel logLevel, + out bool formatMessage) { logDirectory = null; fileSizeInKB = 0; logLevel = EventLevel.LogAlways; + formatMessage = false; try { var configFilePath = ConfigFileName; @@ -66,8 +71,21 @@ public bool TryGetConfiguration( this.configBuffer = buffer; } - file.Read(buffer, 0, buffer.Length); - string configJson = Encoding.UTF8.GetString(buffer); + int bytesRead = 0; + int totalBytesRead = 0; + + while (totalBytesRead < buffer.Length) + { + bytesRead = file.Read(buffer, totalBytesRead, buffer.Length - totalBytesRead); + if (bytesRead == 0) + { + break; + } + + totalBytesRead += bytesRead; + } + + string configJson = Encoding.UTF8.GetString(buffer, 0, totalBytesRead); if (!TryParseLogDirectory(configJson, out logDirectory)) { @@ -94,6 +112,9 @@ public bool TryGetConfiguration( return false; } + // FormatMessage is optional, defaults to false + _ = TryParseFormatMessage(configJson, out formatMessage); + return Enum.TryParse(logLevelString, out logLevel); } catch (Exception) @@ -105,7 +126,6 @@ public bool TryGetConfiguration( internal static bool TryParseLogDirectory( string configJson, - [NotNullWhen(true)] out string logDirectory) { var logDirectoryResult = LogDirectoryRegex.Match(configJson); @@ -129,4 +149,17 @@ internal static bool TryParseLogLevel( logLevel = logLevelResult.Groups["LogLevel"].Value; return logLevelResult.Success && !string.IsNullOrWhiteSpace(logLevel); } + + internal static bool TryParseFormatMessage(string configJson, out bool formatMessage) + { + formatMessage = false; + var formatMessageResult = FormatMessageRegex.Match(configJson); + if (formatMessageResult.Success) + { + var formatMessageValue = formatMessageResult.Groups["FormatMessage"].Value; + return bool.TryParse(formatMessageValue, out formatMessage); + } + + return true; + } } diff --git a/src/OpenTelemetry/Internal/SelfDiagnosticsConfigRefresher.cs b/src/OpenTelemetry/Internal/SelfDiagnosticsConfigRefresher.cs index 3d14dc16e0b..5f515e70ca1 100644 --- a/src/OpenTelemetry/Internal/SelfDiagnosticsConfigRefresher.cs +++ b/src/OpenTelemetry/Internal/SelfDiagnosticsConfigRefresher.cs @@ -35,12 +35,15 @@ internal class SelfDiagnosticsConfigRefresher : IDisposable // Once the configuration file is valid, an eventListener object will be created. private SelfDiagnosticsEventListener? eventListener; +#pragma warning disable CA2213 // Disposable fields should be disposed private volatile FileStream? underlyingFileStreamForMemoryMappedFile; private volatile MemoryMappedFile? memoryMappedFile; +#pragma warning restore CA2213 // Disposable fields should be disposed 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 bool formatMessage; public SelfDiagnosticsConfigRefresher() { @@ -136,25 +139,27 @@ private async Task Worker(CancellationToken cancellationToken) 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, out bool formatMessage)) { int newFileSize = fileSizeInKB * 1024; - if (!newLogDirectory.Equals(this.logDirectory) || this.logFileSize != newFileSize) + if (!newLogDirectory.Equals(this.logDirectory, StringComparison.Ordinal) || this.logFileSize != newFileSize) { this.CloseLogFile(); this.OpenLogFile(newLogDirectory, newFileSize); } - if (!newEventLevel.Equals(this.logEventLevel)) + if (!newEventLevel.Equals(this.logEventLevel) || this.formatMessage != formatMessage) { if (this.eventListener != null) { this.eventListener.Dispose(); } - this.eventListener = new SelfDiagnosticsEventListener(newEventLevel, this); + this.eventListener = new SelfDiagnosticsEventListener(newEventLevel, this, formatMessage); this.logEventLevel = newEventLevel; } + + this.formatMessage = formatMessage; } else { @@ -194,7 +199,11 @@ private void OpenLogFile(string newLogDirectory, int newFileSize) { Directory.CreateDirectory(newLogDirectory); var fileName = Path.GetFileName(Process.GetCurrentProcess().MainModule?.FileName ?? "OpenTelemetrySdk") + "." +#if NET + + Environment.ProcessId + ".log"; +#else + Process.GetCurrentProcess().Id + ".log"; +#endif var filePath = Path.Combine(newLogDirectory, fileName); // Because the API [MemoryMappedFile.CreateFromFile][1](the string version) behaves differently on @@ -255,10 +264,8 @@ private void Dispose(bool disposing) } // Dispose EventListener before files, because EventListener writes to files. - if (this.eventListener != null) - { - this.eventListener.Dispose(); - } + this.eventListener?.Dispose(); + this.eventListener = null; // Ensure worker thread properly finishes. // Or it might have created another MemoryMappedFile in that thread diff --git a/src/OpenTelemetry/Internal/SelfDiagnosticsEventListener.cs b/src/OpenTelemetry/Internal/SelfDiagnosticsEventListener.cs index cdfe233fd66..cd5bed688bf 100644 --- a/src/OpenTelemetry/Internal/SelfDiagnosticsEventListener.cs +++ b/src/OpenTelemetry/Internal/SelfDiagnosticsEventListener.cs @@ -16,20 +16,22 @@ 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 Lock lockObj = new(); private readonly EventLevel logLevel; private readonly SelfDiagnosticsConfigRefresher configRefresher; + private readonly bool formatMessage; private readonly ThreadLocal writeBuffer = new(() => null); - private readonly List? eventSourcesBeforeConstructor = new(); + private readonly List? eventSourcesBeforeConstructor = []; - private bool disposedValue = false; + private bool disposedValue; - public SelfDiagnosticsEventListener(EventLevel logLevel, SelfDiagnosticsConfigRefresher configRefresher) + public SelfDiagnosticsEventListener(EventLevel logLevel, SelfDiagnosticsConfigRefresher configRefresher, bool formatMessage = false) { Guard.ThrowIfNull(configRefresher); this.logLevel = logLevel; this.configRefresher = configRefresher; + this.formatMessage = formatMessage; List eventSources; lock (this.lockObj) @@ -111,61 +113,6 @@ internal static int EncodeInBuffer(string? str, bool isParameter, byte[] buffer, return position; } - internal void WriteEvent(string? eventMessage, ReadOnlyCollection? payload) - { - try - { - 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() ?? "null", true, buffer, pos); - } - else - { - pos = EncodeInBuffer("null", true, buffer, pos); - } - } - } - - buffer[pos++] = (byte)'\n'; - int byteCount = pos - 0; - if (this.configRefresher.TryGetLogStream(byteCount, out Stream? stream, out int availableByteCount)) - { - 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); - } - } - } - catch (Exception) - { - // 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. - } - } - /// /// Write the datetime formatted string into bytes byte-array starting at byteIndex position. /// @@ -188,7 +135,7 @@ internal void WriteEvent(string? eventMessage, ReadOnlyCollection? payl /// Array of bytes to write. /// Starting index into bytes array. /// The number of bytes written. - internal int DateTimeGetBytes(DateTime datetime, byte[] bytes, int byteIndex) + internal static int DateTimeGetBytes(DateTime datetime, byte[] bytes, int byteIndex) { int num; int pos = byteIndex; @@ -271,6 +218,73 @@ internal int DateTimeGetBytes(DateTime datetime, byte[] bytes, int byteIndex) return pos - byteIndex; } + internal void WriteEvent(string? eventMessage, ReadOnlyCollection? payload) + { + try + { + var buffer = this.writeBuffer.Value; + if (buffer == null) + { + buffer = new byte[BUFFERSIZE]; + this.writeBuffer.Value = buffer; + } + + var pos = DateTimeGetBytes(DateTime.UtcNow, buffer, 0); + buffer[pos++] = (byte)':'; + + if (this.formatMessage && eventMessage != null && payload != null && payload.Count > 0) + { + // Use string.Format to format the message with parameters + string messageToWrite = string.Format(System.Globalization.CultureInfo.InvariantCulture, eventMessage, payload.ToArray()); + pos = EncodeInBuffer(messageToWrite, false, buffer, pos); + } + else + { + 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() ?? "null", true, buffer, pos); + } + else + { + pos = EncodeInBuffer("null", true, buffer, pos); + } + } + } + } + + buffer[pos++] = (byte)'\n'; + int byteCount = pos - 0; +#pragma warning disable CA2000 // Dispose objects before losing scope + if (this.configRefresher.TryGetLogStream(byteCount, out Stream? stream, out int availableByteCount)) +#pragma warning restore CA2000 // Dispose objects before losing scope + { + 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); + } + } + } + catch (Exception) + { + // 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. + } + } + protected override void OnEventSourceCreated(EventSource eventSource) { if (eventSource.Name.StartsWith(EventSourceNamePrefix, StringComparison.Ordinal)) @@ -283,7 +297,9 @@ protected override void OnEventSourceCreated(EventSource eventSource) { lock (this.lockObj) { +#pragma warning disable CA1508 // Avoid dead conditional code - see previous comment if (this.eventSourcesBeforeConstructor != null) +#pragma warning restore CA1508 // Avoid dead conditional code - see previous comment { this.eventSourcesBeforeConstructor.Add(eventSource); return; diff --git a/src/OpenTelemetry/Internal/WildcardHelper.cs b/src/OpenTelemetry/Internal/WildcardHelper.cs index 98ee62934ae..e792ade75ab 100644 --- a/src/OpenTelemetry/Internal/WildcardHelper.cs +++ b/src/OpenTelemetry/Internal/WildcardHelper.cs @@ -18,7 +18,11 @@ public static bool ContainsWildcard( return false; } +#if NET || NETSTANDARD2_1_OR_GREATER + return value.Contains('*', StringComparison.Ordinal) || value.Contains('?', StringComparison.Ordinal); +#else return value.Contains('*') || value.Contains('?'); +#endif } public static Regex GetWildcardRegex(IEnumerable patterns) @@ -27,7 +31,11 @@ public static Regex GetWildcardRegex(IEnumerable patterns) var convertedPattern = string.Join( "|", +#if NET || NETSTANDARD2_1_OR_GREATER + from p in patterns select "(?:" + Regex.Escape(p).Replace("\\*", ".*", StringComparison.Ordinal).Replace("\\?", ".", StringComparison.Ordinal) + ')'); +#else from p in patterns select "(?:" + Regex.Escape(p).Replace("\\*", ".*").Replace("\\?", ".") + ')'); +#endif return new Regex("^(?:" + convertedPattern + ")$", RegexOptions.Compiled | RegexOptions.IgnoreCase); } diff --git a/src/OpenTelemetry/Logs/Builder/LoggerProviderBuilderExtensions.cs b/src/OpenTelemetry/Logs/Builder/LoggerProviderBuilderExtensions.cs index 36ac8ac8e6e..775e7ecb5a1 100644 --- a/src/OpenTelemetry/Logs/Builder/LoggerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry/Logs/Builder/LoggerProviderBuilderExtensions.cs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if NET6_0_OR_GREATER +#if NET using System.Diagnostics.CodeAnalysis; #endif using Microsoft.Extensions.DependencyInjection; @@ -96,7 +96,7 @@ public static LoggerProviderBuilder AddProcessor(this LoggerProviderBuilder logg /// . /// The supplied for chaining. public static LoggerProviderBuilder AddProcessor< -#if NET6_0_OR_GREATER +#if NET [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] #endif T>(this LoggerProviderBuilder loggerProviderBuilder) diff --git a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggerProvider.cs b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggerProvider.cs index 0dc251da0b6..c0888f1db77 100644 --- a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggerProvider.cs +++ b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggerProvider.cs @@ -18,7 +18,7 @@ public class OpenTelemetryLoggerProvider : BaseProvider, ILoggerProvider, ISuppo { internal readonly LoggerProvider Provider; private readonly bool ownsProvider; - private readonly Hashtable loggers = new(); + private readonly Hashtable loggers = []; private bool disposed; static OpenTelemetryLoggerProvider() @@ -37,7 +37,9 @@ public OpenTelemetryLoggerProvider(IOptionsMonitor o { Guard.ThrowIfNull(options); +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 var optionsInstance = options.CurrentValue; +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 this.Provider = Sdk .CreateLoggerProviderBuilder() @@ -77,7 +79,9 @@ internal OpenTelemetryLoggerProvider( internal IExternalScopeProvider? ScopeProvider { get; private set; } /// +#pragma warning disable CA1033 // Interface methods should be callable by child types void ISupportExternalScope.SetScopeProvider(IExternalScopeProvider scopeProvider) +#pragma warning restore CA1033 // Interface methods should be callable by child types { this.ScopeProvider = scopeProvider; diff --git a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggingExtensions.cs b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggingExtensions.cs index ba488b00df3..e61434ea7ec 100644 --- a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggingExtensions.cs +++ b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggingExtensions.cs @@ -2,12 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -#if NET6_0_OR_GREATER +#if NET || EXPOSE_EXPERIMENTAL_FEATURES using System.Diagnostics.CodeAnalysis; #endif using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Configuration; using Microsoft.Extensions.Options; using OpenTelemetry.Internal; @@ -42,7 +41,9 @@ public static class OpenTelemetryLoggingExtensions [Obsolete("Call UseOpenTelemetry instead this method will be removed in a future version.")] */ public static ILoggingBuilder AddOpenTelemetry( this ILoggingBuilder builder) +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 => AddOpenTelemetryInternal(builder, configureBuilder: null, configureOptions: null); +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 /// /// Adds an OpenTelemetry logger named 'OpenTelemetry' to the . @@ -58,7 +59,9 @@ public static ILoggingBuilder AddOpenTelemetry( public static ILoggingBuilder AddOpenTelemetry( this ILoggingBuilder builder, Action? configure) +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 => AddOpenTelemetryInternal(builder, configureBuilder: null, configureOptions: configure); +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 #if EXPOSE_EXPERIMENTAL_FEATURES /// @@ -71,16 +74,16 @@ public static ILoggingBuilder AddOpenTelemetry( /// /// The to use. /// The supplied for call chaining. -#if NET8_0_OR_GREATER [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] -#endif public #else internal #endif static ILoggingBuilder UseOpenTelemetry( this ILoggingBuilder builder) +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 => AddOpenTelemetryInternal(builder, configureBuilder: null, configureOptions: null); +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 #if EXPOSE_EXPERIMENTAL_FEATURES /// @@ -90,9 +93,7 @@ static ILoggingBuilder UseOpenTelemetry( /// The to use. /// configuration action. /// The supplied for call chaining. -#if NET8_0_OR_GREATER [Experimental(DiagnosticDefinitions.LoggerProviderExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] -#endif public #else internal @@ -103,7 +104,9 @@ static ILoggingBuilder UseOpenTelemetry( { Guard.ThrowIfNull(configure); +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 return AddOpenTelemetryInternal(builder, configureBuilder: configure, configureOptions: null); +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 } #if EXPOSE_EXPERIMENTAL_FEATURES @@ -115,9 +118,7 @@ static ILoggingBuilder UseOpenTelemetry( /// 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 internal @@ -126,7 +127,9 @@ static ILoggingBuilder UseOpenTelemetry( this ILoggingBuilder builder, Action? configureBuilder, Action? configureOptions) +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 => AddOpenTelemetryInternal(builder, configureBuilder, configureOptions); +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 private static ILoggingBuilder AddOpenTelemetryInternal( ILoggingBuilder builder, @@ -242,7 +245,7 @@ private static ILoggingBuilder AddOpenTelemetryInternal( // 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 +#if NET [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "OpenTelemetryLoggerOptions contains only primitive properties.")] [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "OpenTelemetryLoggerOptions contains only primitive properties.")] #endif diff --git a/src/OpenTelemetry/Logs/LogRecord.cs b/src/OpenTelemetry/Logs/LogRecord.cs index 58d97c38edf..097cdb2d3be 100644 --- a/src/OpenTelemetry/Logs/LogRecord.cs +++ b/src/OpenTelemetry/Logs/LogRecord.cs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +#if EXPOSE_EXPERIMENTAL_FEATURES using System.Diagnostics.CodeAnalysis; #endif using System.Runtime.CompilerServices; @@ -355,9 +355,7 @@ public Exception? Exception /// known at the source. /// /// -#if NET8_0_OR_GREATER [Experimental(DiagnosticDefinitions.LogsBridgeExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] -#endif public #else /// @@ -377,9 +375,7 @@ public Exception? Exception /// Gets or sets the log . /// /// -#if NET8_0_OR_GREATER [Experimental(DiagnosticDefinitions.LogsBridgeExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] -#endif public #else /// @@ -405,9 +401,7 @@ public Exception? Exception /// 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 /// @@ -429,19 +423,19 @@ public void ForEachScope(Action callback, TState { Guard.ThrowIfNull(callback); - var forEachScopeState = new ScopeForEachState(callback, state); - var bufferedScopes = this.ILoggerData.BufferedScopes; if (bufferedScopes != null) { foreach (object? scope in bufferedScopes) { - ScopeForEachState.ForEachScope(scope, forEachScopeState); +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 + callback(new(scope), state); +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 } } else { - this.ILoggerData.ScopeProvider?.ForEachScope(ScopeForEachState.ForEachScope, forEachScopeState); + this.ILoggerData.ScopeProvider?.ForEachScope(ScopeForEachState.ForEachScope, new(callback, state)); } } diff --git a/src/OpenTelemetry/Logs/LoggerProviderExtensions.cs b/src/OpenTelemetry/Logs/LoggerProviderExtensions.cs index f5d795fba2b..617d98e6dcd 100644 --- a/src/OpenTelemetry/Logs/LoggerProviderExtensions.cs +++ b/src/OpenTelemetry/Logs/LoggerProviderExtensions.cs @@ -28,7 +28,9 @@ public static LoggerProvider AddProcessor(this LoggerProvider provider, BaseProc if (provider is LoggerProviderSdk loggerProviderSdk) { +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 loggerProviderSdk.AddProcessor(processor); +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 } return provider; diff --git a/src/OpenTelemetry/Logs/LoggerProviderSdk.cs b/src/OpenTelemetry/Logs/LoggerProviderSdk.cs index 9dd7297dfa8..151f48685cd 100644 --- a/src/OpenTelemetry/Logs/LoggerProviderSdk.cs +++ b/src/OpenTelemetry/Logs/LoggerProviderSdk.cs @@ -2,9 +2,7 @@ // 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; @@ -18,11 +16,11 @@ namespace OpenTelemetry.Logs; internal sealed class LoggerProviderSdk : LoggerProvider { internal readonly IServiceProvider ServiceProvider; - internal readonly IDisposable? OwnedServiceProvider; + internal IDisposable? OwnedServiceProvider; internal bool Disposed; internal int ShutdownCount; - private readonly List instrumentations = new(); + private readonly List instrumentations = []; private ILogRecordPool? threadStaticPool = LogRecordThreadStaticPool.Instance; public LoggerProviderSdk( @@ -90,13 +88,36 @@ public LoggerProviderSdk( public ILogRecordPool LogRecordPool => this.threadStaticPool ?? LogRecordSharedPool.Current; + public static bool ContainsBatchProcessor(BaseProcessor processor) + { + if (processor is BatchExportProcessor) + { + return true; + } + else if (processor is CompositeProcessor compositeProcessor) + { + var current = compositeProcessor.Head; + while (current != null) + { + if (ContainsBatchProcessor(current.Value)) + { + return true; + } + + current = current.Next; + } + } + + return false; + } + public void AddProcessor(BaseProcessor processor) { Guard.ThrowIfNull(processor); processor.SetParentProvider(this); - if (this.threadStaticPool != null && this.ContainsBatchProcessor(processor)) + if (this.threadStaticPool != null && ContainsBatchProcessor(processor)) { OpenTelemetrySdkEventSource.Log.LoggerProviderSdkEvent("Using shared thread pool."); @@ -127,10 +148,10 @@ public void AddProcessor(BaseProcessor processor) processorAdded.Append(processor); processorAdded.Append('\''); - var newCompositeProcessor = new CompositeProcessor(new[] - { + var newCompositeProcessor = new CompositeProcessor( + [ this.Processor, - }); + ]); newCompositeProcessor.SetParentProvider(this); newCompositeProcessor.AddProcessor(processor); this.Processor = newCompositeProcessor; @@ -170,29 +191,6 @@ public bool Shutdown(int timeoutMilliseconds) } } - public bool ContainsBatchProcessor(BaseProcessor processor) - { - if (processor is BatchExportProcessor) - { - return true; - } - else if (processor is CompositeProcessor compositeProcessor) - { - var current = compositeProcessor.Head; - while (current != null) - { - if (this.ContainsBatchProcessor(current.Value)) - { - return true; - } - - current = current.Next; - } - } - - return false; - } - /// #if EXPOSE_EXPERIMENTAL_FEATURES protected @@ -201,9 +199,7 @@ public bool ContainsBatchProcessor(BaseProcessor processor) #endif 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); @@ -217,21 +213,20 @@ protected override void Dispose(bool disposing) { if (disposing) { - 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.instrumentations.Clear(); + // Wait for up to 5 seconds grace period this.Processor?.Shutdown(5000); this.Processor?.Dispose(); + this.Processor = null; this.OwnedServiceProvider?.Dispose(); + this.OwnedServiceProvider = null; } this.Disposed = true; diff --git a/src/OpenTelemetry/Logs/LoggerSdk.cs b/src/OpenTelemetry/Logs/LoggerSdk.cs index a0bc47300d0..19a38cf72bb 100644 --- a/src/OpenTelemetry/Logs/LoggerSdk.cs +++ b/src/OpenTelemetry/Logs/LoggerSdk.cs @@ -1,6 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using Microsoft.Extensions.Logging; using OpenTelemetry.Internal; namespace OpenTelemetry.Logs; @@ -35,6 +36,7 @@ public override void EmitLog(in LogRecordData data, in LogRecordAttributeList at logRecord.Data = data; logRecord.ILoggerData = default; + logRecord.ILoggerData.EventId = new EventId(default, data.EventName); logRecord.Logger = this; diff --git a/src/OpenTelemetry/Logs/BatchExportLogRecordProcessorOptions.cs b/src/OpenTelemetry/Logs/Processor/BatchExportLogRecordProcessorOptions.cs similarity index 100% rename from src/OpenTelemetry/Logs/BatchExportLogRecordProcessorOptions.cs rename to src/OpenTelemetry/Logs/Processor/BatchExportLogRecordProcessorOptions.cs diff --git a/src/OpenTelemetry/Logs/BatchLogRecordExportProcessor.cs b/src/OpenTelemetry/Logs/Processor/BatchLogRecordExportProcessor.cs similarity index 100% rename from src/OpenTelemetry/Logs/BatchLogRecordExportProcessor.cs rename to src/OpenTelemetry/Logs/Processor/BatchLogRecordExportProcessor.cs diff --git a/src/OpenTelemetry/Logs/LogRecordExportProcessorOptions.cs b/src/OpenTelemetry/Logs/Processor/LogRecordExportProcessorOptions.cs similarity index 88% rename from src/OpenTelemetry/Logs/LogRecordExportProcessorOptions.cs rename to src/OpenTelemetry/Logs/Processor/LogRecordExportProcessorOptions.cs index 95cdc830e35..b20accfdb47 100644 --- a/src/OpenTelemetry/Logs/LogRecordExportProcessorOptions.cs +++ b/src/OpenTelemetry/Logs/Processor/LogRecordExportProcessorOptions.cs @@ -1,7 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using System.Diagnostics; using OpenTelemetry.Internal; namespace OpenTelemetry.Logs; @@ -24,9 +23,7 @@ public LogRecordExportProcessorOptions() internal LogRecordExportProcessorOptions( BatchExportLogRecordProcessorOptions defaultBatchExportLogRecordProcessorOptions) { - Debug.Assert(defaultBatchExportLogRecordProcessorOptions != null, "defaultBatchExportLogRecordProcessorOptions was null"); - - this.batchExportProcessorOptions = defaultBatchExportLogRecordProcessorOptions ?? new(); + this.batchExportProcessorOptions = defaultBatchExportLogRecordProcessorOptions; } /// diff --git a/src/OpenTelemetry/Logs/SimpleLogRecordExportProcessor.cs b/src/OpenTelemetry/Logs/Processor/SimpleLogRecordExportProcessor.cs similarity index 100% rename from src/OpenTelemetry/Logs/SimpleLogRecordExportProcessor.cs rename to src/OpenTelemetry/Logs/Processor/SimpleLogRecordExportProcessor.cs diff --git a/src/OpenTelemetry/Metrics/AggregationTemporality.cs b/src/OpenTelemetry/Metrics/AggregationTemporality.cs index 2a9c036d3dc..e2f47d29535 100644 --- a/src/OpenTelemetry/Metrics/AggregationTemporality.cs +++ b/src/OpenTelemetry/Metrics/AggregationTemporality.cs @@ -7,7 +7,11 @@ namespace OpenTelemetry.Metrics; /// Enumeration used to define the aggregation temporality for a . /// +#pragma warning disable CA1008 // Enums should have zero value +#pragma warning disable CA1028 // Enum storage should be Int32 public enum AggregationTemporality : byte +#pragma warning restore CA1028 // Enum storage should be Int32 +#pragma warning restore CA1008 // Enums should have zero value { /// /// Cumulative. diff --git a/src/OpenTelemetry/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs index 34a4bb5f2ce..cd6d33190d3 100644 --- a/src/OpenTelemetry/Metrics/AggregatorStore.cs +++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System.Collections.Concurrent; -#if NET8_0_OR_GREATER +#if NET using System.Collections.Frozen; #endif using System.Diagnostics; @@ -14,25 +14,22 @@ namespace OpenTelemetry.Metrics; internal sealed class AggregatorStore { -#if NET8_0_OR_GREATER +#if NET internal readonly FrozenSet? TagKeysInteresting; #else internal readonly HashSet? TagKeysInteresting; #endif internal readonly bool OutputDelta; - internal readonly bool OutputDeltaWithUnusedMetricPointReclaimEnabled; internal readonly int NumberOfMetricPoints; - internal readonly bool EmitOverflowAttribute; internal readonly ConcurrentDictionary? TagsToMetricPointIndexDictionaryDelta; internal readonly Func? ExemplarReservoirFactory; - internal long DroppedMeasurements = 0; + internal long DroppedMeasurements; 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 static readonly Comparison> DimensionComparisonDelegate = (x, y) => string.Compare(x.Key, y.Key, StringComparison.Ordinal); - private readonly object lockZeroTags = new(); - private readonly object lockOverflowTag = new(); + private readonly Lock lockZeroTags = new(); + private readonly Lock lockOverflowTag = new(); private readonly int tagsKeysInterestingCount; // This holds the reclaimed MetricPoints that are available for reuse. @@ -42,7 +39,6 @@ internal sealed class AggregatorStore new(); private readonly string name; - private readonly string metricPointCapHitMessage; private readonly MetricPoint[] metricPoints; private readonly int[] currentMetricPointBatch; private readonly AggregationType aggType; @@ -54,9 +50,8 @@ internal sealed class AggregatorStore private readonly ExemplarFilterType exemplarFilter; private readonly Func[], int, int> lookupAggregatorStore; - private int metricPointIndex = 0; - private int batchSize = 0; - private int metricCapHitMessageLogged; + private int metricPointIndex; + private int batchSize; private bool zeroTagMetricPointInitialized; private bool overflowTagMetricPointInitialized; @@ -65,8 +60,6 @@ internal AggregatorStore( AggregationType aggType, AggregationTemporality temporality, int cardinalityLimit, - bool emitOverflowAttribute, - bool shouldReclaimUnusedMetricPoints, ExemplarFilterType? exemplarFilter = null, Func? exemplarReservoirFactory = null) { @@ -77,7 +70,6 @@ internal AggregatorStore( // Previously, these were included within the original cardinalityLimit, but now they are explicitly added to enhance clarity. this.NumberOfMetricPoints = cardinalityLimit + 2; - this.metricPointCapHitMessage = $"Maximum MetricPoints limit reached for this Metric stream. Configured limit: {cardinalityLimit}"; this.metricPoints = new MetricPoint[this.NumberOfMetricPoints]; this.currentMetricPointBatch = new int[this.NumberOfMetricPoints]; this.aggType = aggType; @@ -96,7 +88,7 @@ internal AggregatorStore( { this.updateLongCallback = this.UpdateLongCustomTags; this.updateDoubleCallback = this.UpdateDoubleCustomTags; -#if NET8_0_OR_GREATER +#if NET var hs = FrozenSet.ToFrozenSet(metricStreamIdentity.TagKeys, StringComparer.Ordinal); #else var hs = new HashSet(metricStreamIdentity.TagKeys, StringComparer.Ordinal); @@ -105,8 +97,6 @@ internal AggregatorStore( this.tagsKeysInterestingCount = hs.Count; } - this.EmitOverflowAttribute = emitOverflowAttribute; - this.exemplarFilter = exemplarFilter ?? DefaultExemplarFilter; Debug.Assert( this.exemplarFilter == ExemplarFilterType.AlwaysOff @@ -118,9 +108,8 @@ internal AggregatorStore( // Newer attributes should be added starting at the index: 2 this.metricPointIndex = 1; - this.OutputDeltaWithUnusedMetricPointReclaimEnabled = shouldReclaimUnusedMetricPoints && this.OutputDelta; - - if (this.OutputDeltaWithUnusedMetricPointReclaimEnabled) + // Always reclaim unused MetricPoints for Delta aggregation temporality + if (this.OutputDelta) { this.availableMetricPoints = new Queue(cardinalityLimit); @@ -192,15 +181,10 @@ internal void Update(double value, ReadOnlySpan> t internal int Snapshot() { this.batchSize = 0; - if (this.OutputDeltaWithUnusedMetricPointReclaimEnabled) + if (this.OutputDelta) { this.SnapshotDeltaWithMetricPointReclaim(); } - else if (this.OutputDelta) - { - var indexSnapshot = Math.Min(this.metricPointIndex, this.NumberOfMetricPoints - 1); - this.SnapshotDelta(indexSnapshot); - } else { var indexSnapshot = Math.Min(this.metricPointIndex, this.NumberOfMetricPoints - 1); @@ -211,28 +195,6 @@ internal int Snapshot() return this.batchSize; } - internal void SnapshotDelta(int indexSnapshot) - { - for (int i = 0; i <= indexSnapshot; i++) - { - ref var metricPoint = ref this.metricPoints[i]; - if (metricPoint.MetricPointStatus == MetricPointStatus.NoCollectPending) - { - continue; - } - - this.TakeMetricPointSnapshot(ref metricPoint, outputDelta: true); - - this.currentMetricPointBatch[this.batchSize] = i; - this.batchSize++; - } - - if (this.EndTimeInclusive != default) - { - this.StartTimeExclusive = this.EndTimeInclusive; - } - } - internal void SnapshotDeltaWithMetricPointReclaim() { // Index = 0 is reserved for the case where no dimensions are provided. @@ -245,17 +207,14 @@ internal void SnapshotDeltaWithMetricPointReclaim() this.batchSize++; } - if (this.EmitOverflowAttribute) + // TakeSnapshot for the MetricPoint for overflow + ref var metricPointForOverflow = ref this.metricPoints[1]; + if (metricPointForOverflow.MetricPointStatus != MetricPointStatus.NoCollectPending) { - // TakeSnapshot for the MetricPoint for overflow - ref var metricPointForOverflow = ref this.metricPoints[1]; - if (metricPointForOverflow.MetricPointStatus != MetricPointStatus.NoCollectPending) - { - this.TakeMetricPointSnapshot(ref metricPointForOverflow, outputDelta: true); + this.TakeMetricPointSnapshot(ref metricPointForOverflow, outputDelta: true); - this.currentMetricPointBatch[this.batchSize] = 1; - this.batchSize++; - } + this.currentMetricPointBatch[this.batchSize] = 1; + this.batchSize++; } // Index 0 and 1 are reserved for no tags and overflow @@ -284,7 +243,9 @@ internal void SnapshotDeltaWithMetricPointReclaim() // If the Collect thread now wakes up, it would be able to set the ReferenceCount to `int.MinValue`, thereby, marking the MetricPoint // invalid for newer updates. In such cases, the MetricPoint, should not be reclaimed before taking its Snapshot. +#pragma warning disable CA1508 // Avoid dead conditional code - see previous comment if (metricPoint.MetricPointStatus == MetricPointStatus.NoCollectPending) +#pragma warning restore CA1508 // Avoid dead conditional code - see previous comment { this.ReclaimMetricPoint(ref metricPoint, i); } @@ -994,16 +955,8 @@ private void UpdateLongMetricPoint(int metricPointIndex, long value, ReadOnlySpa if (metricPointIndex < 0) { Interlocked.Increment(ref this.DroppedMeasurements); - - if (this.EmitOverflowAttribute) - { - 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); - } + this.InitializeOverflowTagPointIfNotInitialized(); + this.metricPoints[1].Update(value); return; } @@ -1049,16 +1002,8 @@ private void UpdateDoubleMetricPoint(int metricPointIndex, double value, ReadOnl if (metricPointIndex < 0) { Interlocked.Increment(ref this.DroppedMeasurements); - - if (this.EmitOverflowAttribute) - { - 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); - } + this.InitializeOverflowTagPointIfNotInitialized(); + this.metricPoints[1].Update(value); return; } diff --git a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderBase.cs b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderBase.cs index 49b519b66c2..097cbc372c2 100644 --- a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderBase.cs +++ b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderBase.cs @@ -74,7 +74,9 @@ MeterProviderBuilder IMeterProviderBuilder.ConfigureServices(Action +#pragma warning disable CA1033 // Interface methods should be callable by child types MeterProviderBuilder IDeferredMeterProviderBuilder.Configure(Action configure) +#pragma warning restore CA1033 // Interface methods should be callable by child types { this.innerBuilder.ConfigureBuilder(configure); diff --git a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs index 4eea35b59c5..31ad29f7c5a 100644 --- a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if NET6_0_OR_GREATER +#if NET using System.Diagnostics.CodeAnalysis; #endif using System.Diagnostics.Metrics; @@ -50,7 +50,7 @@ public static MeterProviderBuilder AddReader(this MeterProviderBuilder meterProv /// . /// The supplied for chaining. public static MeterProviderBuilder AddReader< -#if NET6_0_OR_GREATER +#if NET [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] #endif T>(this MeterProviderBuilder meterProviderBuilder) @@ -103,12 +103,20 @@ public static MeterProviderBuilder AddReader( /// The supplied for chaining. public static MeterProviderBuilder AddView(this MeterProviderBuilder meterProviderBuilder, string instrumentName, string name) { + Guard.ThrowIfNull(instrumentName); + if (!MeterProviderBuilderSdk.IsValidInstrumentName(name)) { throw new ArgumentException($"Custom view name {name} is invalid.", nameof(name)); } - if (instrumentName.IndexOf('*') != -1) +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 +#if NET || NETSTANDARD2_1_OR_GREATER + if (instrumentName.Contains('*', StringComparison.Ordinal)) +#else + if (instrumentName.Contains('*')) +#endif +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 { throw new ArgumentException( $"Instrument selection criteria is invalid. Instrument name '{instrumentName}' " + @@ -136,7 +144,13 @@ public static MeterProviderBuilder AddView(this MeterProviderBuilder meterProvid Guard.ThrowIfNullOrWhitespace(instrumentName); Guard.ThrowIfNull(metricStreamConfiguration); - if (metricStreamConfiguration.Name != null && instrumentName.IndexOf('*') != -1) +#if NET || NETSTANDARD2_1_OR_GREATER +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 + if (metricStreamConfiguration.Name != null && instrumentName.Contains('*', StringComparison.Ordinal)) +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 +#else + if (metricStreamConfiguration.Name != null && instrumentName.Contains('*')) +#endif { throw new ArgumentException( $"Instrument selection criteria is invalid. Instrument name '{instrumentName}' " + @@ -149,9 +163,17 @@ public static MeterProviderBuilder AddView(this MeterProviderBuilder meterProvid { if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk) { - if (instrumentName.IndexOf('*') != -1) +#if NET || NETSTANDARD2_1_OR_GREATER + if (instrumentName.Contains('*', StringComparison.Ordinal)) +#else + if (instrumentName.Contains('*')) +#endif { +#if NET || NETSTANDARD2_1_OR_GREATER + var pattern = '^' + Regex.Escape(instrumentName).Replace("\\*", ".*", StringComparison.Ordinal); +#else var pattern = '^' + Regex.Escape(instrumentName).Replace("\\*", ".*"); +#endif var regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase); meterProviderBuilderSdk.AddView(instrument => regex.IsMatch(instrument.Name) ? metricStreamConfiguration : null); } @@ -238,9 +260,7 @@ public static MeterProviderBuilder SetMaxMetricStreams(this MeterProviderBuilder /// . /// 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 + [Obsolete("Use MetricStreamConfiguration.CardinalityLimit via the AddView API instead. This method is marked as obsolete in version 1.10.0 and will be removed in a future version.")] public static MeterProviderBuilder SetMaxMetricPointsPerMetricStream(this MeterProviderBuilder meterProviderBuilder, int maxMetricPointsPerMetricStream) { Guard.ThrowIfOutOfRange(maxMetricPointsPerMetricStream, min: 1); diff --git a/src/OpenTelemetry/Metrics/CircularBufferBuckets.cs b/src/OpenTelemetry/Metrics/CircularBufferBuckets.cs index a7205a41eab..63196c9a0af 100644 --- a/src/OpenTelemetry/Metrics/CircularBufferBuckets.cs +++ b/src/OpenTelemetry/Metrics/CircularBufferBuckets.cs @@ -13,7 +13,7 @@ namespace OpenTelemetry.Metrics; internal sealed class CircularBufferBuckets { private long[]? trait; - private int begin = 0; + private int begin; private int end = -1; public CircularBufferBuckets(int capacity) @@ -246,9 +246,7 @@ static void Consolidate(long[] array, uint src, uint dst) [MethodImpl(MethodImplOptions.AggressiveInlining)] static void Exchange(long[] array, uint src, uint dst) { - var value = array[dst]; - array[dst] = array[src]; - array[src] = value; + (array[dst], array[src]) = (array[src], array[dst]); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -263,10 +261,11 @@ internal void Reset() { if (this.trait != null) { - for (var i = 0; i < this.trait.Length; ++i) - { - this.trait[i] = 0; - } +#if NET + Array.Clear(this.trait); +#else + Array.Clear(this.trait, 0, this.trait.Length); +#endif } } diff --git a/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs b/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs index c53a7abf8b4..3fb410d390c 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs @@ -1,10 +1,11 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if NET8_0_OR_GREATER +#if NET using System.Collections.Frozen; #endif using System.Diagnostics; +using System.Globalization; namespace OpenTelemetry.Metrics; @@ -17,13 +18,13 @@ namespace OpenTelemetry.Metrics; /// public struct Exemplar { -#if NET8_0_OR_GREATER +#if NET internal FrozenSet? ViewDefinedTagKeys; #else internal HashSet? ViewDefinedTagKeys; #endif - private static readonly ReadOnlyFilteredTagCollection Empty = new(excludedKeys: null, Array.Empty>(), count: 0); + private static readonly ReadOnlyFilteredTagCollection Empty = new(excludedKeys: null, [], count: 0); private int tagCount; private KeyValuePair[]? tagStorage; private MetricPointValueStorage valueStorage; @@ -115,7 +116,7 @@ internal void Update(in ExemplarMeasurement measurement) else { Debug.Fail("Invalid value type"); - this.DoubleValue = Convert.ToDouble(measurement.Value); + this.DoubleValue = Convert.ToDouble(measurement.Value, CultureInfo.InvariantCulture); } var currentActivity = Activity.Current; diff --git a/src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs index eb3882e6e08..d1ae9cc038b 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/ExemplarMeasurement.cs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +#if EXPOSE_EXPERIMENTAL_FEATURES using System.Diagnostics.CodeAnalysis; using OpenTelemetry.Internal; #endif @@ -14,9 +14,7 @@ namespace OpenTelemetry.Metrics; /// /// /// Measurement type. -#if NET8_0_OR_GREATER [Experimental(DiagnosticDefinitions.ExemplarReservoirExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] -#endif public #else internal diff --git a/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs index 4eb800921f1..e118036ae26 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +#if EXPOSE_EXPERIMENTAL_FEATURES using System.Diagnostics.CodeAnalysis; using OpenTelemetry.Internal; #endif @@ -17,9 +17,7 @@ namespace OpenTelemetry.Metrics; /// Specification: . /// -#if NET8_0_OR_GREATER [Experimental(DiagnosticDefinitions.ExemplarReservoirExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] -#endif public #else internal diff --git a/src/OpenTelemetry/Metrics/Exemplar/FixedSizeExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/FixedSizeExemplarReservoir.cs index dc929e725ed..68417fa09b7 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/FixedSizeExemplarReservoir.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/FixedSizeExemplarReservoir.cs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +#if EXPOSE_EXPERIMENTAL_FEATURES using System.Diagnostics.CodeAnalysis; #endif using OpenTelemetry.Internal; @@ -14,9 +14,7 @@ namespace OpenTelemetry.Metrics; /// number of s. /// /// -#if NET8_0_OR_GREATER [Experimental(DiagnosticDefinitions.ExemplarReservoirExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] -#endif public #else internal diff --git a/src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs b/src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs index 1ebf2a3e365..02c085c428b 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/ReadOnlyExemplarCollection.cs @@ -8,9 +8,11 @@ namespace OpenTelemetry.Metrics; /// /// A read-only collection of s. /// +#pragma warning disable CA1711 // Identifiers should not have incorrect suffix public readonly struct ReadOnlyExemplarCollection +#pragma warning restore CA1711 // Identifiers should not have incorrect suffix { - internal static readonly ReadOnlyExemplarCollection Empty = new(Array.Empty()); + internal static readonly ReadOnlyExemplarCollection Empty = new([]); private readonly Exemplar[] exemplars; internal ReadOnlyExemplarCollection(Exemplar[] exemplars) @@ -77,7 +79,9 @@ internal IReadOnlyList ToReadOnlyList() /// /// Enumerates the elements of a . /// +#pragma warning disable CA1034 // Nested types should not be visible - already part of public API public struct Enumerator +#pragma warning restore CA1034 // Nested types should not be visible - already part of public API { private readonly Exemplar[] exemplars; private int index; diff --git a/src/OpenTelemetry/Metrics/ExportModes.cs b/src/OpenTelemetry/Metrics/ExportModes.cs index 2e3def35a4b..1d71eb2844f 100644 --- a/src/OpenTelemetry/Metrics/ExportModes.cs +++ b/src/OpenTelemetry/Metrics/ExportModes.cs @@ -7,7 +7,9 @@ namespace OpenTelemetry.Metrics; /// Describes the mode of a metric exporter. /// [Flags] +#pragma warning disable CA1028 // Enum storage should be Int32 public enum ExportModes : byte +#pragma warning restore CA1028 // Enum storage should be Int32 { /* 0 0 0 0 0 0 0 0 diff --git a/src/OpenTelemetry/Metrics/ExportModesAttribute.cs b/src/OpenTelemetry/Metrics/ExportModesAttribute.cs index 65b88ae3f34..4235b5dac7f 100644 --- a/src/OpenTelemetry/Metrics/ExportModesAttribute.cs +++ b/src/OpenTelemetry/Metrics/ExportModesAttribute.cs @@ -9,19 +9,17 @@ namespace OpenTelemetry.Metrics; [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public sealed class ExportModesAttribute : Attribute { - private readonly ExportModes supportedExportModes; - /// /// Initializes a new instance of the class. /// /// . public ExportModesAttribute(ExportModes supported) { - this.supportedExportModes = supported; + this.Supported = supported; } /// /// Gets the supported . /// - public ExportModes Supported => this.supportedExportModes; + public ExportModes Supported { get; } } diff --git a/src/OpenTelemetry/Metrics/MeterProviderSdk.cs b/src/OpenTelemetry/Metrics/MeterProviderSdk.cs index 467da3f538b..e2b9077eb19 100644 --- a/src/OpenTelemetry/Metrics/MeterProviderSdk.cs +++ b/src/OpenTelemetry/Metrics/MeterProviderSdk.cs @@ -13,28 +13,24 @@ namespace OpenTelemetry.Metrics; 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 const string ExemplarFilterHistogramsConfigKey = "OTEL_DOTNET_EXPERIMENTAL_METRICS_EXEMPLAR_FILTER_HISTOGRAMS"; internal readonly IServiceProvider ServiceProvider; - internal readonly IDisposable? OwnedServiceProvider; + internal IDisposable? OwnedServiceProvider; internal int ShutdownCount; internal bool Disposed; - internal bool EmitOverflowAttribute; - internal bool ReclaimUnusedMetricPoints; internal ExemplarFilterType? ExemplarFilter; internal ExemplarFilterType? ExemplarFilterForHistograms; internal Action? OnCollectObservableInstruments; - private readonly List instrumentations = new(); + private readonly List instrumentations = []; private readonly List> viewConfigs; - private readonly object collectLock = new(); + private readonly Lock collectLock = new(); private readonly MeterListener listener; - private readonly MetricReader? reader; - private readonly CompositeMetricReader? compositeMetricReader; private readonly Func shouldListenTo = instrument => false; + private CompositeMetricReader? compositeMetricReader; + private MetricReader? reader; internal MeterProviderSdk( IServiceProvider serviceProvider, @@ -75,7 +71,7 @@ internal MeterProviderSdk( this.viewConfigs = state.ViewConfigs; OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent( - $"MeterProvider configuration: {{MetricLimit={state.MetricLimit}, CardinalityLimit={state.CardinalityLimit}, EmitOverflowAttribute={this.EmitOverflowAttribute}, ReclaimUnusedMetricPoints={this.ReclaimUnusedMetricPoints}, ExemplarFilter={this.ExemplarFilter}, ExemplarFilterForHistograms={this.ExemplarFilterForHistograms}}}."); + $"MeterProvider configuration: {{MetricLimit={state.MetricLimit}, CardinalityLimit={state.CardinalityLimit}, ExemplarFilter={this.ExemplarFilter}, ExemplarFilterForHistograms={this.ExemplarFilterForHistograms}}}."); foreach (var reader in state.Readers) { @@ -86,8 +82,6 @@ internal MeterProviderSdk( reader.ApplyParentProviderSettings( state.MetricLimit, state.CardinalityLimit, - this.EmitOverflowAttribute, - this.ReclaimUnusedMetricPoints, this.ExemplarFilter, this.ExemplarFilterForHistograms); @@ -101,7 +95,7 @@ internal MeterProviderSdk( } else { - this.reader = new CompositeMetricReader(new[] { this.reader, reader }); + this.reader = new CompositeMetricReader([this.reader, reader]); } if (reader is PeriodicExportingMetricReader periodicExportingMetricReader) @@ -128,7 +122,7 @@ internal MeterProviderSdk( this.compositeMetricReader = this.reader as CompositeMetricReader; - if (state.Instrumentation.Any()) + if (state.Instrumentation.Count > 0) { foreach (var instrumentation in state.Instrumentation) { @@ -149,12 +143,12 @@ internal MeterProviderSdk( } // Setup Listener - if (state.MeterSources.Any(s => WildcardHelper.ContainsWildcard(s))) + if (state.MeterSources.Exists(WildcardHelper.ContainsWildcard)) { var regex = WildcardHelper.GetWildcardRegex(state.MeterSources); this.shouldListenTo = instrument => regex.IsMatch(instrument.Meter.Name); } - else if (state.MeterSources.Any()) + else if (state.MeterSources.Count > 0) { var meterSourcesToSubscribe = new HashSet(state.MeterSources, StringComparer.OrdinalIgnoreCase); this.shouldListenTo = instrument => meterSourcesToSubscribe.Contains(instrument.Meter.Name); @@ -457,24 +451,25 @@ protected override void Dispose(bool disposing) { if (disposing) { - 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.instrumentations.Clear(); + // Wait for up to 5 seconds grace period this.reader?.Shutdown(5000); this.reader?.Dispose(); + this.reader = null; + this.compositeMetricReader?.Dispose(); + this.compositeMetricReader = null; this.listener?.Dispose(); this.OwnedServiceProvider?.Dispose(); + this.OwnedServiceProvider = null; } this.Disposed = true; @@ -486,16 +481,6 @@ protected override void Dispose(bool disposing) private void ApplySpecificationConfigurationKeys(IConfiguration configuration) { - if (configuration.TryGetBoolValue(OpenTelemetrySdkEventSource.Log, EmitOverFlowAttributeConfigKey, out this.EmitOverflowAttribute)) - { - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent("Overflow attribute feature enabled via configuration."); - } - - if (configuration.TryGetBoolValue(OpenTelemetrySdkEventSource.Log, ReclaimUnusedMetricPointsConfigKey, out this.ReclaimUnusedMetricPoints)) - { - OpenTelemetrySdkEventSource.Log.MeterProviderSdkEvent("Reclaim unused metric point feature enabled via configuration."); - } - var hasProgrammaticExemplarFilterValue = this.ExemplarFilter.HasValue; if (configuration.TryGetStringValue(ExemplarFilterConfigKey, out var configValue)) diff --git a/src/OpenTelemetry/Metrics/Metric.cs b/src/OpenTelemetry/Metrics/Metric.cs index 7801c2dd452..d7f1a3b1a43 100644 --- a/src/OpenTelemetry/Metrics/Metric.cs +++ b/src/OpenTelemetry/Metrics/Metric.cs @@ -1,6 +1,9 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +#if NET +using System.Collections.Frozen; +#endif using System.Diagnostics.Metrics; namespace OpenTelemetry.Metrics; @@ -18,7 +21,13 @@ public sealed class Metric // 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() + internal static readonly +#if NET + FrozenSet<(string, string)> +#else + HashSet<(string, string)> +#endif + DefaultHistogramBoundShortMappings = new HashSet<(string, string)> { ("Microsoft.AspNetCore.Hosting", "http.server.request.duration"), ("Microsoft.AspNetCore.RateLimiting", "aspnetcore.rate_limiting.request.time_in_queue"), @@ -30,16 +39,30 @@ public sealed class Metric ("System.Net.Http", "http.client.request.duration"), ("System.Net.Http", "http.client.request.time_in_queue"), ("System.Net.NameResolution", "dns.lookup.duration"), - }; + } +#if NET + .ToFrozenSet() +#endif + ; // 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() + internal static readonly +#if NET + FrozenSet<(string, string)> +#else + HashSet<(string, string)> +#endif + DefaultHistogramBoundLongMappings = new HashSet<(string, string)> { ("Microsoft.AspNetCore.Http.Connections", "signalr.server.connection.duration"), ("Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"), ("System.Net.Http", "http.client.connection.duration"), - }; + } +#if NET + .ToFrozenSet() +#endif + ; internal readonly AggregatorStore AggregatorStore; @@ -47,8 +70,6 @@ internal Metric( MetricStreamIdentity instrumentIdentity, AggregationTemporality temporality, int cardinalityLimit, - bool emitOverflowAttribute, - bool shouldReclaimUnusedMetricPoints, ExemplarFilterType? exemplarFilter = null, Func? exemplarReservoirFactory = null) { @@ -117,6 +138,12 @@ internal Metric( aggType = AggregationType.DoubleGauge; this.MetricType = MetricType.DoubleGauge; } + else if (instrumentIdentity.InstrumentType == typeof(Gauge) + || instrumentIdentity.InstrumentType == typeof(Gauge)) + { + aggType = AggregationType.DoubleGauge; + this.MetricType = MetricType.DoubleGauge; + } else if (instrumentIdentity.InstrumentType == typeof(ObservableGauge) || instrumentIdentity.InstrumentType == typeof(ObservableGauge) || instrumentIdentity.InstrumentType == typeof(ObservableGauge) @@ -125,6 +152,14 @@ internal Metric( aggType = AggregationType.LongGauge; this.MetricType = MetricType.LongGauge; } + else if (instrumentIdentity.InstrumentType == typeof(Gauge) + || instrumentIdentity.InstrumentType == typeof(Gauge) + || instrumentIdentity.InstrumentType == typeof(Gauge) + || instrumentIdentity.InstrumentType == typeof(Gauge)) + { + aggType = AggregationType.LongGauge; + this.MetricType = MetricType.LongGauge; + } else if (instrumentIdentity.IsHistogram) { var explicitBucketBounds = instrumentIdentity.HistogramBucketBounds; @@ -156,8 +191,6 @@ internal Metric( aggType, temporality, cardinalityLimit, - emitOverflowAttribute, - shouldReclaimUnusedMetricPoints, exemplarFilter, exemplarReservoirFactory); this.Temporality = temporality; @@ -201,7 +234,7 @@ internal Metric( /// /// Gets the attributes (tags) for the metric stream. /// - public IEnumerable>? MeterTags => this.InstrumentIdentity.MeterTags; + public IEnumerable>? MeterTags => this.InstrumentIdentity.MeterTags?.KeyValuePairs; /// /// Gets the for the metric stream. diff --git a/src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogram.cs b/src/OpenTelemetry/Metrics/MetricPoint/Base2ExponentialBucketHistogram.cs similarity index 100% rename from src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogram.cs rename to src/OpenTelemetry/Metrics/MetricPoint/Base2ExponentialBucketHistogram.cs diff --git a/src/OpenTelemetry/Metrics/ExponentialHistogramBuckets.cs b/src/OpenTelemetry/Metrics/MetricPoint/ExponentialHistogramBuckets.cs similarity index 93% rename from src/OpenTelemetry/Metrics/ExponentialHistogramBuckets.cs rename to src/OpenTelemetry/Metrics/MetricPoint/ExponentialHistogramBuckets.cs index a48b8e40c60..8fc9dda8cc8 100644 --- a/src/OpenTelemetry/Metrics/ExponentialHistogramBuckets.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint/ExponentialHistogramBuckets.cs @@ -55,7 +55,9 @@ internal ExponentialHistogramBuckets Copy() /// Enumerates the bucket counts of an exponential histogram. /// // Note: Does not implement IEnumerator<> to prevent accidental boxing. +#pragma warning disable CA1034 // Nested types should not be visible - already part of public API public struct Enumerator +#pragma warning restore CA1034 // Nested types should not be visible - already part of public API { private readonly long[] buckets; private readonly int size; diff --git a/src/OpenTelemetry/Metrics/ExponentialHistogramData.cs b/src/OpenTelemetry/Metrics/MetricPoint/ExponentialHistogramData.cs similarity index 100% rename from src/OpenTelemetry/Metrics/ExponentialHistogramData.cs rename to src/OpenTelemetry/Metrics/MetricPoint/ExponentialHistogramData.cs diff --git a/src/OpenTelemetry/Metrics/HistogramBucket.cs b/src/OpenTelemetry/Metrics/MetricPoint/HistogramBucket.cs similarity index 100% rename from src/OpenTelemetry/Metrics/HistogramBucket.cs rename to src/OpenTelemetry/Metrics/MetricPoint/HistogramBucket.cs diff --git a/src/OpenTelemetry/Metrics/HistogramBuckets.cs b/src/OpenTelemetry/Metrics/MetricPoint/HistogramBuckets.cs similarity index 93% rename from src/OpenTelemetry/Metrics/HistogramBuckets.cs rename to src/OpenTelemetry/Metrics/MetricPoint/HistogramBuckets.cs index 6ef36eb6867..78d8700fe60 100644 --- a/src/OpenTelemetry/Metrics/HistogramBuckets.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint/HistogramBuckets.cs @@ -33,6 +33,7 @@ public class HistogramBuckets internal HistogramBuckets(double[]? explicitBounds) { + explicitBounds = CleanUpInfinitiesFromExplicitBounds(explicitBounds); this.ExplicitBounds = explicitBounds; this.findHistogramBucketIndex = this.FindBucketIndexLinear; if (explicitBounds != null && explicitBounds.Length >= DefaultBoundaryCountForBinarySearch) @@ -158,11 +159,16 @@ internal void Snapshot(bool outputDelta) } } + private static double[]? CleanUpInfinitiesFromExplicitBounds(double[]? explicitBounds) => explicitBounds + ?.Where(b => !double.IsNegativeInfinity(b) && !double.IsPositiveInfinity(b)).ToArray(); + /// /// Enumerates the elements of a . /// // Note: Does not implement IEnumerator<> to prevent accidental boxing. +#pragma warning disable CA1034 // Nested types should not be visible - already part of public API public struct Enumerator +#pragma warning restore CA1034 // Nested types should not be visible - already part of public API { private readonly int numberOfBuckets; private readonly HistogramBuckets histogramMeasurements; diff --git a/src/OpenTelemetry/Metrics/MetricPoint.cs b/src/OpenTelemetry/Metrics/MetricPoint/MetricPoint.cs similarity index 99% rename from src/OpenTelemetry/Metrics/MetricPoint.cs rename to src/OpenTelemetry/Metrics/MetricPoint/MetricPoint.cs index b3d171c9ebb..8c0ad5951fc 100644 --- a/src/OpenTelemetry/Metrics/MetricPoint.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint/MetricPoint.cs @@ -46,7 +46,7 @@ internal MetricPoint( { 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."); + Debug.Assert(!aggregatorStore!.OutputDelta || lookupData != null, "LookupData was null."); this.aggType = aggType; this.Tags = new ReadOnlyTagCollection(tagKeysAndValues); @@ -1086,7 +1086,7 @@ private void CompleteUpdate() [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CompleteUpdateWithoutMeasurement() { - if (this.aggregatorStore.OutputDeltaWithUnusedMetricPointReclaimEnabled) + if (this.aggregatorStore.OutputDelta) { Interlocked.Decrement(ref this.ReferenceCount); } diff --git a/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs b/src/OpenTelemetry/Metrics/MetricPoint/MetricPointOptionalComponents.cs similarity index 97% rename from src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs rename to src/OpenTelemetry/Metrics/MetricPoint/MetricPointOptionalComponents.cs index 440a9cc36b6..e22cbe54cc7 100644 --- a/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint/MetricPointOptionalComponents.cs @@ -22,7 +22,7 @@ internal sealed class MetricPointOptionalComponents public ReadOnlyExemplarCollection Exemplars = ReadOnlyExemplarCollection.Empty; - private int isCriticalSectionOccupied = 0; + private int isCriticalSectionOccupied; public MetricPointOptionalComponents Copy() { diff --git a/src/OpenTelemetry/Metrics/MetricPointStatus.cs b/src/OpenTelemetry/Metrics/MetricPoint/MetricPointStatus.cs similarity index 100% rename from src/OpenTelemetry/Metrics/MetricPointStatus.cs rename to src/OpenTelemetry/Metrics/MetricPoint/MetricPointStatus.cs diff --git a/src/OpenTelemetry/Metrics/MetricPointValueStorage.cs b/src/OpenTelemetry/Metrics/MetricPoint/MetricPointValueStorage.cs similarity index 100% rename from src/OpenTelemetry/Metrics/MetricPointValueStorage.cs rename to src/OpenTelemetry/Metrics/MetricPoint/MetricPointValueStorage.cs diff --git a/src/OpenTelemetry/Metrics/MetricPointsAccessor.cs b/src/OpenTelemetry/Metrics/MetricPoint/MetricPointsAccessor.cs similarity index 85% rename from src/OpenTelemetry/Metrics/MetricPointsAccessor.cs rename to src/OpenTelemetry/Metrics/MetricPoint/MetricPointsAccessor.cs index b79c1ac7096..3a6ab4a1fd6 100644 --- a/src/OpenTelemetry/Metrics/MetricPointsAccessor.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint/MetricPointsAccessor.cs @@ -13,9 +13,9 @@ public readonly struct MetricPointsAccessor { private readonly MetricPoint[] metricsPoints; private readonly int[] metricPointsToProcess; - private readonly long targetCount; + private readonly int targetCount; - internal MetricPointsAccessor(MetricPoint[] metricsPoints, int[] metricPointsToProcess, long targetCount) + internal MetricPointsAccessor(MetricPoint[] metricsPoints, int[] metricPointsToProcess, int targetCount) { Debug.Assert(metricsPoints != null, "metricPoints was null"); Debug.Assert(metricPointsToProcess != null, "metricPointsToProcess was null"); @@ -35,14 +35,16 @@ public Enumerator GetEnumerator() /// /// Enumerates the elements of a . /// +#pragma warning disable CA1034 // Nested types should not be visible - already part of public API public struct Enumerator +#pragma warning restore CA1034 // Nested types should not be visible - already part of public API { private readonly MetricPoint[] metricsPoints; private readonly int[] metricPointsToProcess; - private readonly long targetCount; - private long index; + private readonly int targetCount; + private int index; - internal Enumerator(MetricPoint[] metricsPoints, int[] metricPointsToProcess, long targetCount) + internal Enumerator(MetricPoint[] metricsPoints, int[] metricPointsToProcess, int targetCount) { this.metricsPoints = metricsPoints; this.metricPointsToProcess = metricPointsToProcess; diff --git a/src/OpenTelemetry/Metrics/MetricStreamIdentity.cs b/src/OpenTelemetry/Metrics/MetricStreamIdentity.cs index 712e5eda2a9..7ed33324cd0 100644 --- a/src/OpenTelemetry/Metrics/MetricStreamIdentity.cs +++ b/src/OpenTelemetry/Metrics/MetricStreamIdentity.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Metrics; +using System.Globalization; namespace OpenTelemetry.Metrics; @@ -14,7 +15,7 @@ public MetricStreamIdentity(Instrument instrument, MetricStreamConfiguration? me { this.MeterName = instrument.Meter.Name; this.MeterVersion = instrument.Meter.Version ?? string.Empty; - this.MeterTags = instrument.Meter.Tags; + this.MeterTags = instrument.Meter.Tags != null ? new Tags(instrument.Meter.Tags.ToArray()) : null; this.InstrumentName = metricStreamConfiguration?.Name ?? instrument.Name; this.Unit = instrument.Unit ?? string.Empty; this.Description = metricStreamConfiguration?.Description ?? instrument.Description ?? string.Empty; @@ -22,16 +23,17 @@ public MetricStreamIdentity(Instrument instrument, MetricStreamConfiguration? me this.ViewId = metricStreamConfiguration?.ViewId; this.MetricStreamName = $"{this.MeterName}.{this.MeterVersion}.{this.InstrumentName}"; this.TagKeys = metricStreamConfiguration?.CopiedTagKeys; - this.HistogramBucketBounds = (metricStreamConfiguration as ExplicitBucketHistogramConfiguration)?.CopiedBoundaries; + this.HistogramBucketBounds = GetExplicitBucketHistogramBounds(instrument, metricStreamConfiguration); 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 +#if NET || NETSTANDARD2_1_OR_GREATER HashCode hashCode = default; hashCode.Add(this.InstrumentType); hashCode.Add(this.MeterName); hashCode.Add(this.MeterVersion); + hashCode.Add(this.MeterTags); hashCode.Add(this.InstrumentName); hashCode.Add(this.HistogramRecordMinMax); hashCode.Add(this.Unit); @@ -63,8 +65,7 @@ public MetricStreamIdentity(Instrument instrument, MetricStreamConfiguration? me 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.MeterTags?.GetHashCode() ?? 0; hash = (hash * 31) + this.InstrumentName.GetHashCode(); hash = (hash * 31) + this.HistogramRecordMinMax.GetHashCode(); hash = (hash * 31) + this.ExponentialHistogramMaxSize.GetHashCode(); @@ -91,7 +92,7 @@ public MetricStreamIdentity(Instrument instrument, MetricStreamConfiguration? me public string MeterVersion { get; } - public IEnumerable>? MeterTags { get; } + public Tags? MeterTags { get; } public string InstrumentName { get; } @@ -141,6 +142,7 @@ public bool Equals(MetricStreamIdentity other) && this.Unit == other.Unit && this.Description == other.Description && this.ViewId == other.ViewId + && this.MeterTags == other.MeterTags && this.HistogramRecordMinMax == other.HistogramRecordMinMax && this.ExponentialHistogramMaxSize == other.ExponentialHistogramMaxSize && this.ExponentialHistogramMaxScale == other.ExponentialHistogramMaxScale @@ -150,6 +152,52 @@ public bool Equals(MetricStreamIdentity other) public override readonly int GetHashCode() => this.hashCode; + private static double[]? GetExplicitBucketHistogramBounds(Instrument instrument, MetricStreamConfiguration? metricStreamConfiguration) + { + if (metricStreamConfiguration is ExplicitBucketHistogramConfiguration explicitBucketHistogramConfiguration + && explicitBucketHistogramConfiguration.CopiedBoundaries != null) + { + return explicitBucketHistogramConfiguration.CopiedBoundaries; + } + + return instrument switch + { + Histogram longHistogram => GetExplicitBucketHistogramBoundsFromAdvice(longHistogram), + Histogram intHistogram => GetExplicitBucketHistogramBoundsFromAdvice(intHistogram), + Histogram shortHistogram => GetExplicitBucketHistogramBoundsFromAdvice(shortHistogram), + Histogram byteHistogram => GetExplicitBucketHistogramBoundsFromAdvice(byteHistogram), + Histogram floatHistogram => GetExplicitBucketHistogramBoundsFromAdvice(floatHistogram), + Histogram doubleHistogram => GetExplicitBucketHistogramBoundsFromAdvice(doubleHistogram), + _ => null, + }; + } + + private static double[]? GetExplicitBucketHistogramBoundsFromAdvice(Histogram histogram) + where T : struct + { + var adviceExplicitBucketBoundaries = histogram.Advice?.HistogramBucketBoundaries; + if (adviceExplicitBucketBoundaries == null) + { + return null; + } + + if (typeof(T) == typeof(double)) + { + return ((IReadOnlyList)adviceExplicitBucketBoundaries).ToArray(); + } + else + { + double[] explicitBucketBoundaries = new double[adviceExplicitBucketBoundaries.Count]; + + for (int i = 0; i < adviceExplicitBucketBoundaries.Count; i++) + { + explicitBucketBoundaries[i] = Convert.ToDouble(adviceExplicitBucketBoundaries[i], CultureInfo.InvariantCulture); + } + + return explicitBucketBoundaries; + } + } + private static bool HistogramBoundsEqual(double[]? bounds1, double[]? bounds2) { if (ReferenceEquals(bounds1, bounds2)) diff --git a/src/OpenTelemetry/Metrics/MetricType.cs b/src/OpenTelemetry/Metrics/MetricType.cs index 526bdbb466f..2080ed17f36 100644 --- a/src/OpenTelemetry/Metrics/MetricType.cs +++ b/src/OpenTelemetry/Metrics/MetricType.cs @@ -7,7 +7,11 @@ namespace OpenTelemetry.Metrics; /// Enumeration used to define the type of a . /// [Flags] +#pragma warning disable CA1028 // Enum storage should be Int32 +#pragma warning disable CA2217 // Do not mark enums with FlagsAttribute public enum MetricType : byte +#pragma warning restore CA2217 // Do not mark enums with FlagsAttribute +#pragma warning restore CA1028 // Enum storage should be Int32 { /* Type: diff --git a/src/OpenTelemetry/Metrics/BaseExportingMetricReader.cs b/src/OpenTelemetry/Metrics/Reader/BaseExportingMetricReader.cs similarity index 85% rename from src/OpenTelemetry/Metrics/BaseExportingMetricReader.cs rename to src/OpenTelemetry/Metrics/Reader/BaseExportingMetricReader.cs index 80f96cc143a..0d9d9c4572b 100644 --- a/src/OpenTelemetry/Metrics/BaseExportingMetricReader.cs +++ b/src/OpenTelemetry/Metrics/Reader/BaseExportingMetricReader.cs @@ -10,14 +10,17 @@ namespace OpenTelemetry.Metrics; /// MetricReader implementation which exports metrics to the configured /// MetricExporter upon . /// +#pragma warning disable CA1708 // Identifiers should differ by more than case public class BaseExportingMetricReader : MetricReader +#pragma warning restore CA1708 // Identifiers should differ by more than case { /// /// Gets the exporter used by the metric reader. /// +#pragma warning disable CA1051 // Do not declare visible instance fields protected readonly BaseExporter exporter; +#pragma warning restore CA1051 // Do not declare visible instance fields - private readonly ExportModes supportedExportModes = ExportModes.Push | ExportModes.Pull; private readonly string exportCalledMessage; private readonly string exportSucceededMessage; private readonly string exportFailedMessage; @@ -33,17 +36,19 @@ public BaseExportingMetricReader(BaseExporter exporter) this.exporter = exporter; +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 var exporterType = exporter.GetType(); +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 var attributes = exporterType.GetCustomAttributes(typeof(ExportModesAttribute), true); if (attributes.Length > 0) { var attr = (ExportModesAttribute)attributes[attributes.Length - 1]; - this.supportedExportModes = attr.Supported; + this.SupportedExportModes = attr.Supported; } if (exporter is IPullMetricExporter pullExporter) { - if (this.supportedExportModes.HasFlag(ExportModes.Push)) + if (this.SupportedExportModes.HasFlag(ExportModes.Push)) { pullExporter.Collect = this.Collect; } @@ -69,7 +74,7 @@ public BaseExportingMetricReader(BaseExporter exporter) /// /// Gets the supported . /// - protected ExportModes SupportedExportModes => this.supportedExportModes; + protected ExportModes SupportedExportModes { get; } = ExportModes.Push | ExportModes.Pull; internal override void SetParentProvider(BaseProvider parentProvider) { @@ -106,12 +111,12 @@ internal override bool ProcessMetrics(in Batch metrics, int timeoutMilli /// protected override bool OnCollect(int timeoutMilliseconds) { - if (this.supportedExportModes.HasFlag(ExportModes.Push)) + if (this.SupportedExportModes.HasFlag(ExportModes.Push)) { return base.OnCollect(timeoutMilliseconds); } - if (this.supportedExportModes.HasFlag(ExportModes.Pull) && PullMetricScope.IsPullAllowed) + if (this.SupportedExportModes.HasFlag(ExportModes.Pull) && PullMetricScope.IsPullAllowed) { return base.OnCollect(timeoutMilliseconds); } diff --git a/src/OpenTelemetry/Metrics/CompositeMetricReader.cs b/src/OpenTelemetry/Metrics/Reader/CompositeMetricReader.cs similarity index 99% rename from src/OpenTelemetry/Metrics/CompositeMetricReader.cs rename to src/OpenTelemetry/Metrics/Reader/CompositeMetricReader.cs index a213db5285f..1323b6980b1 100644 --- a/src/OpenTelemetry/Metrics/CompositeMetricReader.cs +++ b/src/OpenTelemetry/Metrics/Reader/CompositeMetricReader.cs @@ -24,7 +24,7 @@ public CompositeMetricReader(IEnumerable 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(readers)); } this.Head = new DoublyLinkedListNode(iter.Current); diff --git a/src/OpenTelemetry/Metrics/CompositeMetricReaderExt.cs b/src/OpenTelemetry/Metrics/Reader/CompositeMetricReaderExt.cs similarity index 100% rename from src/OpenTelemetry/Metrics/CompositeMetricReaderExt.cs rename to src/OpenTelemetry/Metrics/Reader/CompositeMetricReaderExt.cs diff --git a/src/OpenTelemetry/Metrics/MetricReader.cs b/src/OpenTelemetry/Metrics/Reader/MetricReader.cs similarity index 95% rename from src/OpenTelemetry/Metrics/MetricReader.cs rename to src/OpenTelemetry/Metrics/Reader/MetricReader.cs index decbf8ad70d..04d267293d3 100644 --- a/src/OpenTelemetry/Metrics/MetricReader.cs +++ b/src/OpenTelemetry/Metrics/Reader/MetricReader.cs @@ -27,6 +27,7 @@ public abstract partial class MetricReader : IDisposable // 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(Gauge<>) => AggregationTemporality.Delta, var type when type == typeof(UpDownCounter<>) => AggregationTemporality.Cumulative, var type when type == typeof(ObservableUpDownCounter<>) => AggregationTemporality.Cumulative, @@ -36,8 +37,8 @@ public abstract partial class MetricReader : IDisposable }; }; - private readonly object newTaskLock = new(); - private readonly object onCollectLock = new(); + private readonly Lock newTaskLock = new(); + private readonly Lock onCollectLock = new(); private readonly TaskCompletionSource shutdownTcs = new(); private MetricReaderTemporalityPreference temporalityPreference = MetricReaderTemporalityPreferenceUnspecified; private Func temporalityFunc = CumulativeTemporalityPreferenceFunc; @@ -124,7 +125,7 @@ public bool Collect(int timeoutMilliseconds = Timeout.Infinite) if (!shouldRunCollect) { - return Task.WaitAny(tcs.Task, this.shutdownTcs.Task, Task.Delay(timeoutMilliseconds)) == 0 && tcs.Task.Result; + return Task.WaitAny([tcs.Task, this.shutdownTcs.Task], timeoutMilliseconds) == 0 && tcs.Task.Result; } var result = false; @@ -217,6 +218,11 @@ public void Dispose() internal virtual void SetParentProvider(BaseProvider parentProvider) { + if (this.parentProvider != null && this.parentProvider != parentProvider) + { + throw new NotSupportedException("A MetricReader must not be registered with multiple MeterProviders."); + } + this.parentProvider = parentProvider; } diff --git a/src/OpenTelemetry/Metrics/MetricReaderExt.cs b/src/OpenTelemetry/Metrics/Reader/MetricReaderExt.cs similarity index 92% rename from src/OpenTelemetry/Metrics/MetricReaderExt.cs rename to src/OpenTelemetry/Metrics/Reader/MetricReaderExt.cs index a6a0a642d46..e366e63720a 100644 --- a/src/OpenTelemetry/Metrics/MetricReaderExt.cs +++ b/src/OpenTelemetry/Metrics/Reader/MetricReaderExt.cs @@ -15,15 +15,13 @@ namespace OpenTelemetry.Metrics; public abstract partial class MetricReader { private readonly HashSet metricStreamNames = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary instrumentIdentityToMetric = new(); - private readonly object instrumentCreationLock = new(); + private readonly ConcurrentDictionary instrumentIdentityToMetric = new(); + private readonly Lock instrumentCreationLock = new(); private int metricLimit; private int cardinalityLimit; - private Metric?[]? metrics; - private Metric[]? metricsCurrentBatch; + private Metric?[] metrics = []; + private Metric[] metricsCurrentBatch = []; private int metricIndex = -1; - private bool emitOverflowAttribute; - private bool reclaimUnusedMetricPoints; private ExemplarFilterType? exemplarFilter; private ExemplarFilterType? exemplarFilterForHistograms; @@ -82,8 +80,6 @@ internal virtual List AddMetricWithNoViews(Instrument instrument) metricStreamIdentity, this.GetAggregationTemporality(metricStreamIdentity.InstrumentType), this.cardinalityLimit, - this.emitOverflowAttribute, - this.reclaimUnusedMetricPoints, exemplarFilter); } catch (NotSupportedException nse) @@ -164,8 +160,6 @@ internal virtual List AddMetricWithViews(Instrument instrument, List AddMetricWithViews(Instrument instrument, List this.instrumentIdentityToMetric.TryGetValue(metricStreamIdentity, out existingMetric) - && existingMetric.Active; + && existingMetric != null && existingMetric.Active; private void CreateOrUpdateMetricStreamRegistration(in MetricStreamIdentity metricStreamIdentity) { @@ -268,8 +258,12 @@ private void RemoveMetric(ref Metric? metric) OpenTelemetrySdkEventSource.Log.MetricInstrumentRemoved(metric!.Name, metric.MeterName); - var result = this.instrumentIdentityToMetric.TryRemove(metric.InstrumentIdentity, out var _); - Debug.Assert(result, "result was false"); + // Note: This is using TryUpdate and NOT TryRemove because there is a + // race condition. If a metric is deactivated and then reactivated in + // the same collection cycle + // instrumentIdentityToMetric[metric.InstrumentIdentity] may already + // point to the new activated metric and not the old deactivated one. + this.instrumentIdentityToMetric.TryUpdate(metric.InstrumentIdentity, null, metric); // Note: metric is a reference to the array storage so // this clears the metric out of the array. diff --git a/src/OpenTelemetry/Metrics/MetricReaderOptions.cs b/src/OpenTelemetry/Metrics/Reader/MetricReaderOptions.cs similarity index 93% rename from src/OpenTelemetry/Metrics/MetricReaderOptions.cs rename to src/OpenTelemetry/Metrics/Reader/MetricReaderOptions.cs index 59b850e88aa..7be100a64c9 100644 --- a/src/OpenTelemetry/Metrics/MetricReaderOptions.cs +++ b/src/OpenTelemetry/Metrics/Reader/MetricReaderOptions.cs @@ -26,7 +26,9 @@ internal MetricReaderOptions( { Debug.Assert(defaultPeriodicExportingMetricReaderOptions != null, "defaultPeriodicExportingMetricReaderOptions was null"); +#pragma warning disable CA1508 // Avoid dead conditional code this.periodicExportingMetricReaderOptions = defaultPeriodicExportingMetricReaderOptions ?? new(); +#pragma warning restore CA1508 // Avoid dead conditional code } /// diff --git a/src/OpenTelemetry/Metrics/MetricReaderTemporalityPreference.cs b/src/OpenTelemetry/Metrics/Reader/MetricReaderTemporalityPreference.cs similarity index 83% rename from src/OpenTelemetry/Metrics/MetricReaderTemporalityPreference.cs rename to src/OpenTelemetry/Metrics/Reader/MetricReaderTemporalityPreference.cs index 019fdd69027..c3471ffb661 100644 --- a/src/OpenTelemetry/Metrics/MetricReaderTemporalityPreference.cs +++ b/src/OpenTelemetry/Metrics/Reader/MetricReaderTemporalityPreference.cs @@ -7,7 +7,9 @@ namespace OpenTelemetry.Metrics; /// Defines the behavior of a /// with respect to . /// +#pragma warning disable CA1008 // Enums should have zero value public enum MetricReaderTemporalityPreference +#pragma warning restore CA1008 // Enums should have zero value { /// /// All aggregations are performed using cumulative temporality. diff --git a/src/OpenTelemetry/Metrics/PeriodicExportingMetricReader.cs b/src/OpenTelemetry/Metrics/Reader/PeriodicExportingMetricReader.cs similarity index 95% rename from src/OpenTelemetry/Metrics/PeriodicExportingMetricReader.cs rename to src/OpenTelemetry/Metrics/Reader/PeriodicExportingMetricReader.cs index 8f994bbc83d..0596e18af18 100644 --- a/src/OpenTelemetry/Metrics/PeriodicExportingMetricReader.cs +++ b/src/OpenTelemetry/Metrics/Reader/PeriodicExportingMetricReader.cs @@ -47,10 +47,12 @@ public PeriodicExportingMetricReader( this.ExportIntervalMilliseconds = exportIntervalMilliseconds; this.ExportTimeoutMilliseconds = exportTimeoutMilliseconds; - this.exporterThread = new Thread(new ThreadStart(this.ExporterProc)) + this.exporterThread = new Thread(this.ExporterProc) { IsBackground = true, +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 Name = $"OpenTelemetry-{nameof(PeriodicExportingMetricReader)}-{exporter.GetType().Name}", +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 }; this.exporterThread.Start(); } diff --git a/src/OpenTelemetry/Metrics/PeriodicExportingMetricReaderOptions.cs b/src/OpenTelemetry/Metrics/Reader/PeriodicExportingMetricReaderOptions.cs similarity index 100% rename from src/OpenTelemetry/Metrics/PeriodicExportingMetricReaderOptions.cs rename to src/OpenTelemetry/Metrics/Reader/PeriodicExportingMetricReaderOptions.cs diff --git a/src/OpenTelemetry/Metrics/StringArrayEqualityComparer.cs b/src/OpenTelemetry/Metrics/StringArrayEqualityComparer.cs index 696cd40c23d..2a2e394e784 100644 --- a/src/OpenTelemetry/Metrics/StringArrayEqualityComparer.cs +++ b/src/OpenTelemetry/Metrics/StringArrayEqualityComparer.cs @@ -41,7 +41,7 @@ public int GetHashCode(string[] strings) { Debug.Assert(strings != null, "strings was null"); -#if NET6_0_OR_GREATER +#if NET || NETSTANDARD2_1_OR_GREATER HashCode hashCode = default; for (int i = 0; i < strings.Length; i++) diff --git a/src/OpenTelemetry/Metrics/Tags.cs b/src/OpenTelemetry/Metrics/Tags.cs index 1cc4c6bc153..7460231b7b0 100644 --- a/src/OpenTelemetry/Metrics/Tags.cs +++ b/src/OpenTelemetry/Metrics/Tags.cs @@ -3,7 +3,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; -#if NET6_0_OR_GREATER +#if NET using System.Runtime.InteropServices; #endif @@ -11,7 +11,7 @@ namespace OpenTelemetry.Metrics; internal readonly struct Tags : IEquatable { - public static readonly Tags EmptyTags = new(Array.Empty>()); + public static readonly Tags EmptyTags = new([]); private readonly int hashCode; @@ -44,7 +44,7 @@ public readonly bool Equals(Tags other) return false; } -#if NET6_0_OR_GREATER +#if NET // Note: This loop uses unsafe code (pointers) to elide bounds checks on // two arrays we know to be of equal length. if (length > 0) @@ -53,8 +53,7 @@ public readonly bool Equals(Tags other) ref var theirs = ref MemoryMarshal.GetArrayDataReference(theirKvps); while (true) { - // Note: string.Equals performs an ordinal comparison - if (!ours.Key.Equals(theirs.Key)) + if (!ours.Key.Equals(theirs.Key, StringComparison.Ordinal)) { return false; } @@ -81,8 +80,7 @@ public readonly bool Equals(Tags other) // Note: Bounds check happens here for theirKvps element access ref var theirs = ref theirKvps[i]; - // Note: string.Equals performs an ordinal comparison - if (!ours.Key.Equals(theirs.Key)) + if (!ours.Key.Equals(theirs.Key, StringComparison.Ordinal)) { return false; } @@ -104,13 +102,13 @@ private static int ComputeHashCode(KeyValuePair[] keyValuePairs { Debug.Assert(keyValuePairs != null, "keyValuePairs was null"); -#if NET6_0_OR_GREATER +#if NET || NETSTANDARD2_1_OR_GREATER HashCode hashCode = default; for (int i = 0; i < keyValuePairs.Length; i++) { ref var item = ref keyValuePairs[i]; - hashCode.Add(item.Key.GetHashCode()); + hashCode.Add(item.Key.GetHashCode(StringComparison.Ordinal)); hashCode.Add(item.Value); } diff --git a/src/OpenTelemetry/Metrics/ThreadStaticStorage.cs b/src/OpenTelemetry/Metrics/ThreadStaticStorage.cs index 35f9be5b5da..a66460bfb79 100644 --- a/src/OpenTelemetry/Metrics/ThreadStaticStorage.cs +++ b/src/OpenTelemetry/Metrics/ThreadStaticStorage.cs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if NET8_0_OR_GREATER +#if NET using System.Collections.Frozen; #endif using System.Diagnostics; @@ -57,7 +57,7 @@ internal void SplitToKeysAndValues( internal void SplitToKeysAndValues( ReadOnlySpan> tags, int tagLength, -#if NET8_0_OR_GREATER +#if NET FrozenSet tagKeysInteresting, #else HashSet tagKeysInteresting, diff --git a/src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogramConfiguration.cs b/src/OpenTelemetry/Metrics/View/Base2ExponentialBucketHistogramConfiguration.cs similarity index 100% rename from src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogramConfiguration.cs rename to src/OpenTelemetry/Metrics/View/Base2ExponentialBucketHistogramConfiguration.cs diff --git a/src/OpenTelemetry/Metrics/ExplicitBucketHistogramConfiguration.cs b/src/OpenTelemetry/Metrics/View/ExplicitBucketHistogramConfiguration.cs similarity index 78% rename from src/OpenTelemetry/Metrics/ExplicitBucketHistogramConfiguration.cs rename to src/OpenTelemetry/Metrics/View/ExplicitBucketHistogramConfiguration.cs index 8d040da86a5..3149b0d8d18 100644 --- a/src/OpenTelemetry/Metrics/ExplicitBucketHistogramConfiguration.cs +++ b/src/OpenTelemetry/Metrics/View/ExplicitBucketHistogramConfiguration.cs @@ -23,19 +23,11 @@ public class ExplicitBucketHistogramConfiguration : HistogramConfiguration /// /// Note: A copy is made of the provided array. /// +#pragma warning disable CA1819 // Properties should not return arrays public double[]? Boundaries +#pragma warning restore CA1819 // Properties should not return arrays { - get - { - if (this.CopiedBoundaries != null) - { - double[] copy = new double[this.CopiedBoundaries.Length]; - this.CopiedBoundaries.AsSpan().CopyTo(copy); - return copy; - } - - return null; - } + get => this.CopiedBoundaries?.ToArray(); set { @@ -46,9 +38,7 @@ public double[]? Boundaries 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; + this.CopiedBoundaries = value.ToArray(); } else { diff --git a/src/OpenTelemetry/Metrics/HistogramConfiguration.cs b/src/OpenTelemetry/Metrics/View/HistogramConfiguration.cs similarity index 100% rename from src/OpenTelemetry/Metrics/HistogramConfiguration.cs rename to src/OpenTelemetry/Metrics/View/HistogramConfiguration.cs diff --git a/src/OpenTelemetry/Metrics/MetricStreamConfiguration.cs b/src/OpenTelemetry/Metrics/View/MetricStreamConfiguration.cs similarity index 79% rename from src/OpenTelemetry/Metrics/MetricStreamConfiguration.cs rename to src/OpenTelemetry/Metrics/View/MetricStreamConfiguration.cs index 578049f329f..ba1e55941b1 100644 --- a/src/OpenTelemetry/Metrics/MetricStreamConfiguration.cs +++ b/src/OpenTelemetry/Metrics/View/MetricStreamConfiguration.cs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +#if EXPOSE_EXPERIMENTAL_FEATURES using System.Diagnostics.CodeAnalysis; #endif using OpenTelemetry.Internal; @@ -15,7 +15,7 @@ public class MetricStreamConfiguration { private string? name; - private int? cardinalityLimit = null; + private int? cardinalityLimit; /// /// Gets the drop configuration. @@ -69,43 +69,19 @@ public string? Name /// A copy is made of the provided array. /// /// +#pragma warning disable CA1819 // Properties should not return arrays public string[]? TagKeys +#pragma warning restore CA1819 // Properties should not return arrays { - get - { - if (this.CopiedTagKeys != null) - { - string[] copy = new string[this.CopiedTagKeys.Length]; - this.CopiedTagKeys.AsSpan().CopyTo(copy); - return copy; - } - - return null; - } - - set - { - if (value != null) - { - string[] copy = new string[value.Length]; - value.AsSpan().CopyTo(copy); - this.CopiedTagKeys = copy; - } - else - { - this.CopiedTagKeys = null; - } - } + get => this.CopiedTagKeys?.ToArray(); + set => this.CopiedTagKeys = value?.ToArray(); } -#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. @@ -116,14 +92,7 @@ public string[]? TagKeys /// 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 + public int? CardinalityLimit { get => this.cardinalityLimit; set @@ -151,9 +120,7 @@ public string[]? TagKeys /// Specification: . /// -#if NET8_0_OR_GREATER [Experimental(DiagnosticDefinitions.ExemplarReservoirExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] -#endif public Func? ExemplarReservoirFactory { get; set; } #else internal Func? ExemplarReservoirFactory { get; set; } diff --git a/src/OpenTelemetry/OpenTelemetry.csproj b/src/OpenTelemetry/OpenTelemetry.csproj index 71e3b242cd0..0e8202d99e1 100644 --- a/src/OpenTelemetry/OpenTelemetry.csproj +++ b/src/OpenTelemetry/OpenTelemetry.csproj @@ -4,6 +4,7 @@ $(TargetFrameworksForLibrariesExtended) OpenTelemetry .NET SDK core- + $(NoWarn);CA1815 @@ -26,4 +27,19 @@ + + + + + + + + + + + + + + + diff --git a/src/OpenTelemetry/OpenTelemetryBuilderSdkExtensions.cs b/src/OpenTelemetry/OpenTelemetryBuilderSdkExtensions.cs index c499ef02631..acb2822b0f7 100644 --- a/src/OpenTelemetry/OpenTelemetryBuilderSdkExtensions.cs +++ b/src/OpenTelemetry/OpenTelemetryBuilderSdkExtensions.cs @@ -37,7 +37,9 @@ public static IOpenTelemetryBuilder ConfigureResource( Guard.ThrowIfNull(builder); Guard.ThrowIfNull(configure); +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 builder.Services.ConfigureOpenTelemetryMeterProvider( +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 builder => builder.ConfigureResource(configure)); builder.Services.ConfigureOpenTelemetryTracerProvider( @@ -84,8 +86,10 @@ public static IOpenTelemetryBuilder WithMetrics( Action configure) { OpenTelemetryMetricsBuilderExtensions.RegisterMetricsListener( +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 builder.Services, configure); +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 return builder; } @@ -119,9 +123,11 @@ public static IOpenTelemetryBuilder WithTracing( { Guard.ThrowIfNull(configure); +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 var tracerProviderBuilder = new TracerProviderBuilderBase(builder.Services); configure(tracerProviderBuilder); +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 return builder; } @@ -183,7 +189,11 @@ public static IOpenTelemetryBuilder WithLogging( Action? configureBuilder, Action? configureOptions) { + Guard.ThrowIfNull(builder); + +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 builder.Services.AddLogging( +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 logging => logging.UseOpenTelemetry(configureBuilder, configureOptions)); return builder; diff --git a/src/OpenTelemetry/OpenTelemetrySdk.cs b/src/OpenTelemetry/OpenTelemetrySdk.cs new file mode 100644 index 00000000000..7020d05ce1c --- /dev/null +++ b/src/OpenTelemetry/OpenTelemetrySdk.cs @@ -0,0 +1,124 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Internal; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace OpenTelemetry; + +/// +/// Contains methods for configuring the OpenTelemetry SDK and accessing +/// logging, metrics, and tracing providers. +/// +public sealed class OpenTelemetrySdk : IDisposable +{ + private readonly ServiceProvider serviceProvider; + + private OpenTelemetrySdk( + Action configure) + { + Debug.Assert(configure != null, "configure was null"); + + var services = new ServiceCollection(); + + var builder = new OpenTelemetrySdkBuilder(services); + + configure!(builder); + + this.serviceProvider = services.BuildServiceProvider(); + + this.LoggerProvider = (LoggerProvider?)this.serviceProvider.GetService(typeof(LoggerProvider)) + ?? new NoopLoggerProvider(); + this.MeterProvider = (MeterProvider?)this.serviceProvider.GetService(typeof(MeterProvider)) + ?? new NoopMeterProvider(); + this.TracerProvider = (TracerProvider?)this.serviceProvider.GetService(typeof(TracerProvider)) + ?? new NoopTracerProvider(); + } + + /// + /// Gets the . + /// + /// + /// Note: The default will be a no-op instance. + /// Call to + /// enable logging. + /// + public LoggerProvider LoggerProvider { get; } + + /// + /// Gets the . + /// + /// + /// Note: The default will be a no-op instance. + /// Call + /// to enable metrics. + /// + public MeterProvider MeterProvider { get; } + + /// + /// Gets the . + /// + /// + /// Note: The default will be a no-op instance. + /// Call + /// to enable tracing. + /// + public TracerProvider TracerProvider { get; } + + /// + /// Gets the containing SDK services. + /// + internal IServiceProvider Services => this.serviceProvider; + + /// + /// Create an instance. + /// + /// configuration delegate. + /// Created . + public static OpenTelemetrySdk Create( + Action configure) + { + Guard.ThrowIfNull(configure); + + return new(configure); + } + + /// + public void Dispose() + { + this.serviceProvider.Dispose(); + } + + internal sealed class NoopLoggerProvider : LoggerProvider + { + } + + internal sealed class NoopMeterProvider : MeterProvider + { + } + + internal sealed class NoopTracerProvider : TracerProvider + { + } + + private sealed class OpenTelemetrySdkBuilder : IOpenTelemetryBuilder + { + public OpenTelemetrySdkBuilder(IServiceCollection services) + { + Debug.Assert(services != null, "services was null"); + + services!.AddOpenTelemetrySharedProviderBuilderServices(); + + this.Services = services!; + } + + public IServiceCollection Services { get; } + } +} diff --git a/src/OpenTelemetry/OpenTelemetrySdkExtensions.cs b/src/OpenTelemetry/OpenTelemetrySdkExtensions.cs new file mode 100644 index 00000000000..f7d15ef8014 --- /dev/null +++ b/src/OpenTelemetry/OpenTelemetrySdkExtensions.cs @@ -0,0 +1,36 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using OpenTelemetry.Internal; + +namespace OpenTelemetry; + +/// +/// Contains methods for extending the class. +/// +public static class OpenTelemetrySdkExtensions +{ + /// + /// Gets the contained in an instance. + /// + /// + /// Note: The default will be a no-op instance. + /// Call + /// to enable logging. + /// + /// . + /// . + public static ILoggerFactory GetLoggerFactory(this OpenTelemetrySdk sdk) + { + Guard.ThrowIfNull(sdk); + +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 + return (ILoggerFactory?)sdk.Services.GetService(typeof(ILoggerFactory)) +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 + ?? NullLoggerFactory.Instance; + } +} diff --git a/src/OpenTelemetry/README.md b/src/OpenTelemetry/README.md index 8e444a6f167..bffb7bf03e2 100644 --- a/src/OpenTelemetry/README.md +++ b/src/OpenTelemetry/README.md @@ -3,16 +3,19 @@ [![NuGet](https://img.shields.io/nuget/v/OpenTelemetry.svg)](https://www.nuget.org/packages/OpenTelemetry) [![NuGet](https://img.shields.io/nuget/dt/OpenTelemetry.svg)](https://www.nuget.org/packages/OpenTelemetry) +
+Table of Contents + * [Installation](#installation) * [Introduction](#introduction) -* [Getting started with Logging](#getting-started-with-logging) -* [Getting started with Metrics](#getting-started-with-metrics) -* [Getting started with Tracing](#getting-started-with-tracing) * [Troubleshooting](#troubleshooting) - * [Configuration Parameters](#configuration-parameters) - * [Remarks](#remarks) + * [Self-diagnostics](#self-diagnostics) + * [Configuration Parameters](#configuration-parameters) + * [Remarks](#remarks) * [References](#references) +
+ ## Installation ```shell @@ -22,61 +25,25 @@ dotnet add package OpenTelemetry ## Introduction OpenTelemetry SDK is a reference implementation of the OpenTelemetry API. It -implements the Tracing API, the Metrics API, and the Context API. Once a valid -SDK is installed and configured, all the OpenTelemetry API methods, which were -no-ops without an SDK, will start emitting telemetry. -This SDK also supports [ILogger](https://docs.microsoft.com/dotnet/api/microsoft.extensions.logging.ilogger) -integration. - -The SDK deals with concerns such as sampling, processing pipeline, exporting -telemetry to a particular backend etc. In most cases, users indirectly install -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 - 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/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 -[provider](https://docs.microsoft.com/dotnet/core/extensions/logging-providers), -which provides the ability to enrich logs emitted with `ILogger` with -`ActivityContext`, and export them to multiple destinations, similar to tracing. -`ILogger` based API will become the OpenTelemetry .NET implementation of -OpenTelemetry logging. - -## Getting started with Metrics - -If you are new to -[metrics](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md), -it is recommended to first follow the [getting started in 5 minutes - ASP.NET -Core Application](../../docs/metrics/getting-started-aspnetcore/README.md) guide -or the [getting started in 5 minutes - Console -Application](../../docs/metrics/getting-started-console/README.md) guide to get up -and running. - -For a more detailed explanation of SDK metric features see [Customizing -OpenTelemetry .NET SDK for -Metrics](../../docs/metrics/customizing-the-sdk/README.md). - -## Getting started with Tracing - -If you are new to -[traces](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md), -it is recommended to first follow the [getting started in 5 minutes - ASP.NET -Core Application](../../docs/trace/getting-started-aspnetcore/README.md) guide -or the [getting started in 5 minutes - Console -Application](../../docs/trace/getting-started-console/README.md) guide to get up -and running. - -For a more detailed explanation of SDK tracing features see [Customizing -OpenTelemetry .NET SDK for -Tracing](../../docs/trace/customizing-the-sdk/README.md). +implements the Logging API, Metrics API, Tracing API, Resource API, and the +Context API. Once a valid SDK is installed and configured all the OpenTelemetry +API methods, which were no-ops without an SDK, will start emitting telemetry. +This SDK also ships with +[ILogger](https://learn.microsoft.com/dotnet/core/extensions/logging) +integration to automatically capture and enrich logs emitted using +`Microsoft.Extensions.Logging`. + +The SDK deals with concerns such as sampling, processing pipelines (exporting +telemetry to a particular backend, etc.), metrics aggregation, and other +concerns outlined in the [OpenTelemetry +Specification](https://github.com/open-telemetry/opentelemetry-specification). +In most cases, users indirectly install and enable the SDK when they install an +exporter. + +To learn how to set up and configure the OpenTelemetry SDK see: [Getting +started](../../README.md#getting-started). For additional details about +initialization patterns see: [Initialize the +SDK](../../docs/README.md#initialize-the-sdk). ## Troubleshooting @@ -90,9 +57,9 @@ While it is possible to view these logs using tools such as [PerfView](https://github.com/microsoft/perfview), [dotnet-trace](https://docs.microsoft.com/dotnet/core/diagnostics/dotnet-trace) etc., this SDK also ships a [self-diagnostics](#self-diagnostics) feature, which -helps troubleshooting. +helps with troubleshooting. -## Self-diagnostics +### Self-diagnostics OpenTelemetry SDK ships with built-in self-diagnostics feature. This feature, when enabled, will listen to internal logs generated by all OpenTelemetry @@ -115,7 +82,8 @@ the following content: { "LogDirectory": ".", "FileSize": 32768, - "LogLevel": "Warning" + "LogLevel": "Warning", + "FormatMessage": "true" } ``` @@ -130,7 +98,7 @@ Internally, it looks for the configuration file located in and then [AppContext.BaseDirectory](https://docs.microsoft.com/dotnet/api/system.appcontext.basedirectory). You can also find the exact directory by calling these methods from your code. -### Configuration Parameters +#### Configuration Parameters 1. `LogDirectory` is the directory where the output log file will be stored. It can be an absolute path or a relative path to the current directory. @@ -150,7 +118,26 @@ You can also find the exact directory by calling these methods from your code. higher severity levels. For example, `Warning` includes the `Error` and `Critical` levels. -### Remarks +4. `FormatMessage` is a boolean value that controls whether log messages should + be formatted by replacing placeholders (`{0}`, `{1}`, etc.) with their actual + parameter values. When set to `false` (default), messages are logged with + unformatted placeholders followed by raw parameter values. When set to + `true`, placeholders are replaced with formatted parameter values for + improved readability. + + **Example with `FormatMessage: false` (default):** + + ```txt + 2025-07-24T01:45:04.1020880Z:Measurements from Instrument '{0}', Meter '{1}' will be ignored. Reason: '{2}'. Suggested action: '{3}'{dotnet.gc.collections}{System.Runtime}{Instrument belongs to a Meter not subscribed by the provider.}{Use AddMeter to add the Meter to the provider.} + ``` + + **Example with `FormatMessage: true`:** + + ```txt + 2025-07-24T01:44:44.7059260Z:Measurements from Instrument 'dotnet.gc.collections', Meter 'System.Runtime' will be ignored. Reason: 'Instrument belongs to a Meter not subscribed by the provider.'. Suggested action: 'Use AddMeter to add the Meter to the provider.' + ``` + +#### Remarks A `FileSize`-KiB log file named as `ExecutableName.ProcessId.log` (e.g. `foobar.exe.12345.log`) will be generated at the specified directory @@ -172,6 +159,7 @@ start from beginning and overwrite existing text. ## References -* [OpenTelemetry Project](https://opentelemetry.io/) +* [OpenTelemetry Logging SDK specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/sdk.md) +* [OpenTelemetry Metrics SDK specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md) * [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/README.md) +* [OpenTelemetry Resource SDK specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md) diff --git a/src/OpenTelemetry/ReadOnlyFilteredTagCollection.cs b/src/OpenTelemetry/ReadOnlyFilteredTagCollection.cs index 8b8d7d12b3d..4eb4320d56f 100644 --- a/src/OpenTelemetry/ReadOnlyFilteredTagCollection.cs +++ b/src/OpenTelemetry/ReadOnlyFilteredTagCollection.cs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if NET8_0_OR_GREATER +#if NET using System.Collections.Frozen; #endif using System.Diagnostics; @@ -14,18 +14,19 @@ namespace OpenTelemetry; ///
// Note: Does not implement IReadOnlyCollection<> or IEnumerable<> to // prevent accidental boxing. +#pragma warning disable CA1711 // Identifiers should not have incorrect suffix public readonly struct ReadOnlyFilteredTagCollection +#pragma warning restore CA1711 // Identifiers should not have incorrect suffix { -#if NET8_0_OR_GREATER +#if NET 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 +#if NET FrozenSet? excludedKeys, #else HashSet? excludedKeys, @@ -38,7 +39,7 @@ internal ReadOnlyFilteredTagCollection( this.excludedKeys = excludedKeys; this.tags = tags; - this.count = count; + this.MaximumCount = count; } /// @@ -48,7 +49,7 @@ internal ReadOnlyFilteredTagCollection( /// Note: Enumerating the collection may return fewer results depending on /// the filter. /// - internal int MaximumCount => this.count; + internal int MaximumCount { get; } /// /// Returns an enumerator that iterates through the tags. @@ -72,7 +73,9 @@ internal ReadOnlyFilteredTagCollection( /// Enumerates the elements of a . /// // Note: Does not implement IEnumerator<> to prevent accidental boxing. +#pragma warning disable CA1034 // Nested types should not be visible - already part of public API public struct Enumerator +#pragma warning restore CA1034 // Nested types should not be visible - already part of public API { private readonly ReadOnlyFilteredTagCollection source; private int index; diff --git a/src/OpenTelemetry/ReadOnlyTagCollection.cs b/src/OpenTelemetry/ReadOnlyTagCollection.cs index f8582e1af99..c89386da8d7 100644 --- a/src/OpenTelemetry/ReadOnlyTagCollection.cs +++ b/src/OpenTelemetry/ReadOnlyTagCollection.cs @@ -8,13 +8,15 @@ namespace OpenTelemetry; /// // Note: Does not implement IReadOnlyCollection<> or IEnumerable<> to // prevent accidental boxing. +#pragma warning disable CA1711 // Identifiers should not have incorrect suffix public readonly struct ReadOnlyTagCollection +#pragma warning restore CA1711 // Identifiers should not have incorrect suffix { internal readonly KeyValuePair[] KeyAndValues; internal ReadOnlyTagCollection(KeyValuePair[]? keyAndValues) { - this.KeyAndValues = keyAndValues ?? Array.Empty>(); + this.KeyAndValues = keyAndValues ?? []; } /// @@ -32,7 +34,9 @@ internal ReadOnlyTagCollection(KeyValuePair[]? keyAndValues) /// Enumerates the elements of a . /// // Note: Does not implement IEnumerator<> to prevent accidental boxing. +#pragma warning disable CA1034 // Nested types should not be visible - already part of public API public struct Enumerator +#pragma warning restore CA1034 // Nested types should not be visible - already part of public API { private readonly ReadOnlyTagCollection source; private int index; diff --git a/src/OpenTelemetry/Resources/OtelEnvResourceDetector.cs b/src/OpenTelemetry/Resources/OtelEnvResourceDetector.cs index 41b5e7f28b1..468b37ca1a9 100644 --- a/src/OpenTelemetry/Resources/OtelEnvResourceDetector.cs +++ b/src/OpenTelemetry/Resources/OtelEnvResourceDetector.cs @@ -24,14 +24,14 @@ public Resource Detect() if (this.configuration.TryGetStringValue(EnvVarKey, out string? envResourceAttributeValue)) { - var attributes = ParseResourceAttributes(envResourceAttributeValue!); + var attributes = ParseResourceAttributes(envResourceAttributeValue); resource = new Resource(attributes); } return resource; } - private static IEnumerable> ParseResourceAttributes(string resourceAttributes) + private static List> ParseResourceAttributes(string resourceAttributes) { var attributes = new List>(); diff --git a/src/OpenTelemetry/Resources/OtelServiceNameEnvVarDetector.cs b/src/OpenTelemetry/Resources/OtelServiceNameEnvVarDetector.cs index 0b58f1d55b4..4bee4eadbbb 100644 --- a/src/OpenTelemetry/Resources/OtelServiceNameEnvVarDetector.cs +++ b/src/OpenTelemetry/Resources/OtelServiceNameEnvVarDetector.cs @@ -24,7 +24,7 @@ public Resource Detect() { resource = new Resource(new Dictionary { - [ResourceSemanticConventions.AttributeServiceName] = envResourceAttributeValue!, + [ResourceSemanticConventions.AttributeServiceName] = envResourceAttributeValue, }); } diff --git a/src/OpenTelemetry/Resources/Resource.cs b/src/OpenTelemetry/Resources/Resource.cs index a911deddf2e..afb21552675 100644 --- a/src/OpenTelemetry/Resources/Resource.cs +++ b/src/OpenTelemetry/Resources/Resource.cs @@ -24,7 +24,7 @@ public Resource(IEnumerable> attributes) if (attributes == null) { OpenTelemetrySdkEventSource.Log.InvalidArgument("Create resource", "attributes", "are null"); - this.Attributes = Enumerable.Empty>(); + this.Attributes = []; return; } @@ -35,7 +35,7 @@ public Resource(IEnumerable> attributes) /// /// Gets an empty Resource. /// - public static Resource Empty { get; } = new Resource(Enumerable.Empty>()); + public static Resource Empty { get; } = new([]); /// /// Gets the collection of key-value pairs describing the resource. @@ -105,8 +105,8 @@ private static object SanitizeValue(object value, string keyName) bool[] => value, double[] => value, long[] => value, - int => Convert.ToInt64(value), - short => Convert.ToInt64(value), + int => Convert.ToInt64(value, CultureInfo.InvariantCulture), + short => Convert.ToInt64(value, CultureInfo.InvariantCulture), float => Convert.ToDouble(value, CultureInfo.InvariantCulture), int[] v => Array.ConvertAll(v, Convert.ToInt64), short[] v => Array.ConvertAll(v, Convert.ToInt64), diff --git a/src/OpenTelemetry/Resources/ResourceBuilder.cs b/src/OpenTelemetry/Resources/ResourceBuilder.cs index f85755d7d02..0ab88d4d3f9 100644 --- a/src/OpenTelemetry/Resources/ResourceBuilder.cs +++ b/src/OpenTelemetry/Resources/ResourceBuilder.cs @@ -11,31 +11,8 @@ namespace OpenTelemetry.Resources; /// public class ResourceBuilder { - internal readonly List ResourceDetectors = new(); - private static readonly Resource DefaultResource; - - static ResourceBuilder() - { - var defaultServiceName = "unknown_service"; - - try - { - var processName = Process.GetCurrentProcess().ProcessName; - if (!string.IsNullOrWhiteSpace(processName)) - { - defaultServiceName = $"{defaultServiceName}:{processName}"; - } - } - catch - { - // GetCurrentProcess can throw PlatformNotSupportedException - } - - DefaultResource = new Resource(new Dictionary - { - [ResourceSemanticConventions.AttributeServiceName] = defaultServiceName, - }); - } + internal readonly List ResourceDetectors = []; + private static readonly Resource DefaultResource = PrepareDefaultResource(); private ResourceBuilder() { @@ -155,6 +132,29 @@ internal ResourceBuilder AddResource(Resource resource) return this; } + private static Resource PrepareDefaultResource() + { + var defaultServiceName = "unknown_service"; + + try + { + var processName = Process.GetCurrentProcess().ProcessName; + if (!string.IsNullOrWhiteSpace(processName)) + { + defaultServiceName = $"{defaultServiceName}:{processName}"; + } + } + catch + { + // GetCurrentProcess can throw PlatformNotSupportedException + } + + return new Resource(new Dictionary + { + [ResourceSemanticConventions.AttributeServiceName] = defaultServiceName, + }); + } + internal sealed class WrapperResourceDetector : IResourceDetector { private readonly Resource resource; @@ -189,7 +189,9 @@ public Resource Detect() Debug.Assert(detector != null, "detector was null"); +#pragma warning disable CA1508 // Avoid dead conditional code return detector?.Detect() ?? Resource.Empty; +#pragma warning restore CA1508 // Avoid dead conditional code } } } diff --git a/src/OpenTelemetry/Resources/ResourceBuilderExtensions.cs b/src/OpenTelemetry/Resources/ResourceBuilderExtensions.cs index 1f02b55381f..07b0040490a 100644 --- a/src/OpenTelemetry/Resources/ResourceBuilderExtensions.cs +++ b/src/OpenTelemetry/Resources/ResourceBuilderExtensions.cs @@ -42,10 +42,11 @@ public static ResourceBuilder AddService( bool autoGenerateServiceInstanceId = true, string? serviceInstanceId = null) { - Dictionary resourceAttributes = new Dictionary(); - + Guard.ThrowIfNull(resourceBuilder); Guard.ThrowIfNullOrEmpty(serviceName); + Dictionary resourceAttributes = new Dictionary(); + resourceAttributes.Add(ResourceSemanticConventions.AttributeServiceName, serviceName); if (!string.IsNullOrEmpty(serviceNamespace)) @@ -68,7 +69,9 @@ public static ResourceBuilder AddService( resourceAttributes.Add(ResourceSemanticConventions.AttributeServiceInstance, serviceInstanceId); } +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 return resourceBuilder.AddResource(new Resource(resourceAttributes)); +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 } /// @@ -81,7 +84,10 @@ public static ResourceBuilder AddService( /// Returns for chaining. public static ResourceBuilder AddTelemetrySdk(this ResourceBuilder resourceBuilder) { + Guard.ThrowIfNull(resourceBuilder); +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 return resourceBuilder.AddResource(TelemetryResource); +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 } /// @@ -92,7 +98,10 @@ public static ResourceBuilder AddTelemetrySdk(this ResourceBuilder resourceBuild /// Returns for chaining. public static ResourceBuilder AddAttributes(this ResourceBuilder resourceBuilder, IEnumerable> attributes) { + Guard.ThrowIfNull(resourceBuilder); +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 return resourceBuilder.AddResource(new Resource(attributes)); +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 } /// @@ -105,9 +114,12 @@ public static ResourceBuilder AddAttributes(this ResourceBuilder resourceBuilder /// Returns for chaining. public static ResourceBuilder AddEnvironmentVariableDetector(this ResourceBuilder resourceBuilder) { + Guard.ThrowIfNull(resourceBuilder); Lazy configuration = new Lazy(() => new ConfigurationBuilder().AddEnvironmentVariables().Build()); +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 return resourceBuilder +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 .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 3a95120e3ef..0711a414828 100644 --- a/src/OpenTelemetry/Sdk.cs +++ b/src/OpenTelemetry/Sdk.cs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -#if EXPOSE_EXPERIMENTAL_FEATURES && NET8_0_OR_GREATER +#if EXPOSE_EXPERIMENTAL_FEATURES using System.Diagnostics.CodeAnalysis; #endif using OpenTelemetry.Context.Propagation; @@ -89,9 +89,7 @@ public static TracerProviderBuilder CreateTracerProviderBuilder() /// 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.LogsBridgeExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] -#endif public #else /// diff --git a/src/OpenTelemetry/SimpleExportProcessor.cs b/src/OpenTelemetry/SimpleExportProcessor.cs index 10951cd8533..95387ef997f 100644 --- a/src/OpenTelemetry/SimpleExportProcessor.cs +++ b/src/OpenTelemetry/SimpleExportProcessor.cs @@ -12,7 +12,7 @@ namespace OpenTelemetry; public abstract class SimpleExportProcessor : BaseExportProcessor where T : class { - private readonly object syncObject = new(); + private readonly Lock syncObject = new(); /// /// Initializes a new instance of the class. diff --git a/src/OpenTelemetry/SuppressInstrumentationScope.cs b/src/OpenTelemetry/SuppressInstrumentationScope.cs index 68168bc79d1..f568c0053f6 100644 --- a/src/OpenTelemetry/SuppressInstrumentationScope.cs +++ b/src/OpenTelemetry/SuppressInstrumentationScope.cs @@ -17,7 +17,9 @@ public sealed class SuppressInstrumentationScope : IDisposable // * Depth = [1, int.MaxValue]: instrumentation is suppressed in a reference-counting mode private static readonly RuntimeContextSlot Slot = RuntimeContext.RegisterSlot("otel.suppress_instrumentation"); +#pragma warning disable CA2213 // Disposable fields should be disposed private readonly SuppressInstrumentationScope? previousScope; +#pragma warning restore CA2213 // Disposable fields should be disposed private bool disposed; internal SuppressInstrumentationScope(bool value = true) @@ -73,10 +75,12 @@ public static int Enter() if (currentScope == null) { Slot.Set( +#pragma warning disable CA2000 // Dispose objects before losing scope new SuppressInstrumentationScope() { Depth = 1, }); +#pragma warning restore CA2000 // Dispose objects before losing scope return 1; } diff --git a/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderBase.cs b/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderBase.cs index 062fea7faee..894ebe7b265 100644 --- a/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderBase.cs +++ b/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderBase.cs @@ -82,7 +82,9 @@ TracerProviderBuilder ITracerProviderBuilder.ConfigureServices(Action +#pragma warning disable CA1033 // Interface methods should be callable by child types TracerProviderBuilder IDeferredTracerProviderBuilder.Configure(Action configure) +#pragma warning restore CA1033 // Interface methods should be callable by child types { this.innerBuilder.ConfigureBuilder(configure); diff --git a/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderExtensions.cs index a4c39becf16..c76befd9b75 100644 --- a/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry/Trace/Builder/TracerProviderBuilderExtensions.cs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -#if NET6_0_OR_GREATER +#if NET using System.Diagnostics.CodeAnalysis; #endif using Microsoft.Extensions.DependencyInjection; @@ -27,7 +27,7 @@ public static class TracerProviderBuilderExtensions /// /// This method is not supported in native AOT or Mono Runtime as of .NET 8. /// -#if NET7_0_OR_GREATER +#if NET [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) @@ -75,7 +75,7 @@ public static TracerProviderBuilder SetSampler(this TracerProviderBuilder tracer /// . /// The supplied for chaining. public static TracerProviderBuilder SetSampler< -#if NET6_0_OR_GREATER +#if NET [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] #endif T>(this TracerProviderBuilder tracerProviderBuilder) @@ -195,7 +195,7 @@ public static TracerProviderBuilder AddProcessor(this TracerProviderBuilder trac /// . /// The supplied for chaining. public static TracerProviderBuilder AddProcessor< -#if NET6_0_OR_GREATER +#if NET [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] #endif T>(this TracerProviderBuilder tracerProviderBuilder) diff --git a/src/OpenTelemetry/Trace/ActivityExportProcessorOptions.cs b/src/OpenTelemetry/Trace/Processor/ActivityExportProcessorOptions.cs similarity index 93% rename from src/OpenTelemetry/Trace/ActivityExportProcessorOptions.cs rename to src/OpenTelemetry/Trace/Processor/ActivityExportProcessorOptions.cs index 86acb043015..895863cb41e 100644 --- a/src/OpenTelemetry/Trace/ActivityExportProcessorOptions.cs +++ b/src/OpenTelemetry/Trace/Processor/ActivityExportProcessorOptions.cs @@ -26,7 +26,9 @@ internal ActivityExportProcessorOptions( { Debug.Assert(defaultBatchExportActivityProcessorOptions != null, "defaultBatchExportActivityProcessorOptions was null"); +#pragma warning disable CA1508 // Avoid dead conditional code this.batchExportProcessorOptions = defaultBatchExportActivityProcessorOptions ?? new(); +#pragma warning restore CA1508 // Avoid dead conditional code } /// diff --git a/src/OpenTelemetry/Trace/BatchActivityExportProcessor.cs b/src/OpenTelemetry/Trace/Processor/BatchActivityExportProcessor.cs similarity index 88% rename from src/OpenTelemetry/Trace/BatchActivityExportProcessor.cs rename to src/OpenTelemetry/Trace/Processor/BatchActivityExportProcessor.cs index 22b55f0e39f..73e7968c78f 100644 --- a/src/OpenTelemetry/Trace/BatchActivityExportProcessor.cs +++ b/src/OpenTelemetry/Trace/Processor/BatchActivityExportProcessor.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +using OpenTelemetry.Internal; namespace OpenTelemetry; @@ -36,7 +37,10 @@ public BatchActivityExportProcessor( /// public override void OnEnd(Activity data) { + Guard.ThrowIfNull(data); +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 if (!data.Recorded) +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 { return; } diff --git a/src/OpenTelemetry/Trace/BatchExportActivityProcessorOptions.cs b/src/OpenTelemetry/Trace/Processor/BatchExportActivityProcessorOptions.cs similarity index 100% rename from src/OpenTelemetry/Trace/BatchExportActivityProcessorOptions.cs rename to src/OpenTelemetry/Trace/Processor/BatchExportActivityProcessorOptions.cs diff --git a/src/OpenTelemetry/Trace/ExceptionProcessor.cs b/src/OpenTelemetry/Trace/Processor/ExceptionProcessor.cs similarity index 88% rename from src/OpenTelemetry/Trace/ExceptionProcessor.cs rename to src/OpenTelemetry/Trace/Processor/ExceptionProcessor.cs index f0084d36cfb..a9297f2ded6 100644 --- a/src/OpenTelemetry/Trace/ExceptionProcessor.cs +++ b/src/OpenTelemetry/Trace/Processor/ExceptionProcessor.cs @@ -3,7 +3,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; -#if !NET6_0_OR_GREATER && !NETFRAMEWORK +#if !NET && !NETFRAMEWORK using System.Linq.Expressions; using System.Reflection; #endif @@ -18,7 +18,7 @@ internal sealed class ExceptionProcessor : BaseProcessor public ExceptionProcessor() { -#if NET6_0_OR_GREATER || NETFRAMEWORK +#if NET || NETFRAMEWORK this.fnGetExceptionPointers = Marshal.GetExceptionPointers; #else // When running on netstandard or similar the Marshal class is not a part of the netstandard API @@ -62,11 +62,9 @@ public override void OnEnd(Activity activity) if (snapshot != pointers) { - // TODO: Remove this when SetStatus is deprecated +#pragma warning disable CS0618 // Type or member is obsolete activity.SetStatus(Status.Error); - - // For processors/exporters checking `Status` property. - activity.SetStatus(ActivityStatusCode.Error); +#pragma warning restore CS0618 // Type or member is obsolete } } } diff --git a/src/OpenTelemetry/Trace/SimpleActivityExportProcessor.cs b/src/OpenTelemetry/Trace/Processor/SimpleActivityExportProcessor.cs similarity index 77% rename from src/OpenTelemetry/Trace/SimpleActivityExportProcessor.cs rename to src/OpenTelemetry/Trace/Processor/SimpleActivityExportProcessor.cs index 463bc6d111f..956d035e2c4 100644 --- a/src/OpenTelemetry/Trace/SimpleActivityExportProcessor.cs +++ b/src/OpenTelemetry/Trace/Processor/SimpleActivityExportProcessor.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +using OpenTelemetry.Internal; namespace OpenTelemetry; @@ -22,7 +23,10 @@ public SimpleActivityExportProcessor(BaseExporter exporter) /// public override void OnEnd(Activity data) { + Guard.ThrowIfNull(data); +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 if (!data.Recorded) +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 { return; } diff --git a/src/OpenTelemetry/Trace/AlwaysOffSampler.cs b/src/OpenTelemetry/Trace/Sampler/AlwaysOffSampler.cs similarity index 100% rename from src/OpenTelemetry/Trace/AlwaysOffSampler.cs rename to src/OpenTelemetry/Trace/Sampler/AlwaysOffSampler.cs diff --git a/src/OpenTelemetry/Trace/AlwaysOnSampler.cs b/src/OpenTelemetry/Trace/Sampler/AlwaysOnSampler.cs similarity index 100% rename from src/OpenTelemetry/Trace/AlwaysOnSampler.cs rename to src/OpenTelemetry/Trace/Sampler/AlwaysOnSampler.cs diff --git a/src/OpenTelemetry/Trace/ParentBasedSampler.cs b/src/OpenTelemetry/Trace/Sampler/ParentBasedSampler.cs similarity index 96% rename from src/OpenTelemetry/Trace/ParentBasedSampler.cs rename to src/OpenTelemetry/Trace/Sampler/ParentBasedSampler.cs index c7665cc3f36..354cf5bf0db 100644 --- a/src/OpenTelemetry/Trace/ParentBasedSampler.cs +++ b/src/OpenTelemetry/Trace/Sampler/ParentBasedSampler.cs @@ -33,7 +33,9 @@ public ParentBasedSampler(Sampler rootSampler) Guard.ThrowIfNull(rootSampler); this.rootSampler = rootSampler; +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 this.Description = $"ParentBased{{{rootSampler.Description}}}"; +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 this.remoteParentSampled = new AlwaysOnSampler(); this.remoteParentNotSampled = new AlwaysOffSampler(); diff --git a/src/OpenTelemetry/Trace/Sampler.cs b/src/OpenTelemetry/Trace/Sampler/Sampler.cs similarity index 100% rename from src/OpenTelemetry/Trace/Sampler.cs rename to src/OpenTelemetry/Trace/Sampler/Sampler.cs diff --git a/src/OpenTelemetry/Trace/SamplingDecision.cs b/src/OpenTelemetry/Trace/Sampler/SamplingDecision.cs similarity index 100% rename from src/OpenTelemetry/Trace/SamplingDecision.cs rename to src/OpenTelemetry/Trace/Sampler/SamplingDecision.cs diff --git a/src/OpenTelemetry/Trace/SamplingParameters.cs b/src/OpenTelemetry/Trace/Sampler/SamplingParameters.cs similarity index 100% rename from src/OpenTelemetry/Trace/SamplingParameters.cs rename to src/OpenTelemetry/Trace/Sampler/SamplingParameters.cs diff --git a/src/OpenTelemetry/Trace/SamplingResult.cs b/src/OpenTelemetry/Trace/Sampler/SamplingResult.cs similarity index 99% rename from src/OpenTelemetry/Trace/SamplingResult.cs rename to src/OpenTelemetry/Trace/Sampler/SamplingResult.cs index 96a2fcbeeb2..bedebd0abaa 100644 --- a/src/OpenTelemetry/Trace/SamplingResult.cs +++ b/src/OpenTelemetry/Trace/Sampler/SamplingResult.cs @@ -102,7 +102,7 @@ public override bool Equals(object? obj) /// public override int GetHashCode() { -#if NET6_0_OR_GREATER +#if NET || NETSTANDARD2_1_OR_GREATER HashCode hashCode = default; hashCode.Add(this.Decision); hashCode.Add(this.Attributes); diff --git a/src/OpenTelemetry/Trace/TraceIdRatioBasedSampler.cs b/src/OpenTelemetry/Trace/Sampler/TraceIdRatioBasedSampler.cs similarity index 100% rename from src/OpenTelemetry/Trace/TraceIdRatioBasedSampler.cs rename to src/OpenTelemetry/Trace/Sampler/TraceIdRatioBasedSampler.cs diff --git a/src/OpenTelemetry/Trace/TracerProviderExtensions.cs b/src/OpenTelemetry/Trace/TracerProviderExtensions.cs index 864e36959b1..7dc8c8f1376 100644 --- a/src/OpenTelemetry/Trace/TracerProviderExtensions.cs +++ b/src/OpenTelemetry/Trace/TracerProviderExtensions.cs @@ -24,7 +24,9 @@ public static TracerProvider AddProcessor(this TracerProvider provider, BaseProc if (provider is TracerProviderSdk tracerProviderSdk) { +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 tracerProviderSdk.AddProcessor(processor); +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 } return provider; diff --git a/src/OpenTelemetry/Trace/TracerProviderSdk.cs b/src/OpenTelemetry/Trace/TracerProviderSdk.cs index 2da7f0ea4d5..d79fe0b051c 100644 --- a/src/OpenTelemetry/Trace/TracerProviderSdk.cs +++ b/src/OpenTelemetry/Trace/TracerProviderSdk.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +using System.Globalization; using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; @@ -18,11 +19,11 @@ internal sealed class TracerProviderSdk : TracerProvider internal const string TracesSamplerArgConfigKey = "OTEL_TRACES_SAMPLER_ARG"; internal readonly IServiceProvider ServiceProvider; - internal readonly IDisposable? OwnedServiceProvider; + internal IDisposable? OwnedServiceProvider; internal int ShutdownCount; internal bool Disposed; - private readonly List instrumentations = new(); + private readonly List instrumentations = []; private readonly ActivityListener listener; private readonly Sampler sampler; private readonly Action getRequestedDataAction; @@ -111,7 +112,7 @@ internal TracerProviderSdk( OpenTelemetrySdkEventSource.Log.TracerProviderSdkEvent($"Instrumentations added = \"{instrumentationFactoriesAdded}\"."); } - var listener = new ActivityListener(); + var activityListener = new ActivityListener(); if (this.supportLegacyActivity) { @@ -125,7 +126,7 @@ internal TracerProviderSdk( legacyActivityPredicate = activity => state.LegacyActivityOperationNames.Contains(activity.OperationName); } - listener.ActivityStarted = activity => + activityListener.ActivityStarted = activity => { OpenTelemetrySdkEventSource.Log.ActivityStarted(activity); @@ -163,7 +164,7 @@ internal TracerProviderSdk( } }; - listener.ActivityStopped = activity => + activityListener.ActivityStopped = activity => { OpenTelemetrySdkEventSource.Log.ActivityStopped(activity); @@ -194,7 +195,7 @@ internal TracerProviderSdk( } else { - listener.ActivityStarted = activity => + activityListener.ActivityStarted = activity => { OpenTelemetrySdkEventSource.Log.ActivityStarted(activity); @@ -204,7 +205,7 @@ internal TracerProviderSdk( } }; - listener.ActivityStopped = activity => + activityListener.ActivityStopped = activity => { OpenTelemetrySdkEventSource.Log.ActivityStopped(activity); @@ -230,20 +231,20 @@ internal TracerProviderSdk( if (this.sampler is AlwaysOnSampler) { - listener.Sample = (ref ActivityCreationOptions options) => + activityListener.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; + activityListener.Sample = (ref ActivityCreationOptions options) => + !Sdk.SuppressInstrumentation ? PropagateOrIgnoreData(ref options) : 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) => + activityListener.Sample = (ref ActivityCreationOptions options) => !Sdk.SuppressInstrumentation ? ComputeActivitySamplingResult(ref options, this.sampler) : ActivitySamplingResult.None; this.getRequestedDataAction = this.RunGetRequestedDataOtherSampler; } @@ -251,7 +252,7 @@ internal TracerProviderSdk( // Sources can be null. This happens when user // is only interested in InstrumentationLibraries // which do not depend on ActivitySources. - if (state.Sources.Any()) + if (state.Sources.Count > 0) { // Validation of source name is already done in builder. if (state.Sources.Any(s => WildcardHelper.ContainsWildcard(s))) @@ -260,7 +261,7 @@ internal TracerProviderSdk( // Function which takes ActivitySource and returns true/false to indicate if it should be subscribed to // or not. - listener.ShouldListenTo = (activitySource) => + activityListener.ShouldListenTo = activitySource => this.supportLegacyActivity ? string.IsNullOrEmpty(activitySource.Name) || regex.IsMatch(activitySource.Name) : regex.IsMatch(activitySource.Name); @@ -276,19 +277,19 @@ internal TracerProviderSdk( // 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); + activityListener.ShouldListenTo = activitySource => activitySources.Contains(activitySource.Name); } } else { if (this.supportLegacyActivity) { - listener.ShouldListenTo = (activitySource) => string.IsNullOrEmpty(activitySource.Name); + activityListener.ShouldListenTo = activitySource => string.IsNullOrEmpty(activitySource.Name); } } - ActivitySource.AddActivityListener(listener); - this.listener = listener; + ActivitySource.AddActivityListener(activityListener); + this.listener = activityListener; OpenTelemetrySdkEventSource.Log.TracerProviderSdkEvent("TracerProvider built successfully."); } @@ -352,18 +353,14 @@ internal bool OnForceFlush(int timeoutMilliseconds) 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) { - foreach (var item in this.instrumentations) - { - (item as IDisposable)?.Dispose(); - } - - this.instrumentations.Clear(); + (item as IDisposable)?.Dispose(); } - result = this.processor?.Shutdown(timeoutMilliseconds); + this.instrumentations.Clear(); + + bool? result = this.processor?.Shutdown(timeoutMilliseconds); this.listener?.Dispose(); return result ?? true; } @@ -374,21 +371,19 @@ protected override void Dispose(bool disposing) { if (disposing) { - 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.instrumentations.Clear(); + (this.sampler as IDisposable)?.Dispose(); // Wait for up to 5 seconds grace period this.processor?.Shutdown(5000); this.processor?.Dispose(); + this.processor = null; // 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 @@ -396,6 +391,7 @@ protected override void Dispose(bool disposing) this.listener?.Dispose(); this.OwnedServiceProvider?.Dispose(); + this.OwnedServiceProvider = null; } this.Disposed = true; @@ -447,7 +443,7 @@ private static Sampler GetSampler(IConfiguration configuration, Sampler? stateSa } default: - OpenTelemetrySdkEventSource.Log.TracesSamplerConfigInvalid(configValue ?? string.Empty); + OpenTelemetrySdkEventSource.Log.TracesSamplerConfigInvalid(configValue); break; } @@ -463,7 +459,7 @@ private static Sampler GetSampler(IConfiguration configuration, Sampler? stateSa private static double ReadTraceIdRatio(IConfiguration configuration) { if (configuration.TryGetStringValue(TracesSamplerArgConfigKey, out var configValue) && - double.TryParse(configValue, out var traceIdRatio)) + double.TryParse(configValue, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var traceIdRatio)) { return traceIdRatio; } @@ -493,47 +489,46 @@ private static ActivitySamplingResult ComputeActivitySamplingResult( { SamplingDecision.RecordAndSample => ActivitySamplingResult.AllDataAndRecorded, SamplingDecision.RecordOnly => ActivitySamplingResult.AllData, - _ => ActivitySamplingResult.PropagationData, + _ => PropagateOrIgnoreData(ref options), }; - if (activitySamplingResult != ActivitySamplingResult.PropagationData) + if (activitySamplingResult > ActivitySamplingResult.PropagationData) { foreach (var att in samplingResult.Attributes) { options.SamplingTags.Add(att.Key, att.Value); } + } + if (activitySamplingResult != ActivitySamplingResult.None + && samplingResult.TraceStateString != null) + { // 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 + // if sampler returns null, its treated as if it has not intended // 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 activitySamplingResult; + options = options with { TraceState = samplingResult.TraceStateString }; } - return PropagateOrIgnoreData(options.Parent); + return activitySamplingResult; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ActivitySamplingResult PropagateOrIgnoreData(in ActivityContext parentContext) + private static ActivitySamplingResult PropagateOrIgnoreData(ref ActivityCreationOptions options) { - var isRootSpan = parentContext.TraceId == default; + var isRootSpan = options.Parent.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) + return (isRootSpan || options.Parent.IsRemote) ? ActivitySamplingResult.PropagationData : ActivitySamplingResult.None; } @@ -606,11 +601,11 @@ private void RunGetRequestedDataOtherSampler(Activity activity) { activity.SetTag(att.Key, att.Value); } + } - if (samplingResult.TraceStateString != null) - { - activity.TraceStateString = samplingResult.TraceStateString; - } + if (samplingResult.TraceStateString != null) + { + activity.TraceStateString = samplingResult.TraceStateString; } } } diff --git a/src/Shared/ActivityHelperExtensions.cs b/src/Shared/ActivityHelperExtensions.cs index b237ccd5457..4fd1f517dab 100644 --- a/src/Shared/ActivityHelperExtensions.cs +++ b/src/Shared/ActivityHelperExtensions.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; using System.Runtime.CompilerServices; using OpenTelemetry.Internal; @@ -24,6 +22,7 @@ internal static class ActivityHelperExtensions /// Status description. /// if was found on the supplied Activity. [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Obsolete] public static bool TryGetStatus(this Activity activity, out StatusCode statusCode, out string? statusDescription) { Debug.Assert(activity != null, "Activity should not be null"); diff --git a/src/Shared/AssemblyVersionExtensions.cs b/src/Shared/AssemblyVersionExtensions.cs index 029a3c2451c..9d4b351a3ab 100644 --- a/src/Shared/AssemblyVersionExtensions.cs +++ b/src/Shared/AssemblyVersionExtensions.cs @@ -1,9 +1,8 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace OpenTelemetry.Internal; @@ -11,6 +10,33 @@ namespace OpenTelemetry.Internal; internal static class AssemblyVersionExtensions { public static string GetPackageVersion(this Assembly assembly) + { + Debug.Assert(assembly != null, "assembly was null"); + + var informationalVersion = assembly.GetCustomAttribute()?.InformationalVersion; + + Debug.Assert(!string.IsNullOrEmpty(informationalVersion), "AssemblyInformationalVersionAttribute was not found in assembly"); + + return ParsePackageVersion(informationalVersion!); + } + + public static bool TryGetPackageVersion(this Assembly assembly, [NotNullWhen(true)] out string? packageVersion) + { + Debug.Assert(assembly != null, "assembly was null"); + + var informationalVersion = assembly.GetCustomAttribute()?.InformationalVersion; + + if (string.IsNullOrEmpty(informationalVersion)) + { + packageVersion = null; + return false; + } + + packageVersion = ParsePackageVersion(informationalVersion!); + return true; + } + + private static string ParsePackageVersion(string informationalVersion) { // MinVer https://github.com/adamralph/minver?tab=readme-ov-file#version-numbers // together with Microsoft.SourceLink.GitHub https://github.com/dotnet/sourcelink @@ -20,10 +46,11 @@ public static string GetPackageVersion(this Assembly assembly) // The following parts are optional: pre-release label, pre-release version, git height, Git SHA of current commit // For package version, value of AssemblyInformationalVersionAttribute without commit hash is returned. - var informationalVersion = assembly.GetCustomAttribute()?.InformationalVersion; - Debug.Assert(!string.IsNullOrEmpty(informationalVersion), "AssemblyInformationalVersionAttribute was not found in assembly"); - - var indexOfPlusSign = informationalVersion!.IndexOf('+'); +#if NET || NETSTANDARD2_1_OR_GREATER + var indexOfPlusSign = informationalVersion.IndexOf('+', StringComparison.Ordinal); +#else + var indexOfPlusSign = informationalVersion.IndexOf('+'); +#endif return indexOfPlusSign > 0 ? informationalVersion.Substring(0, indexOfPlusSign) : informationalVersion; diff --git a/src/Shared/Configuration/IConfigurationExtensionsLogger.cs b/src/Shared/Configuration/IConfigurationExtensionsLogger.cs index 0835cf6ff89..196a485bf33 100644 --- a/src/Shared/Configuration/IConfigurationExtensionsLogger.cs +++ b/src/Shared/Configuration/IConfigurationExtensionsLogger.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - namespace Microsoft.Extensions.Configuration; internal interface IConfigurationExtensionsLogger diff --git a/src/Shared/Configuration/OpenTelemetryConfigurationExtensions.cs b/src/Shared/Configuration/OpenTelemetryConfigurationExtensions.cs index d591ae22076..589f960d512 100644 --- a/src/Shared/Configuration/OpenTelemetryConfigurationExtensions.cs +++ b/src/Shared/Configuration/OpenTelemetryConfigurationExtensions.cs @@ -1,12 +1,8 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; -#if !NETFRAMEWORK && !NETSTANDARD2_0 using System.Diagnostics.CodeAnalysis; -#endif using System.Globalization; namespace Microsoft.Extensions.Configuration; @@ -15,17 +11,13 @@ internal static class OpenTelemetryConfigurationExtensions { public delegate bool TryParseFunc( string value, -#if !NETFRAMEWORK && !NETSTANDARD2_0 [NotNullWhen(true)] -#endif out T? parsedValue); public static bool TryGetStringValue( this IConfiguration configuration, string key, -#if !NETFRAMEWORK && !NETSTANDARD2_0 [NotNullWhen(true)] -#endif out string? value) { Debug.Assert(configuration != null, "configuration was null"); @@ -39,9 +31,7 @@ public static bool TryGetUriValue( this IConfiguration configuration, IConfigurationExtensionsLogger logger, string key, -#if !NETFRAMEWORK && !NETSTANDARD2_0 [NotNullWhen(true)] -#endif out Uri? value) { Debug.Assert(logger != null, "logger was null"); @@ -54,7 +44,7 @@ public static bool TryGetUriValue( if (!Uri.TryCreate(stringValue, UriKind.Absolute, out value)) { - logger!.LogInvalidConfigurationValue(key, stringValue!); + logger!.LogInvalidConfigurationValue(key, stringValue); return false; } @@ -77,7 +67,7 @@ public static bool TryGetIntValue( if (!int.TryParse(stringValue, NumberStyles.None, CultureInfo.InvariantCulture, out value)) { - logger!.LogInvalidConfigurationValue(key, stringValue!); + logger!.LogInvalidConfigurationValue(key, stringValue); return false; } @@ -100,7 +90,7 @@ public static bool TryGetBoolValue( if (!bool.TryParse(stringValue, out value)) { - logger!.LogInvalidConfigurationValue(key, stringValue!); + logger!.LogInvalidConfigurationValue(key, stringValue); return false; } @@ -112,9 +102,7 @@ public static bool TryGetValue( IConfigurationExtensionsLogger logger, string key, TryParseFunc tryParseFunc, -#if !NETFRAMEWORK && !NETSTANDARD2_0 [NotNullWhen(true)] -#endif out T? value) { Debug.Assert(logger != null, "logger was null"); @@ -125,9 +113,9 @@ public static bool TryGetValue( return false; } - if (!tryParseFunc(stringValue!, out value)) + if (!tryParseFunc(stringValue, out value)) { - logger!.LogInvalidConfigurationValue(key, stringValue!); + logger!.LogInvalidConfigurationValue(key, stringValue); return false; } diff --git a/src/Shared/DiagnosticDefinitions.cs b/src/Shared/DiagnosticDefinitions.cs index 94bf1c5f52d..f6b449352ff 100644 --- a/src/Shared/DiagnosticDefinitions.cs +++ b/src/Shared/DiagnosticDefinitions.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - namespace OpenTelemetry.Internal; internal static class DiagnosticDefinitions @@ -11,10 +9,10 @@ internal static class DiagnosticDefinitions public const string LoggerProviderExperimentalApi = "OTEL1000"; public const string LogsBridgeExperimentalApi = "OTEL1001"; - public const string CardinalityLimitExperimentalApi = "OTEL1003"; public const string ExemplarReservoirExperimentalApi = "OTEL1004"; /* Definitions which have been released stable: public const string ExemplarExperimentalApi = "OTEL1002"; + public const string CardinalityLimitExperimentalApi = "OTEL1003"; */ } diff --git a/src/Shared/ExceptionExtensions.cs b/src/Shared/ExceptionExtensions.cs index 9070b59c206..5071a8feebb 100644 --- a/src/Shared/ExceptionExtensions.cs +++ b/src/Shared/ExceptionExtensions.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Globalization; namespace OpenTelemetry.Internal; diff --git a/src/Shared/Guard.cs b/src/Shared/Guard.cs index e768d676045..dbbe87e0e8f 100644 --- a/src/Shared/Guard.cs +++ b/src/Shared/Guard.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -12,24 +10,24 @@ #pragma warning disable SA1403 // File may only contain a single namespace #pragma warning disable SA1649 // File name should match first type name -#if !NET6_0_OR_GREATER +#if !NET namespace System.Runtime.CompilerServices { /// Allows capturing of the expressions passed to a method. [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] internal sealed class CallerArgumentExpressionAttribute : Attribute { - public CallerArgumentExpressionAttribute(string parameterName) + public CallerArgumentExpressionAttribute(string? parameterName) { this.ParameterName = parameterName; } - public string ParameterName { get; } + public string? ParameterName { get; } } } #endif -#if !NET6_0_OR_GREATER && !NETSTANDARD2_1_OR_GREATER +#if !NET && !NETSTANDARD2_1_OR_GREATER namespace System.Diagnostics.CodeAnalysis { /// Specifies that an output is not even if @@ -57,7 +55,7 @@ internal static class Guard /// The parameter name to use in the thrown exception. [DebuggerHidden] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfNull([NotNull] object? value, [CallerArgumentExpression("value")] string? paramName = null) + public static void ThrowIfNull([NotNull] object? value, [CallerArgumentExpression(nameof(value))] string? paramName = null) { if (value is null) { @@ -72,7 +70,7 @@ public static void ThrowIfNull([NotNull] object? value, [CallerArgumentExpressio /// The parameter name to use in the thrown exception. [DebuggerHidden] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfNullOrEmpty([NotNull] string? value, [CallerArgumentExpression("value")] string? paramName = null) + public static void ThrowIfNullOrEmpty([NotNull] string? value, [CallerArgumentExpression(nameof(value))] string? paramName = null) #pragma warning disable CS8777 // Parameter must have a non-null value when exiting. { if (string.IsNullOrEmpty(value)) @@ -89,7 +87,7 @@ public static void ThrowIfNullOrEmpty([NotNull] string? value, [CallerArgumentEx /// The parameter name to use in the thrown exception. [DebuggerHidden] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfNullOrWhitespace([NotNull] string? value, [CallerArgumentExpression("value")] string? paramName = null) + public static void ThrowIfNullOrWhitespace([NotNull] string? value, [CallerArgumentExpression(nameof(value))] string? paramName = null) #pragma warning disable CS8777 // Parameter must have a non-null value when exiting. { if (string.IsNullOrWhiteSpace(value)) @@ -107,7 +105,7 @@ public static void ThrowIfNullOrWhitespace([NotNull] string? value, [CallerArgum /// The parameter name to use in the thrown exception. [DebuggerHidden] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfZero(int value, string message = "Must not be zero", [CallerArgumentExpression("value")] string? paramName = null) + public static void ThrowIfZero(int value, string message = "Must not be zero", [CallerArgumentExpression(nameof(value))] string? paramName = null) { if (value == 0) { @@ -122,7 +120,7 @@ public static void ThrowIfZero(int value, string message = "Must not be zero", [ /// The parameter name to use in the thrown exception. [DebuggerHidden] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfInvalidTimeout(int value, [CallerArgumentExpression("value")] string? paramName = null) + public static void ThrowIfInvalidTimeout(int value, [CallerArgumentExpression(nameof(value))] string? paramName = null) { ThrowIfOutOfRange(value, paramName, min: Timeout.Infinite, message: $"Must be non-negative or '{nameof(Timeout)}.{nameof(Timeout.Infinite)}'"); } @@ -139,7 +137,7 @@ public static void ThrowIfInvalidTimeout(int value, [CallerArgumentExpression("v /// An optional custom message to use in the thrown exception. [DebuggerHidden] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfOutOfRange(int value, [CallerArgumentExpression("value")] string? paramName = null, int min = int.MinValue, int max = int.MaxValue, string? minName = null, string? maxName = null, string? message = null) + public static void ThrowIfOutOfRange(int value, [CallerArgumentExpression(nameof(value))] string? paramName = null, int min = int.MinValue, int max = int.MaxValue, string? minName = null, string? maxName = null, string? message = null) { Range(value, paramName, min, max, minName, maxName, message); } @@ -156,7 +154,7 @@ public static void ThrowIfOutOfRange(int value, [CallerArgumentExpression("value /// An optional custom message to use in the thrown exception. [DebuggerHidden] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfOutOfRange(double value, [CallerArgumentExpression("value")] string? paramName = null, double min = double.MinValue, double max = double.MaxValue, string? minName = null, string? maxName = null, string? message = null) + public static void ThrowIfOutOfRange(double value, [CallerArgumentExpression(nameof(value))] string? paramName = null, double min = double.MinValue, double max = double.MaxValue, string? minName = null, string? maxName = null, string? message = null) { Range(value, paramName, min, max, minName, maxName, message); } @@ -170,7 +168,7 @@ public static void ThrowIfOutOfRange(double value, [CallerArgumentExpression("va /// The value casted to the specified type. [DebuggerHidden] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static T ThrowIfNotOfType([NotNull] object? value, [CallerArgumentExpression("value")] string? paramName = null) + public static T ThrowIfNotOfType([NotNull] object? value, [CallerArgumentExpression(nameof(value))] string? paramName = null) { if (value is not T result) { diff --git a/src/Shared/MathHelper.cs b/src/Shared/MathHelper.cs index 6bf5b3ac6c5..bb6edaa00a9 100644 --- a/src/Shared/MathHelper.cs +++ b/src/Shared/MathHelper.cs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -#if NET6_0_OR_GREATER +#if NET using System.Numerics; #endif using System.Runtime.CompilerServices; @@ -12,8 +12,8 @@ namespace OpenTelemetry.Internal; internal static class MathHelper { // https://en.wikipedia.org/wiki/Leading_zero - private static readonly byte[] LeadingZeroLookupTable = new byte[] - { + private static readonly byte[] LeadingZeroLookupTable = + [ 8, 7, 6, 6, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, @@ -29,8 +29,8 @@ internal static class MathHelper 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - }; + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ]; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int LeadingZero8(byte value) @@ -73,7 +73,7 @@ public static int LeadingZero32(int value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int LeadingZero64(long value) { -#if NET6_0_OR_GREATER +#if NET return BitOperations.LeadingZeroCount((ulong)value); #else unchecked @@ -123,7 +123,7 @@ public static long PositiveModulo64(long value, long divisor) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsFinite(double value) { -#if NET6_0_OR_GREATER +#if NET return double.IsFinite(value); #else return !double.IsInfinity(value) && !double.IsNaN(value); diff --git a/src/Shared/Metrics/Base2ExponentialBucketHistogramHelper.cs b/src/Shared/Metrics/Base2ExponentialBucketHistogramHelper.cs index 096eb51828d..f9a1ece4c6f 100644 --- a/src/Shared/Metrics/Base2ExponentialBucketHistogramHelper.cs +++ b/src/Shared/Metrics/Base2ExponentialBucketHistogramHelper.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - namespace OpenTelemetry.Metrics; /// @@ -23,7 +21,7 @@ public static double CalculateLowerBoundary(int index, int scale) { if (scale > 0) { -#if NET6_0_OR_GREATER +#if NET var inverseFactor = Math.ScaleB(Ln2, -scale); #else var inverseFactor = ScaleB(Ln2, -scale); @@ -49,7 +47,7 @@ public static double CalculateLowerBoundary(int index, int scale) return double.Epsilon; } -#if NET6_0_OR_GREATER +#if NET return Math.ScaleB(1, n); #else return ScaleB(1, n); @@ -57,7 +55,7 @@ public static double CalculateLowerBoundary(int index, int scale) } } -#if !NET6_0_OR_GREATER +#if !NET // Math.ScaleB was introduced in .NET Core 3.0. // This implementation is from: // https://github.com/dotnet/runtime/blob/v7.0.0/src/libraries/System.Private.CoreLib/src/System/Math.cs#L1494 @@ -107,7 +105,7 @@ private static double ScaleB(double x, int n) } } - double u = BitConverter.Int64BitsToDouble(((long)(0x3ff + n) << 52)); + double u = BitConverter.Int64BitsToDouble((long)(0x3ff + n) << 52); return y * u; } #pragma warning restore SA1119 // Statement should not use unnecessary parenthesis diff --git a/src/Shared/Options/DelegatingOptionsFactory.cs b/src/Shared/Options/DelegatingOptionsFactory.cs index 1b8fe621887..ac5a59fa94b 100644 --- a/src/Shared/Options/DelegatingOptionsFactory.cs +++ b/src/Shared/Options/DelegatingOptionsFactory.cs @@ -13,10 +13,8 @@ we take a dependency on Microsoft.Extensions.Options v5.0.0 (or greater), much example of how that works. */ -#nullable enable - using System.Diagnostics; -#if NET6_0_OR_GREATER +#if NET using System.Diagnostics.CodeAnalysis; #endif using Microsoft.Extensions.Configuration; @@ -27,7 +25,7 @@ namespace Microsoft.Extensions.Options; /// Implementation of . /// /// The type of options being requested. -#if NET6_0_OR_GREATER +#if NET internal sealed class DelegatingOptionsFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TOptions> : #else internal sealed class DelegatingOptionsFactory : @@ -66,9 +64,9 @@ public DelegatingOptionsFactory( this.optionsFactoryFunc = optionsFactoryFunc!; this.configuration = configuration!; - _setups = setups as IConfigureOptions[] ?? new List>(setups).ToArray(); - _postConfigures = postConfigures as IPostConfigureOptions[] ?? new List>(postConfigures).ToArray(); - _validations = validations as IValidateOptions[] ?? new List>(validations).ToArray(); + _setups = setups as IConfigureOptions[] ?? setups.ToArray(); + _postConfigures = postConfigures as IPostConfigureOptions[] ?? postConfigures.ToArray(); + _validations = validations as IValidateOptions[] ?? validations.ToArray(); } /// diff --git a/src/Shared/Options/DelegatingOptionsFactoryServiceCollectionExtensions.cs b/src/Shared/Options/DelegatingOptionsFactoryServiceCollectionExtensions.cs index 69c7b6c3b62..81e9ac0a664 100644 --- a/src/Shared/Options/DelegatingOptionsFactoryServiceCollectionExtensions.cs +++ b/src/Shared/Options/DelegatingOptionsFactoryServiceCollectionExtensions.cs @@ -1,10 +1,8 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; -#if NET6_0_OR_GREATER +#if NET using System.Diagnostics.CodeAnalysis; #endif using Microsoft.Extensions.Configuration; @@ -15,7 +13,7 @@ namespace Microsoft.Extensions.DependencyInjection; internal static class DelegatingOptionsFactoryServiceCollectionExtensions { -#if NET6_0_OR_GREATER +#if NET public static IServiceCollection RegisterOptionsFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>( #else public static IServiceCollection RegisterOptionsFactory( @@ -40,7 +38,7 @@ public static IServiceCollection RegisterOptionsFactory( return services!; } -#if NET6_0_OR_GREATER +#if NET public static IServiceCollection RegisterOptionsFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>( #else public static IServiceCollection RegisterOptionsFactory( @@ -65,7 +63,7 @@ public static IServiceCollection RegisterOptionsFactory( return services!; } -#if NET6_0_OR_GREATER +#if NET public static IServiceCollection DisableOptionsReloading<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>( #else public static IServiceCollection DisableOptionsReloading( diff --git a/src/Shared/Options/SingletonOptionsManager.cs b/src/Shared/Options/SingletonOptionsManager.cs index c1807183e3f..d3b1928d55a 100644 --- a/src/Shared/Options/SingletonOptionsManager.cs +++ b/src/Shared/Options/SingletonOptionsManager.cs @@ -1,15 +1,13 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - -#if NET6_0_OR_GREATER +#if NET using System.Diagnostics.CodeAnalysis; #endif namespace Microsoft.Extensions.Options; -#if NET6_0_OR_GREATER +#if NET internal sealed class SingletonOptionsManager<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TOptions> : IOptionsMonitor, IOptionsSnapshot #else internal sealed class SingletonOptionsManager : IOptionsMonitor, IOptionsSnapshot diff --git a/src/Shared/PeerServiceResolver.cs b/src/Shared/PeerServiceResolver.cs deleted file mode 100644 index d4c0b8fcd13..00000000000 --- a/src/Shared/PeerServiceResolver.cs +++ /dev/null @@ -1,95 +0,0 @@ -// 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/Shared/PeriodicExportingMetricReaderHelper.cs b/src/Shared/PeriodicExportingMetricReaderHelper.cs index d24c74c8f9c..e7dc244a94c 100644 --- a/src/Shared/PeriodicExportingMetricReaderHelper.cs +++ b/src/Shared/PeriodicExportingMetricReaderHelper.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - namespace OpenTelemetry.Metrics; internal static class PeriodicExportingMetricReaderHelper diff --git a/src/Shared/Proto/README.md b/src/Shared/Proto/README.md new file mode 100644 index 00000000000..fdd15e95f94 --- /dev/null +++ b/src/Shared/Proto/README.md @@ -0,0 +1,5 @@ +# OpenTelemetry Protocol (OTLP) Specification + +`.proto` files are copied from the +[`opentelemetry-proto`](https://github.com/open-telemetry/opentelemetry-proto/commit/1a931b4b57c34e7fd8f7dddcaa9b7587840e9c08) +repo. diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/google/rpc/error_details.proto b/src/Shared/Proto/google/rpc/error_details.proto similarity index 100% rename from src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/google/rpc/error_details.proto rename to src/Shared/Proto/google/rpc/error_details.proto diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/google/rpc/status.proto b/src/Shared/Proto/google/rpc/status.proto similarity index 100% rename from src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/google/rpc/status.proto rename to src/Shared/Proto/google/rpc/status.proto diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/collector/README.md b/src/Shared/Proto/opentelemetry/proto/collector/README.md similarity index 100% rename from src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/collector/README.md rename to src/Shared/Proto/opentelemetry/proto/collector/README.md diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/collector/logs/v1/logs_service.proto b/src/Shared/Proto/opentelemetry/proto/collector/logs/v1/logs_service.proto similarity index 100% rename from src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/collector/logs/v1/logs_service.proto rename to src/Shared/Proto/opentelemetry/proto/collector/logs/v1/logs_service.proto diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/collector/logs/v1/logs_service_http.yaml b/src/Shared/Proto/opentelemetry/proto/collector/logs/v1/logs_service_http.yaml similarity index 100% rename from src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/collector/logs/v1/logs_service_http.yaml rename to src/Shared/Proto/opentelemetry/proto/collector/logs/v1/logs_service_http.yaml diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/collector/metrics/v1/metrics_service.proto b/src/Shared/Proto/opentelemetry/proto/collector/metrics/v1/metrics_service.proto similarity index 100% rename from src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/collector/metrics/v1/metrics_service.proto rename to src/Shared/Proto/opentelemetry/proto/collector/metrics/v1/metrics_service.proto diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/collector/metrics/v1/metrics_service_http.yaml b/src/Shared/Proto/opentelemetry/proto/collector/metrics/v1/metrics_service_http.yaml similarity index 100% rename from src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/collector/metrics/v1/metrics_service_http.yaml rename to src/Shared/Proto/opentelemetry/proto/collector/metrics/v1/metrics_service_http.yaml diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/collector/profiles/v1experimental/profiles_service.proto b/src/Shared/Proto/opentelemetry/proto/collector/profiles/v1development/profiles_service.proto similarity index 91% rename from src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/collector/profiles/v1experimental/profiles_service.proto rename to src/Shared/Proto/opentelemetry/proto/collector/profiles/v1development/profiles_service.proto index d0e7894b29c..ab2433ed29d 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/collector/profiles/v1experimental/profiles_service.proto +++ b/src/Shared/Proto/opentelemetry/proto/collector/profiles/v1development/profiles_service.proto @@ -14,15 +14,15 @@ syntax = "proto3"; -package opentelemetry.proto.collector.profiles.v1experimental; +package opentelemetry.proto.collector.profiles.v1development; -import "opentelemetry/proto/profiles/v1experimental/profiles.proto"; +import "opentelemetry/proto/profiles/v1development/profiles.proto"; -option csharp_namespace = "OpenTelemetry.Proto.Collector.Profiles.V1Experimental"; +option csharp_namespace = "OpenTelemetry.Proto.Collector.Profiles.V1Development"; option java_multiple_files = true; -option java_package = "io.opentelemetry.proto.collector.profiles.v1experimental"; +option java_package = "io.opentelemetry.proto.collector.profiles.v1development"; option java_outer_classname = "ProfilesServiceProto"; -option go_package = "go.opentelemetry.io/proto/otlp/collector/profiles/v1experimental"; +option go_package = "go.opentelemetry.io/proto/otlp/collector/profiles/v1development"; // Service that can be used to push profiles between one Application instrumented with // OpenTelemetry and a collector, or between a collector and a central collector. @@ -38,7 +38,7 @@ message ExportProfilesServiceRequest { // element. Intermediary nodes (such as OpenTelemetry Collector) that receive // data from multiple origins typically batch the data before forwarding further and // in that case this array will contain multiple elements. - repeated opentelemetry.proto.profiles.v1experimental.ResourceProfiles resource_profiles = 1; + repeated opentelemetry.proto.profiles.v1development.ResourceProfiles resource_profiles = 1; } message ExportProfilesServiceResponse { diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/collector/profiles/v1experimental/profiles_service_http.yaml b/src/Shared/Proto/opentelemetry/proto/collector/profiles/v1development/profiles_service_http.yaml similarity index 64% rename from src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/collector/profiles/v1experimental/profiles_service_http.yaml rename to src/Shared/Proto/opentelemetry/proto/collector/profiles/v1development/profiles_service_http.yaml index 6bb7cf740b9..6b3b91da07e 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/collector/profiles/v1experimental/profiles_service_http.yaml +++ b/src/Shared/Proto/opentelemetry/proto/collector/profiles/v1development/profiles_service_http.yaml @@ -4,6 +4,6 @@ type: google.api.Service config_version: 3 http: rules: - - selector: opentelemetry.proto.collector.profiles.v1.ProfilesService.Export - post: /v1experimental/profiles + - selector: opentelemetry.proto.collector.profiles.v1development.ProfilesService.Export + post: /v1development/profiles body: "*" diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/collector/trace/v1/trace_service.proto b/src/Shared/Proto/opentelemetry/proto/collector/trace/v1/trace_service.proto similarity index 100% rename from src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/collector/trace/v1/trace_service.proto rename to src/Shared/Proto/opentelemetry/proto/collector/trace/v1/trace_service.proto diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/collector/trace/v1/trace_service_http.yaml b/src/Shared/Proto/opentelemetry/proto/collector/trace/v1/trace_service_http.yaml similarity index 100% rename from src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/collector/trace/v1/trace_service_http.yaml rename to src/Shared/Proto/opentelemetry/proto/collector/trace/v1/trace_service_http.yaml diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/common/v1/common.proto b/src/Shared/Proto/opentelemetry/proto/common/v1/common.proto similarity index 100% rename from src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/common/v1/common.proto rename to src/Shared/Proto/opentelemetry/proto/common/v1/common.proto diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/logs/v1/logs.proto b/src/Shared/Proto/opentelemetry/proto/logs/v1/logs.proto similarity index 91% rename from src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/logs/v1/logs.proto rename to src/Shared/Proto/opentelemetry/proto/logs/v1/logs.proto index f9b97dd7451..5ce16fa0f86 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/logs/v1/logs.proto +++ b/src/Shared/Proto/opentelemetry/proto/logs/v1/logs.proto @@ -56,7 +56,8 @@ message ResourceLogs { 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 + // is recorded in. Notably, the last part of the URL path is the version number of the + // schema: http[s]://server[:port]/path/. 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. @@ -74,7 +75,8 @@ message ScopeLogs { 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 + // is recorded in. Notably, the last part of the URL path is the version number of the + // schema: http[s]://server[:port]/path/. 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; @@ -208,4 +210,16 @@ message LogRecord { // - the field is not present, // - the field contains an invalid value. bytes span_id = 10; -} + + // A unique identifier of event category/type. + // All events with the same event_name are expected to conform to the same + // schema for both their attributes and their body. + // + // Recommended to be fully qualified and short (no longer than 256 characters). + // + // Presence of event_name on the log record identifies this record + // as an event. + // + // [Optional]. + string event_name = 12; +} \ No newline at end of file diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/metrics/v1/metrics.proto b/src/Shared/Proto/opentelemetry/proto/metrics/v1/metrics.proto similarity index 95% rename from src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/metrics/v1/metrics.proto rename to src/Shared/Proto/opentelemetry/proto/metrics/v1/metrics.proto index 19bb7ff8d53..00c5112ce8b 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/metrics/v1/metrics.proto +++ b/src/Shared/Proto/opentelemetry/proto/metrics/v1/metrics.proto @@ -29,6 +29,24 @@ option go_package = "go.opentelemetry.io/proto/otlp/metrics/v1"; // storage, OR can be embedded by other protocols that transfer OTLP metrics // data but do not implement the OTLP protocol. // +// MetricsData +// └─── ResourceMetrics +// ├── Resource +// ├── SchemaURL +// └── ScopeMetrics +// ├── Scope +// ├── SchemaURL +// └── Metric +// ├── Name +// ├── Description +// ├── Unit +// └── data +// ├── Gauge +// ├── Sum +// ├── Histogram +// ├── ExponentialHistogram +// └── Summary +// // The main difference between this message and collector protocol is that // in this message there will not be any "control" or "metadata" specific to // OTLP protocol. @@ -56,7 +74,8 @@ message ResourceMetrics { 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 + // is recorded in. Notably, the last part of the URL path is the version number of the + // schema: http[s]://server[:port]/path/. 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. @@ -74,7 +93,8 @@ message ScopeMetrics { 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 + // is recorded in. Notably, the last part of the URL path is the version number of the + // schema: http[s]://server[:port]/path/. 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; @@ -85,7 +105,6 @@ message ScopeMetrics { // // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md // -// // The data model and relation between entities is shown in the // diagram below. Here, "DataPoint" is the term used to refer to any // one of the specific data point value types, and "points" is the term used @@ -97,7 +116,7 @@ message ScopeMetrics { // - DataPoint contains timestamps, attributes, and one of the possible value type // fields. // -// Metric +// Metric // +------------+ // |name | // |description | @@ -251,6 +270,9 @@ message ExponentialHistogram { // data type. These data points cannot always be merged in a meaningful way. // While they can be useful in some applications, histogram data points are // recommended for new applications. +// Summary metrics do not have an aggregation temporality field. This is +// because the count and sum fields of a SummaryDataPoint are assumed to be +// cumulative values. message Summary { repeated SummaryDataPoint data_points = 1; } @@ -430,7 +452,7 @@ message HistogramDataPoint { // events, and is assumed to be monotonic over the values of these events. // Negative events *can* be recorded, but sum should not be filled out when // doing so. This is specifically to enforce compatibility w/ OpenMetrics, - // see: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#histogram + // see: https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#histogram optional double sum = 5; // bucket_counts is an optional field contains the count values of histogram @@ -509,7 +531,7 @@ message ExponentialHistogramDataPoint { // events, and is assumed to be monotonic over the values of these events. // Negative events *can* be recorded, but sum should not be filled out when // doing so. This is specifically to enforce compatibility w/ OpenMetrics, - // see: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#histogram + // see: https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#histogram optional double sum = 5; // scale describes the resolution of the histogram. Boundaries are @@ -589,7 +611,8 @@ message ExponentialHistogramDataPoint { } // SummaryDataPoint is a single data point in a timeseries that describes the -// time-varying values of a Summary metric. +// time-varying values of a Summary metric. The count and sum fields represent +// cumulative values. message SummaryDataPoint { reserved 1; @@ -622,7 +645,7 @@ message SummaryDataPoint { // events, and is assumed to be monotonic over the values of these events. // Negative events *can* be recorded, but sum should not be filled out when // doing so. This is specifically to enforce compatibility w/ OpenMetrics, - // see: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#summary + // see: https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#summary double sum = 5; // Represents the value at a given quantile of a distribution. diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/profiles/v1experimental/pprofextended.proto b/src/Shared/Proto/opentelemetry/proto/profiles/v1development/profiles.proto similarity index 54% rename from src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/profiles/v1experimental/pprofextended.proto rename to src/Shared/Proto/opentelemetry/proto/profiles/v1development/profiles.proto index bd300835546..1cb20b05c10 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/profiles/v1experimental/pprofextended.proto +++ b/src/Shared/Proto/opentelemetry/proto/profiles/v1development/profiles.proto @@ -28,6 +28,126 @@ // See the License for the specific language governing permissions and // limitations under the License. +syntax = "proto3"; + +package opentelemetry.proto.profiles.v1development; + +import "opentelemetry/proto/common/v1/common.proto"; +import "opentelemetry/proto/resource/v1/resource.proto"; + +option csharp_namespace = "OpenTelemetry.Proto.Profiles.V1Development"; +option java_multiple_files = true; +option java_package = "io.opentelemetry.proto.profiles.v1development"; +option java_outer_classname = "ProfilesProto"; +option go_package = "go.opentelemetry.io/proto/otlp/profiles/v1development"; + +// Relationships Diagram +// +// ┌──────────────────┐ LEGEND +// │ ProfilesData │ +// └──────────────────┘ ─────▶ embedded +// │ +// │ 1-n ─────▷ referenced by index +// ▼ +// ┌──────────────────┐ +// │ ResourceProfiles │ +// └──────────────────┘ +// │ +// │ 1-n +// ▼ +// ┌──────────────────┐ +// │ ScopeProfiles │ +// └──────────────────┘ +// │ +// │ 1-1 +// ▼ +// ┌──────────────────┐ +// │ Profile │ +// └──────────────────┘ +// │ n-1 +// │ 1-n ┌───────────────────────────────────────┐ +// ▼ │ ▽ +// ┌──────────────────┐ 1-n ┌──────────────┐ ┌──────────┐ +// │ Sample │ ──────▷ │ KeyValue │ │ Link │ +// └──────────────────┘ └──────────────┘ └──────────┘ +// │ 1-n △ △ +// │ 1-n ┌─────────────────┘ │ 1-n +// ▽ │ │ +// ┌──────────────────┐ n-1 ┌──────────────┐ +// │ Location │ ──────▷ │ Mapping │ +// └──────────────────┘ └──────────────┘ +// │ +// │ 1-n +// ▼ +// ┌──────────────────┐ +// │ Line │ +// └──────────────────┘ +// │ +// │ 1-1 +// ▽ +// ┌──────────────────┐ +// │ Function │ +// └──────────────────┘ +// + +// ProfilesData represents the profiles data that can be stored in persistent storage, +// OR can be embedded by other protocols that transfer OTLP profiles data but do not +// implement the OTLP protocol. +// +// The main difference between this message and collector protocol is that +// in this message there will not be any "control" or "metadata" specific to +// OTLP protocol. +// +// When new fields are added into this message, the OTLP request MUST be updated +// as well. +message ProfilesData { + // An array of ResourceProfiles. + // For data coming from a single resource this array will typically contain + // one element. Intermediary nodes that receive data from multiple origins + // typically batch the data before forwarding further and in that case this + // array will contain multiple elements. + repeated ResourceProfiles resource_profiles = 1; +} + + +// A collection of ScopeProfiles from a Resource. +message ResourceProfiles { + reserved 1000; + + // The resource for the profiles in this message. + // If this field is not set then no resource info is known. + opentelemetry.proto.resource.v1.Resource resource = 1; + + // A list of ScopeProfiles that originate from a resource. + repeated ScopeProfiles scope_profiles = 2; + + // The Schema URL, if known. This is the identifier of the Schema that the resource data + // is recorded in. Notably, the last part of the URL path is the version number of the + // schema: http[s]://server[:port]/path/. 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_profiles" field which have their own schema_url field. + string schema_url = 3; +} + +// A collection of Profiles produced by an InstrumentationScope. +message ScopeProfiles { + // The instrumentation scope information for the profiles in this message. + // Semantically when InstrumentationScope isn't set, it is equivalent with + // an empty instrumentation scope name (unknown). + opentelemetry.proto.common.v1.InstrumentationScope scope = 1; + + // A list of Profiles that originate from an instrumentation scope. + repeated Profile profiles = 2; + + // The Schema URL, if known. This is the identifier of the Schema that the profile data + // is recorded in. Notably, the last part of the URL path is the version number of the + // schema: http[s]://server[:port]/path/. To learn more about Schema URL see + // https://opentelemetry.io/docs/specs/otel/schemas/#schema-url + // This schema_url applies to all profiles in the "profiles" field. + string schema_url = 3; +} + // Profile is a common stacktrace profile format. // // Measurements represented with this format should follow the @@ -52,18 +172,15 @@ // mappings. For every nonzero Location.mapping_id there must be a // unique Mapping with that index. -syntax = "proto3"; - -package opentelemetry.proto.profiles.v1experimental; - -import "opentelemetry/proto/common/v1/common.proto"; - -option csharp_namespace = "OpenTelemetry.Proto.Profiles.V1Experimental"; -option go_package = "go.opentelemetry.io/proto/otlp/profiles/v1experimental"; - // Represents a complete profile, including sample types, samples, // mappings to binaries, locations, functions, string table, and additional metadata. +// It modifies and annotates pprof Profile with OpenTelemetry specific fields. +// +// Note that whilst fields in this message retain the name and field id from pprof in most cases +// for ease of understanding data migration, it is not intended that pprof:Profile and +// OpenTelemetry:Profile encoding be wire compatible. message Profile { + // A description of the samples associated with each Sample.value. // For a cpu profile this might be: // [["cpu","nanoseconds"]] or [["wall","seconds"]] or [["syscall","count"]] @@ -77,58 +194,92 @@ message Profile { repeated Sample sample = 2; // Mapping from address ranges to the image/binary/library mapped // into that address range. mapping[0] will be the main binary. - repeated Mapping mapping = 3; + // If multiple binaries contribute to the Profile and no main + // binary can be identified, mapping[0] has no special meaning. + repeated Mapping mapping_table = 3; // Locations referenced by samples via location_indices. - repeated Location location = 4; + repeated Location location_table = 4; // Array of locations referenced by samples. - repeated int64 location_indices = 15; + repeated int32 location_indices = 5; // Functions referenced by locations. - repeated Function function = 5; + repeated Function function_table = 6; // Lookup table for attributes. - repeated opentelemetry.proto.common.v1.KeyValue attribute_table = 16; + repeated opentelemetry.proto.common.v1.KeyValue attribute_table = 7; // Represents a mapping between Attribute Keys and Units. - repeated AttributeUnit attribute_units = 17; + repeated AttributeUnit attribute_units = 8; // Lookup table for links. - repeated Link link_table = 18; + repeated Link link_table = 9; // A common table for strings referenced by various messages. // string_table[0] must always be "". - repeated string string_table = 6; - // frames with Function.function_name fully matching the following - // regexp will be dropped from the samples, along with their successors. - int64 drop_frames = 7; // Index into string table. - // frames with Function.function_name fully matching the following - // regexp will be kept, even if it matches drop_frames. - int64 keep_frames = 8; // Index into string table. - - // The following fields are informational, do not affect + repeated string string_table = 10; + + // The following fields 9-14 are informational, do not affect // interpretation of results. // Time of collection (UTC) represented as nanoseconds past the epoch. - int64 time_nanos = 9; + int64 time_nanos = 11; // Duration of the profile, if a duration makes sense. - int64 duration_nanos = 10; + int64 duration_nanos = 12; // The kind of events between sampled occurrences. // e.g [ "cpu","cycles" ] or [ "heap","bytes" ] - ValueType period_type = 11; + ValueType period_type = 13; // The number of events between sampled occurrences. - int64 period = 12; + int64 period = 14; // Free-form text associated with the profile. The text is displayed as is // to the user by the tools that read profiles (e.g. by pprof). This field // should not be used to store any machine-readable information, it is only // for human-friendly content. The profile must stay functional if this field // is cleaned. - repeated int64 comment = 13; // Indices into string table. + repeated int32 comment_strindices = 15; // Indices into string table. // Index into the string table of the type of the preferred sample // value. If unset, clients should default to the last sample value. - int64 default_sample_type = 14; + int32 default_sample_type_strindex = 16; + + + // A globally unique identifier for a profile. The ID is a 16-byte array. An ID with + // all zeroes is considered invalid. + // + // This field is required. + bytes profile_id = 17; + + // dropped_attributes_count is the number of attributes that were discarded. Attributes + // can be discarded because their keys are too long or because there are too many + // attributes. If this value is 0, then no attributes were dropped. + uint32 dropped_attributes_count = 19; + + // Specifies format of the original payload. Common values are defined in semantic conventions. [required if original_payload is present] + string original_payload_format = 20; + + // Original payload can be stored in this field. This can be useful for users who want to get the original payload. + // Formats such as JFR are highly extensible and can contain more information than what is defined in this spec. + // Inclusion of original payload should be configurable by the user. Default behavior should be to not include the original payload. + // If the original payload is in pprof format, it SHOULD not be included in this field. + // The field is optional, however if it is present then equivalent converted data should be populated in other fields + // of this message as far as is practicable. + bytes original_payload = 21; + + // References to attributes in attribute_table. [optional] + // It is a collection of key/value pairs. Note, global attributes + // like server name can be set using the resource API. Examples of attributes: + // + // "/http/user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" + // "/http/server_latency": 300 + // "abc.com/myattribute": true + // "abc.com/score": 10.239 + // + // The OpenTelemetry API specification further restricts the allowed value types: + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/README.md#attribute + // Attribute keys MUST be unique (it is not allowed to have more than one + // attribute with the same key). + repeated int32 attribute_indices = 22; } // Represents a mapping between Attribute Keys and Units. message AttributeUnit { // Index into string table. - int64 attribute_key = 1; + int32 attribute_key_strindex = 1; // Index into string table. - int64 unit = 2; + int32 unit_strindex = 2; } // A pointer from a profile Sample to a trace Span. @@ -203,7 +354,7 @@ enum AggregationTemporality { 11. A request is received, the system measures 1 request. 12. The 1 second collection cycle ends. A metric is exported for the number of requests received over the interval of time t_1 to - t_0+1 with a value of 1. + t_1+1 with a value of 1. Note: Even though, when reporting changes since last report time, using CUMULATIVE is valid, it is not recommended. */ @@ -212,8 +363,8 @@ enum AggregationTemporality { // ValueType describes the type and units of a value, with an optional aggregation temporality. message ValueType { - int64 type = 1; // Index into string table. - int64 unit = 2; // Index into string table. + int32 type_strindex = 1; // Index into string table. + int32 unit_strindex = 2; // Index into string table. AggregationTemporality aggregation_temporality = 3; } @@ -223,120 +374,63 @@ message ValueType { // augmented with auxiliary information like the thread-id, some // indicator of a higher level request being handled etc. message Sample { - // The indices recorded here correspond to locations in Profile.location. - // The leaf is at location_index[0]. [deprecated, superseded by locations_start_index / locations_length] - repeated uint64 location_index = 1; - // locations_start_index along with locations_length refers to to a slice of locations in Profile.location. + // locations_start_index along with locations_length refers to to a slice of locations in Profile.location_indices. + int32 locations_start_index = 1; + // locations_length along with locations_start_index refers to a slice of locations in Profile.location_indices. // Supersedes location_index. - uint64 locations_start_index = 7; - // locations_length along with locations_start_index refers to a slice of locations in Profile.location. - // Supersedes location_index. - uint64 locations_length = 8; - // A 128bit id that uniquely identifies this stacktrace, globally. Index into string table. [optional] - uint32 stacktrace_id_index = 9; + int32 locations_length = 2; // The type and unit of each value is defined by the corresponding // entry in Profile.sample_type. All samples must have the same // number of values, the same as the length of Profile.sample_type. // When aggregating multiple samples into a single sample, the // result has a list of values that is the element-wise sum of the // lists of the originals. - repeated int64 value = 2; - // label includes additional context for this sample. It can include - // things like a thread id, allocation size, etc. - // - // NOTE: While possible, having multiple values for the same label key is - // strongly discouraged and should never be used. Most tools (e.g. pprof) do - // not have good (or any) support for multi-value labels. And an even more - // discouraged case is having a string label and a numeric label of the same - // name on a sample. Again, possible to express, but should not be used. - // [deprecated, superseded by attributes] - repeated Label label = 3; + repeated int64 value = 3; // References to attributes in Profile.attribute_table. [optional] - repeated uint64 attributes = 10; + repeated int32 attribute_indices = 4; // Reference to link in Profile.link_table. [optional] - uint64 link = 12; + optional int32 link_index = 5; // Timestamps associated with Sample represented in nanoseconds. These timestamps are expected // to fall within the Profile's time range. [optional] - repeated uint64 timestamps_unix_nano = 13; -} - -// Provides additional context for a sample, -// such as thread ID or allocation size, with optional units. [deprecated] -message Label { - int64 key = 1; // Index into string table - - // At most one of the following must be present - int64 str = 2; // Index into string table - int64 num = 3; - - // Should only be present when num is present. - // Specifies the units of num. - // Use arbitrary string (for example, "requests") as a custom count unit. - // If no unit is specified, consumer may apply heuristic to deduce the unit. - // Consumers may also interpret units like "bytes" and "kilobytes" as memory - // units and units like "seconds" and "nanoseconds" as time units, - // and apply appropriate unit conversions to these. - int64 num_unit = 4; // Index into string table -} - -// Indicates the semantics of the build_id field. -enum BuildIdKind { - // Linker-generated build ID, stored in the ELF binary notes. - BUILD_ID_LINKER = 0; - // Build ID based on the content hash of the binary. Currently no particular - // hashing approach is standardized, so a given producer needs to define it - // themselves and thus unlike BUILD_ID_LINKER this kind of hash is producer-specific. - // We may choose to provide a standardized stable hash recommendation later. - BUILD_ID_BINARY_HASH = 1; + repeated uint64 timestamps_unix_nano = 6; } // Describes the mapping of a binary in memory, including its address range, // file offset, and metadata like build ID message Mapping { - // Unique nonzero id for the mapping. [deprecated] - uint64 id = 1; // Address at which the binary (or DLL) is loaded into memory. - uint64 memory_start = 2; + uint64 memory_start = 1; // The limit of the address range occupied by this mapping. - uint64 memory_limit = 3; + uint64 memory_limit = 2; // Offset in the binary that corresponds to the first mapped address. - uint64 file_offset = 4; + uint64 file_offset = 3; // The object this entry is loaded from. This can be a filename on // disk for the main binary and shared libraries, or virtual // abstractions like "[vdso]". - int64 filename = 5; // Index into string table - // A string that uniquely identifies a particular program version - // with high probability. E.g., for binaries generated by GNU tools, - // it could be the contents of the .note.gnu.build-id field. - int64 build_id = 6; // Index into string table - // Specifies the kind of build id. See BuildIdKind enum for more details [optional] - BuildIdKind build_id_kind = 11; + int32 filename_strindex = 4; // Index into string table // References to attributes in Profile.attribute_table. [optional] - repeated uint64 attributes = 12; + repeated int32 attribute_indices = 5; // The following fields indicate the resolution of symbolic info. - bool has_functions = 7; - bool has_filenames = 8; - bool has_line_numbers = 9; - bool has_inline_frames = 10; + bool has_functions = 6; + bool has_filenames = 7; + bool has_line_numbers = 8; + bool has_inline_frames = 9; } // Describes function and line table debug information. message Location { - // Unique nonzero id for the location. A profile could use - // instruction addresses or any integer sequence as ids. [deprecated] - uint64 id = 1; - // The index of the corresponding profile.Mapping for this location. + // Reference to mapping in Profile.mapping_table. // It can be unset if the mapping is unknown or not applicable for // this profile type. - uint64 mapping_index = 2; + optional int32 mapping_index = 1; // The instruction address for this location, if available. It // should be within [Mapping.memory_start...Mapping.memory_limit] // for the corresponding mapping. A non-leaf address may be in the // middle of a call instruction. It is up to display tools to find // the beginning of the instruction if necessary. - uint64 address = 3; + uint64 address = 2; // Multiple line indicates this location has inlined functions, // where the last entry represents the caller into which the // preceding entries were inlined. @@ -344,25 +438,22 @@ message Location { // E.g., if memcpy() is inlined into printf: // line[0].function_name == "memcpy" // line[1].function_name == "printf" - repeated Line line = 4; + repeated Line line = 3; // Provides an indication that multiple symbols map to this location's // address, for example due to identical code folding by the linker. In that // case the line information above represents one of the multiple // symbols. This field must be recomputed when the symbolization state of the // profile changes. - bool is_folded = 5; - - // Type of frame (e.g. kernel, native, python, hotspot, php). Index into string table. - uint32 type_index = 6; + bool is_folded = 4; // References to attributes in Profile.attribute_table. [optional] - repeated uint64 attributes = 7; + repeated int32 attribute_indices = 5; } // Details a specific line in a source code, linked to a function. message Line { - // The index of the corresponding profile.Function for this line. - uint64 function_index = 1; + // Reference to function in Profile.function_table. + int32 function_index = 1; // Line number in source code. int64 line = 2; // Column number in source code. @@ -372,15 +463,13 @@ message Line { // Describes a function, including its human-readable name, system name, // source file, and starting line number in the source. message Function { - // Unique nonzero id for the function. [deprecated] - uint64 id = 1; // Name of the function, in human-readable form if available. - int64 name = 2; // Index into string table + int32 name_strindex = 1; // Index into string table // Name of the function, as identified by the system. // For instance, it can be a C++ mangled name. - int64 system_name = 3; // Index into string table + int32 system_name_strindex = 2; // Index into string table // Source file containing the function. - int64 filename = 4; // Index into string table + int32 filename_strindex = 3; // Index into string table // Line number in source file. - int64 start_line = 5; + int64 start_line = 4; } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/resource/v1/resource.proto b/src/Shared/Proto/opentelemetry/proto/resource/v1/resource.proto similarity index 100% rename from src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/resource/v1/resource.proto rename to src/Shared/Proto/opentelemetry/proto/resource/v1/resource.proto diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/trace/v1/trace.proto b/src/Shared/Proto/opentelemetry/proto/trace/v1/trace.proto similarity index 97% rename from src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/trace/v1/trace.proto rename to src/Shared/Proto/opentelemetry/proto/trace/v1/trace.proto index 5cb2f3ce1cd..24442853edc 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/trace/v1/trace.proto +++ b/src/Shared/Proto/opentelemetry/proto/trace/v1/trace.proto @@ -56,7 +56,8 @@ message ResourceSpans { 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 + // is recorded in. Notably, the last part of the URL path is the version number of the + // schema: http[s]://server[:port]/path/. 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. @@ -74,7 +75,8 @@ message ScopeSpans { 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 + // is recorded in. Notably, the last part of the URL path is the version number of the + // schema: http[s]://server[:port]/path/. 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; diff --git a/src/Shared/ResourceSemanticConventions.cs b/src/Shared/ResourceSemanticConventions.cs index a3f93c02fa8..aa5dae4005f 100644 --- a/src/Shared/ResourceSemanticConventions.cs +++ b/src/Shared/ResourceSemanticConventions.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - namespace OpenTelemetry.Resources; internal static class ResourceSemanticConventions diff --git a/src/Shared/SemanticConventions.cs b/src/Shared/SemanticConventions.cs index 2624acd29cd..22c5f9b626c 100644 --- a/src/Shared/SemanticConventions.cs +++ b/src/Shared/SemanticConventions.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - namespace OpenTelemetry.Trace; /// @@ -26,4 +24,22 @@ internal static class SemanticConventions public const string AttributeExceptionType = "exception.type"; public const string AttributeExceptionMessage = "exception.message"; public const string AttributeExceptionStacktrace = "exception.stacktrace"; + + public const string AttributeServerAddress = "server.address"; + + public const string AttributeNetworkPeerAddress = "network.peer.address"; + public const string AttributeNetworkPeerPort = "network.peer.port"; + + public const string AttributeServerSocketDomain = "server.socket.domain"; + public const string AttributeServerSocketAddress = "server.socket.address"; + public const string AttributeServerSocketPort = "server.socket.port"; + + public const string AttributeNetSockPeerName = "net.sock.peer.name"; + public const string AttributeNetSockPeerAddr = "net.sock.peer.addr"; + public const string AttributeNetSockPeerPort = "net.sock.peer.port"; + + public const string AttributePeerHostname = "peer.hostname"; + public const string AttributePeerAddress = "peer.address"; + + public const string AttributeDbName = "db.name"; } diff --git a/src/Shared/Shims/ExperimentalAttribute.cs b/src/Shared/Shims/ExperimentalAttribute.cs new file mode 100644 index 00000000000..2794b177c37 --- /dev/null +++ b/src/Shared/Shims/ExperimentalAttribute.cs @@ -0,0 +1,76 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Source: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/ExperimentalAttribute.cs + +#if NETFRAMEWORK || NETSTANDARD2_0_OR_GREATER + +#nullable enable + +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Indicates that an API is experimental and it may change in the future. + /// + /// + /// This attribute allows call sites to be flagged with a diagnostic that indicates that an experimental + /// feature is used. Authors can use this attribute to ship preview features in their assemblies. + /// + [AttributeUsage(AttributeTargets.Assembly | + AttributeTargets.Module | + AttributeTargets.Class | + AttributeTargets.Struct | + AttributeTargets.Enum | + AttributeTargets.Constructor | + AttributeTargets.Method | + AttributeTargets.Property | + AttributeTargets.Field | + AttributeTargets.Event | + AttributeTargets.Interface | + AttributeTargets.Delegate, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class ExperimentalAttribute : Attribute + { + /// + /// Initializes a new instance of the class, specifying the ID that the compiler will use + /// when reporting a use of the API the attribute applies to. + /// + /// The ID that the compiler will use when reporting a use of the API the attribute applies to. + public ExperimentalAttribute(string diagnosticId) + { + DiagnosticId = diagnosticId; + } + + /// + /// Gets the ID that the compiler will use when reporting a use of the API the attribute applies to. + /// + /// The unique diagnostic ID. + /// + /// The diagnostic ID is shown in build output for warnings and errors. + /// This property represents the unique ID that can be used to suppress the warnings or errors, if needed. + /// + public string DiagnosticId { get; } + + /// + /// Gets or sets an optional message associated with the experimental attribute. + /// + /// The message that provides additional information about the experimental feature. + /// + /// This message can be used to provide more context or guidance about the experimental feature. + /// + public string? Message { get; set; } + + /// + /// Gets or sets the URL for corresponding documentation. + /// The API accepts a format string instead of an actual URL, creating a generic URL that includes the diagnostic ID. + /// + /// The format string that represents a URL to corresponding documentation. + /// An example format string is https://contoso.com/obsoletion-warnings/{0}. + public string? UrlFormat { get; set; } + } +} +#endif diff --git a/src/Shared/Shims/Lock.cs b/src/Shared/Shims/Lock.cs new file mode 100644 index 00000000000..8b82deb4d5c --- /dev/null +++ b/src/Shared/Shims/Lock.cs @@ -0,0 +1,18 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if !NET9_0_OR_GREATER +namespace OpenTelemetry; + +// Note: .NET9 added the System.Threading.Lock class. The goal here is when +// compiling against .NET9+ code should use System.Threading.Lock class for +// better perf. Legacy code can use this class which will perform a classic +// monitor-based lock against a reference of this class. This type is not in the +// System.Threading namespace so that the compiler doesn't get confused when it +// sees it used. It is in OpenTelemetry namespace and not OpenTelemetry.Internal +// namespace so that code should be able to use it without the presence of a +// dedicated "using OpenTelemetry.Internal" just for the shim. +internal sealed class Lock +{ +} +#endif diff --git a/src/Shared/Shims/NullableAttributes.cs b/src/Shared/Shims/NullableAttributes.cs index 72d877e3dae..a5f776258e4 100644 --- a/src/Shared/Shims/NullableAttributes.cs +++ b/src/Shared/Shims/NullableAttributes.cs @@ -6,9 +6,10 @@ #pragma warning disable SA1649 // File name should match first type name #pragma warning disable SA1402 // File may only contain a single type -#if NETFRAMEWORK || NETSTANDARD2_0 +#if NETFRAMEWORK || NETSTANDARD2_0_OR_GREATER namespace System.Diagnostics.CodeAnalysis { +#if NETFRAMEWORK || NETSTANDARD2_0 /// Specifies that null is allowed as an input even if the corresponding type disallows it. [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] internal sealed class AllowNullAttribute : Attribute @@ -42,5 +43,26 @@ internal sealed class NotNullIfNotNullAttribute : Attribute public string ParameterName { get; } } +#endif + + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullWhenAttribute : Attribute + { + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = [member]; + } + + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + + public bool ReturnValue { get; } + + public string[] Members { get; } + } } #endif diff --git a/src/Shared/SpanAttributeConstants.cs b/src/Shared/SpanAttributeConstants.cs index dcd7e5b8ef4..a3467bdf4ab 100644 --- a/src/Shared/SpanAttributeConstants.cs +++ b/src/Shared/SpanAttributeConstants.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - namespace OpenTelemetry.Trace; /// diff --git a/src/Shared/StatusHelper.cs b/src/Shared/StatusHelper.cs index 5d9245010c9..00e5a587f9e 100644 --- a/src/Shared/StatusHelper.cs +++ b/src/Shared/StatusHelper.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Runtime.CompilerServices; using OpenTelemetry.Trace; diff --git a/src/Shared/TagWriter/ArrayTagWriter.cs b/src/Shared/TagWriter/ArrayTagWriter.cs index ac5bba8bf0d..92cf3a317e2 100644 --- a/src/Shared/TagWriter/ArrayTagWriter.cs +++ b/src/Shared/TagWriter/ArrayTagWriter.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - namespace OpenTelemetry.Internal; internal abstract class ArrayTagWriter @@ -21,4 +19,6 @@ internal abstract class ArrayTagWriter public abstract void WriteStringValue(ref TArrayState state, ReadOnlySpan value); public abstract void EndWriteArray(ref TArrayState state); + + public virtual bool TryResize() => false; } diff --git a/src/Shared/TagWriter/JsonStringArrayTagWriter.cs b/src/Shared/TagWriter/JsonStringArrayTagWriter.cs index 8dd2c086d79..d79b6446bee 100644 --- a/src/Shared/TagWriter/JsonStringArrayTagWriter.cs +++ b/src/Shared/TagWriter/JsonStringArrayTagWriter.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; using System.Text.Json; @@ -27,6 +25,8 @@ protected sealed override void WriteArrayTag(ref TTagState writer, string key, r protected abstract void WriteArrayTag(ref TTagState writer, string key, ArraySegment arrayUtf8JsonBytes); + protected override bool TryWriteByteArrayTag(ref TTagState consoleTag, string key, ReadOnlySpan value) => false; + internal readonly struct JsonArrayTagWriterState(MemoryStream stream, Utf8JsonWriter writer) { public MemoryStream Stream { get; } = stream; diff --git a/src/Shared/TagWriter/TagWriter.cs b/src/Shared/TagWriter/TagWriter.cs index fb56903363a..3794a9d76eb 100644 --- a/src/Shared/TagWriter/TagWriter.cs +++ b/src/Shared/TagWriter/TagWriter.cs @@ -1,9 +1,8 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; +using System.Globalization; namespace OpenTelemetry.Internal; @@ -26,24 +25,33 @@ public bool TryWriteTag( KeyValuePair tag, int? tagValueMaxLength = null) { - if (tag.Value == null) + return this.TryWriteTag(ref state, tag.Key, tag.Value, tagValueMaxLength); + } + + public bool TryWriteTag( + ref TTagState state, + string key, + object? value, + int? tagValueMaxLength = null) + { + if (value == null) { - return false; + return this.TryWriteEmptyTag(ref state, key, value); } - switch (tag.Value) + switch (value) { case char c: - this.WriteCharTag(ref state, tag.Key, c); + this.WriteCharTag(ref state, key, c); break; case string s: this.WriteStringTag( ref state, - tag.Key, + key, TruncateString(s.AsSpan(), tagValueMaxLength)); break; case bool b: - this.WriteBooleanTag(ref state, tag.Key, b); + this.WriteBooleanTag(ref state, key, b); break; case byte: case sbyte: @@ -52,23 +60,32 @@ public bool TryWriteTag( case int: case uint: case long: - this.WriteIntegralTag(ref state, tag.Key, Convert.ToInt64(tag.Value)); + this.WriteIntegralTag(ref state, key, Convert.ToInt64(value, CultureInfo.InvariantCulture)); break; case float: case double: - this.WriteFloatingPointTag(ref state, tag.Key, Convert.ToDouble(tag.Value)); + this.WriteFloatingPointTag(ref state, key, Convert.ToDouble(value, CultureInfo.InvariantCulture)); break; case Array array: + if (value.GetType() == typeof(byte[]) && this.TryWriteByteArrayTag(ref state, key, ((byte[])value).AsSpan())) + { + return true; + } + try { - this.WriteArrayTagInternal(ref state, tag.Key, array, tagValueMaxLength); + this.WriteArrayTagInternal(ref state, key, array, tagValueMaxLength); + } + catch (Exception ex) when (ex is IndexOutOfRangeException || ex is ArgumentException) + { + throw; } catch { // If an exception is thrown when calling ToString // on any element of the array, then the entire array value // is ignored. - return this.LogUnsupportedTagTypeAndReturnFalse(tag.Key, tag.Value); + return this.LogUnsupportedTagTypeAndReturnFalse(key, value); } break; @@ -82,21 +99,21 @@ public bool TryWriteTag( default: try { - var stringValue = Convert.ToString(tag.Value/*TODO: , CultureInfo.InvariantCulture*/); + var stringValue = Convert.ToString(value, CultureInfo.InvariantCulture); if (stringValue == null) { - return this.LogUnsupportedTagTypeAndReturnFalse(tag.Key, tag.Value); + return this.LogUnsupportedTagTypeAndReturnFalse(key, value); } this.WriteStringTag( ref state, - tag.Key, + key, TruncateString(stringValue.AsSpan(), tagValueMaxLength)); } catch { // If ToString throws an exception then the tag is ignored. - return this.LogUnsupportedTagTypeAndReturnFalse(tag.Key, tag.Value); + return this.LogUnsupportedTagTypeAndReturnFalse(key, value); } break; @@ -105,6 +122,10 @@ public bool TryWriteTag( return true; } + protected abstract bool TryWriteEmptyTag(ref TTagState state, string key, object? value); + + protected abstract bool TryWriteByteArrayTag(ref TTagState state, string key, ReadOnlySpan value); + protected abstract void WriteIntegralTag(ref TTagState state, string key, long value); protected abstract void WriteFloatingPointTag(ref TTagState state, string key, double value); @@ -128,15 +149,13 @@ private static ReadOnlySpan TruncateString(ReadOnlySpan value, int? private void WriteCharTag(ref TTagState state, string key, char value) { - Span destination = stackalloc char[1]; - destination[0] = value; + Span destination = [value]; this.WriteStringTag(ref state, key, destination); } private void WriteCharValue(ref TArrayState state, char value) { - Span destination = stackalloc char[1]; - destination[0] = value; + Span destination = [value]; this.arrayWriter.WriteStringValue(ref state, destination); } @@ -144,27 +163,49 @@ private void WriteArrayTagInternal(ref TTagState state, string key, Array array, { var arrayState = this.arrayWriter.BeginWriteArray(); - // This switch ensures the values of the resultant array-valued tag are of the same type. - switch (array) + try { - case char[] charArray: this.WriteStructToArray(ref arrayState, charArray); break; - case string?[] stringArray: this.WriteStringsToArray(ref arrayState, stringArray, tagValueMaxLength); break; - case bool[] boolArray: this.WriteStructToArray(ref arrayState, boolArray); break; - case byte[] byteArray: this.WriteToArrayCovariant(ref arrayState, byteArray); break; - case short[] shortArray: this.WriteToArrayCovariant(ref arrayState, shortArray); break; + // This switch ensures the values of the resultant array-valued tag are of the same type. + switch (array) + { + case char[] charArray: this.WriteStructToArray(ref arrayState, charArray); break; + case string?[] stringArray: this.WriteStringsToArray(ref arrayState, stringArray, tagValueMaxLength); break; + case bool[] boolArray: this.WriteStructToArray(ref arrayState, boolArray); break; + case byte[] byteArray: this.WriteToArrayCovariant(ref arrayState, byteArray); break; + case short[] shortArray: this.WriteToArrayCovariant(ref arrayState, shortArray); break; #if NETFRAMEWORK - case int[]: this.WriteArrayTagIntNetFramework(ref arrayState, array, tagValueMaxLength); break; - case long[]: this.WriteArrayTagLongNetFramework(ref arrayState, array, tagValueMaxLength); break; + case int[]: this.WriteArrayTagIntNetFramework(ref arrayState, array, tagValueMaxLength); break; + case long[]: this.WriteArrayTagLongNetFramework(ref arrayState, array, tagValueMaxLength); break; #else - case int[] intArray: this.WriteToArrayCovariant(ref arrayState, intArray); break; - case long[] longArray: this.WriteToArrayCovariant(ref arrayState, longArray); break; + case int[] intArray: this.WriteToArrayCovariant(ref arrayState, intArray); break; + case long[] longArray: this.WriteToArrayCovariant(ref arrayState, longArray); break; #endif - case float[] floatArray: this.WriteStructToArray(ref arrayState, floatArray); break; - case double[] doubleArray: this.WriteStructToArray(ref arrayState, doubleArray); break; - default: this.WriteToArrayTypeChecked(ref arrayState, array, tagValueMaxLength); break; + case float[] floatArray: this.WriteStructToArray(ref arrayState, floatArray); break; + case double[] doubleArray: this.WriteStructToArray(ref arrayState, doubleArray); break; + default: this.WriteToArrayTypeChecked(ref arrayState, array, tagValueMaxLength); break; + } + + this.arrayWriter.EndWriteArray(ref arrayState); } + catch (Exception ex) when (ex is IndexOutOfRangeException || ex is ArgumentException) + { + // If the array writer cannot be resized, TryResize should log a message to the event source, return false. + if (this.arrayWriter.TryResize()) + { + this.WriteArrayTagInternal(ref state, key, array, tagValueMaxLength); + return; + } + + // Drop the array value and set "TRUNCATED" as value for easier isolation. + // This is a best effort to avoid dropping the entire tag. + this.WriteStringTag( + ref state, + key, + "TRUNCATED".AsSpan()); - this.arrayWriter.EndWriteArray(ref arrayState); + this.LogUnsupportedTagTypeAndReturnFalse(key, array.GetType().ToString()); + return; + } this.WriteArrayTag(ref state, key, ref arrayState); } @@ -232,11 +273,11 @@ private void WriteToArrayTypeChecked(ref TArrayState arrayState, Array array, in case int: case uint: case long: - this.arrayWriter.WriteIntegralValue(ref arrayState, Convert.ToInt64(item)); + this.arrayWriter.WriteIntegralValue(ref arrayState, Convert.ToInt64(item, CultureInfo.InvariantCulture)); break; case float: case double: - this.arrayWriter.WriteFloatingPointValue(ref arrayState, Convert.ToDouble(item)); + this.arrayWriter.WriteFloatingPointValue(ref arrayState, Convert.ToDouble(item, CultureInfo.InvariantCulture)); break; // All other types are converted to strings including the following @@ -247,7 +288,7 @@ private void WriteToArrayTypeChecked(ref TArrayState arrayState, Array array, in // case ulong: May throw an exception on overflow. // case decimal: Converting to double produces rounding errors. default: - var stringValue = Convert.ToString(item/*TODO: , CultureInfo.InvariantCulture*/); + var stringValue = Convert.ToString(item, CultureInfo.InvariantCulture); if (stringValue == null) { this.arrayWriter.WriteNullValue(ref arrayState); diff --git a/src/Shared/ThreadSafeRandom.cs b/src/Shared/ThreadSafeRandom.cs index 1ba1b4d91c7..b0419b482fb 100644 --- a/src/Shared/ThreadSafeRandom.cs +++ b/src/Shared/ThreadSafeRandom.cs @@ -6,10 +6,12 @@ namespace OpenTelemetry.Internal; // Note: Inspired by https://devblogs.microsoft.com/pfxteam/getting-random-numbers-in-a-thread-safe-way/ internal static class ThreadSafeRandom { -#if NET6_0_OR_GREATER +#if NET public static int Next(int min, int max) { +#pragma warning disable CA5394 // Do not use insecure randomness return Random.Shared.Next(min, max); +#pragma warning restore CA5394 // Do not use insecure randomness } #else private static readonly Random GlobalRandom = new(); @@ -25,6 +27,7 @@ public static int Next(int min, int max) int seed; lock (GlobalRandom) { +#pragma warning disable CA5394 // Do not use insecure randomness seed = GlobalRandom.Next(); } @@ -32,6 +35,7 @@ public static int Next(int min, int max) } return local.Next(min, max); +#pragma warning restore CA5394 // Do not use insecure randomness } #endif } diff --git a/test/Benchmarks/Benchmarks.csproj b/test/Benchmarks/Benchmarks.csproj index 915a68730e1..c820c9e0d14 100644 --- a/test/Benchmarks/Benchmarks.csproj +++ b/test/Benchmarks/Benchmarks.csproj @@ -1,11 +1,9 @@ - + Exe $(TargetFrameworksForTests) - - - disable + $(NoWarn);CA1515 @@ -30,4 +28,17 @@ + + + + + + + + + + $(RepoRoot)\src\Shared\Proto + + + diff --git a/test/Benchmarks/Context/Propagation/TraceContextPropagatorBenchmarks.cs b/test/Benchmarks/Context/Propagation/TraceContextPropagatorBenchmarks.cs new file mode 100644 index 00000000000..e73d65c9f3f --- /dev/null +++ b/test/Benchmarks/Context/Propagation/TraceContextPropagatorBenchmarks.cs @@ -0,0 +1,72 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using BenchmarkDotNet.Attributes; +using OpenTelemetry.Context.Propagation; + +namespace Benchmarks.Context.Propagation; + +public class TraceContextPropagatorBenchmarks +{ + private const string TraceParent = "traceparent"; + private const string TraceState = "tracestate"; + private const string TraceId = "0af7651916cd43dd8448eb211c80319c"; + private const string SpanId = "b9c7c989f97918e1"; + + private static readonly Random Random = new(455946); + private static readonly TraceContextPropagator TraceContextPropagator = new(); + + private static readonly Func, string, IEnumerable> Getter = (headers, name) => + { + if (headers.TryGetValue(name, out var value)) + { + return [value]; + } + + return []; + }; + + [Params(true, false)] + public bool LongListMember { get; set; } + + [Params(0, 4, 32)] + public int MembersCount { get; set; } + + public Dictionary? Headers { get; private set; } + + [GlobalSetup] + public void Setup() + { + var length = this.LongListMember ? 256 : 20; + + var value = new string('a', length); + + Span keyBuffer = stackalloc char[length - 2]; + + string traceState = string.Empty; + for (var i = 0; i < this.MembersCount; i++) + { + // We want a unique key for each member + for (var j = 0; j < length - 2; j++) + { +#pragma warning disable CA5394 // Do not use insecure randomness + keyBuffer[j] = (char)('a' + Random.Next(0, 26)); +#pragma warning restore CA5394 // Do not use insecure randomness + } + + var key = keyBuffer.ToString(); + + var listMember = $"{key}{i:00}={value}"; + traceState += i < this.MembersCount - 1 ? $"{listMember}," : listMember; + } + + this.Headers = new Dictionary + { + { TraceParent, $"00-{TraceId}-{SpanId}-01" }, + { TraceState, traceState }, + }; + } + + [Benchmark(Baseline = true)] + public void Extract() => _ = TraceContextPropagator.Extract(default, this.Headers!, Getter); +} diff --git a/test/Benchmarks/EventSourceBenchmarks.cs b/test/Benchmarks/EventSourceBenchmarks.cs index f8304368632..5e4460bc33d 100644 --- a/test/Benchmarks/EventSourceBenchmarks.cs +++ b/test/Benchmarks/EventSourceBenchmarks.cs @@ -10,18 +10,22 @@ namespace OpenTelemetry.Benchmarks; public class EventSourceBenchmarks { [Benchmark] +#pragma warning disable CA1822 // Mark members as static public void EventWithIdAllocation() +#pragma warning restore CA1822 // Mark members as static { 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] +#pragma warning disable CA1822 // Mark members as static public void EventWithCheck() +#pragma warning restore CA1822 // Mark members as static { using var activity = new Activity("TestActivity"); activity.SetIdFormat(ActivityIdFormat.W3C); diff --git a/test/Benchmarks/Exporter/OtlpGrpcExporterBenchmarks.cs b/test/Benchmarks/Exporter/OtlpGrpcExporterBenchmarks.cs index 2a9dd7cebbb..d84c9e599e4 100644 --- a/test/Benchmarks/Exporter/OtlpGrpcExporterBenchmarks.cs +++ b/test/Benchmarks/Exporter/OtlpGrpcExporterBenchmarks.cs @@ -12,15 +12,16 @@ 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; +#pragma warning disable CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup public class OtlpGrpcExporterBenchmarks +#pragma warning restore CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup { - private OtlpTraceExporter exporter; - private Activity activity; - private CircularBuffer activityBatch; + private OtlpTraceExporter? exporter; + private Activity? activity; + private CircularBuffer? activityBatch; [Params(1, 10, 100)] public int NumberOfBatches { get; set; } @@ -36,7 +37,9 @@ public void GlobalSetup() options, new SdkLimitOptions(), new ExperimentalOptions(), - new OtlpExporterTransmissionHandler(new OtlpGrpcTraceExportClient(options, new TestTraceServiceClient()), options.TimeoutMilliseconds)); +#pragma warning disable CA2000 // Dispose objects before losing scope + new OtlpExporterTransmissionHandler(new OtlpGrpcExportClient(options, options.HttpClientFactory(), "opentelemetry.proto.collector.trace.v1.TraceService/Export"), options.TimeoutMilliseconds)); +#pragma warning restore CA2000 // Dispose objects before losing scope this.activity = ActivityHelper.CreateTestActivity(); this.activityBatch = new CircularBuffer(this.NumberOfSpans); @@ -45,8 +48,9 @@ public void GlobalSetup() [GlobalCleanup] public void GlobalCleanup() { - this.exporter.Shutdown(); - this.exporter.Dispose(); + this.activity?.Dispose(); + this.exporter?.Shutdown(); + this.exporter?.Dispose(); } [Benchmark] @@ -56,10 +60,10 @@ public void OtlpExporter_Batching() { for (int c = 0; c < this.NumberOfSpans; c++) { - this.activityBatch.Add(this.activity); + this.activityBatch!.Add(this.activity!); } - this.exporter.Export(new Batch(this.activityBatch, this.NumberOfSpans)); + this.exporter!.Export(new Batch(this.activityBatch!, this.NumberOfSpans)); } } } diff --git a/test/Benchmarks/Exporter/OtlpHttpExporterBenchmarks.cs b/test/Benchmarks/Exporter/OtlpHttpExporterBenchmarks.cs index d595eefd9f1..0137f9055d8 100644 --- a/test/Benchmarks/Exporter/OtlpHttpExporterBenchmarks.cs +++ b/test/Benchmarks/Exporter/OtlpHttpExporterBenchmarks.cs @@ -13,19 +13,20 @@ 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; +#pragma warning disable CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup public class OtlpHttpExporterBenchmarks +#pragma warning restore CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup { private readonly byte[] buffer = new byte[1024 * 1024]; - private IDisposable server; - private string serverHost; + private IDisposable? server; + private string? serverHost; private int serverPort; - private OtlpTraceExporter exporter; - private Activity activity; - private CircularBuffer activityBatch; + private OtlpTraceExporter? exporter; + private Activity? activity; + private CircularBuffer? activityBatch; [Params(1, 10, 100)] public int NumberOfBatches { get; set; } @@ -64,7 +65,9 @@ public void GlobalSetup() options, new SdkLimitOptions(), new ExperimentalOptions(), - new OtlpExporterTransmissionHandler(new OtlpHttpTraceExportClient(options, options.HttpClientFactory()), options.TimeoutMilliseconds)); +#pragma warning disable CA2000 // Dispose objects before losing scope + new OtlpExporterTransmissionHandler(new OtlpHttpExportClient(options, options.HttpClientFactory(), "v1/traces"), options.TimeoutMilliseconds)); +#pragma warning restore CA2000 // Dispose objects before losing scope this.activity = ActivityHelper.CreateTestActivity(); this.activityBatch = new CircularBuffer(this.NumberOfSpans); @@ -73,9 +76,10 @@ public void GlobalSetup() [GlobalCleanup] public void GlobalCleanup() { - this.exporter.Shutdown(); - this.exporter.Dispose(); - this.server.Dispose(); + this.activity?.Dispose(); + this.exporter?.Shutdown(); + this.exporter?.Dispose(); + this.server?.Dispose(); } [Benchmark] @@ -85,10 +89,10 @@ public void OtlpExporter_Batching() { for (int c = 0; c < this.NumberOfSpans; c++) { - this.activityBatch.Add(this.activity); + this.activityBatch!.Add(this.activity!); } - this.exporter.Export(new Batch(this.activityBatch, this.NumberOfSpans)); + this.exporter!.Export(new Batch(this.activityBatch!, this.NumberOfSpans)); } } } diff --git a/test/Benchmarks/Exporter/OtlpLogExporterBenchmarks.cs b/test/Benchmarks/Exporter/OtlpLogExporterBenchmarks.cs index 17bcf3dda49..eba68dfd7e5 100644 --- a/test/Benchmarks/Exporter/OtlpLogExporterBenchmarks.cs +++ b/test/Benchmarks/Exporter/OtlpLogExporterBenchmarks.cs @@ -16,7 +16,7 @@ using OpenTelemetry.Logs; using OpenTelemetry.Tests; using OpenTelemetryProtocol::OpenTelemetry.Exporter; -using OtlpCollector = OpenTelemetryProtocol::OpenTelemetry.Proto.Collector.Logs.V1; +using OtlpCollector = OpenTelemetry.Proto.Collector.Logs.V1; /* BenchmarkDotNet v0.13.6, Windows 11 (10.0.22621.2134/22H2/2022Update/SunValley2) (Hyper-V) @@ -34,15 +34,17 @@ .NET SDK 7.0.400 namespace Benchmarks.Exporter; +#pragma warning disable CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup public class OtlpLogExporterBenchmarks +#pragma warning restore CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup { - private OtlpLogExporter exporter; - private LogRecord logRecord; - private CircularBuffer logRecordBatch; + private OtlpLogExporter? exporter; + private LogRecord? logRecord; + private CircularBuffer? logRecordBatch; - private IHost host; - private IDisposable server; - private string serverHost; + private IHost? host; + private IDisposable? server; + private string? serverHost; private int serverPort; [GlobalSetup(Target = nameof(OtlpLogExporter_Grpc))] @@ -103,38 +105,40 @@ public void GlobalSetupHttp() [GlobalCleanup(Target = nameof(OtlpLogExporter_Grpc))] public void GlobalCleanupGrpc() { - this.exporter.Shutdown(); - this.exporter.Dispose(); - this.host.Dispose(); + 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(); + this.exporter?.Shutdown(); + this.exporter?.Dispose(); + this.server?.Dispose(); } [Benchmark] public void OtlpLogExporter_Http() { - this.exporter.Export(new Batch(this.logRecordBatch, 1)); + this.exporter!.Export(new Batch(this.logRecordBatch!, 1)); } [Benchmark] public void OtlpLogExporter_Grpc() { - this.exporter.Export(new Batch(this.logRecordBatch, 1)); + this.exporter!.Export(new Batch(this.logRecordBatch!, 1)); } +#pragma warning disable CA1812 // Avoid uninstantiated internal classes private sealed class MockLogService : OtlpCollector.LogsService.LogsServiceBase +#pragma warning restore CA1812 // Avoid uninstantiated internal classes { - private static OtlpCollector.ExportLogsServiceResponse response = new OtlpCollector.ExportLogsServiceResponse(); + private static readonly OtlpCollector.ExportLogsServiceResponse Response = new(); public override Task Export(OtlpCollector.ExportLogsServiceRequest request, ServerCallContext context) { - return Task.FromResult(response); + return Task.FromResult(Response); } } } diff --git a/test/Benchmarks/Exporter/OtlpTraceExporterBenchmarks.cs b/test/Benchmarks/Exporter/OtlpTraceExporterBenchmarks.cs index 392e0612c0e..7afea3fa8be 100644 --- a/test/Benchmarks/Exporter/OtlpTraceExporterBenchmarks.cs +++ b/test/Benchmarks/Exporter/OtlpTraceExporterBenchmarks.cs @@ -16,7 +16,7 @@ using OpenTelemetry.Internal; using OpenTelemetry.Tests; using OpenTelemetryProtocol::OpenTelemetry.Exporter; -using OtlpCollector = OpenTelemetryProtocol::OpenTelemetry.Proto.Collector.Trace.V1; +using OtlpCollector = OpenTelemetry.Proto.Collector.Trace.V1; /* BenchmarkDotNet v0.13.6, Windows 11 (10.0.22621.2134/22H2/2022Update/SunValley2) (Hyper-V) @@ -34,15 +34,17 @@ .NET SDK 7.0.400 namespace Benchmarks.Exporter; +#pragma warning disable CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup public class OtlpTraceExporterBenchmarks +#pragma warning restore CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup { - private OtlpTraceExporter exporter; - private Activity activity; - private CircularBuffer activityBatch; + private OtlpTraceExporter? exporter; + private Activity? activity; + private CircularBuffer? activityBatch; - private IHost host; - private IDisposable server; - private string serverHost; + private IHost? host; + private IDisposable? server; + private string? serverHost; private int serverPort; [GlobalSetup(Target = nameof(OtlpTraceExporter_Grpc))] @@ -103,40 +105,42 @@ public void GlobalSetupHttp() [GlobalCleanup(Target = nameof(OtlpTraceExporter_Grpc))] public void GlobalCleanupGrpc() { - this.exporter.Shutdown(); - this.exporter.Dispose(); - this.activity.Dispose(); - this.host.Dispose(); + 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(); + 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)); + this.exporter!.Export(new Batch(this.activityBatch!, 1)); } [Benchmark] public void OtlpTraceExporter_Grpc() { - this.exporter.Export(new Batch(this.activityBatch, 1)); + this.exporter!.Export(new Batch(this.activityBatch!, 1)); } +#pragma warning disable CA1812 // Avoid uninstantiated internal classes private sealed class MockTraceService : OtlpCollector.TraceService.TraceServiceBase +#pragma warning restore CA1812 // Avoid uninstantiated internal classes { - private static OtlpCollector.ExportTraceServiceResponse response = new OtlpCollector.ExportTraceServiceResponse(); + private static readonly OtlpCollector.ExportTraceServiceResponse Response = new(); public override Task Export(OtlpCollector.ExportTraceServiceRequest request, ServerCallContext context) { - return Task.FromResult(response); + return Task.FromResult(Response); } } } diff --git a/test/Benchmarks/Exporter/PrometheusSerializerBenchmarks.cs b/test/Benchmarks/Exporter/PrometheusSerializerBenchmarks.cs index caeda40f44c..47d04dd0d10 100644 --- a/test/Benchmarks/Exporter/PrometheusSerializerBenchmarks.cs +++ b/test/Benchmarks/Exporter/PrometheusSerializerBenchmarks.cs @@ -10,13 +10,15 @@ namespace Benchmarks.Exporter; +#pragma warning disable CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup public class PrometheusSerializerBenchmarks +#pragma warning restore CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup { - private readonly List metrics = new(); + private readonly List metrics = []; private readonly byte[] buffer = new byte[85000]; - private Meter meter; - private MeterProvider meterProvider; - private Dictionary cache = new Dictionary(); + private readonly Dictionary cache = []; + private Meter? meter; + private MeterProvider? meterProvider; [Params(1, 1000, 10000)] public int NumberOfSerializeCalls { get; set; } @@ -45,7 +47,7 @@ public void GlobalSetup() public void GlobalCleanup() { this.meter?.Dispose(); - this.meterProvider.Dispose(); + this.meterProvider?.Dispose(); } // TODO: this has a dependency on https://github.com/open-telemetry/opentelemetry-dotnet/issues/2361 diff --git a/test/Benchmarks/Exporter/ZipkinExporterBenchmarks.cs b/test/Benchmarks/Exporter/ZipkinExporterBenchmarks.cs index fb0f802b55a..983b6dce2cd 100644 --- a/test/Benchmarks/Exporter/ZipkinExporterBenchmarks.cs +++ b/test/Benchmarks/Exporter/ZipkinExporterBenchmarks.cs @@ -19,10 +19,10 @@ namespace Benchmarks.Exporter; public class ZipkinExporterBenchmarks { private readonly byte[] buffer = new byte[4096]; - private Activity activity; - private CircularBuffer activityBatch; - private IDisposable server; - private string serverHost; + private Activity? activity; + private CircularBuffer? activityBatch; + private IDisposable? server; + private string? serverHost; private int serverPort; [Params(1, 10, 100)] @@ -60,7 +60,7 @@ public void GlobalSetup() [GlobalCleanup] public void GlobalCleanup() { - this.server.Dispose(); + this.server?.Dispose(); } [Benchmark] @@ -76,10 +76,10 @@ public void ZipkinExporter_Batching() { for (int c = 0; c < this.NumberOfSpans; c++) { - this.activityBatch.Add(this.activity); + this.activityBatch!.Add(this.activity!); } - exporter.Export(new Batch(this.activityBatch, this.NumberOfSpans)); + exporter.Export(new Batch(this.activityBatch!, this.NumberOfSpans)); } exporter.Shutdown(); diff --git a/test/Benchmarks/Helper/ActivityHelper.cs b/test/Benchmarks/Helper/ActivityHelper.cs index 33f55dafe57..1c5e5e2ea7e 100644 --- a/test/Benchmarks/Helper/ActivityHelper.cs +++ b/test/Benchmarks/Helper/ActivityHelper.cs @@ -31,14 +31,14 @@ public static Activity CreateTestActivity() new ActivityEvent( "Event1", eventTimestamp, - new ActivityTagsCollection(new Dictionary + new ActivityTagsCollection(new Dictionary { { "key", "value" }, })), new ActivityEvent( "Event2", eventTimestamp, - new ActivityTagsCollection(new Dictionary + new ActivityTagsCollection(new Dictionary { { "key", "value" }, })), @@ -48,7 +48,7 @@ public static Activity CreateTestActivity() using var activitySource = new ActivitySource(nameof(CreateTestActivity)); - var tags = attributes.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.ToString())); + var tags = attributes.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.ToString())); var links = new[] { new ActivityLink(new ActivityContext( @@ -75,10 +75,10 @@ public static Activity CreateTestActivity() foreach (var evnt in events) { - activity.AddEvent(evnt); + activity!.AddEvent(evnt); } - activity.SetEndTime(endTimestamp); + activity!.SetEndTime(endTimestamp); activity.Stop(); return activity; diff --git a/test/Benchmarks/Helper/LogRecordHelper.cs b/test/Benchmarks/Helper/LogRecordHelper.cs index f08e62527f5..40b6c4983e2 100644 --- a/test/Benchmarks/Helper/LogRecordHelper.cs +++ b/test/Benchmarks/Helper/LogRecordHelper.cs @@ -1,12 +1,13 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using Benchmarks.Logs; using Microsoft.Extensions.Logging; using OpenTelemetry.Logs; namespace Benchmarks.Helper; -internal class LogRecordHelper +internal static class LogRecordHelper { internal static LogRecord CreateTestLogRecord() { @@ -18,7 +19,7 @@ internal static LogRecord CreateTestLogRecord() })); var logger = factory.CreateLogger("TestLogger"); - logger.LogInformation("Hello from {Food} {Price}.", "artichoke", 3.99); + logger.HelloFrom("artichoke", 3.99); return items[0]; } } diff --git a/test/Benchmarks/Helper/TestExporter.cs b/test/Benchmarks/Helper/TestExporter.cs index 3d2b0038f8d..9aba87deec2 100644 --- a/test/Benchmarks/Helper/TestExporter.cs +++ b/test/Benchmarks/Helper/TestExporter.cs @@ -3,7 +3,7 @@ namespace OpenTelemetry.Tests; -internal class TestExporter : BaseExporter +internal sealed class TestExporter : BaseExporter where T : class { private readonly Action> processBatchAction; diff --git a/test/Benchmarks/Logs/Food.cs b/test/Benchmarks/Logs/Food.cs deleted file mode 100644 index b3a9096cc8f..00000000000 --- a/test/Benchmarks/Logs/Food.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using Microsoft.Extensions.Logging; - -namespace Benchmarks.Logs; - -public static partial class Food -{ - [LoggerMessage(Level = LogLevel.Information, Message = "Hello from {food} {price}.")] - public static partial void SayHello(this ILogger logger, string food, double price); -} diff --git a/test/Benchmarks/Logs/LogBenchmarks.cs b/test/Benchmarks/Logs/LogBenchmarks.cs index 1854eb26f2a..0ba40f0a8cc 100644 --- a/test/Benchmarks/Logs/LogBenchmarks.cs +++ b/test/Benchmarks/Logs/LogBenchmarks.cs @@ -7,23 +7,24 @@ using OpenTelemetry.Logs; /* -BenchmarkDotNet v0.13.10, Windows 11 (10.0.22621.3007/22H2/2022Update/SunValley2) +BenchmarkDotNet v0.13.10, Windows 11 (10.0.22631.3880/23H2/2023Update/SunValley3) 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 | - | - | -| UnnecessaryIsEnabledCheck | 1.332 ns | 0.0225 ns | 0.0188 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 | +.NET SDK 8.0.107 + [Host] : .NET 8.0.7 (8.0.724.31311), X64 RyuJIT AVX2 + DefaultJob : .NET 8.0.7 (8.0.724.31311), X64 RyuJIT AVX2 + + +| Method | Mean | Error | StdDev | Median | Gen0 | Gen1 | Allocated | +|------------------------------ |-----------:|----------:|-----------:|-----------:|-------:|-------:|----------:| +| NoListenerStringInterpolation | 135.503 ns | 2.7458 ns | 4.5114 ns | 135.391 ns | 0.0114 | - | 72 B | +| NoListenerExtensionMethod | 40.218 ns | 0.8249 ns | 2.2581 ns | 39.809 ns | 0.0102 | - | 64 B | +| NoListener | 1.930 ns | 0.0626 ns | 0.1264 ns | 1.889 ns | - | - | - | +| UnnecessaryIsEnabledCheck | 1.531 ns | 0.0542 ns | 0.1267 ns | 1.518 ns | - | - | - | +| CreateLoggerRepeatedly | 53.797 ns | 1.0927 ns | 1.7331 ns | 53.401 ns | 0.0038 | - | 24 B | +| OneProcessor | 111.558 ns | 2.9821 ns | 8.5082 ns | 109.311 ns | 0.0063 | - | 40 B | +| BatchProcessor | 263.650 ns | 5.2908 ns | 14.1223 ns | 258.984 ns | 0.0200 | 0.0043 | 128 B | +| TwoProcessors | 108.701 ns | 2.1964 ns | 4.3355 ns | 108.025 ns | 0.0063 | - | 40 B | +| ThreeProcessors | 105.099 ns | 1.8106 ns | 2.1554 ns | 105.796 ns | 0.0063 | - | 40 B | */ namespace Benchmarks.Logs; @@ -31,15 +32,17 @@ namespace Benchmarks.Logs; public class LogBenchmarks { private const double FoodPrice = 2.99; - private static readonly string FoodName = "tomato"; + private const string FoodName = "tomato"; private readonly ILogger loggerWithNoListener; private readonly ILogger loggerWithOneProcessor; + private readonly ILogger loggerWithBatchProcessor; private readonly ILogger loggerWithTwoProcessors; private readonly ILogger loggerWithThreeProcessors; private readonly ILoggerFactory loggerFactoryWithNoListener; private readonly ILoggerFactory loggerFactoryWithOneProcessor; + private readonly ILoggerFactory loggerFactoryWithBatchProcessor; private readonly ILoggerFactory loggerFactoryWithTwoProcessor; private readonly ILoggerFactory loggerFactoryWithThreeProcessor; @@ -51,24 +54,31 @@ public LogBenchmarks() this.loggerFactoryWithOneProcessor = LoggerFactory.Create(builder => { builder.UseOpenTelemetry(logging => logging - .AddProcessor(new DummyLogProcessor())); + .AddProcessor(new NoopLogProcessor())); }); this.loggerWithOneProcessor = this.loggerFactoryWithOneProcessor.CreateLogger(); + this.loggerFactoryWithBatchProcessor = LoggerFactory.Create(builder => + { + builder.UseOpenTelemetry(logging => logging + .AddProcessor(new BatchLogRecordExportProcessor(new NoopExporter()))); + }); + this.loggerWithBatchProcessor = this.loggerFactoryWithBatchProcessor.CreateLogger(); + this.loggerFactoryWithTwoProcessor = LoggerFactory.Create(builder => { builder.UseOpenTelemetry(logging => logging - .AddProcessor(new DummyLogProcessor()) - .AddProcessor(new DummyLogProcessor())); + .AddProcessor(new NoopLogProcessor()) + .AddProcessor(new NoopLogProcessor())); }); this.loggerWithTwoProcessors = this.loggerFactoryWithTwoProcessor.CreateLogger(); this.loggerFactoryWithThreeProcessor = LoggerFactory.Create(builder => { builder.UseOpenTelemetry(logging => logging - .AddProcessor(new DummyLogProcessor()) - .AddProcessor(new DummyLogProcessor()) - .AddProcessor(new DummyLogProcessor())); + .AddProcessor(new NoopLogProcessor()) + .AddProcessor(new NoopLogProcessor()) + .AddProcessor(new NoopLogProcessor())); }); this.loggerWithThreeProcessors = this.loggerFactoryWithThreeProcessor.CreateLogger(); } @@ -85,19 +95,30 @@ public void GlobalCleanup() [Benchmark] public void NoListenerStringInterpolation() { +#pragma warning disable CA2254 // Template should be a static expression +#pragma warning disable CA1848 // Use the LoggerMessage delegates this.loggerWithNoListener.LogInformation($"Hello from {FoodName} {FoodPrice}."); +#pragma warning restore CA1848 // Use the LoggerMessage delegates +#pragma warning restore CA2254 // Template should be a static expression } [Benchmark] public void NoListenerExtensionMethod() { - this.loggerWithNoListener.LogInformation("Hello from {name} {price}.", FoodName, FoodPrice); +#pragma warning disable CA1848 // Use the LoggerMessage delegates + this.loggerWithNoListener.LogInformation("Hello from {Name} {Price}.", FoodName, FoodPrice); +#pragma warning restore CA1848 // Use the LoggerMessage delegates } [Benchmark] public void NoListener() { - this.loggerWithNoListener.SayHello(FoodName, FoodPrice); + this.loggerWithNoListener.FoodRecallNotice( + brandName: "Contoso", + productDescription: "Salads", + productType: "Food & Beverages", + recallReasonDescription: "due to a possible health risk from Listeria monocytogenes", + companyName: "Contoso Fresh Vegetables, Inc."); } [Benchmark] @@ -105,7 +126,12 @@ public void UnnecessaryIsEnabledCheck() { if (this.loggerWithNoListener.IsEnabled(LogLevel.Information)) { - this.loggerWithNoListener.SayHello(FoodName, FoodPrice); + this.loggerWithNoListener.FoodRecallNotice( + brandName: "Contoso", + productDescription: "Salads", + productType: "Food & Beverages", + recallReasonDescription: "due to a possible health risk from Listeria monocytogenes", + companyName: "Contoso Fresh Vegetables, Inc."); } } @@ -113,28 +139,67 @@ public void UnnecessaryIsEnabledCheck() public void CreateLoggerRepeatedly() { var logger = this.loggerFactoryWithNoListener.CreateLogger(); - logger.SayHello(FoodName, FoodPrice); + logger.FoodRecallNotice( + brandName: "Contoso", + productDescription: "Salads", + productType: "Food & Beverages", + recallReasonDescription: "due to a possible health risk from Listeria monocytogenes", + companyName: "Contoso Fresh Vegetables, Inc."); } [Benchmark] public void OneProcessor() { - this.loggerWithOneProcessor.SayHello(FoodName, FoodPrice); + this.loggerWithOneProcessor.FoodRecallNotice( + brandName: "Contoso", + productDescription: "Salads", + productType: "Food & Beverages", + recallReasonDescription: "due to a possible health risk from Listeria monocytogenes", + companyName: "Contoso Fresh Vegetables, Inc."); + } + + [Benchmark] + public void BatchProcessor() + { + this.loggerWithBatchProcessor.FoodRecallNotice( + brandName: "Contoso", + productDescription: "Salads", + productType: "Food & Beverages", + recallReasonDescription: "due to a possible health risk from Listeria monocytogenes", + companyName: "Contoso Fresh Vegetables, Inc."); } [Benchmark] public void TwoProcessors() { - this.loggerWithTwoProcessors.SayHello(FoodName, FoodPrice); + this.loggerWithTwoProcessors.FoodRecallNotice( + brandName: "Contoso", + productDescription: "Salads", + productType: "Food & Beverages", + recallReasonDescription: "due to a possible health risk from Listeria monocytogenes", + companyName: "Contoso Fresh Vegetables, Inc."); } [Benchmark] public void ThreeProcessors() { - this.loggerWithThreeProcessors.SayHello(FoodName, FoodPrice); + this.loggerWithThreeProcessors.FoodRecallNotice( + brandName: "Contoso", + productDescription: "Salads", + productType: "Food & Beverages", + recallReasonDescription: "due to a possible health risk from Listeria monocytogenes", + companyName: "Contoso Fresh Vegetables, Inc."); + } + + internal sealed class NoopLogProcessor : BaseProcessor + { } - internal class DummyLogProcessor : BaseProcessor + internal sealed class NoopExporter : BaseExporter { + public override ExportResult Export(in Batch batch) + { + return ExportResult.Success; + } } } diff --git a/test/Benchmarks/Logs/LogScopeBenchmarks.cs b/test/Benchmarks/Logs/LogScopeBenchmarks.cs index affa9f5f66c..ffea0671d9e 100644 --- a/test/Benchmarks/Logs/LogScopeBenchmarks.cs +++ b/test/Benchmarks/Logs/LogScopeBenchmarks.cs @@ -39,24 +39,21 @@ public class LogScopeBenchmarks public LogScopeBenchmarks() { this.scopeProvider.Push(new ReadOnlyCollection>( - new List> - { + [ new("item1", "value1"), new("item2", "value2"), - })); + ])); this.scopeProvider.Push(new ReadOnlyCollection>( - new List> - { + [ new("item3", "value3"), - })); + ])); 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( @@ -75,6 +72,6 @@ public LogScopeBenchmarks() [Benchmark] public void ForEachScope() { - this.logRecord.ForEachScope(this.callback, null); + this.logRecord.ForEachScope(this.callback!, null); } } diff --git a/test/Benchmarks/Logs/LoggerExtensions.cs b/test/Benchmarks/Logs/LoggerExtensions.cs new file mode 100644 index 00000000000..caf13626284 --- /dev/null +++ b/test/Benchmarks/Logs/LoggerExtensions.cs @@ -0,0 +1,21 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.Logging; + +namespace Benchmarks.Logs; + +internal static partial class LoggerExtensions +{ + [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); + + [LoggerMessage(LogLevel.Information, "Hello from {Food} {Price}.")] + public static partial void HelloFrom(this ILogger logger, string food, double price); +} diff --git a/test/Benchmarks/Metrics/Base2ExponentialHistogramBenchmarks.cs b/test/Benchmarks/Metrics/Base2ExponentialHistogramBenchmarks.cs index aec8ba6fce5..c37c9517cd5 100644 --- a/test/Benchmarks/Metrics/Base2ExponentialHistogramBenchmarks.cs +++ b/test/Benchmarks/Metrics/Base2ExponentialHistogramBenchmarks.cs @@ -27,14 +27,16 @@ .NET SDK 8.0.100 namespace Benchmarks.Metrics; +#pragma warning disable CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup public class Base2ExponentialHistogramBenchmarks +#pragma warning restore CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup { 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 readonly string[] dimensionValues = ["DimVal1", "DimVal2", "DimVal3", "DimVal4", "DimVal5", "DimVal6", "DimVal7", "DimVal8", "DimVal9", "DimVal10"]; + private Histogram? histogram; + private MeterProvider? meterProvider; + private Meter? meter; [GlobalSetup] public void Setup() @@ -58,29 +60,30 @@ public void Setup() public void Cleanup() { this.meter?.Dispose(); - this.meterProvider.Dispose(); + this.meterProvider?.Dispose(); } [Benchmark] public void HistogramHotPath() { - this.histogram.Record(this.random.Next(MaxValue)); +#pragma warning disable CA5394 // Do not use insecure randomness + 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); + 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); + 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] @@ -94,7 +97,7 @@ public void HistogramWith5LabelsHotPath() { "DimName4", this.dimensionValues[this.random.Next(0, 5)] }, { "DimName5", this.dimensionValues[this.random.Next(0, 10)] }, }; - this.histogram.Record(this.random.Next(MaxValue), tags); + this.histogram!.Record(this.random.Next(MaxValue), tags); } [Benchmark] @@ -110,6 +113,7 @@ public void HistogramWith7LabelsHotPath() { "DimName6", this.dimensionValues[this.random.Next(0, 2)] }, { "DimName7", this.dimensionValues[this.random.Next(0, 1)] }, }; - this.histogram.Record(this.random.Next(MaxValue), tags); + this.histogram!.Record(this.random.Next(MaxValue), tags); +#pragma warning restore CA5394 // Do not use insecure randomness } } diff --git a/test/Benchmarks/Metrics/Base2ExponentialHistogramMapToIndexBenchmarks.cs b/test/Benchmarks/Metrics/Base2ExponentialHistogramMapToIndexBenchmarks.cs index a7cc5edb5ca..71d0ee64174 100644 --- a/test/Benchmarks/Metrics/Base2ExponentialHistogramMapToIndexBenchmarks.cs +++ b/test/Benchmarks/Metrics/Base2ExponentialHistogramMapToIndexBenchmarks.cs @@ -25,7 +25,7 @@ public class Base2ExponentialHistogramMapToIndexBenchmarks { private const int MaxValue = 10000; private readonly Random random = new(); - private Base2ExponentialBucketHistogram exponentialHistogram; + private Base2ExponentialBucketHistogram? exponentialHistogram; [Params(-11, 3, 20)] public int Scale { get; set; } @@ -39,6 +39,8 @@ public void Setup() [Benchmark] public void MapToIndex() { - this.exponentialHistogram.MapToIndex(this.random.Next(MaxValue)); +#pragma warning disable CA5394 // Do not use insecure randomness + this.exponentialHistogram!.MapToIndex(this.random.Next(MaxValue)); +#pragma warning restore CA5394 // Do not use insecure randomness } } diff --git a/test/Benchmarks/Metrics/Base2ExponentialHistogramScaleBenchmarks.cs b/test/Benchmarks/Metrics/Base2ExponentialHistogramScaleBenchmarks.cs index d11d8743ebf..8e72b079ff1 100644 --- a/test/Benchmarks/Metrics/Base2ExponentialHistogramScaleBenchmarks.cs +++ b/test/Benchmarks/Metrics/Base2ExponentialHistogramScaleBenchmarks.cs @@ -24,13 +24,15 @@ .NET SDK 8.0.100 namespace Benchmarks.Metrics; +#pragma warning disable CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup public class Base2ExponentialHistogramScaleBenchmarks +#pragma warning restore CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup { private const int MaxValue = 10000; private readonly Random random = new(); - private Histogram histogram; - private MeterProvider meterProvider; - private Meter meter; + private Histogram? histogram; + private MeterProvider? meterProvider; + private Meter? meter; // This is a simple benchmark that records values in the range [0, 10000]. // The reason the following scales are benchmarked are as follows: @@ -66,12 +68,14 @@ public void Setup() public void Cleanup() { this.meter?.Dispose(); - this.meterProvider.Dispose(); + this.meterProvider?.Dispose(); } [Benchmark] public void HistogramHotPath() { - this.histogram.Record(this.random.Next(MaxValue)); +#pragma warning disable CA5394 // Do not use insecure randomness + this.histogram!.Record(this.random.Next(MaxValue)); +#pragma warning restore CA5394 // Do not use insecure randomness } } diff --git a/test/Benchmarks/Metrics/ExemplarBenchmarks.cs b/test/Benchmarks/Metrics/ExemplarBenchmarks.cs index ac1d347fba7..e2ac358ac7e 100644 --- a/test/Benchmarks/Metrics/ExemplarBenchmarks.cs +++ b/test/Benchmarks/Metrics/ExemplarBenchmarks.cs @@ -38,16 +38,18 @@ .NET SDK 8.0.200 namespace Benchmarks.Metrics; +#pragma warning disable CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup public class ExemplarBenchmarks +#pragma warning restore CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup { 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; + private readonly string[] dimensionValues = ["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 @@ -82,11 +84,15 @@ public void Setup() .SetExemplarFilter(exemplarFilter) .AddView(i => { +#if NET + if (i.Name.Contains("WithTagReduction", StringComparison.Ordinal)) +#else if (i.Name.Contains("WithTagReduction")) +#endif { return new MetricStreamConfiguration() { - TagKeys = new string[] { "DimName1", "DimName2", "DimName3" }, + TagKeys = ["DimName1", "DimName2", "DimName3"], ExemplarReservoirFactory = CreateExemplarReservoir, }; } @@ -104,7 +110,7 @@ public void Setup() }) .Build(); - ExemplarReservoir CreateExemplarReservoir() + ExemplarReservoir? CreateExemplarReservoir() { return this.ExemplarConfiguration == ExemplarConfigurationType.AlwaysOnWithHighValueSampling ? new HighValueExemplarReservoir(800D) @@ -116,15 +122,16 @@ ExemplarReservoir CreateExemplarReservoir() public void Cleanup() { this.meter?.Dispose(); - this.meterProvider.Dispose(); + this.meterProvider?.Dispose(); } [Benchmark] public void HistogramNoTagReduction() { - var random = ThreadLocalRandom.Value; + var random = ThreadLocalRandom.Value!; var tags = new TagList { +#pragma warning disable CA5394 // Do not use insecure randomness { "DimName1", this.dimensionValues[random.Next(0, 2)] }, { "DimName2", this.dimensionValues[random.Next(0, 2)] }, { "DimName3", this.dimensionValues[random.Next(0, 5)] }, @@ -132,13 +139,13 @@ public void HistogramNoTagReduction() { "DimName5", this.dimensionValues[random.Next(0, 10)] }, }; - this.histogramWithoutTagReduction.Record(random.NextDouble() * 1000D, tags); + this.histogramWithoutTagReduction!.Record(random.NextDouble() * 1000D, tags); } [Benchmark] public void HistogramWithTagReduction() { - var random = ThreadLocalRandom.Value; + var random = ThreadLocalRandom.Value!; var tags = new TagList { { "DimName1", this.dimensionValues[random.Next(0, 2)] }, @@ -148,13 +155,13 @@ public void HistogramWithTagReduction() { "DimName5", this.dimensionValues[random.Next(0, 10)] }, }; - this.histogramWithTagReduction.Record(random.NextDouble() * 1000D, tags); + this.histogramWithTagReduction!.Record(random.NextDouble() * 1000D, tags); } [Benchmark] public void CounterNoTagReduction() { - var random = ThreadLocalRandom.Value; + var random = ThreadLocalRandom.Value!; var tags = new TagList { { "DimName1", this.dimensionValues[random.Next(0, 2)] }, @@ -164,13 +171,13 @@ public void CounterNoTagReduction() { "DimName5", this.dimensionValues[random.Next(0, 10)] }, }; - this.counterWithoutTagReduction.Add(random.Next(1000), tags); + this.counterWithoutTagReduction!.Add(random.Next(1000), tags); } [Benchmark] public void CounterWithTagReduction() { - var random = ThreadLocalRandom.Value; + var random = ThreadLocalRandom.Value!; var tags = new TagList { { "DimName1", this.dimensionValues[random.Next(0, 2)] }, @@ -180,7 +187,8 @@ public void CounterWithTagReduction() { "DimName5", this.dimensionValues[random.Next(0, 10)] }, }; - this.counterWithTagReduction.Add(random.Next(1000), tags); + this.counterWithTagReduction!.Add(random.Next(1000), tags); +#pragma warning restore CA5394 // Do not use insecure randomness } private sealed class HighValueExemplarReservoir : FixedSizeExemplarReservoir diff --git a/test/Benchmarks/Metrics/HistogramBenchmarks.cs b/test/Benchmarks/Metrics/HistogramBenchmarks.cs index 17bbad18c67..d5f92fdae64 100644 --- a/test/Benchmarks/Metrics/HistogramBenchmarks.cs +++ b/test/Benchmarks/Metrics/HistogramBenchmarks.cs @@ -42,15 +42,17 @@ .NET SDK 8.0.100 namespace Benchmarks.Metrics; +#pragma warning disable CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup public class HistogramBenchmarks +#pragma warning restore CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup { 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; + private readonly string[] dimensionValues = ["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)] @@ -85,29 +87,30 @@ public void Setup() public void Cleanup() { this.meter?.Dispose(); - this.meterProvider.Dispose(); + this.meterProvider?.Dispose(); } [Benchmark] public void HistogramHotPath() { - this.histogram.Record(this.random.Next(MaxValue)); +#pragma warning disable CA5394 // Do not use insecure randomness + 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); + 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); + 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] @@ -121,7 +124,7 @@ public void HistogramWith5LabelsHotPath() { "DimName4", this.dimensionValues[this.random.Next(0, 5)] }, { "DimName5", this.dimensionValues[this.random.Next(0, 10)] }, }; - this.histogram.Record(this.random.Next(MaxValue), tags); + this.histogram!.Record(this.random.Next(MaxValue), tags); } [Benchmark] @@ -137,6 +140,7 @@ public void HistogramWith7LabelsHotPath() { "DimName6", this.dimensionValues[this.random.Next(0, 2)] }, { "DimName7", this.dimensionValues[this.random.Next(0, 1)] }, }; - this.histogram.Record(this.random.Next(MaxValue), tags); + this.histogram!.Record(this.random.Next(MaxValue), tags); +#pragma warning restore CA5394 // Do not use insecure randomness } } diff --git a/test/Benchmarks/Metrics/MetricCollectBenchmarks.cs b/test/Benchmarks/Metrics/MetricCollectBenchmarks.cs index c0e0bdebb42..cd921b99f11 100644 --- a/test/Benchmarks/Metrics/MetricCollectBenchmarks.cs +++ b/test/Benchmarks/Metrics/MetricCollectBenchmarks.cs @@ -23,18 +23,20 @@ .NET SDK 8.0.100 namespace Benchmarks.Metrics; +#pragma warning disable CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup public class MetricCollectBenchmarks +#pragma warning restore CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup { - private readonly string[] dimensionValues = new string[] { "DimVal1", "DimVal2", "DimVal3", "DimVal4", "DimVal5", "DimVal6", "DimVal7", "DimVal8", "DimVal9", "DimVal10" }; + private readonly string[] dimensionValues = ["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; + 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; } @@ -42,7 +44,9 @@ public class MetricCollectBenchmarks [GlobalSetup] public void Setup() { +#pragma warning disable CA2000 // Dispose objects before losing scope var metricExporter = new TestExporter(ProcessExport); +#pragma warning restore CA2000 // Dispose objects before losing scope void ProcessExport(Batch batch) { double sum = 0; @@ -86,9 +90,11 @@ void ProcessExport(Batch batch) { 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)]); +#pragma warning disable CA5394 // Do not use insecure randomness + 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)]); +#pragma warning restore CA5394 // Do not use insecure randomness this.counter.Add(100.00, tag1, tag2, tag3); } }); @@ -98,16 +104,16 @@ void ProcessExport(Batch batch) [GlobalCleanup] public void Cleanup() { - this.token.Cancel(); - this.token.Dispose(); - this.writeMetricTask.Wait(); - this.meter.Dispose(); - this.provider.Dispose(); + this.token?.Cancel(); + this.token?.Dispose(); + this.writeMetricTask?.Wait(); + this.meter?.Dispose(); + this.provider?.Dispose(); } [Benchmark] public void Collect() { - this.reader.Collect(); + this.reader!.Collect(); } } diff --git a/test/Benchmarks/Metrics/MetricsBenchmarks.cs b/test/Benchmarks/Metrics/MetricsBenchmarks.cs index e63aeb87576..0adc5895a3b 100644 --- a/test/Benchmarks/Metrics/MetricsBenchmarks.cs +++ b/test/Benchmarks/Metrics/MetricsBenchmarks.cs @@ -56,13 +56,15 @@ .NET SDK 8.0.101 namespace Benchmarks.Metrics; +#pragma warning disable CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup public class MetricsBenchmarks +#pragma warning restore CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup { 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; + private readonly string[] dimensionValues = ["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; } @@ -89,83 +91,84 @@ public void Setup() public void Cleanup() { this.meter?.Dispose(); - this.meterProvider.Dispose(); + this.meterProvider?.Dispose(); } [Benchmark] public void CounterHotPath() { - this.counter.Add(100); + this.counter!.Add(100); } [Benchmark] public void CounterWith1LabelsHotPath() { - var tag1 = new KeyValuePair("DimName1", this.dimensionValues[this.random.Next(0, 10)]); - this.counter.Add(100, tag1); +#pragma warning disable CA5394 // Do not use insecure randomness + 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); + 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); + 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); + 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); + 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); + 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); + 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] @@ -175,7 +178,7 @@ public void CounterWith1LabelsHotPathUsingTagList() { { "DimName1", this.dimensionValues[this.random.Next(0, 10)] }, }; - this.counter.Add(100, tags); + this.counter!.Add(100, tags); } [Benchmark] @@ -186,7 +189,7 @@ public void CounterWith2LabelsHotPathUsingTagList() { "DimName1", this.dimensionValues[this.random.Next(0, 10)] }, { "DimName2", this.dimensionValues[this.random.Next(0, 10)] }, }; - this.counter.Add(100, tags); + this.counter!.Add(100, tags); } [Benchmark] @@ -198,7 +201,7 @@ public void CounterWith3LabelsHotPathUsingTagList() { "DimName2", this.dimensionValues[this.random.Next(0, 10)] }, { "DimName3", this.dimensionValues[this.random.Next(0, 10)] }, }; - this.counter.Add(100, tags); + this.counter!.Add(100, tags); } [Benchmark] @@ -211,7 +214,7 @@ public void CounterWith4LabelsHotPathUsingTagList() { "DimName3", this.dimensionValues[this.random.Next(0, 10)] }, { "DimName4", this.dimensionValues[this.random.Next(0, 10)] }, }; - this.counter.Add(100, tags); + this.counter!.Add(100, tags); } [Benchmark] @@ -225,7 +228,7 @@ public void CounterWith5LabelsHotPathUsingTagList() { "DimName4", this.dimensionValues[this.random.Next(0, 5)] }, { "DimName5", this.dimensionValues[this.random.Next(0, 10)] }, }; - this.counter.Add(100, tags); + this.counter!.Add(100, tags); } [Benchmark] @@ -240,7 +243,7 @@ public void CounterWith6LabelsHotPathUsingTagList() { "DimName5", this.dimensionValues[this.random.Next(0, 5)] }, { "DimName6", this.dimensionValues[this.random.Next(0, 5)] }, }; - this.counter.Add(100, tags); + this.counter!.Add(100, tags); } [Benchmark] @@ -256,7 +259,7 @@ public void CounterWith7LabelsHotPathUsingTagList() { "DimName6", this.dimensionValues[this.random.Next(0, 5)] }, { "DimName7", this.dimensionValues[this.random.Next(0, 5)] }, }; - this.counter.Add(100, tags); + this.counter!.Add(100, tags); } [Benchmark] @@ -273,7 +276,7 @@ public void CounterWith8LabelsHotPathUsingTagList() { "DimName7", this.dimensionValues[this.random.Next(0, 5)] }, { "DimName8", this.dimensionValues[this.random.Next(0, 5)] }, }; - this.counter.Add(100, tags); + this.counter!.Add(100, tags); } [Benchmark] @@ -290,7 +293,8 @@ public void CounterWith9LabelsHotPathUsingTagList() { "DimName7", this.dimensionValues[this.random.Next(0, 5)] }, { "DimName8", this.dimensionValues[this.random.Next(0, 5)] }, { "DimName9", this.dimensionValues[this.random.Next(0, 5)] }, +#pragma warning restore CA5394 // Do not use insecure randomness }; - this.counter.Add(100, tags); + this.counter!.Add(100, tags); } } diff --git a/test/Benchmarks/Metrics/MetricsViewBenchmarks.cs b/test/Benchmarks/Metrics/MetricsViewBenchmarks.cs index 359313ac9bf..fc08872018e 100644 --- a/test/Benchmarks/Metrics/MetricsViewBenchmarks.cs +++ b/test/Benchmarks/Metrics/MetricsViewBenchmarks.cs @@ -27,15 +27,16 @@ .NET SDK 8.0.100 namespace Benchmarks.Metrics; +#pragma warning disable CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup public class MetricsViewBenchmarks +#pragma warning restore CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup { 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; + private static readonly string[] DimensionValues = ["DimVal1", "DimVal2", "DimVal3", "DimVal4", "DimVal5", "DimVal6", "DimVal7", "DimVal8", "DimVal9", "DimVal10"]; + private List? metrics; + private Counter? counter; + private MeterProvider? meterProvider; + private Meter? meter; public enum ViewConfiguration { @@ -83,7 +84,7 @@ public void Setup() { this.meter = new Meter(Utils.GetCurrentMethodName()); this.counter = this.meter.CreateCounter("counter"); - this.metrics = new List(); + this.metrics = []; if (this.ViewConfig == ViewConfiguration.NoView) { @@ -96,7 +97,7 @@ public void Setup() { this.meterProvider = Sdk.CreateMeterProviderBuilder() .AddMeter(this.meter.Name) - .AddView("nomatch", new MetricStreamConfiguration() { TagKeys = new string[] { "DimName1", "DimName2", "DimName3" } }) + .AddView("nomatch", new MetricStreamConfiguration() { TagKeys = ["DimName1", "DimName2", "DimName3"] }) .AddInMemoryExporter(this.metrics) .Build(); } @@ -104,7 +105,7 @@ public void Setup() { this.meterProvider = Sdk.CreateMeterProviderBuilder() .AddMeter(this.meter.Name) - .AddView(this.counter.Name, new MetricStreamConfiguration() { TagKeys = new string[] { "DimName1", "DimName2", "DimName3" } }) + .AddView(this.counter.Name, new MetricStreamConfiguration() { TagKeys = ["DimName1", "DimName2", "DimName3"] }) .AddInMemoryExporter(this.metrics) .Build(); } @@ -120,7 +121,7 @@ public void Setup() { this.meterProvider = Sdk.CreateMeterProviderBuilder() .AddMeter(this.meter.Name) - .AddView(this.counter.Name, new MetricStreamConfiguration() { TagKeys = Array.Empty() }) + .AddView(this.counter.Name, new MetricStreamConfiguration() { TagKeys = [] }) .AddInMemoryExporter(this.metrics) .Build(); } @@ -130,20 +131,22 @@ public void Setup() public void Cleanup() { this.meter?.Dispose(); - this.meterProvider.Dispose(); + this.meterProvider?.Dispose(); } [Benchmark] public void CounterHotPath() { - var random = ThreadLocalRandom.Value; + var random = ThreadLocalRandom.Value!; var tags = new TagList { +#pragma warning disable CA5394 // Do not use insecure randomness { "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)] }, +#pragma warning restore CA5394 // Do not use insecure randomness }; this.counter?.Add( diff --git a/test/Benchmarks/SuppressInstrumentationScopeBenchmarks.cs b/test/Benchmarks/SuppressInstrumentationScopeBenchmarks.cs index b99b20c23d5..a757ef697eb 100644 --- a/test/Benchmarks/SuppressInstrumentationScopeBenchmarks.cs +++ b/test/Benchmarks/SuppressInstrumentationScopeBenchmarks.cs @@ -8,7 +8,9 @@ namespace OpenTelemetry.Benchmarks; public class SuppressInstrumentationScopeBenchmarks { [Benchmark] +#pragma warning disable CA1822 // Mark members as static public void Begin() +#pragma warning restore CA1822 // Mark members as static { using var scope1 = SuppressInstrumentationScope.Begin(); @@ -18,7 +20,9 @@ public void Begin() } [Benchmark] +#pragma warning disable CA1822 // Mark members as static public void Enter() +#pragma warning restore CA1822 // Mark members as static { SuppressInstrumentationScope.Enter(); diff --git a/test/Benchmarks/TestTraceServiceClient.cs b/test/Benchmarks/TestTraceServiceClient.cs deleted file mode 100644 index 968ddd75715..00000000000 --- a/test/Benchmarks/TestTraceServiceClient.cs +++ /dev/null @@ -1,17 +0,0 @@ -// 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 index 447e550bd87..f85c919a2ee 100644 --- a/test/Benchmarks/Trace/ActivityCreationBenchmarks.cs +++ b/test/Benchmarks/Trace/ActivityCreationBenchmarks.cs @@ -25,26 +25,30 @@ .NET SDK 8.0.100 namespace Benchmarks.Trace; +#pragma warning disable CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup public class ActivityCreationBenchmarks +#pragma warning restore CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup { private readonly ActivitySource benchmarkSource = new("Benchmark"); private readonly ActivityContext parentCtx = new(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None); - private TracerProvider tracerProvider; + private TracerProvider? tracerProvider; [GlobalSetup] public void GlobalSetup() { this.tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("BenchMark") +#pragma warning disable CA2000 // Dispose objects before losing scope .AddProcessor(new NoopActivityProcessor()) +#pragma warning restore CA2000 // Dispose objects before losing scope .Build(); } [GlobalCleanup] public void GlobalCleanup() { - this.tracerProvider.Dispose(); - this.benchmarkSource.Dispose(); + this.tracerProvider?.Dispose(); + this.benchmarkSource?.Dispose(); } [Benchmark] @@ -59,7 +63,7 @@ public void GlobalCleanup() [Benchmark] public void CreateActivity_WithAddTags_NoopProcessor() => ActivityCreationScenarios.CreateActivityWithAddTags(this.benchmarkSource); - internal class NoopActivityProcessor : BaseProcessor + internal sealed class NoopActivityProcessor : BaseProcessor { } } diff --git a/test/Benchmarks/Trace/SamplerBenchmarks.cs b/test/Benchmarks/Trace/SamplerBenchmarks.cs index 440536f281e..819de8d466c 100644 --- a/test/Benchmarks/Trace/SamplerBenchmarks.cs +++ b/test/Benchmarks/Trace/SamplerBenchmarks.cs @@ -23,15 +23,26 @@ .NET SDK 8.0.100 namespace Benchmarks.Trace; +#pragma warning disable CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup public class SamplerBenchmarks +#pragma warning restore CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup { - 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() + private ActivitySource? sourceNotModifyTracestate; + private ActivitySource? sourceModifyTracestate; + private ActivitySource? sourceAppendTracestate; + private ActivityContext parentContext; + private TracerProvider? tracerProviderNotModifyTracestate; + private TracerProvider? tracerProviderModifyTracestate; + private TracerProvider? tracerProviderAppendTracestate; + + [GlobalSetup] + public void Setup() { + this.sourceNotModifyTracestate = new("SamplerNotModifyingTraceState"); + this.sourceModifyTracestate = new("SamplerModifyingTraceState"); + this.sourceAppendTracestate = new("SamplerAppendingTraceState"); + this.parentContext = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded, "a=b", true); + var testSamplerNotModifyTracestate = new TestSampler { SamplingAction = (samplingParams) => @@ -56,43 +67,54 @@ public SamplerBenchmarks() }, }; - Sdk.CreateTracerProviderBuilder() + this.tracerProviderNotModifyTracestate = Sdk.CreateTracerProviderBuilder() .SetSampler(testSamplerNotModifyTracestate) .AddSource(this.sourceNotModifyTracestate.Name) .Build(); - Sdk.CreateTracerProviderBuilder() + this.tracerProviderModifyTracestate = Sdk.CreateTracerProviderBuilder() .SetSampler(testSamplerModifyTracestate) .AddSource(this.sourceModifyTracestate.Name) .Build(); - Sdk.CreateTracerProviderBuilder() + this.tracerProviderAppendTracestate = Sdk.CreateTracerProviderBuilder() .SetSampler(testSamplerAppendTracestate) .AddSource(this.sourceAppendTracestate.Name) .Build(); } + [GlobalCleanup] + public void Cleanup() + { + this.sourceNotModifyTracestate?.Dispose(); + this.sourceModifyTracestate?.Dispose(); + this.sourceAppendTracestate?.Dispose(); + this.tracerProviderNotModifyTracestate?.Dispose(); + this.tracerProviderModifyTracestate?.Dispose(); + this.tracerProviderAppendTracestate?.Dispose(); + } + [Benchmark] public void SamplerNotModifyingTraceState() { - using var activity = this.sourceNotModifyTracestate.StartActivity("Benchmark", ActivityKind.Server, this.parentContext); + 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); + 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); + using var activity = this.sourceAppendTracestate!.StartActivity("Benchmark", ActivityKind.Server, this.parentContext); } - internal class TestSampler : Sampler + internal sealed class TestSampler : Sampler { - public Func SamplingAction { get; set; } + public Func? SamplingAction { get; set; } public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) { diff --git a/test/Benchmarks/Trace/SpanCreationBenchmarks.cs b/test/Benchmarks/Trace/SpanCreationBenchmarks.cs index 9ca218f2dc8..4aacae1586d 100644 --- a/test/Benchmarks/Trace/SpanCreationBenchmarks.cs +++ b/test/Benchmarks/Trace/SpanCreationBenchmarks.cs @@ -32,11 +32,11 @@ namespace Benchmarks.Trace; public class SpanCreationBenchmarks { - private Tracer alwaysSampleTracer; - private Tracer neverSampleTracer; - private Tracer noopTracer; - private TracerProvider tracerProviderAlwaysOnSample; - private TracerProvider tracerProviderAlwaysOffSample; + private Tracer? alwaysSampleTracer; + private Tracer? neverSampleTracer; + private Tracer? noopTracer; + private TracerProvider? tracerProviderAlwaysOnSample; + private TracerProvider? tracerProviderAlwaysOffSample; [GlobalSetup] public void GlobalSetup() @@ -61,37 +61,37 @@ public void GlobalSetup() [GlobalCleanup] public void GlobalCleanup() { - this.tracerProviderAlwaysOffSample.Dispose(); - this.tracerProviderAlwaysOnSample.Dispose(); + this.tracerProviderAlwaysOffSample?.Dispose(); + this.tracerProviderAlwaysOnSample?.Dispose(); } [Benchmark] - public void CreateSpan_Sampled() => SpanCreationScenarios.CreateSpan(this.alwaysSampleTracer); + public void CreateSpan_Sampled() => SpanCreationScenarios.CreateSpan(this.alwaysSampleTracer!); [Benchmark] - public void CreateSpan_ParentContext() => SpanCreationScenarios.CreateSpan_ParentContext(this.alwaysSampleTracer); + public void CreateSpan_ParentContext() => SpanCreationScenarios.CreateSpan_ParentContext(this.alwaysSampleTracer!); [Benchmark] - public void CreateSpan_Attributes_Sampled() => SpanCreationScenarios.CreateSpan_Attributes(this.alwaysSampleTracer); + public void CreateSpan_Attributes_Sampled() => SpanCreationScenarios.CreateSpan_Attributes(this.alwaysSampleTracer!); [Benchmark] - public void CreateSpan_WithSpan() => SpanCreationScenarios.CreateSpan_Propagate(this.alwaysSampleTracer); + public void CreateSpan_WithSpan() => SpanCreationScenarios.CreateSpan_Propagate(this.alwaysSampleTracer!); [Benchmark] - public void CreateSpan_Active() => SpanCreationScenarios.CreateSpan_Active(this.alwaysSampleTracer); + public void CreateSpan_Active() => SpanCreationScenarios.CreateSpan_Active(this.alwaysSampleTracer!); [Benchmark] - public void CreateSpan_Active_GetCurrent() => SpanCreationScenarios.CreateSpan_Active_GetCurrent(this.alwaysSampleTracer); + public void CreateSpan_Active_GetCurrent() => SpanCreationScenarios.CreateSpan_Active_GetCurrent(this.alwaysSampleTracer!); [Benchmark] - public void CreateSpan_Attributes_NotSampled() => SpanCreationScenarios.CreateSpan_Attributes(this.neverSampleTracer); + public void CreateSpan_Attributes_NotSampled() => SpanCreationScenarios.CreateSpan_Attributes(this.neverSampleTracer!); [Benchmark(Baseline = true)] - public void CreateSpan_Noop() => SpanCreationScenarios.CreateSpan(this.noopTracer); + public void CreateSpan_Noop() => SpanCreationScenarios.CreateSpan(this.noopTracer!); [Benchmark] - public void CreateSpan_Attributes_Noop() => SpanCreationScenarios.CreateSpan_Attributes(this.noopTracer); + public void CreateSpan_Attributes_Noop() => SpanCreationScenarios.CreateSpan_Attributes(this.noopTracer!); [Benchmark] - public void CreateSpan_Propagate_Noop() => SpanCreationScenarios.CreateSpan_Propagate(this.noopTracer); + 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 e85e52cbd5a..d9e10bda977 100644 --- a/test/Benchmarks/Trace/TraceBenchmarks.cs +++ b/test/Benchmarks/Trace/TraceBenchmarks.cs @@ -31,60 +31,92 @@ .NET SDK 8.0.100 namespace Benchmarks.Trace; +#pragma warning disable CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup public class TraceBenchmarks +#pragma warning restore CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup { - 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 ActivitySource? sourceWithNoListener; + private ActivitySource? sourceWithPropagationDataListner; + private ActivitySource? sourceWithAllDataListner; + private ActivitySource? sourceWithAllDataAndRecordedListner; + private ActivitySource? sourceWithOneProcessor; + private ActivitySource? sourceWithTwoProcessors; + private ActivitySource? sourceWithThreeProcessors; + private ActivitySource? sourceWithOneLegacyActivityOperationNameSubscription; + private ActivitySource? sourceWithTwoLegacyActivityOperationNameSubscriptions; + + private TracerProvider? tracerProvierWithOneProcessor; + private TracerProvider? tracerProvierWithTwoProcessors; + private TracerProvider? tracerProvierWithThreeProcessors; + private TracerProvider? tracerProvierWithOneLegacyActivityOperationNameSubscription; + private TracerProvider? tracerProvierWithTwoLegacyActivityOperationNameSubscriptions; + private TracerProvider? tracerProvierWithExactMatchLegacyActivityListner; + private TracerProvider? tracerProvierWithWildcardMatchLegacyActivityListner; + + private ActivityListener? activityListenerPropagationData; + private ActivityListener? activityListenerAllData; + private ActivityListener? activityListenerAllDataAndRecordedData; + + [GlobalSetup] + public void Setup() { Activity.DefaultIdFormat = ActivityIdFormat.W3C; - ActivitySource.AddActivityListener(new ActivityListener + this.sourceWithNoListener = new("Benchmark.NoListener"); + this.sourceWithPropagationDataListner = new("Benchmark.PropagationDataListner"); + this.sourceWithAllDataListner = new("Benchmark.AllDataListner"); + this.sourceWithAllDataAndRecordedListner = new("Benchmark.AllDataAndRecordedListner"); + this.sourceWithOneProcessor = new("Benchmark.OneProcessor"); + this.sourceWithTwoProcessors = new("Benchmark.TwoProcessors"); + this.sourceWithThreeProcessors = new("Benchmark.ThreeProcessors"); + this.sourceWithOneLegacyActivityOperationNameSubscription = new("Benchmark.OneInstrumentation"); + this.sourceWithTwoLegacyActivityOperationNameSubscriptions = new("Benchmark.TwoInstrumentations"); + + this.activityListenerPropagationData = new ActivityListener { ActivityStarted = null, ActivityStopped = null, ShouldListenTo = (activitySource) => activitySource.Name == this.sourceWithPropagationDataListner.Name, Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.PropagationData, - }); + }; - ActivitySource.AddActivityListener(new ActivityListener + ActivitySource.AddActivityListener(this.activityListenerPropagationData); + + this.activityListenerAllData = new ActivityListener { ActivityStarted = null, ActivityStopped = null, ShouldListenTo = (activitySource) => activitySource.Name == this.sourceWithAllDataListner.Name, Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, - }); + }; + + ActivitySource.AddActivityListener(this.activityListenerAllData); - ActivitySource.AddActivityListener(new ActivityListener + this.activityListenerAllDataAndRecordedData = new ActivityListener { ActivityStarted = null, ActivityStopped = null, ShouldListenTo = (activitySource) => activitySource.Name == this.sourceWithAllDataAndRecordedListner.Name, Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllDataAndRecorded, - }); + }; + + ActivitySource.AddActivityListener(this.activityListenerAllDataAndRecordedData); - Sdk.CreateTracerProviderBuilder() + this.tracerProvierWithOneProcessor = Sdk.CreateTracerProviderBuilder() .SetSampler(new AlwaysOnSampler()) .AddSource(this.sourceWithOneProcessor.Name) +#pragma warning disable CA2000 // Dispose objects before losing scope .AddProcessor(new DummyActivityProcessor()) .Build(); - Sdk.CreateTracerProviderBuilder() + this.tracerProvierWithTwoProcessors = Sdk.CreateTracerProviderBuilder() .SetSampler(new AlwaysOnSampler()) .AddSource(this.sourceWithTwoProcessors.Name) .AddProcessor(new DummyActivityProcessor()) .AddProcessor(new DummyActivityProcessor()) .Build(); - Sdk.CreateTracerProviderBuilder() + this.tracerProvierWithThreeProcessors = Sdk.CreateTracerProviderBuilder() .SetSampler(new AlwaysOnSampler()) .AddSource(this.sourceWithThreeProcessors.Name) .AddProcessor(new DummyActivityProcessor()) @@ -92,14 +124,14 @@ public TraceBenchmarks() .AddProcessor(new DummyActivityProcessor()) .Build(); - Sdk.CreateTracerProviderBuilder() + this.tracerProvierWithOneLegacyActivityOperationNameSubscription = Sdk.CreateTracerProviderBuilder() .SetSampler(new AlwaysOnSampler()) .AddSource(this.sourceWithOneLegacyActivityOperationNameSubscription.Name) .AddLegacySource("TestOperationName") .AddProcessor(new DummyActivityProcessor()) .Build(); - Sdk.CreateTracerProviderBuilder() + this.tracerProvierWithTwoLegacyActivityOperationNameSubscriptions = Sdk.CreateTracerProviderBuilder() .SetSampler(new AlwaysOnSampler()) .AddSource(this.sourceWithTwoLegacyActivityOperationNameSubscriptions.Name) .AddLegacySource("TestOperationName1") @@ -107,90 +139,121 @@ public TraceBenchmarks() .AddProcessor(new DummyActivityProcessor()) .Build(); - Sdk.CreateTracerProviderBuilder() + this.tracerProvierWithExactMatchLegacyActivityListner = Sdk.CreateTracerProviderBuilder() .SetSampler(new AlwaysOnSampler()) .AddLegacySource("ExactMatch.OperationName1") .AddProcessor(new DummyActivityProcessor()) .Build(); - Sdk.CreateTracerProviderBuilder() + this.tracerProvierWithWildcardMatchLegacyActivityListner = Sdk.CreateTracerProviderBuilder() .SetSampler(new AlwaysOnSampler()) .AddLegacySource("WildcardMatch.*") .AddProcessor(new DummyActivityProcessor()) +#pragma warning restore CA2000 // Dispose objects before losing scope .Build(); } + [GlobalCleanup] + public void Cleanup() + { + this.sourceWithNoListener?.Dispose(); + this.sourceWithPropagationDataListner?.Dispose(); + this.sourceWithAllDataListner?.Dispose(); + this.sourceWithAllDataAndRecordedListner?.Dispose(); + this.sourceWithOneProcessor?.Dispose(); + this.sourceWithTwoProcessors?.Dispose(); + this.sourceWithThreeProcessors?.Dispose(); + this.sourceWithOneLegacyActivityOperationNameSubscription?.Dispose(); + this.sourceWithTwoLegacyActivityOperationNameSubscriptions?.Dispose(); + + this.tracerProvierWithOneProcessor?.Dispose(); + this.tracerProvierWithTwoProcessors?.Dispose(); + this.tracerProvierWithThreeProcessors?.Dispose(); + this.tracerProvierWithOneLegacyActivityOperationNameSubscription?.Dispose(); + this.tracerProvierWithTwoLegacyActivityOperationNameSubscriptions?.Dispose(); + this.tracerProvierWithExactMatchLegacyActivityListner?.Dispose(); + this.tracerProvierWithWildcardMatchLegacyActivityListner?.Dispose(); + + this.activityListenerPropagationData?.Dispose(); + this.activityListenerAllData?.Dispose(); + this.activityListenerAllDataAndRecordedData?.Dispose(); + } + [Benchmark] public void NoListener() { // this activity won't be created as there is no listener - using var activity = this.sourceWithNoListener.StartActivity("Benchmark"); + using var activity = this.sourceWithNoListener!.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"); + using var activity = this.sourceWithPropagationDataListner!.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"); + using var activity = this.sourceWithAllDataListner!.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"); + using var activity = this.sourceWithAllDataAndRecordedListner!.StartActivity("Benchmark"); } [Benchmark] public void OneProcessor() { - using var activity = this.sourceWithOneProcessor.StartActivity("Benchmark"); + using var activity = this.sourceWithOneProcessor!.StartActivity("Benchmark"); } [Benchmark] public void TwoProcessors() { - using var activity = this.sourceWithTwoProcessors.StartActivity("Benchmark"); + using var activity = this.sourceWithTwoProcessors!.StartActivity("Benchmark"); } [Benchmark] public void ThreeProcessors() { - using var activity = this.sourceWithThreeProcessors.StartActivity("Benchmark"); + using var activity = this.sourceWithThreeProcessors!.StartActivity("Benchmark"); } [Benchmark] public void OneInstrumentation() { - using var activity = this.sourceWithOneLegacyActivityOperationNameSubscription.StartActivity("Benchmark"); + using var activity = this.sourceWithOneLegacyActivityOperationNameSubscription!.StartActivity("Benchmark"); } [Benchmark] public void TwoInstrumentations() { - using var activity = this.sourceWithTwoLegacyActivityOperationNameSubscriptions.StartActivity("Benchmark"); + using var activity = this.sourceWithTwoLegacyActivityOperationNameSubscriptions!.StartActivity("Benchmark"); } [Benchmark] +#pragma warning disable CA1822 // Mark members as static public void LegacyActivity_ExactMatchMode() +#pragma warning restore CA1822 // Mark members as static { - using var activity = new Activity("ExactMatch.OperationName1").Start(); + using var activity = new Activity("ExactMatch.OperationName1"); + activity.Start(); } [Benchmark] +#pragma warning disable CA1822 // Mark members as static public void LegacyActivity_WildcardMatchMode() +#pragma warning restore CA1822 // Mark members as static { - using var activity = new Activity("WildcardMatch.OperationName1").Start(); + using var activity = new Activity("WildcardMatch.OperationName1"); + activity.Start(); } - internal class DummyActivityProcessor : BaseProcessor - { - } + internal sealed class DummyActivityProcessor : BaseProcessor; } diff --git a/test/Benchmarks/Trace/TraceShimBenchmarks.cs b/test/Benchmarks/Trace/TraceShimBenchmarks.cs index b9d11939936..2b0526fffd8 100644 --- a/test/Benchmarks/Trace/TraceShimBenchmarks.cs +++ b/test/Benchmarks/Trace/TraceShimBenchmarks.cs @@ -38,6 +38,7 @@ public TraceShimBenchmarks() Sdk.CreateTracerProviderBuilder() .SetSampler(new AlwaysOnSampler()) .AddSource("Benchmark.OneProcessor") +#pragma warning disable CA2000 // Dispose objects before losing scope .AddProcessor(new DummyActivityProcessor()) .Build(); @@ -54,6 +55,7 @@ public TraceShimBenchmarks() .AddProcessor(new DummyActivityProcessor()) .AddProcessor(new DummyActivityProcessor()) .AddProcessor(new DummyActivityProcessor()) +#pragma warning restore CA2000 // Dispose objects before losing scope .Build(); } @@ -82,7 +84,5 @@ public void ThreeProcessors() using var activity = this.tracerWithThreeProcessors.StartActiveSpan("Benchmark"); } - internal class DummyActivityProcessor : BaseProcessor - { - } + internal sealed class DummyActivityProcessor : BaseProcessor; } diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 032e1e27ec9..4c709d51681 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -1,3 +1,4 @@ + diff --git a/test/Directory.Build.targets b/test/Directory.Build.targets index 03d36ba8a59..55ac9b04c79 100644 --- a/test/Directory.Build.targets +++ b/test/Directory.Build.targets @@ -1,10 +1,15 @@ - - - + + + + + + + + + diff --git a/test/Directory.Packages.props b/test/Directory.Packages.props deleted file mode 100644 index 575224321a8..00000000000 --- a/test/Directory.Packages.props +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Metrics/TestMeterProviderBuilder.cs b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Metrics/TestMeterProviderBuilder.cs index 424b271417a..922b8f61a20 100644 --- a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Metrics/TestMeterProviderBuilder.cs +++ b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Metrics/TestMeterProviderBuilder.cs @@ -6,7 +6,7 @@ namespace OpenTelemetry.Api.ProviderBuilderExtensions.Tests; -public sealed class TestMeterProviderBuilder : MeterProviderBuilder, IMeterProviderBuilder, IDisposable +internal sealed class TestMeterProviderBuilder : MeterProviderBuilder, IMeterProviderBuilder, IDisposable { public TestMeterProviderBuilder() { @@ -17,9 +17,9 @@ public TestMeterProviderBuilder() public ServiceProvider? ServiceProvider { get; private set; } - public List Meters { get; } = new(); + public List Meters { get; } = []; - public List Instrumentation { get; } = new(); + public List Instrumentation { get; } = []; public MeterProvider? Provider { get; private set; } diff --git a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/OpenTelemetry.Api.ProviderBuilderExtensions.Tests.csproj b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/OpenTelemetry.Api.ProviderBuilderExtensions.Tests.csproj index de24f0131fe..95ac41acb81 100644 --- a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/OpenTelemetry.Api.ProviderBuilderExtensions.Tests.csproj +++ b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/OpenTelemetry.Api.ProviderBuilderExtensions.Tests.csproj @@ -1,4 +1,4 @@ - + Unit test project for OpenTelemetry .NET dependency injection extensions $(TargetFrameworksForTests) @@ -9,11 +9,6 @@ - - - - runtime; build; native; contentfiles; analyzers - diff --git a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/ServiceCollectionExtensionsTests.cs b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/ServiceCollectionExtensionsTests.cs index fa3fb4e13d9..ccbc88938d8 100644 --- a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/ServiceCollectionExtensionsTests.cs +++ b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/ServiceCollectionExtensionsTests.cs @@ -30,7 +30,7 @@ public void ConfigureOpenTelemetryTracerProvider(int numberOfCalls) using var serviceProvider = services.BuildServiceProvider(); - var registrations = serviceProvider.GetServices(); + var registrations = serviceProvider.GetServices().ToArray(); Assert.Equal(numberOfCalls, beforeServiceProviderInvocations); Assert.Equal(0, afterServiceProviderInvocations); @@ -43,7 +43,7 @@ public void ConfigureOpenTelemetryTracerProvider(int numberOfCalls) Assert.Equal(numberOfCalls, beforeServiceProviderInvocations); Assert.Equal(numberOfCalls, afterServiceProviderInvocations); - Assert.Equal(numberOfCalls * 2, registrations.Count()); + Assert.Equal(numberOfCalls * 2, registrations.Length); } [Theory] @@ -65,7 +65,7 @@ public void ConfigureOpenTelemetryMeterProvider(int numberOfCalls) using var serviceProvider = services.BuildServiceProvider(); - var registrations = serviceProvider.GetServices(); + var registrations = serviceProvider.GetServices().ToArray(); Assert.Equal(numberOfCalls, beforeServiceProviderInvocations); Assert.Equal(0, afterServiceProviderInvocations); @@ -78,7 +78,7 @@ public void ConfigureOpenTelemetryMeterProvider(int numberOfCalls) Assert.Equal(numberOfCalls, beforeServiceProviderInvocations); Assert.Equal(numberOfCalls, afterServiceProviderInvocations); - Assert.Equal(numberOfCalls * 2, registrations.Count()); + Assert.Equal(numberOfCalls * 2, registrations.Length); } [Theory] @@ -100,7 +100,7 @@ public void ConfigureOpenTelemetryLoggerProvider(int numberOfCalls) using var serviceProvider = services.BuildServiceProvider(); - var registrations = serviceProvider.GetServices(); + var registrations = serviceProvider.GetServices().ToArray(); Assert.Equal(numberOfCalls, beforeServiceProviderInvocations); Assert.Equal(0, afterServiceProviderInvocations); @@ -113,6 +113,6 @@ public void ConfigureOpenTelemetryLoggerProvider(int numberOfCalls) Assert.Equal(numberOfCalls, beforeServiceProviderInvocations); Assert.Equal(numberOfCalls, afterServiceProviderInvocations); - Assert.Equal(numberOfCalls * 2, registrations.Count()); + Assert.Equal(numberOfCalls * 2, registrations.Length); } } diff --git a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Trace/TestTracerProviderBuilder.cs b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Trace/TestTracerProviderBuilder.cs index db64a8549c2..89a8bc10ec7 100644 --- a/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Trace/TestTracerProviderBuilder.cs +++ b/test/OpenTelemetry.Api.ProviderBuilderExtensions.Tests/Trace/TestTracerProviderBuilder.cs @@ -6,7 +6,7 @@ namespace OpenTelemetry.Api.ProviderBuilderExtensions.Tests; -public sealed class TestTracerProviderBuilder : TracerProviderBuilder, ITracerProviderBuilder, IDisposable +internal sealed class TestTracerProviderBuilder : TracerProviderBuilder, ITracerProviderBuilder, IDisposable { public TestTracerProviderBuilder() { @@ -17,11 +17,11 @@ public TestTracerProviderBuilder() public ServiceProvider? ServiceProvider { get; private set; } - public List Sources { get; } = new(); + public List Sources { get; } = []; - public List LegacySources { get; } = new(); + public List LegacySources { get; } = []; - public List Instrumentation { get; } = new(); + public List Instrumentation { get; } = []; public TracerProvider? Provider { get; private set; } diff --git a/test/OpenTelemetry.Api.Tests/BaggageTests.cs b/test/OpenTelemetry.Api.Tests/BaggageTests.cs index e13aa68a502..2c17b782f87 100644 --- a/test/OpenTelemetry.Api.Tests/BaggageTests.cs +++ b/test/OpenTelemetry.Api.Tests/BaggageTests.cs @@ -1,6 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Globalization; using Xunit; namespace OpenTelemetry.Tests; @@ -27,8 +28,8 @@ public void SetAndGetTest() { var list = new List>(2) { - new KeyValuePair(K1, V1), - new KeyValuePair(K2, V2), + new(K1, V1), + new(K2, V2), }; Baggage.SetBaggage(K1, V1); @@ -39,12 +40,14 @@ public void SetAndGetTest() 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())); +#pragma warning disable CA1308 // Normalize strings to uppercase + Assert.Equal(V1, Baggage.GetBaggage(K1.ToLower(CultureInfo.InvariantCulture))); +#pragma warning restore CA1308 // Normalize strings to uppercase + Assert.Equal(V1, Baggage.GetBaggage(K1.ToUpper(CultureInfo.InvariantCulture))); Assert.Null(Baggage.GetBaggage("NO_KEY")); Assert.Equal(V2, Baggage.Current.GetBaggage(K2)); - Assert.Throws(() => Baggage.GetBaggage(null)); + Assert.Throws(() => Baggage.GetBaggage(null!)); } [Fact] @@ -52,12 +55,12 @@ public void SetExistingKeyTest() { var list = new List>(2) { - new KeyValuePair(K1, V1), + new(K1, V1), }; - Baggage.Current.SetBaggage(new KeyValuePair(K1, V1)); + Baggage.Current.SetBaggage(new KeyValuePair(K1, V1)); var baggage = Baggage.SetBaggage(K1, V1); - Baggage.SetBaggage(new Dictionary { [K1] = V1 }, baggage); + Baggage.SetBaggage(new Dictionary { [K1] = V1 }, baggage); Assert.Equal(list, Baggage.GetBaggage()); } @@ -78,7 +81,7 @@ public void SetNullValueTest() Assert.Empty(Baggage.SetBaggage(K1, null).GetBaggage()); Baggage.SetBaggage(K1, V1); - Baggage.SetBaggage(new Dictionary + Baggage.SetBaggage(new Dictionary { [K1] = null, [K2] = V2, @@ -94,7 +97,7 @@ public void RemoveTest() var empty2 = Baggage.RemoveBaggage(K1); Assert.True(empty == empty2); - var baggage = Baggage.SetBaggage(new Dictionary + var baggage = Baggage.SetBaggage(new Dictionary { [K1] = V1, [K2] = V2, @@ -112,7 +115,7 @@ public void RemoveTest() [Fact] public void ClearTest() { - var baggage = Baggage.SetBaggage(new Dictionary + var baggage = Baggage.SetBaggage(new Dictionary { [K1] = V1, [K2] = V2, @@ -151,8 +154,8 @@ public void EnumeratorTest() { var list = new List>(2) { - new KeyValuePair(K1, V1), - new KeyValuePair(K2, V2), + new(K1, V1), + new(K2, V2), }; var baggage = Baggage.SetBaggage(K1, V1); @@ -166,7 +169,7 @@ public void EnumeratorTest() var tag2 = enumerator.Current; Assert.False(enumerator.MoveNext()); - Assert.Equal(list, new List> { tag1, tag2 }); + Assert.Equal(list, [tag1, tag2]); Baggage.ClearBaggage(); @@ -178,11 +181,11 @@ public void EnumeratorTest() [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 }); + 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)); @@ -207,7 +210,7 @@ public void CreateBaggageTest() ["key2"] = "value2", ["KEY2"] = "VALUE2", ["KEY3"] = "VALUE3", - ["Key3"] = null, + ["Key3"] = null!, // Note: This causes Key3 to be removed }); Assert.Equal(2, baggage.Count); @@ -232,7 +235,7 @@ public void EqualityTests() baggage = Baggage.SetBaggage(K1, V1); - var baggage2 = Baggage.SetBaggage(null); + var baggage2 = Baggage.SetBaggage(null!); Assert.Equal(baggage, baggage2); diff --git a/test/OpenTelemetry.Api.Tests/Trace/Propagation/B3PropagatorTest.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/B3PropagatorTests.cs similarity index 97% rename from test/OpenTelemetry.Api.Tests/Trace/Propagation/B3PropagatorTest.cs rename to test/OpenTelemetry.Api.Tests/Context/Propagation/B3PropagatorTests.cs index 8cc20c6ff5e..e7f93d78848 100644 --- a/test/OpenTelemetry.Api.Tests/Trace/Propagation/B3PropagatorTest.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/B3PropagatorTests.cs @@ -7,7 +7,7 @@ namespace OpenTelemetry.Context.Propagation.Tests; -public class B3PropagatorTest +public class B3PropagatorTests { private const string TraceIdBase16 = "ff000000000000000000000000000041"; private const string TraceIdBase16EightBytes = "0000000000000041"; @@ -24,8 +24,12 @@ public class B3PropagatorTest private static readonly Func, string, IEnumerable> Getter = (d, k) => { - d.TryGetValue(k, out var v); - return new string[] { v }; + if (d.TryGetValue(k, out var v)) + { + return [v]; + } + + return []; }; private readonly B3Propagator b3propagator = new(); @@ -33,7 +37,7 @@ public class B3PropagatorTest private readonly ITestOutputHelper output; - public B3PropagatorTest(ITestOutputHelper output) + public B3PropagatorTests(ITestOutputHelper output) { this.output = output; } @@ -355,7 +359,7 @@ public void Fields_list() { ContainsExactly( this.b3propagator.Fields, - new List { B3Propagator.XB3TraceId, B3Propagator.XB3SpanId, B3Propagator.XB3ParentSpanId, B3Propagator.XB3Sampled, B3Propagator.XB3Flags }); + [B3Propagator.XB3TraceId, B3Propagator.XB3SpanId, B3Propagator.XB3ParentSpanId, B3Propagator.XB3Sampled, B3Propagator.XB3Flags]); } private static void ContainsExactly(ISet list, List items) @@ -367,7 +371,7 @@ private static void ContainsExactly(ISet list, List items) } } - private void ContainsExactly(IDictionary dict, IDictionary items) + private void ContainsExactly(Dictionary dict, Dictionary items) { foreach (var d in dict) { diff --git a/test/OpenTelemetry.Api.Tests/Trace/Propagation/BaggagePropagatorTest.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs similarity index 97% rename from test/OpenTelemetry.Api.Tests/Trace/Propagation/BaggagePropagatorTest.cs rename to test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index 097f63e039d..13c21c28cec 100644 --- a/test/OpenTelemetry.Api.Tests/Trace/Propagation/BaggagePropagatorTest.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -6,13 +6,17 @@ namespace OpenTelemetry.Context.Propagation.Tests; -public class BaggagePropagatorTest +public class BaggagePropagatorTests { private static readonly Func, string, IEnumerable> Getter = (d, k) => { - d.TryGetValue(k, out var v); - return new string[] { v }; + if (d.TryGetValue(k, out var v)) + { + return [v]; + } + + return []; }; private static readonly Func>, string, IEnumerable> GetterList = @@ -38,7 +42,7 @@ public void ValidateFieldsProperty() [Fact] public void ValidateDefaultCarrierExtraction() { - var propagationContext = this.baggage.Extract(default, null, null); + var propagationContext = this.baggage.Extract(default, null!, null!); Assert.Equal(default, propagationContext); } @@ -46,7 +50,7 @@ public void ValidateDefaultCarrierExtraction() public void ValidateDefaultGetterExtraction() { var carrier = new Dictionary(); - var propagationContext = this.baggage.Extract(default, carrier, null); + var propagationContext = this.baggage.Extract(default, carrier, null!); Assert.Equal(default, propagationContext); } diff --git a/test/OpenTelemetry.Api.Tests/Trace/Propagation/CompositePropagatorTest.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/CompositePropagatorTests.cs similarity index 52% rename from test/OpenTelemetry.Api.Tests/Trace/Propagation/CompositePropagatorTest.cs rename to test/OpenTelemetry.Api.Tests/Context/Propagation/CompositePropagatorTests.cs index f4066edcc1e..105941ecd70 100644 --- a/test/OpenTelemetry.Api.Tests/Trace/Propagation/CompositePropagatorTest.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/CompositePropagatorTests.cs @@ -6,15 +6,15 @@ namespace OpenTelemetry.Context.Propagation.Tests; -public class CompositePropagatorTest +public class CompositePropagatorTests { - private static readonly string[] Empty = Array.Empty(); + private static readonly string[] Empty = []; private static readonly Func, string, IEnumerable> Getter = (headers, name) => { count++; if (headers.TryGetValue(name, out var value)) { - return new[] { value }; + return [value]; } return Empty; @@ -25,34 +25,78 @@ public class CompositePropagatorTest carrier[name] = value; }; - private static int count = 0; + private static int count; private readonly ActivityTraceId traceId = ActivityTraceId.CreateRandom(); private readonly ActivitySpanId spanId = ActivitySpanId.CreateRandom(); [Fact] - public void CompositePropagator_NullTextFormatList() + public void CompositePropagator_NullTextMapPropagators() { - Assert.Throws(() => new CompositeTextMapPropagator(null)); + Assert.Throws(() => new CompositeTextMapPropagator(null!)); + } + + [Fact] + public void CompositePropagator_EmptyTextMapPropagators() + { + var compositePropagator = new CompositeTextMapPropagator([]); + Assert.Empty(compositePropagator.Fields); + } + + [Fact] + public void CompositePropagator_NullTextMapPropagator() + { + var compositePropagator = new CompositeTextMapPropagator([null!]); + Assert.Empty(compositePropagator.Fields); + } + + [Fact] + public void CompositePropagator_NoOpTextMapPropagators() + { + var compositePropagator = new CompositeTextMapPropagator([new NoopTextMapPropagator()]); + Assert.Empty(compositePropagator.Fields); + } + + [Fact] + public void CompositePropagator_SingleTextMapPropagator() + { + var testPropagator = new TestPropagator("custom-traceparent-1", "custom-tracestate-1"); + + var compositePropagator = new CompositeTextMapPropagator([testPropagator]); + + // We expect a new HashSet, with a copy of the values from the propagator. + Assert.Equal(testPropagator.Fields, compositePropagator.Fields); + Assert.NotSame(testPropagator.Fields, compositePropagator.Fields); } [Fact] public void CompositePropagator_TestPropagator() { - var compositePropagator = new CompositeTextMapPropagator(new List - { - new TestPropagator("custom-traceparent-1", "custom-tracestate-1"), - new TestPropagator("custom-traceparent-2", "custom-tracestate-2"), - }); + var testPropagatorA = new TestPropagator("custom-traceparent-1", "custom-tracestate-1"); + var testPropagatorB = new TestPropagator("custom-traceparent-2", "custom-tracestate-2"); + + var compositePropagator = new CompositeTextMapPropagator([testPropagatorA, testPropagatorB,]); var activityContext = new ActivityContext(this.traceId, this.spanId, ActivityTraceFlags.Recorded, traceState: null); - PropagationContext propagationContext = new PropagationContext(activityContext, default); + var 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"); + + Assert.Equal(testPropagatorA.Fields.Count + testPropagatorB.Fields.Count, compositePropagator.Fields.Count); + Assert.Subset(compositePropagator.Fields, testPropagatorA.Fields); + Assert.Subset(compositePropagator.Fields, testPropagatorB.Fields); + + Assert.Equal(1, testPropagatorA.InjectCount); + Assert.Equal(1, testPropagatorB.InjectCount); + + compositePropagator.Extract(default, new Dictionary(), Getter); + + Assert.Equal(1, testPropagatorA.ExtractCount); + Assert.Equal(1, testPropagatorB.ExtractCount); } [Fact] @@ -61,20 +105,24 @@ 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 testPropagatorA = new TestPropagator("custom-traceparent", header01, true); + var testPropagatorB = new TestPropagator("custom-traceparent", header02); + + var compositePropagator = new CompositeTextMapPropagator([testPropagatorA, testPropagatorB,]); var activityContext = new ActivityContext(this.traceId, this.spanId, ActivityTraceFlags.Recorded, traceState: null); - PropagationContext propagationContext = new PropagationContext(activityContext, default); + var propagationContext = new PropagationContext(activityContext, default); var carrier = new Dictionary(); compositePropagator.Inject(propagationContext, carrier, Setter); Assert.Contains(carrier, kv => kv.Key == "custom-traceparent"); + Assert.Equal(3, compositePropagator.Fields.Count); + + Assert.Equal(1, testPropagatorA.InjectCount); + Assert.Equal(1, testPropagatorB.InjectCount); + // 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"]); @@ -85,35 +133,38 @@ public void CompositePropagator_UsingSameTag() // 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); + + Assert.Equal(1, testPropagatorA.ExtractCount); + Assert.Equal(1, testPropagatorB.ExtractCount); } [Fact] public void CompositePropagator_ActivityContext_Baggage() { - var compositePropagator = new CompositeTextMapPropagator(new List - { + var compositePropagator = new CompositeTextMapPropagator( + [ 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 propagationContextActivityOnly = new PropagationContext(activityContext, default); + var propagationContextBaggageOnly = new PropagationContext(default, new Baggage(baggage)); + var propagationContextBoth = new PropagationContext(activityContext, new Baggage(baggage)); var carrier = new Dictionary(); compositePropagator.Inject(propagationContextActivityOnly, carrier, Setter); - PropagationContext extractedContext = compositePropagator.Extract(default, carrier, Getter); + var extractedContext = compositePropagator.Extract(default, carrier, Getter); Assert.Equal(propagationContextActivityOnly, extractedContext); - carrier = new Dictionary(); + carrier = []; compositePropagator.Inject(propagationContextBaggageOnly, carrier, Setter); extractedContext = compositePropagator.Extract(default, carrier, Getter); Assert.Equal(propagationContextBaggageOnly, extractedContext); - carrier = new Dictionary(); + carrier = []; compositePropagator.Inject(propagationContextBoth, carrier, Setter); extractedContext = compositePropagator.Extract(default, carrier, Getter); Assert.Equal(propagationContextBoth, extractedContext); diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/PropagatorsTest.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/PropagatorsTests.cs similarity index 91% rename from test/OpenTelemetry.Api.Tests/Context/Propagation/PropagatorsTest.cs rename to test/OpenTelemetry.Api.Tests/Context/Propagation/PropagatorsTests.cs index b7993e0d848..a80574db7f8 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/PropagatorsTest.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/PropagatorsTests.cs @@ -5,9 +5,9 @@ namespace OpenTelemetry.Context.Propagation.Tests; -public class PropagatorsTest : IDisposable +public sealed class PropagatorsTests : IDisposable { - public PropagatorsTest() + public PropagatorsTests() { Propagators.Reset(); } diff --git a/test/OpenTelemetry.Api.Tests/Trace/Propagation/TestPropagator.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/TestPropagator.cs similarity index 72% rename from test/OpenTelemetry.Api.Tests/Trace/Propagation/TestPropagator.cs rename to test/OpenTelemetry.Api.Tests/Context/Propagation/TestPropagator.cs index abd4a17b1e9..ab652152473 100644 --- a/test/OpenTelemetry.Api.Tests/Trace/Propagation/TestPropagator.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/TestPropagator.cs @@ -5,12 +5,15 @@ namespace OpenTelemetry.Context.Propagation.Tests; -public class TestPropagator : TextMapPropagator +internal sealed class TestPropagator : TextMapPropagator { private readonly string idHeaderName; private readonly string stateHeaderName; private readonly bool defaultContext; + private int extractCount; + private int injectCount; + public TestPropagator(string idHeaderName, string stateHeaderName, bool defaultContext = false) { this.idHeaderName = idHeaderName; @@ -18,17 +21,23 @@ public TestPropagator(string idHeaderName, string stateHeaderName, bool defaultC this.defaultContext = defaultContext; } + public int ExtractCount => this.extractCount; + + public int InjectCount => this.injectCount; + public override ISet Fields => new HashSet() { this.idHeaderName, this.stateHeaderName }; - public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) + public override PropagationContext Extract(PropagationContext context, T carrier, Func?> getter) { + Interlocked.Increment(ref this.extractCount); + if (this.defaultContext) { return context; } - IEnumerable id = getter(carrier, this.idHeaderName); - if (!id.Any()) + var id = getter(carrier, this.idHeaderName); + if (id == null || !id.Any()) { return context; } @@ -39,8 +48,8 @@ public override PropagationContext Extract(PropagationContext context, T carr return context; } - string tracestate = string.Empty; - IEnumerable tracestateCollection = getter(carrier, this.stateHeaderName); + var tracestate = string.Empty; + var tracestateCollection = getter(carrier, this.stateHeaderName); if (tracestateCollection?.Any() ?? false) { TraceContextPropagator.TryExtractTracestate(tracestateCollection.ToArray(), out tracestate); @@ -53,14 +62,16 @@ public override PropagationContext Extract(PropagationContext context, T carr public override void Inject(PropagationContext context, T carrier, Action setter) { - string headerNumber = this.stateHeaderName.Split('-').Last(); + Interlocked.Increment(ref this.injectCount); + + var headerNumber = this.stateHeaderName.Split('-').Last(); var traceparent = string.Concat("00-", context.ActivityContext.TraceId.ToHexString(), "-", context.ActivityContext.SpanId.ToHexString()); traceparent = string.Concat(traceparent, "-", headerNumber); setter(carrier, this.idHeaderName, traceparent); - string tracestateStr = context.ActivityContext.TraceState; + var 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/Context/Propagation/TracestateUtilsTests.cs similarity index 79% rename from test/OpenTelemetry.Api.Tests/Trace/Propagation/TracestateUtilsTests.cs rename to test/OpenTelemetry.Api.Tests/Context/Propagation/TracestateUtilsTests.cs index 499fe66cbf4..ae9de6f0ff0 100644 --- a/test/OpenTelemetry.Api.Tests/Trace/Propagation/TracestateUtilsTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/TracestateUtilsTests.cs @@ -7,15 +7,22 @@ namespace OpenTelemetry.Context.Propagation.Tests; public class TracestateUtilsTests { + [Fact] + public void NullTracestate() + { + var tracestateEntries = new List>(); + Assert.False(TraceStateUtils.AppendTraceState(null!, tracestateEntries)); + Assert.Empty(tracestateEntries); + } + [Theory] [InlineData("")] - [InlineData(null)] [InlineData(" ")] [InlineData("\t")] public void EmptyTracestate(string tracestate) { var tracestateEntries = new List>(); - Assert.False(TraceStateUtilsNew.AppendTraceState(tracestate, tracestateEntries)); + Assert.False(TraceStateUtils.AppendTraceState(tracestate, tracestateEntries)); Assert.Empty(tracestateEntries); } @@ -32,7 +39,7 @@ public void EmptyTracestate(string tracestate) public void InvalidTracestate(string tracestate) { var tracestateEntries = new List>(); - Assert.False(TraceStateUtilsNew.AppendTraceState(tracestate, tracestateEntries)); + Assert.False(TraceStateUtils.AppendTraceState(tracestate, tracestateEntries)); Assert.Empty(tracestateEntries); } @@ -42,11 +49,11 @@ 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.True(TraceStateUtils.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)); + TraceStateUtils.GetString(tracestateEntries)); } [Fact] @@ -55,7 +62,7 @@ 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.False(TraceStateUtils.AppendTraceState(tracestate, tracestateEntries)); Assert.Empty(tracestateEntries); } @@ -72,10 +79,10 @@ public void TooManyEntries() public void ValidPair(string pair, string expectedKey, string expectedValue) { var tracestateEntries = new List>(); - Assert.True(TraceStateUtilsNew.AppendTraceState(pair, tracestateEntries)); + Assert.True(TraceStateUtils.AppendTraceState(pair, tracestateEntries)); Assert.Single(tracestateEntries); Assert.Equal(new KeyValuePair(expectedKey, expectedValue), tracestateEntries.Single()); - Assert.Equal($"{expectedKey}={expectedValue}", TraceStateUtilsNew.GetString(tracestateEntries)); + Assert.Equal($"{expectedKey}={expectedValue}", TraceStateUtils.GetString(tracestateEntries)); } [Theory] @@ -86,11 +93,11 @@ public void ValidPair(string pair, string expectedKey, string expectedValue) public void ValidPairs(string tracestate) { var tracestateEntries = new List>(); - Assert.True(TraceStateUtilsNew.AppendTraceState(tracestate, tracestateEntries)); + Assert.True(TraceStateUtils.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", TraceStateUtils.GetString(tracestateEntries)); } } diff --git a/test/OpenTelemetry.Api.Tests/Context/RuntimeContextTest.cs b/test/OpenTelemetry.Api.Tests/Context/RuntimeContextTests.cs similarity index 54% rename from test/OpenTelemetry.Api.Tests/Context/RuntimeContextTest.cs rename to test/OpenTelemetry.Api.Tests/Context/RuntimeContextTests.cs index 1d25b4bf546..81570185c2f 100644 --- a/test/OpenTelemetry.Api.Tests/Context/RuntimeContextTest.cs +++ b/test/OpenTelemetry.Api.Tests/Context/RuntimeContextTests.cs @@ -5,9 +5,9 @@ namespace OpenTelemetry.Context.Tests; -public class RuntimeContextTest : IDisposable +public sealed class RuntimeContextTests : IDisposable { - public RuntimeContextTest() + public RuntimeContextTests() { RuntimeContext.Clear(); } @@ -16,7 +16,7 @@ public RuntimeContextTest() public static void RegisterSlotWithInvalidNameThrows() { Assert.Throws(() => RuntimeContext.RegisterSlot(string.Empty)); - Assert.Throws(() => RuntimeContext.RegisterSlot(null)); + Assert.Throws(() => RuntimeContext.RegisterSlot(null!)); } [Fact] @@ -31,7 +31,7 @@ public static void RegisterSlotWithSameName() public static void GetSlotWithInvalidNameThrows() { Assert.Throws(() => RuntimeContext.GetSlot(string.Empty)); - Assert.Throws(() => RuntimeContext.GetSlot(null)); + Assert.Throws(() => RuntimeContext.GetSlot(null!)); } [Fact] @@ -58,12 +58,88 @@ public void RegisterAndGetSlot() Assert.Equal(100, expectedSlot.Get()); } + [Fact] + public void ValueTypeSlotNullableTests() + { + var expectedSlot = RuntimeContext.RegisterSlot("testslot_valuetype"); + Assert.NotNull(expectedSlot); + + var slotValueAccessor = expectedSlot as IRuntimeContextSlotValueAccessor; + Assert.NotNull(slotValueAccessor); + + Assert.Equal(0, expectedSlot.Get()); + Assert.Equal(0, slotValueAccessor.Value); + + slotValueAccessor.Value = 100; + + Assert.Equal(100, expectedSlot.Get()); + Assert.Equal(100, slotValueAccessor.Value); + + slotValueAccessor.Value = null; + + Assert.Equal(0, expectedSlot.Get()); + Assert.Equal(0, slotValueAccessor.Value); + + Assert.Throws(() => slotValueAccessor.Value = false); + } + + [Fact] + public void NullableValueTypeSlotNullableTests() + { + var expectedSlot = RuntimeContext.RegisterSlot("testslot_nullablevaluetype"); + Assert.NotNull(expectedSlot); + + var slotValueAccessor = expectedSlot as IRuntimeContextSlotValueAccessor; + Assert.NotNull(slotValueAccessor); + + Assert.Null(expectedSlot.Get()); + Assert.Null(slotValueAccessor.Value); + + slotValueAccessor.Value = 100; + + Assert.Equal(100, expectedSlot.Get()); + Assert.Equal(100, slotValueAccessor.Value); + + slotValueAccessor.Value = null; + + Assert.Null(expectedSlot.Get()); + Assert.Null(slotValueAccessor.Value); + + Assert.Throws(() => slotValueAccessor.Value = false); + } + + [Fact] + public void ReferenceTypeSlotNullableTests() + { + var expectedSlot = RuntimeContext.RegisterSlot("testslot_referencetype"); + Assert.NotNull(expectedSlot); + + var slotValueAccessor = expectedSlot as IRuntimeContextSlotValueAccessor; + Assert.NotNull(slotValueAccessor); + + Assert.Null(expectedSlot.Get()); + Assert.Null(slotValueAccessor.Value); + + slotValueAccessor.Value = this; + + Assert.Equal(this, expectedSlot.Get()); + Assert.Equal(this, slotValueAccessor.Value); + + slotValueAccessor.Value = null; + + Assert.Null(expectedSlot.Get()); + Assert.Null(slotValueAccessor.Value); + + Assert.Throws(() => slotValueAccessor.Value = new object()); + } + #if NETFRAMEWORK [Fact] public void NetFrameworkGetSlotInAnotherAppDomain() { const string slotName = "testSlot"; var slot = RuntimeContext.RegisterSlot(slotName); + Assert.NotNull(slot); slot.Set(100); // Create an object in another AppDomain and try to access the slot @@ -92,9 +168,13 @@ public void Dispose() } #if NETFRAMEWORK - private class RemoteObject : ContextBoundObject +#pragma warning disable CA1812 // Avoid uninstantiated internal classes + private sealed class RemoteObject : ContextBoundObject +#pragma warning restore CA1812 // Avoid uninstantiated internal classes { +#pragma warning disable CA1822 // Mark members as static public int GetValueFromContextSlot(string slotName) +#pragma warning restore CA1822 // Mark members as static { // Slot is not propagated across AppDomains, attempting to get // an existing slot here should throw an ArgumentException. @@ -102,7 +182,7 @@ public int GetValueFromContextSlot(string slotName) { RuntimeContext.GetSlot(slotName); - throw new Exception("Should not have found an existing slot: " + slotName); + throw new InvalidOperationException("Should not have found an existing slot: " + slotName); } catch (ArgumentException) { diff --git a/test/OpenTelemetry.Api.Tests/EventSourceTest.cs b/test/OpenTelemetry.Api.Tests/EventSourceTests.cs similarity index 92% rename from test/OpenTelemetry.Api.Tests/EventSourceTest.cs rename to test/OpenTelemetry.Api.Tests/EventSourceTests.cs index 0912a5e3f5f..4f2ee52d14c 100644 --- a/test/OpenTelemetry.Api.Tests/EventSourceTest.cs +++ b/test/OpenTelemetry.Api.Tests/EventSourceTests.cs @@ -7,7 +7,7 @@ namespace OpenTelemetry.Api.Tests; -public class EventSourceTest +public class EventSourceTests { [Fact] public void EventSourceTest_OpenTelemetryApiEventSource() diff --git a/test/OpenTelemetry.Api.Tests/Internal/GuardTest.cs b/test/OpenTelemetry.Api.Tests/Internal/GuardTests.cs similarity index 68% rename from test/OpenTelemetry.Api.Tests/Internal/GuardTest.cs rename to test/OpenTelemetry.Api.Tests/Internal/GuardTests.cs index 455ba44d4b2..ab20d85f169 100644 --- a/test/OpenTelemetry.Api.Tests/Internal/GuardTest.cs +++ b/test/OpenTelemetry.Api.Tests/Internal/GuardTests.cs @@ -8,7 +8,7 @@ namespace OpenTelemetry.Internal.Tests; -public class GuardTest +public class GuardTests { [Fact] public void NullTest() @@ -20,19 +20,19 @@ public void NullTest() Guard.ThrowIfNull("hello"); // Invalid - object potato = null; + object? potato = null; var ex1 = Assert.Throws(() => Guard.ThrowIfNull(potato)); - Assert.Contains("Must not be null", ex1.Message); + Assert.Contains("Must not be null", ex1.Message, StringComparison.OrdinalIgnoreCase); Assert.Equal("potato", ex1.ParamName); - object @event = null; + object? @event = null; var ex2 = Assert.Throws(() => Guard.ThrowIfNull(@event)); - Assert.Contains("Must not be null", ex2.Message); + Assert.Contains("Must not be null", ex2.Message, StringComparison.OrdinalIgnoreCase); Assert.Equal("@event", ex2.ParamName); - Thing thing = null; + Thing? thing = null; var ex3 = Assert.Throws(() => Guard.ThrowIfNull(thing?.Bar)); - Assert.Contains("Must not be null", ex3.Message); + Assert.Contains("Must not be null", ex3.Message, StringComparison.OrdinalIgnoreCase); Assert.Equal("thing?.Bar", ex3.ParamName); } @@ -45,16 +45,16 @@ public void NullOrEmptyTest() // Invalid var ex1 = Assert.Throws(() => Guard.ThrowIfNullOrEmpty(null)); - Assert.Contains("Must not be null or empty", ex1.Message); + Assert.Contains("Must not be null or empty", ex1.Message, StringComparison.OrdinalIgnoreCase); Assert.Equal("null", ex1.ParamName); var ex2 = Assert.Throws(() => Guard.ThrowIfNullOrEmpty(string.Empty)); - Assert.Contains("Must not be null or empty", ex2.Message); + Assert.Contains("Must not be null or empty", ex2.Message, StringComparison.OrdinalIgnoreCase); 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.Contains("Must not be null or empty", ex3.Message, StringComparison.OrdinalIgnoreCase); Assert.Equal("x", ex3.ParamName); } @@ -66,15 +66,15 @@ public void NullOrWhitespaceTest() // Invalid var ex1 = Assert.Throws(() => Guard.ThrowIfNullOrWhitespace(null)); - Assert.Contains("Must not be null or whitespace", ex1.Message); + Assert.Contains("Must not be null or whitespace", ex1.Message, StringComparison.OrdinalIgnoreCase); Assert.Equal("null", ex1.ParamName); var ex2 = Assert.Throws(() => Guard.ThrowIfNullOrWhitespace(string.Empty)); - Assert.Contains("Must not be null or whitespace", ex2.Message); + Assert.Contains("Must not be null or whitespace", ex2.Message, StringComparison.OrdinalIgnoreCase); 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.Contains("Must not be null or whitespace", ex3.Message, StringComparison.OrdinalIgnoreCase); Assert.Equal("\" \\t\\n\\r\"", ex3.ParamName); } @@ -88,7 +88,7 @@ public void InvalidTimeoutTest() // Invalid var ex1 = Assert.Throws(() => Guard.ThrowIfInvalidTimeout(-100)); - Assert.Contains("Must be non-negative or 'Timeout.Infinite'", ex1.Message); + Assert.Contains("Must be non-negative or 'Timeout.Infinite'", ex1.Message, StringComparison.OrdinalIgnoreCase); Assert.Equal("-100", ex1.ParamName); } @@ -104,10 +104,10 @@ public void RangeIntTest() // 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); + Assert.Contains("Must be in the range: [0: empty, 100: full]", ex1.Message, StringComparison.OrdinalIgnoreCase); var ex2 = Assert.Throws(() => Guard.ThrowIfOutOfRange(-1, min: 0, max: 100, message: "error")); - Assert.Contains("error", ex2.Message); + Assert.Contains("error", ex2.Message, StringComparison.OrdinalIgnoreCase); } [Fact] @@ -120,10 +120,10 @@ public void RangeDoubleTest() // 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); + Assert.Contains("Must be in the range: [0.1: empty, 99.9: full]", ex3.Message, StringComparison.OrdinalIgnoreCase); 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); + Assert.Contains("Must be in the range: [0, 100]", ex4.Message, StringComparison.OrdinalIgnoreCase); } [Fact] @@ -147,40 +147,45 @@ public void ZeroTest() // Invalid var ex1 = Assert.Throws(() => Guard.ThrowIfZero(0)); - Assert.Contains("Must not be zero", ex1.Message); + Assert.Contains("Must not be zero", ex1.Message, StringComparison.OrdinalIgnoreCase); Assert.Equal("0", ex1.ParamName); } - public class Thing +#pragma warning disable CA1812 // Avoid uninstantiated internal classes + internal sealed class Thing +#pragma warning restore CA1812 // Avoid uninstantiated internal classes { - public string Bar { get; set; } + public string? Bar { get; set; } } +} -#if !NET6_0_OR_GREATER - /// - /// Borrowed from: . - /// - public class CallerArgumentExpressionAttributeTests +#if !NET +/// +/// Borrowed from: . +/// +#pragma warning disable SA1402 // File may only contain a single type +public class CallerArgumentExpressionAttributeTests +#pragma warning restore SA1402 // File may only contain a single type +{ + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("paramName")] + public void Ctor_ParameterName_Roundtrip(string? value) { - [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; + var caea = new CallerArgumentExpressionAttribute(value); + Assert.Equal(value, caea.ParameterName); } -#endif + + [Fact] + public void BasicTest() + { + Assert.Equal("null", GetValue(null)); + 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(nameof(argument))] string? expr = null) => expr; } +#endif diff --git a/test/OpenTelemetry.Api.Tests/Logs/LogRecordAttributeListTests.cs b/test/OpenTelemetry.Api.Tests/Logs/LogRecordAttributeListTests.cs index c09c728cd55..4e0aaacb89f 100644 --- a/test/OpenTelemetry.Api.Tests/Logs/LogRecordAttributeListTests.cs +++ b/test/OpenTelemetry.Api.Tests/Logs/LogRecordAttributeListTests.cs @@ -29,13 +29,15 @@ public void ReadWriteTest(int numberOfItems) var item = attributes[i]; Assert.Equal($"key{i}", item.Key); + Assert.NotNull(item.Value); Assert.Equal(i, (int)item.Value); } int index = 0; - foreach (KeyValuePair item in attributes) + foreach (KeyValuePair item in attributes) { Assert.Equal($"key{index}", item.Key); + Assert.NotNull(item.Value); Assert.Equal(index, (int)item.Value); index++; } @@ -74,6 +76,7 @@ public void ClearTest(int numberOfItems) var item = attributes[i]; Assert.Equal($"key{i}", item.Key); + Assert.NotNull(item.Value); Assert.Equal(i, (int)item.Value); } @@ -98,7 +101,7 @@ public void ExportTest(int numberOfItems) attributes.Add($"key{i}", i); } - List> storage = null; + List>? storage = null; var exportedAttributes = attributes.Export(ref storage); @@ -122,11 +125,36 @@ public void ExportTest(int numberOfItems) } int index = 0; - foreach (KeyValuePair item in exportedAttributes) + foreach (KeyValuePair item in exportedAttributes) { Assert.Equal($"key{index}", item.Key); + Assert.NotNull(item.Value); Assert.Equal(index, (int)item.Value); index++; } } + + [Fact] + public void InitializerAddSyntaxTest() + { + LogRecordAttributeList list = new LogRecordAttributeList + { + { "key1", new object() }, + { "key2", 2 }, + }; + + Assert.Equal(2, list.Count); + } + + [Fact] + public void InitializerIndexesSyntaxTest() + { + LogRecordAttributeList list = new LogRecordAttributeList + { + ["key1"] = new object(), + ["key2"] = 2, + }; + + Assert.Equal(2, list.Count); + } } diff --git a/test/OpenTelemetry.Api.Tests/Logs/LogRecordDataTests.cs b/test/OpenTelemetry.Api.Tests/Logs/LogRecordDataTests.cs index 89d32795d6f..03d65ab3dd0 100644 --- a/test/OpenTelemetry.Api.Tests/Logs/LogRecordDataTests.cs +++ b/test/OpenTelemetry.Api.Tests/Logs/LogRecordDataTests.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; using Xunit; diff --git a/test/OpenTelemetry.Api.Tests/Logs/LogRecordSeverityExtensionsTests.cs b/test/OpenTelemetry.Api.Tests/Logs/LogRecordSeverityExtensionsTests.cs index 9035898cc66..10d2f90896d 100644 --- a/test/OpenTelemetry.Api.Tests/Logs/LogRecordSeverityExtensionsTests.cs +++ b/test/OpenTelemetry.Api.Tests/Logs/LogRecordSeverityExtensionsTests.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using Xunit; namespace OpenTelemetry.Logs.Tests; diff --git a/test/OpenTelemetry.Api.Tests/Logs/LoggerProviderTests.cs b/test/OpenTelemetry.Api.Tests/Logs/LoggerProviderTests.cs index 24d343d98bd..24415c12fb4 100644 --- a/test/OpenTelemetry.Api.Tests/Logs/LoggerProviderTests.cs +++ b/test/OpenTelemetry.Api.Tests/Logs/LoggerProviderTests.cs @@ -1,11 +1,7 @@ // Copyright The OpenTelemetry Authors // 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; @@ -68,9 +64,7 @@ protected override bool TryCreateLogger( internal override bool TryCreateLogger( #endif string? name, -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER [NotNullWhen(true)] -#endif out Logger? logger) { logger = new TestLogger(name); diff --git a/test/OpenTelemetry.Api.Tests/OpenTelemetry.Api.Tests.csproj b/test/OpenTelemetry.Api.Tests/OpenTelemetry.Api.Tests.csproj index b2e0fa8b6db..0bb3a3d310c 100644 --- a/test/OpenTelemetry.Api.Tests/OpenTelemetry.Api.Tests.csproj +++ b/test/OpenTelemetry.Api.Tests/OpenTelemetry.Api.Tests.csproj @@ -1,14 +1,14 @@ + Unit test project for OpenTelemetry.Api $(TargetFrameworksForTests) $(NoWarn),CS0618 - - - disable + true + @@ -17,14 +17,7 @@ + - - - - - runtime; build; native; contentfiles; analyzers - - - diff --git a/test/OpenTelemetry.Api.Tests/Trace/ActivityExtensionsTest.cs b/test/OpenTelemetry.Api.Tests/Trace/ActivityExtensionsTests.cs similarity index 95% rename from test/OpenTelemetry.Api.Tests/Trace/ActivityExtensionsTest.cs rename to test/OpenTelemetry.Api.Tests/Trace/ActivityExtensionsTests.cs index 31fbf806725..4de20d9bc4b 100644 --- a/test/OpenTelemetry.Api.Tests/Trace/ActivityExtensionsTest.cs +++ b/test/OpenTelemetry.Api.Tests/Trace/ActivityExtensionsTests.cs @@ -7,7 +7,7 @@ namespace OpenTelemetry.Trace.Tests; -public class ActivityExtensionsTest +public class ActivityExtensionsTests { private const string ActivityName = "Test Activity"; @@ -134,7 +134,7 @@ public void LastSetStatusWins() public void CheckRecordException() { var message = "message"; - var exception = new ArgumentNullException(message, new Exception(message)); + var exception = new ArgumentNullException(message, new InvalidOperationException(message)); using var activity = new Activity("test-activity"); activity.RecordException(exception); @@ -147,7 +147,7 @@ public void CheckRecordException() public void RecordExceptionWithAdditionalTags() { var message = "message"; - var exception = new ArgumentNullException(message, new Exception(message)); + var exception = new ArgumentNullException(message, new InvalidOperationException(message)); using var activity = new Activity("test-activity"); var tags = new TagList @@ -202,7 +202,7 @@ public void GetTagValue() [Theory] [InlineData("Key", "Value", true)] [InlineData("CustomTag", null, false)] - public void TryCheckFirstTag(string tagName, object expectedTagValue, bool expectedResult) + public void TryCheckFirstTag(string tagName, object? expectedTagValue, bool expectedResult) { using var activity = new Activity("Test"); activity.SetTag("Key", "Value"); diff --git a/test/OpenTelemetry.Api.Tests/Trace/SpanAttributesTest.cs b/test/OpenTelemetry.Api.Tests/Trace/SpanAttributesTests.cs similarity index 71% rename from test/OpenTelemetry.Api.Tests/Trace/SpanAttributesTest.cs rename to test/OpenTelemetry.Api.Tests/Trace/SpanAttributesTests.cs index f0c5c7c110f..30a05938e1d 100644 --- a/test/OpenTelemetry.Api.Tests/Trace/SpanAttributesTest.cs +++ b/test/OpenTelemetry.Api.Tests/Trace/SpanAttributesTests.cs @@ -5,7 +5,7 @@ namespace OpenTelemetry.Trace.Tests; -public class SpanAttributesTest +public class SpanAttributesTests { [Fact] public void ValidateConstructor() @@ -19,16 +19,16 @@ public void ValidateAddMethods() { var spanAttribute = new SpanAttributes(); spanAttribute.Add("key_string", "string"); - spanAttribute.Add("key_a_string", new string[] { "string" }); + spanAttribute.Add("key_a_string", ["string"]); spanAttribute.Add("key_double", 1.01); - spanAttribute.Add("key_a_double", new double[] { 1.01 }); + spanAttribute.Add("key_a_double", [1.01]); spanAttribute.Add("key_bool", true); - spanAttribute.Add("key_a_bool", new bool[] { true }); + spanAttribute.Add("key_a_bool", [true]); spanAttribute.Add("key_long", 1); - spanAttribute.Add("key_a_long", new long[] { 1 }); + spanAttribute.Add("key_a_long", [1L]); Assert.Equal(8, spanAttribute.Attributes.Count); } @@ -37,7 +37,7 @@ public void ValidateAddMethods() public void ValidateNullKey() { var spanAttribute = new SpanAttributes(); - Assert.Throws(() => spanAttribute.Add(null, "null key")); + Assert.Throws(() => spanAttribute.Add(null!, "null key")); } [Fact] @@ -53,17 +53,17 @@ public void ValidateSameKey() public void ValidateConstructorWithList() { var spanAttributes = new SpanAttributes( - new List>() - { - new KeyValuePair("Span attribute int", 1), - new KeyValuePair("Span attribute string", "str"), - }); + new List> + { + new("Span attribute int", 1), + new("Span attribute string", "str"), + }); Assert.Equal(2, spanAttributes.Attributes.Count); } [Fact] public void ValidateConstructorWithNullList() { - Assert.Throws(() => new SpanAttributes(null)); + Assert.Throws(() => new SpanAttributes(null!)); } } diff --git a/test/OpenTelemetry.Api.Tests/Trace/StatusTest.cs b/test/OpenTelemetry.Api.Tests/Trace/StatusTests.cs similarity index 99% rename from test/OpenTelemetry.Api.Tests/Trace/StatusTest.cs rename to test/OpenTelemetry.Api.Tests/Trace/StatusTests.cs index f3898d06891..3da0655cd73 100644 --- a/test/OpenTelemetry.Api.Tests/Trace/StatusTest.cs +++ b/test/OpenTelemetry.Api.Tests/Trace/StatusTests.cs @@ -5,7 +5,7 @@ namespace OpenTelemetry.Trace.Tests; -public class StatusTest +public class StatusTests { [Fact] public void Status_Ok() diff --git a/test/OpenTelemetry.Api.Tests/Trace/TelemetrySpanTest.cs b/test/OpenTelemetry.Api.Tests/Trace/TelemetrySpanTests.cs similarity index 56% rename from test/OpenTelemetry.Api.Tests/Trace/TelemetrySpanTest.cs rename to test/OpenTelemetry.Api.Tests/Trace/TelemetrySpanTests.cs index 6434569e1d9..53a6a4fd82b 100644 --- a/test/OpenTelemetry.Api.Tests/Trace/TelemetrySpanTest.cs +++ b/test/OpenTelemetry.Api.Tests/Trace/TelemetrySpanTests.cs @@ -6,7 +6,7 @@ namespace OpenTelemetry.Trace.Tests; -public class TelemetrySpanTest +public class TelemetrySpanTests { [Fact] public void CheckRecordExceptionData() @@ -15,9 +15,10 @@ public void CheckRecordExceptionData() using Activity activity = new Activity("exception-test"); using TelemetrySpan telemetrySpan = new TelemetrySpan(activity); - telemetrySpan.RecordException(new ArgumentNullException(message, new Exception("new-exception"))); + telemetrySpan.RecordException(new ArgumentNullException(message, new InvalidOperationException("new-exception"))); Assert.Single(activity.Events); + Assert.NotNull(telemetrySpan.Activity); 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); @@ -35,6 +36,7 @@ public void CheckRecordExceptionData2() telemetrySpan.RecordException(type, message, stack); Assert.Single(activity.Events); + Assert.NotNull(telemetrySpan.Activity); 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); @@ -62,10 +64,73 @@ public void ParentIds() // ParentId should be unset Assert.Equal(default, parentSpan.ParentSpanId); + Assert.NotNull(parentActivity.Id); - using var childActivity = new Activity("childOperation").SetParentId(parentActivity.Id); + using var childActivity = new Activity("childOperation"); + childActivity.SetParentId(parentActivity.Id); using var childSpan = new TelemetrySpan(childActivity); Assert.Equal(parentSpan.Context.SpanId, childSpan.ParentSpanId); } + + [Fact] + public void CheckAddLinkData() + { + using var activity = new Activity("test-activity"); + activity.Start(); + using var span = new TelemetrySpan(activity); + + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + var context = new SpanContext(traceId, spanId, ActivityTraceFlags.Recorded); + + span.AddLink(context); + + Assert.Single(activity.Links); + var link = activity.Links.First(); + Assert.Equal(traceId, link.Context.TraceId); + Assert.Equal(spanId, link.Context.SpanId); + Assert.Null(link.Tags); + } + + [Fact] + public void CheckAddLinkAttributes() + { + using var activity = new Activity("test-activity"); + activity.Start(); + using var span = new TelemetrySpan(activity); + + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + var context = new SpanContext(traceId, spanId, ActivityTraceFlags.Recorded); + + var attributes = new SpanAttributes(); + attributes.Add("key1", "value1"); + + span.AddLink(context, attributes); + + Assert.Single(activity.Links); + var link = activity.Links.First(); + Assert.NotNull(link.Tags); + Assert.Single(link.Tags); + var tag = link.Tags.First(); + Assert.Equal("key1", tag.Key); + Assert.Equal("value1", tag.Value); + } + + [Fact] + public void CheckAddLinkNotRecording() + { + using var activity = new Activity("test-activity"); + + // Simulate not recording + activity.IsAllDataRequested = false; + using var span = new TelemetrySpan(activity); + + var context = new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None); + + span.AddLink(context, null); + + Assert.Empty(activity.Links); + } } diff --git a/test/OpenTelemetry.Api.Tests/Trace/TracerTest.cs b/test/OpenTelemetry.Api.Tests/Trace/TracerTests.cs similarity index 57% rename from test/OpenTelemetry.Api.Tests/Trace/TracerTest.cs rename to test/OpenTelemetry.Api.Tests/Trace/TracerTests.cs index 7c71f41cda0..b2006654a4e 100644 --- a/test/OpenTelemetry.Api.Tests/Trace/TracerTest.cs +++ b/test/OpenTelemetry.Api.Tests/Trace/TracerTests.cs @@ -10,12 +10,12 @@ namespace OpenTelemetry.Trace.Tests; -public class TracerTest : IDisposable +public sealed class TracerTests : IDisposable { private readonly ITestOutputHelper output; private readonly Tracer tracer; - public TracerTest(ITestOutputHelper output) + public TracerTests(ITestOutputHelper output) { this.output = output; this.tracer = TracerProvider.Default.GetTracer("tracername", "tracerversion"); @@ -53,13 +53,16 @@ public void Tracer_StartRootSpan_BadArgs_NullSpanName() .AddSource("tracername") .Build(); - var span1 = this.tracer.StartRootSpan(null); + var span1 = this.tracer.StartRootSpan(null!); + Assert.NotNull(span1.Activity); Assert.True(string.IsNullOrEmpty(span1.Activity.DisplayName)); - var span2 = this.tracer.StartRootSpan(null, SpanKind.Client); + var span2 = this.tracer.StartRootSpan(null!, SpanKind.Client); + Assert.NotNull(span2.Activity); Assert.True(string.IsNullOrEmpty(span2.Activity.DisplayName)); - var span3 = this.tracer.StartRootSpan(null, SpanKind.Client, default); + var span3 = this.tracer.StartRootSpan(null!, SpanKind.Client, default); + Assert.NotNull(span3.Activity); Assert.True(string.IsNullOrEmpty(span3.Activity.DisplayName)); } @@ -109,13 +112,16 @@ public void Tracer_StartSpan_BadArgs_NullSpanName() .AddSource("tracername") .Build(); - var span1 = this.tracer.StartSpan(null); + var span1 = this.tracer.StartSpan(null!); + Assert.NotNull(span1.Activity); Assert.True(string.IsNullOrEmpty(span1.Activity.DisplayName)); - var span2 = this.tracer.StartSpan(null, SpanKind.Client); + var span2 = this.tracer.StartSpan(null!, SpanKind.Client); + Assert.NotNull(span2.Activity); Assert.True(string.IsNullOrEmpty(span2.Activity.DisplayName)); - var span3 = this.tracer.StartSpan(null, SpanKind.Client, null); + var span3 = this.tracer.StartSpan(null!, SpanKind.Client, null); + Assert.NotNull(span3.Activity); Assert.True(string.IsNullOrEmpty(span3.Activity.DisplayName)); } @@ -126,13 +132,16 @@ public void Tracer_StartActiveSpan_BadArgs_NullSpanName() .AddSource("tracername") .Build(); - var span1 = this.tracer.StartActiveSpan(null); + var span1 = this.tracer.StartActiveSpan(null!); + Assert.NotNull(span1.Activity); Assert.True(string.IsNullOrEmpty(span1.Activity.DisplayName)); - var span2 = this.tracer.StartActiveSpan(null, SpanKind.Client); + var span2 = this.tracer.StartActiveSpan(null!, SpanKind.Client); + Assert.NotNull(span2.Activity); Assert.True(string.IsNullOrEmpty(span2.Activity.DisplayName)); - var span3 = this.tracer.StartActiveSpan(null, SpanKind.Client, null); + var span3 = this.tracer.StartActiveSpan(null!, SpanKind.Client, null); + Assert.NotNull(span3.Activity); Assert.True(string.IsNullOrEmpty(span3.Activity.DisplayName)); } @@ -143,10 +152,12 @@ public void Tracer_StartSpan_FromParent_BadArgs_NullSpanName() .AddSource("tracername") .Build(); - var span1 = this.tracer.StartSpan(null, SpanKind.Client, TelemetrySpan.NoopInstance); + var span1 = this.tracer.StartSpan(null!, SpanKind.Client, TelemetrySpan.NoopInstance); + Assert.NotNull(span1.Activity); Assert.True(string.IsNullOrEmpty(span1.Activity.DisplayName)); - var span2 = this.tracer.StartSpan(null, SpanKind.Client, TelemetrySpan.NoopInstance, default); + var span2 = this.tracer.StartSpan(null!, SpanKind.Client, TelemetrySpan.NoopInstance, default); + Assert.NotNull(span2.Activity); Assert.True(string.IsNullOrEmpty(span2.Activity.DisplayName)); } @@ -159,10 +170,12 @@ public void Tracer_StartSpan_FromParentContext_BadArgs_NullSpanName() var blankContext = default(SpanContext); - var span1 = this.tracer.StartSpan(null, SpanKind.Client, blankContext); + var span1 = this.tracer.StartSpan(null!, SpanKind.Client, blankContext); + Assert.NotNull(span1.Activity); Assert.True(string.IsNullOrEmpty(span1.Activity.DisplayName)); - var span2 = this.tracer.StartSpan(null, SpanKind.Client, blankContext, default); + var span2 = this.tracer.StartSpan(null!, SpanKind.Client, blankContext, default); + Assert.NotNull(span2.Activity); Assert.True(string.IsNullOrEmpty(span2.Activity.DisplayName)); } @@ -173,10 +186,12 @@ public void Tracer_StartActiveSpan_FromParent_BadArgs_NullSpanName() .AddSource("tracername") .Build(); - var span1 = this.tracer.StartActiveSpan(null, SpanKind.Client, TelemetrySpan.NoopInstance); + var span1 = this.tracer.StartActiveSpan(null!, SpanKind.Client, TelemetrySpan.NoopInstance); + Assert.NotNull(span1.Activity); Assert.True(string.IsNullOrEmpty(span1.Activity.DisplayName)); - var span2 = this.tracer.StartActiveSpan(null, SpanKind.Client, TelemetrySpan.NoopInstance, default); + var span2 = this.tracer.StartActiveSpan(null!, SpanKind.Client, TelemetrySpan.NoopInstance, default); + Assert.NotNull(span2.Activity); Assert.True(string.IsNullOrEmpty(span2.Activity.DisplayName)); } @@ -189,10 +204,12 @@ public void Tracer_StartActiveSpan_FromParentContext_BadArgs_NullSpanName() var blankContext = default(SpanContext); - var span1 = this.tracer.StartActiveSpan(null, SpanKind.Client, blankContext); + var span1 = this.tracer.StartActiveSpan(null!, SpanKind.Client, blankContext); + Assert.NotNull(span1.Activity); Assert.True(string.IsNullOrEmpty(span1.Activity.DisplayName)); - var span2 = this.tracer.StartActiveSpan(null, SpanKind.Client, blankContext, default); + var span2 = this.tracer.StartActiveSpan(null!, SpanKind.Client, blankContext, default); + Assert.NotNull(span2.Activity); Assert.True(string.IsNullOrEmpty(span2.Activity.DisplayName)); } @@ -204,19 +221,23 @@ public void Tracer_StartActiveSpan_CreatesActiveSpan() .Build(); var span1 = this.tracer.StartActiveSpan("Test"); + Assert.NotNull(span1.Activity); Assert.Equal(span1.Activity.SpanId, Tracer.CurrentSpan.Context.SpanId); var span2 = this.tracer.StartActiveSpan("Test", SpanKind.Client); + Assert.NotNull(span2.Activity); Assert.Equal(span2.Activity.SpanId, Tracer.CurrentSpan.Context.SpanId); var span = this.tracer.StartSpan("foo"); Tracer.WithSpan(span); var span3 = this.tracer.StartActiveSpan("Test", SpanKind.Client, span); + Assert.NotNull(span3.Activity); Assert.Equal(span3.Activity.SpanId, Tracer.CurrentSpan.Context.SpanId); var spanContext = new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded); var span4 = this.tracer.StartActiveSpan("Test", SpanKind.Client, spanContext); + Assert.NotNull(span4.Activity); Assert.Equal(span4.Activity.SpanId, Tracer.CurrentSpan.Context.SpanId); } @@ -278,21 +299,21 @@ public void CreateSpan_NotSampled() [Fact] public void TracerBecomesNoopWhenParentProviderIsDisposedTest() { - TracerProvider provider = null; - Tracer tracer = null; + TracerProvider? provider; + Tracer? tracer1; using (var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("mytracer") - .Build()) + .AddSource("mytracer") + .Build()) { provider = tracerProvider; - tracer = tracerProvider.GetTracer("mytracer"); + tracer1 = tracerProvider.GetTracer("mytracer"); - var span1 = tracer.StartSpan("foo"); + var span1 = tracer1.StartSpan("foo"); Assert.True(span1.IsRecording); } - var span2 = tracer.StartSpan("foo"); + var span2 = tracer1.StartSpan("foo"); Assert.False(span2.IsRecording); var tracer2 = provider.GetTracer("mytracer"); @@ -309,7 +330,7 @@ public void TracerConcurrencyTest() .WithTestingIterations(100) .WithMemoryAccessRaceCheckingEnabled(true); - var test = TestingEngine.Create(config, InnerTest); + using var test = TestingEngine.Create(config, InnerTest); test.Run(); @@ -349,9 +370,10 @@ static void InnerTest() Thread[] getTracerThreads = new Thread[testTracerProvider.ExpectedNumberOfThreads]; for (int i = 0; i < testTracerProvider.ExpectedNumberOfThreads; i++) { - getTracerThreads[i] = new Thread((object state) => + getTracerThreads[i] = new Thread(state => { var testTracerProvider = state as TestTracerProvider; + Assert.NotNull(testTracerProvider); var id = Interlocked.Increment(ref testTracerProvider.NumberOfThreads); var name = $"Tracer{id}"; @@ -375,17 +397,154 @@ static void InnerTest() testTracerProvider.StartHandle.WaitOne(); - testTracerProvider.Dispose(); - foreach (var getTracerThread in getTracerThreads) { getTracerThread.Join(); } + testTracerProvider.Dispose(); + Assert.Empty(tracers); } } + [Fact] + public void GetTracer_WithSameTags_ReturnsSameInstance() + { + var tags1 = new List> { new("tag1", "value1"), new("tag2", "value2") }; + var tags2 = new List> { new("tag1", "value1"), new("tag2", "value2") }; + + using var tracerProvider = new TestTracerProvider(); + var tracer1 = tracerProvider.GetTracer("test", "1.0.0", tags1); + var tracer2 = tracerProvider.GetTracer("test", "1.0.0", tags2); + + Assert.Same(tracer1, tracer2); + } + + [Fact] + public void GetTracer_WithoutTags_ReturnsSameInstance() + { + using var tracerProvider = new TestTracerProvider(); + var tracer1 = tracerProvider.GetTracer("test", "1.0.0"); + var tracer2 = tracerProvider.GetTracer("test", "1.0.0"); + + Assert.Same(tracer1, tracer2); + } + + [Fact] + public void GetTracer_WithDifferentTags_ReturnsDifferentInstances() + { + var tags1 = new List> { new("tag1", "value1") }; + var tags2 = new List> { new("tag2", "value2") }; + + using var tracerProvider = new TestTracerProvider(); + var tracer1 = tracerProvider.GetTracer("test", "1.0.0", tags1); + var tracer2 = tracerProvider.GetTracer("test", "1.0.0", tags2); + + Assert.NotSame(tracer1, tracer2); + } + + [Fact] + public void GetTracer_WithDifferentOrderTags_ReturnsSameInstance() + { + var tags1 = new List> { new("tag2", "value2"), new("tag1", "value1"), }; + var tags2 = new List> { new("tag1", "value1"), new("tag2", "value2"), }; + + using var tracerProvider = new TestTracerProvider(); + var tracer1 = tracerProvider.GetTracer("test", "1.0.0", tags1); + var tracer2 = tracerProvider.GetTracer("test", "1.0.0", tags2); + + Assert.Same(tracer1, tracer2); + } + + [Fact] + public void GetTracer_TagsValuesAreIntType_ReturnsSameInstance() + { + var tags1 = new List> { new("tag2", 2), new("tag1", 1) }; + var tags2 = new List> { new("tag1", 1), new("tag2", 2) }; + + using var tracerProvider = new TestTracerProvider(); + var tracer1 = tracerProvider.GetTracer("test", "1.0.0", tags1); + var tracer2 = tracerProvider.GetTracer("test", "1.0.0", tags2); + + Assert.Same(tracer1, tracer2); + } + + [Fact] + public void GetTracer_TagsValuesAreSameWithDifferentOrder_ReturnsSameInstance() + { + var tags1 = new List> { new("tag3", 1), new("tag1", 1), new("tag2", 1), new("tag1", 2), new("tag2", 2) }; + var tags2 = new List> { new("tag2", 1), new("tag1", 2), new("tag1", 1), new("tag2", 2), new("tag3", 1) }; + + using var tracerProvider = new TestTracerProvider(); + var tracer1 = tracerProvider.GetTracer("test", "1.0.0", tags1); + var tracer2 = tracerProvider.GetTracer("test", "1.0.0", tags2); + + Assert.Same(tracer1, tracer2); + } + + [Fact] + public void GetTracer_TagsContainNullValues_ReturnsSameInstance() + { + var tags1 = new List> { new("tag3", 1), new("tag2", 3), new("tag1", null), new("tag2", null), new("tag1", 2), new("tag2", 2) }; + var tags2 = new List> { new("tag2", null), new("tag1", 2), new("tag2", 3), new("tag1", null), new("tag2", 2), new("tag3", 1) }; + + using var tracerProvider = new TestTracerProvider(); + var tracer1 = tracerProvider.GetTracer("test", "1.0.0", tags1); + var tracer2 = tracerProvider.GetTracer("test", "1.0.0", tags2); + + Assert.Same(tracer1, tracer2); + } + + [Fact] + public void GetTracer_WithDifferentTagsSize_ReturnsDifferentInstances() + { + var tags1 = new List> { new("tag2", 2), new("tag1", 1) }; + var tags2 = new List> { new("tag1", 1), new("tag2", 2), new("tag3", 3) }; + + using var tracerProvider = new TestTracerProvider(); + var tracer1 = tracerProvider.GetTracer("test", "1.0.0", tags1); + var tracer2 = tracerProvider.GetTracer("test", "1.0.0", tags2); + + Assert.NotSame(tracer1, tracer2); + } + + [Fact] + public void GetTracer_WithTagsAndWithoutTags_ReturnsDifferentInstances() + { + var tags = new List> { new("tag1", "value1") }; + + using var tracerProvider = new TestTracerProvider(); + var tracerWithTags = tracerProvider.GetTracer("test", "1.0.0", tags); + var tracerWithoutTags = tracerProvider.GetTracer("test", "1.0.0"); + + Assert.NotEqual(tracerWithTags, tracerWithoutTags); + } + + [Fact] + public void GetTracer_WithTags_AppliesTagsToActivities() + { + var exportedItems = new List(); + var tags = new List> { new("tracerTag", "tracerValue") }; + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("test") + .AddInMemoryExporter(exportedItems) + .SetSampler(new AlwaysOnSampler()) + .Build(); + + var tracer = tracerProvider.GetTracer("test", "1.0.0", tags); + + using (var span = tracer.StartActiveSpan("TestSpan")) + { + // Activity started by the tracer with tags + } + + var activity = Assert.Single(exportedItems); + + Assert.Contains(activity.Source.Tags!, kvp => kvp.Key == "tracerTag" && (string)kvp.Value! == "tracerValue"); + } + public void Dispose() { Activity.Current = null; @@ -402,5 +561,15 @@ private sealed class TestTracerProvider : TracerProvider public int ExpectedNumberOfThreads; public int NumberOfThreads; public EventWaitHandle StartHandle = new ManualResetEvent(false); + + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.StartHandle.Dispose(); + } + + base.Dispose(disposing); + } } } diff --git a/test/OpenTelemetry.Exporter.Console.Tests/ConsoleActivityExporterTest.cs b/test/OpenTelemetry.Exporter.Console.Tests/ConsoleActivityExporterTests.cs similarity index 89% rename from test/OpenTelemetry.Exporter.Console.Tests/ConsoleActivityExporterTest.cs rename to test/OpenTelemetry.Exporter.Console.Tests/ConsoleActivityExporterTests.cs index e3305513d1d..31aa5f0297c 100644 --- a/test/OpenTelemetry.Exporter.Console.Tests/ConsoleActivityExporterTest.cs +++ b/test/OpenTelemetry.Exporter.Console.Tests/ConsoleActivityExporterTests.cs @@ -8,10 +8,10 @@ namespace OpenTelemetry.Exporter.Console.Tests; -public class ConsoleActivityExporterTest +public class ConsoleActivityExporterTests { [Fact] - public void VerifyConsoleActivityExporterDoesntFailWithoutActivityLinkTags() + public void VerifyConsoleActivityExporterDoesNotFailWithoutActivityLinkTags() { var activitySourceName = Utils.GetCurrentMethodName(); using var activitySource = new ActivitySource(activitySourceName); @@ -43,6 +43,6 @@ public void VerifyConsoleActivityExporterDoesntFailWithoutActivityLinkTags() // Test that the ConsoleExporter correctly handles an Activity without Tags. using var consoleExporter = new ConsoleActivityExporter(new ConsoleExporterOptions()); - Assert.Equal(ExportResult.Success, consoleExporter.Export(new Batch(new[] { activity }, 1))); + Assert.Equal(ExportResult.Success, consoleExporter.Export(new Batch([activity], 1))); } } 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 8745db77efc..28e5579a72d 100644 --- a/test/OpenTelemetry.Exporter.Console.Tests/OpenTelemetry.Exporter.Console.Tests.csproj +++ b/test/OpenTelemetry.Exporter.Console.Tests/OpenTelemetry.Exporter.Console.Tests.csproj @@ -5,14 +5,6 @@ $(TargetFrameworksForTests) - - - - - runtime; build; native; contentfiles; analyzers - - - diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/EventSourceTest.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/EventSourceTests.cs similarity index 97% rename from test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/EventSourceTest.cs rename to test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/EventSourceTests.cs index f786289ef49..69787cec48e 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/EventSourceTest.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/EventSourceTests.cs @@ -9,7 +9,7 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; -public class EventSourceTest +public class EventSourceTests { [Fact] public void EventSourceTest_OpenTelemetryProtocolExporterEventSource() diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/GrpcRetryTestCase.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/GrpcRetryTestCase.cs new file mode 100644 index 00000000000..ca659c79a4b --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/GrpcRetryTestCase.cs @@ -0,0 +1,144 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient.Grpc; +using Xunit; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient.Tests; + +#pragma warning disable CA1515 // Consider making public types internal +public class GrpcRetryTestCase +#pragma warning restore CA1515 // Consider making public types internal +{ + private readonly string testRunnerName; + + private GrpcRetryTestCase(string testRunnerName, GrpcRetryAttempt[] retryAttempts, int expectedRetryAttempts = 1) + { + this.ExpectedRetryAttempts = expectedRetryAttempts; + this.RetryAttempts = retryAttempts; + this.testRunnerName = testRunnerName; + } + + public int ExpectedRetryAttempts { get; } + + internal GrpcRetryAttempt[] RetryAttempts { get; } + + public static TheoryData GetGrpcTestCases() + { + return + [ + new("Cancelled", [new(StatusCode.Cancelled)]), + new("DeadlineExceeded", [new(StatusCode.DeadlineExceeded)]), + new("Aborted", [new(StatusCode.Aborted)]), + new("OutOfRange", [new(StatusCode.OutOfRange)]), + new("DataLoss", [new(StatusCode.DataLoss)]), + new("Unavailable", [new(StatusCode.Unavailable)]), + + new("OK", [new(StatusCode.OK, expectedSuccess: false)]), + new("PermissionDenied", [new(StatusCode.PermissionDenied, expectedSuccess: false)]), + new("Unknown", [new(StatusCode.Unknown, expectedSuccess: false)]), + + new("ResourceExhausted w/o RetryInfo", [new(StatusCode.ResourceExhausted, expectedSuccess: false)]), + new("ResourceExhausted w/ RetryInfo", [new(StatusCode.ResourceExhausted, throttleDelay: GetThrottleDelayString(new Duration { Seconds = 2 }), expectedNextRetryDelayMilliseconds: 3000)]), + + new("Unavailable w/ RetryInfo", [new(StatusCode.Unavailable, throttleDelay: GetThrottleDelayString(Duration.FromTimeSpan(TimeSpan.FromMilliseconds(2000))), expectedNextRetryDelayMilliseconds: 3000)]), + + new("Expired deadline", [new(StatusCode.Unavailable, deadlineExceeded: true, expectedSuccess: false)]), + + new( + "Exponential backoff", + [ + 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), + + new( + "Retry until non-retryable status code encountered", + [ + 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. + new( + "Exponential backoff after throttling", + [ + new(StatusCode.Unavailable, expectedNextRetryDelayMilliseconds: 1500), + new(StatusCode.Unavailable, expectedNextRetryDelayMilliseconds: 2250), + new(StatusCode.Unavailable, throttleDelay: GetThrottleDelayString(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), + ]; + } + + public override string ToString() + { + return this.testRunnerName; + } + + private static string GetThrottleDelayString(Duration throttleDelay) + { + var status = new Google.Rpc.Status + { + Code = 4, + Message = "Only nanos", + Details = + { + Any.Pack(new Google.Rpc.RetryInfo + { + RetryDelay = throttleDelay, + }), + }, + }; + + return Convert.ToBase64String(status.ToByteArray()); + } + + internal struct GrpcRetryAttempt + { + internal GrpcRetryAttempt( + StatusCode statusCode, + bool deadlineExceeded = false, + string? throttleDelay = null, + int expectedNextRetryDelayMilliseconds = 1500, + bool expectedSuccess = true) + { + var status = new Status(statusCode, "Error"); + + // Using arbitrary +1 hr for deadline for test purposes. + var deadlineUtc = deadlineExceeded ? DateTime.UtcNow.AddSeconds(-1) : DateTime.UtcNow.AddHours(1); + + this.ThrottleDelay = throttleDelay; + + this.Response = new ExportClientGrpcResponse(expectedSuccess, deadlineUtc, null, status, this.ThrottleDelay); + + this.ExpectedNextRetryDelayMilliseconds = expectedNextRetryDelayMilliseconds; + + this.ExpectedSuccess = expectedSuccess; + } + + public string? ThrottleDelay { get; } + + public int? ExpectedNextRetryDelayMilliseconds { get; } + + public bool ExpectedSuccess { get; } + + internal ExportClientGrpcResponse Response { get; } + } +} diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/HttpRetryTestCase.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/HttpRetryTestCase.cs new file mode 100644 index 00000000000..6bea0eb7d3a --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/HttpRetryTestCase.cs @@ -0,0 +1,113 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Net; +#if NETFRAMEWORK +using System.Net.Http; +#endif +using System.Net.Http.Headers; +using Xunit; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient.Tests; + +#pragma warning disable CA1515 // Consider making public types internal +public class HttpRetryTestCase +#pragma warning restore CA1515 // Consider making public types internal +{ + private readonly string testRunnerName; + + private HttpRetryTestCase(string testRunnerName, HttpRetryAttempt[] retryAttempts, int expectedRetryAttempts = 1) + { + this.ExpectedRetryAttempts = expectedRetryAttempts; + this.RetryAttempts = retryAttempts; + this.testRunnerName = testRunnerName; + } + + public int ExpectedRetryAttempts { get; } + + internal HttpRetryAttempt[] RetryAttempts { get; } + + public static TheoryData GetHttpTestCases() + { + return + [ + new("NetworkError", [new(statusCode: null)]), + new("GatewayTimeout", [new(statusCode: HttpStatusCode.GatewayTimeout, throttleDelay: TimeSpan.FromSeconds(1))]), +#if NETSTANDARD2_1_OR_GREATER || NET + new("ServiceUnavailable", [new(statusCode: HttpStatusCode.TooManyRequests, throttleDelay: TimeSpan.FromSeconds(1))]), +#endif + + new( + "Exponential Backoff", + [ + new(statusCode: null, expectedNextRetryDelayMilliseconds: 1500), + new(statusCode: null, expectedNextRetryDelayMilliseconds: 2250), + new(statusCode: null, expectedNextRetryDelayMilliseconds: 3375), + new(statusCode: null, expectedNextRetryDelayMilliseconds: 5000), + new(statusCode: null, expectedNextRetryDelayMilliseconds: 5000) + ], + expectedRetryAttempts: 5), + new( + "Retry until non-retryable status code encountered", + [ + new(statusCode: HttpStatusCode.ServiceUnavailable, expectedNextRetryDelayMilliseconds: 1500), + new(statusCode: HttpStatusCode.ServiceUnavailable, expectedNextRetryDelayMilliseconds: 2250), + new(statusCode: HttpStatusCode.ServiceUnavailable, expectedNextRetryDelayMilliseconds: 3375), + new(statusCode: HttpStatusCode.BadRequest, expectedSuccess: false), + new(statusCode: HttpStatusCode.ServiceUnavailable, expectedNextRetryDelayMilliseconds: 5000) + ], + expectedRetryAttempts: 4), + new( + "Expired deadline", + [ + new(statusCode: HttpStatusCode.ServiceUnavailable, isDeadlineExceeded: true, expectedSuccess: false) + ]), + ]; + + // TODO: Add more cases. + } + + public override string ToString() + { + return this.testRunnerName; + } + + internal sealed class HttpRetryAttempt + { + public ExportClientHttpResponse Response; + public TimeSpan? ThrottleDelay; + public int? ExpectedNextRetryDelayMilliseconds; + public bool ExpectedSuccess; + + internal HttpRetryAttempt( + HttpStatusCode? statusCode, + TimeSpan? throttleDelay = null, + bool isDeadlineExceeded = false, + int expectedNextRetryDelayMilliseconds = 1500, + bool expectedSuccess = true) + { + this.ThrottleDelay = throttleDelay; + + HttpResponseMessage? responseMessage = null; + if (statusCode != null) + { +#pragma warning disable CA2000 // Dispose objects before losing scope + responseMessage = new HttpResponseMessage(); +#pragma warning restore CA2000 // Dispose objects before losing scope + + if (throttleDelay != null) + { + responseMessage.Headers.RetryAfter = new RetryConditionHeaderValue(throttleDelay.Value); + } + + responseMessage.StatusCode = (HttpStatusCode)statusCode; + } + + // Using arbitrary +1 hr for deadline for test purposes. + var deadlineUtc = isDeadlineExceeded ? DateTime.UtcNow.AddMilliseconds(-1) : DateTime.UtcNow.AddHours(1); + this.Response = new ExportClientHttpResponse(expectedSuccess, deadlineUtc, responseMessage, new HttpRequestException()); + this.ExpectedNextRetryDelayMilliseconds = expectedNextRetryDelayMilliseconds; + this.ExpectedSuccess = expectedSuccess; + } + } +} diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/GrpcStatusDeserializerTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/GrpcStatusDeserializerTests.cs new file mode 100644 index 00000000000..36e5eb521ae --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/GrpcStatusDeserializerTests.cs @@ -0,0 +1,339 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient.Grpc; +using Xunit; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.Implementation.ExportClient; + +public class GrpcStatusDeserializerTests +{ + [Fact] + public void DeserializeStatus_ValidBase64Input_ReturnsExpectedStatus() + { + var status = new Google.Rpc.Status + { + Code = 5, + Message = "Test error", + Details = + { + Any.Pack(new StringValue { Value = "Example detail" }), + }, + }; + + // Serialize the Status message and encode to base64 + string grpcStatusDetailsBin = Convert.ToBase64String(status.ToByteArray()); + + // Use the GrpcStatusDeserializer to deserialize from the base64 input + var deserializedStatus = GrpcStatusDeserializer.DeserializeStatus(grpcStatusDetailsBin); + + // Assertions to validate the deserialized Status object + Assert.NotNull(deserializedStatus); + Assert.Equal(status.Code, deserializedStatus.Value.Code); + Assert.Equal(status.Message, deserializedStatus.Value.Message); + Assert.Single(deserializedStatus.Value.Details); + Assert.Equal("type.googleapis.com/google.protobuf.StringValue", deserializedStatus.Value.Details[0].TypeUrl); + var stringValue = StringValue.Parser.ParseFrom(deserializedStatus.Value.Details[0].Value); + Assert.Equal("Example detail", stringValue.Value); + } + + [Fact] + public void DeserializeStatus_WithRetryInfo_ReturnsExpectedStatus() + { + // Arrange + var status = new Google.Rpc.Status + { + Code = 4, + Message = "Retry later", + Details = + { + Any.Pack(new Google.Rpc.RetryInfo + { + RetryDelay = new Duration { Seconds = 5 }, + }), + }, + }; + + string grpcStatusDetailsBin = Convert.ToBase64String(status.ToByteArray()); + + // Act + var retryInfo = GrpcStatusDeserializer.ExtractRetryInfo(grpcStatusDetailsBin); + + // Assert + Assert.NotNull(retryInfo); + Assert.Equal(5, retryInfo.Value.RetryDelay?.Seconds); + } + + [Fact] + public void DeserializeStatus_EmptyStatus_ReturnsEmptyStatus() + { + // Arrange + var status = new Google.Rpc.Status(); + string grpcStatusDetailsBin = Convert.ToBase64String(status.ToByteArray()); + + // Act + var deserializedStatus = GrpcStatusDeserializer.DeserializeStatus(grpcStatusDetailsBin); + + // Assert + Assert.Null(deserializedStatus); + } + + [Fact] + public void DeserializeStatus_MultipleDetails_ReturnsAllDetails() + { + // Arrange + var status = new Google.Rpc.Status + { + Code = 7, + Message = "Multiple details", + Details = + { + Any.Pack(new StringValue { Value = "First detail" }), + Any.Pack(new Google.Rpc.RetryInfo + { + RetryDelay = new Duration { Seconds = 10 }, + }), + }, + }; + + string grpcStatusDetailsBin = Convert.ToBase64String(status.ToByteArray()); + + // Act + var deserializedStatus = GrpcStatusDeserializer.DeserializeStatus(grpcStatusDetailsBin); + var retryInfo = GrpcStatusDeserializer.ExtractRetryInfo(grpcStatusDetailsBin); + + // Assert + Assert.NotNull(deserializedStatus); + Assert.Equal(status.Code, deserializedStatus.Value.Code); + Assert.Equal(status.Message, deserializedStatus.Value.Message); + Assert.Equal(2, deserializedStatus.Value.Details.Count); + + // Verify first detail (StringValue) + Assert.Equal("type.googleapis.com/google.protobuf.StringValue", deserializedStatus.Value.Details[0].TypeUrl); + var stringValue = StringValue.Parser.ParseFrom(deserializedStatus.Value.Details[0].Value); + Assert.Equal("First detail", stringValue.Value); + + // Verify second detail (RetryInfo) + Assert.Equal("type.googleapis.com/google.rpc.RetryInfo", deserializedStatus.Value.Details[1].TypeUrl); + Assert.NotNull(retryInfo); + Assert.Equal(10, retryInfo.Value.RetryDelay?.Seconds); + } + + [Fact] + public void DeserializeStatus_ComplexRetryInfo_ReturnsExpectedValues() + { + // Arrange + var status = new Google.Rpc.Status + { + Code = 4, + Message = "Complex retry scenario", + Details = + { + Any.Pack(new Google.Rpc.RetryInfo + { + RetryDelay = new Duration + { + Seconds = 5, + Nanos = 500000000, // 0.5 seconds + }, + }), + }, + }; + + string grpcStatusDetailsBin = Convert.ToBase64String(status.ToByteArray()); + + // Act + byte[] data = Convert.FromBase64String(grpcStatusDetailsBin); + var retryInfo = GrpcStatusDeserializer.ExtractRetryInfo(grpcStatusDetailsBin); + + // Assert + Assert.NotNull(retryInfo); + Assert.Equal(5, retryInfo.Value.RetryDelay?.Seconds); + Assert.Equal(500000000, retryInfo.Value.RetryDelay?.Nanos); + } + + [Fact] + public void ExtractRetryInfo_WithNoRetryInfoTypeUrl_ReturnsNull() + { + // Arrange + var status = new Google.Rpc.Status + { + Code = 3, + Message = "No retry info", + Details = { Any.Pack(new Google.Rpc.Status { Code = 5 }) }, // A different type packed + }; + + string grpcStatusDetailsBin = Convert.ToBase64String(status.ToByteArray()); + + // Act + var retryInfo = GrpcStatusDeserializer.ExtractRetryInfo(grpcStatusDetailsBin); + + // Assert + Assert.Null(retryInfo); + } + + [Fact] + public void DeserializeStatus_WithBoundaryCode_ReturnsExpectedStatus() + { + // Arrange + var status = new Google.Rpc.Status + { + Code = int.MaxValue, + Message = "Boundary code test", + }; + + string grpcStatusDetailsBin = Convert.ToBase64String(status.ToByteArray()); + + // Act + var deserializedStatus = GrpcStatusDeserializer.DeserializeStatus(grpcStatusDetailsBin); + + // Assert + Assert.NotNull(deserializedStatus); + Assert.Equal(int.MaxValue, deserializedStatus.Value.Code); + Assert.Equal("Boundary code test", deserializedStatus.Value.Message); + } + + [Fact] + public void TryGetGrpcRetryDelay_NullOrEmptyInput_ReturnsNull() + { + Assert.Null(GrpcStatusDeserializer.TryGetGrpcRetryDelay(null)); + Assert.Null(GrpcStatusDeserializer.TryGetGrpcRetryDelay(string.Empty)); + Assert.Null(GrpcStatusDeserializer.TryGetGrpcRetryDelay(" ")); + } + + [Fact] + public void TryGetGrpcRetryDelay_InvalidBase64Input_ReturnsNull() + { + Assert.Null(GrpcStatusDeserializer.TryGetGrpcRetryDelay("invalid-base64")); + } + + [Fact] + public void TryGetGrpcRetryDelay_NoRetryInfo_ReturnsNull() + { + // Arrange + var status = new Google.Rpc.Status + { + Code = 3, + Message = "No retry info", + Details = { Any.Pack(new Google.Rpc.Status { Code = 5 }) }, // Non-RetryInfo type + }; + + string grpcStatusDetailsBin = Convert.ToBase64String(status.ToByteArray()); + + // Act + var result = GrpcStatusDeserializer.TryGetGrpcRetryDelay(grpcStatusDetailsBin); + + // Assert + Assert.Null(result); + } + + [Fact] + public void TryGetGrpcRetryDelay_BoundaryValuesForDuration_ReturnsNull() + { + // Arrange + var status = new Google.Rpc.Status + { + Code = 4, + Message = "Boundary test", + Details = + { + Any.Pack(new Google.Rpc.RetryInfo + { + RetryDelay = new Duration + { + Seconds = long.MaxValue, + Nanos = int.MaxValue, + }, + }), + }, + }; + + string grpcStatusDetailsBin = Convert.ToBase64String(status.ToByteArray()); + + // Act + var result = GrpcStatusDeserializer.TryGetGrpcRetryDelay(grpcStatusDetailsBin); + + // Assert + Assert.Null(result); + } + + [Fact] + public void TryGetGrpcRetryDelay_MultipleRetryInfos_UsesFirstRetryInfo() + { + // Arrange + var status = new Google.Rpc.Status + { + Code = 4, + Message = "Multiple RetryInfos", + Details = + { + Any.Pack(new Google.Rpc.RetryInfo + { + RetryDelay = new Duration { Seconds = 5 }, + }), + Any.Pack(new Google.Rpc.RetryInfo + { + RetryDelay = new Duration { Seconds = 10 }, + }), + }, + }; + + string grpcStatusDetailsBin = Convert.ToBase64String(status.ToByteArray()); + + // Act + var result = GrpcStatusDeserializer.TryGetGrpcRetryDelay(grpcStatusDetailsBin); + + // Assert + Assert.NotNull(result); + Assert.Equal(TimeSpan.FromSeconds(5), result); + } + + [Fact] + public void TryGetGrpcRetryDelay_OnlyNanos_ReturnsExpected() + { + // Arrange + var status = new Google.Rpc.Status + { + Code = 4, + Message = "Only nanos", + Details = + { + Any.Pack(new Google.Rpc.RetryInfo + { + RetryDelay = new Duration { Nanos = 500000000 }, // 0.5 seconds + }), + }, + }; + + string grpcStatusDetailsBin = Convert.ToBase64String(status.ToByteArray()); + + // Act + var result = GrpcStatusDeserializer.TryGetGrpcRetryDelay(grpcStatusDetailsBin); + + // Assert + Assert.NotNull(result); + Assert.Equal(TimeSpan.FromSeconds(0.5), result); + } + + [Fact] + public void DeserializeStatus_TruncatedStream_ThrowsEndOfStreamException() + { + // Arrange: Create valid Base64 data and truncate it + var status = new Google.Rpc.Status + { + Code = 3, + Message = "Truncated stream test", + }; + + byte[] fullData = status.ToByteArray(); + byte[] truncatedData = fullData.Take(fullData.Length / 2).ToArray(); // Truncate the data + + string grpcStatusDetailsBin = Convert.ToBase64String(truncatedData); + + // Act & Assert: Attempt to deserialize and expect an EndOfStreamException + Assert.Throws(() => + GrpcStatusDeserializer.DeserializeStatus(grpcStatusDetailsBin)); + } +} diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs index 7593a672873..d256b4fcf79 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs @@ -2,11 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -#if !NET6_0_OR_GREATER +#if !NET using System.Net.Http; #endif using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; using OpenTelemetry.Resources; using OpenTelemetry.Trace; using Xunit; @@ -14,22 +15,32 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; -public class OtlpHttpTraceExportClientTests +public sealed class OtlpHttpTraceExportClientTests : IDisposable { private static readonly SdkLimitOptions DefaultSdkLimitOptions = new(); + private readonly ActivityListener activityListener; + static OtlpHttpTraceExportClientTests() { Activity.DefaultIdFormat = ActivityIdFormat.W3C; Activity.ForceDefaultIdFormat = true; + } - var listener = new ActivityListener + public OtlpHttpTraceExportClientTests() + { + this.activityListener = new ActivityListener { ShouldListenTo = _ => true, - Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, + Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllDataAndRecorded, }; - ActivitySource.AddActivityListener(listener); + ActivitySource.AddActivityListener(this.activityListener); + } + + public void Dispose() + { + this.activityListener.Dispose(); } [Fact] @@ -43,7 +54,7 @@ public void NewOtlpHttpTraceExportClient_OtlpExporterOptions_ExporterHasCorrectP Headers = $"{header1.Name}={header1.Value}, {header2.Name} = {header2.Value}", }; - var client = new OtlpHttpTraceExportClient(options, options.HttpClientFactory()); + var client = new OtlpHttpExportClient(options, options.HttpClientFactory(), "/v1/traces"); Assert.NotNull(client.HttpClient); @@ -63,8 +74,8 @@ public void NewOtlpHttpTraceExportClient_OtlpExporterOptions_ExporterHasCorrectP public void SendExportRequest_ExportTraceServiceRequest_SendsCorrectHttpRequest(bool includeServiceNameInResource) { // Arrange - var evenTags = new[] { new KeyValuePair("k0", "v0") }; - var oddTags = new[] { new KeyValuePair("k1", "v1") }; + var evenTags = new[] { new KeyValuePair("k0", "v0") }; + var oddTags = new[] { new KeyValuePair("k1", "v1") }; var sources = new[] { new ActivitySource("even", "2.4.6"), @@ -79,13 +90,13 @@ public void SendExportRequest_ExportTraceServiceRequest_SendsCorrectHttpRequest( Headers = $"{header1.Name}={header1.Value}, {header2.Name} = {header2.Value}", }; +#pragma warning disable CA2000 // Dispose objects before losing scope var testHttpHandler = new TestHttpMessageHandler(); +#pragma warning restore CA2000 // Dispose objects before losing scope - var httpRequestContent = Array.Empty(); + using var httpClient = new HttpClient(testHttpHandler); - var httpClient = new HttpClient(testHttpHandler); - - var exportClient = new OtlpHttpTraceExportClient(options, httpClient); + var exportClient = new OtlpHttpExportClient(options, httpClient, string.Empty); var resourceBuilder = ResourceBuilder.CreateEmpty(); if (includeServiceNameInResource) @@ -106,7 +117,9 @@ public void SendExportRequest_ExportTraceServiceRequest_SendsCorrectHttpRequest( using var openTelemetrySdk = builder.Build(); var exportedItems = new List(); +#pragma warning disable CA2000 // Dispose objects before losing scope var processor = new BatchActivityExportProcessor(new InMemoryExporter(exportedItems)); +#pragma warning restore CA2000 // Dispose objects before losing scope const int numOfSpans = 10; bool isEven; for (var i = 0; i < numOfSpans; i++) @@ -116,7 +129,8 @@ public void SendExportRequest_ExportTraceServiceRequest_SendsCorrectHttpRequest( var activityKind = isEven ? ActivityKind.Client : ActivityKind.Server; var activityTags = isEven ? evenTags : oddTags; - using Activity activity = source.StartActivity($"span-{i}", activityKind, parentContext: default, activityTags); + using Activity? activity = source.StartActivity($"span-{i}", activityKind, parentContext: default, activityTags); + Assert.NotNull(activity); processor.OnEnd(activity); } @@ -130,10 +144,10 @@ void RunTest(Batch batch) var deadlineUtc = DateTime.UtcNow.AddMilliseconds(httpClient.Timeout.TotalMilliseconds); var request = new OtlpCollector.ExportTraceServiceRequest(); - request.AddBatch(DefaultSdkLimitOptions, resourceBuilder.Build().ToOtlpResource(), batch); + var (buffer, contentLength) = CreateTraceExportRequest(DefaultSdkLimitOptions, batch, resourceBuilder.Build()); // Act - var result = exportClient.SendExportRequest(request, deadlineUtc); + var result = exportClient.SendExportRequest(buffer, contentLength, deadlineUtc); var httpRequest = testHttpHandler.HttpRequestMessage; @@ -141,6 +155,7 @@ void RunTest(Batch batch) Assert.True(result.Success); Assert.NotNull(httpRequest); Assert.Equal(HttpMethod.Post, httpRequest.Method); + Assert.NotNull(httpRequest.RequestUri); 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); @@ -152,8 +167,11 @@ void RunTest(Batch batch) } Assert.NotNull(testHttpHandler.HttpRequestContent); - Assert.IsType(httpRequest.Content); - Assert.Contains(httpRequest.Content.Headers, h => h.Key == "Content-Type" && h.Value.First() == OtlpHttpTraceExportClient.MediaContentType); + + // TODO: Revisit once the HttpClient part is overridden. + // Assert.IsType(httpRequest.Content); + Assert.NotNull(httpRequest.Content); + Assert.Contains(httpRequest.Content.Headers, h => h.Key == "Content-Type" && h.Value.First() == OtlpHttpExportClient.MediaHeaderValue.ToString()); var exportTraceRequest = OtlpCollector.ExportTraceServiceRequest.Parser.ParseFrom(testHttpHandler.HttpRequestContent); Assert.NotNull(exportTraceRequest); @@ -167,8 +185,15 @@ void RunTest(Batch batch) } else { - Assert.Contains(resourceSpan.Resource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.ToString().Contains("unknown_service:")); + Assert.DoesNotContain(resourceSpan.Resource.Attributes, kvp => kvp.Key == ResourceSemanticConventions.AttributeServiceName); } } } + + private static (byte[] Buffer, int ContentLength) CreateTraceExportRequest(SdkLimitOptions sdkOptions, in Batch batch, Resource resource) + { + var buffer = new byte[4096]; + var writePosition = ProtobufOtlpTraceSerializer.WriteTraceData(ref buffer, 0, sdkOptions, resource, batch); + return (buffer, writePosition); + } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/Serializer/OtlpArrayTagWriterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/Serializer/OtlpArrayTagWriterTests.cs new file mode 100644 index 00000000000..d8ada0f2f80 --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/Serializer/OtlpArrayTagWriterTests.cs @@ -0,0 +1,289 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; +using OpenTelemetry.Resources; +using Xunit; +using OtlpCollector = OpenTelemetry.Proto.Collector.Trace.V1; +using OtlpTrace = OpenTelemetry.Proto.Trace.V1; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.Implementation.Serializer; + +public sealed class OtlpArrayTagWriterTests : IDisposable +{ + private readonly ProtobufOtlpTagWriter.OtlpArrayTagWriter arrayTagWriter; + private readonly ActivityListener activityListener; + + static OtlpArrayTagWriterTests() + { + Activity.DefaultIdFormat = ActivityIdFormat.W3C; + Activity.ForceDefaultIdFormat = true; + } + + public OtlpArrayTagWriterTests() + { + this.arrayTagWriter = new ProtobufOtlpTagWriter.OtlpArrayTagWriter(); + this.activityListener = new ActivityListener + { + ShouldListenTo = _ => true, + Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllDataAndRecorded, + }; + + ActivitySource.AddActivityListener(this.activityListener); + } + + [Fact] + public void BeginWriteArray_InitializesArrayState() + { + // Act + var arrayState = this.arrayTagWriter.BeginWriteArray(); + + // Assert + Assert.NotNull(arrayState.Buffer); + Assert.Equal(0, arrayState.WritePosition); + Assert.Equal(2048, arrayState.Buffer.Length); + } + + [Fact] + public void WriteNullValue_AddsNullValueToBuffer() + { + // Arrange + var arrayState = this.arrayTagWriter.BeginWriteArray(); + + // Act + this.arrayTagWriter.WriteNullValue(ref arrayState); + + // Assert + // Check that the buffer contains the correct tag and length for a null value + Assert.True(arrayState.WritePosition > 0); + } + + [Theory] + [InlineData(0L)] + [InlineData(long.MaxValue)] + [InlineData(long.MinValue)] + public void WriteIntegralValue_WritesIntegralValueToBuffer(long value) + { + // Arrange + var arrayState = this.arrayTagWriter.BeginWriteArray(); + + // Act + this.arrayTagWriter.WriteIntegralValue(ref arrayState, value); + + // Assert + Assert.True(arrayState.WritePosition > 0); + } + + [Theory] + [InlineData(0.0)] + [InlineData(double.MaxValue)] + [InlineData(double.MinValue)] + public void WriteFloatingPointValue_WritesFloatingPointValueToBuffer(double value) + { + // Arrange + var arrayState = this.arrayTagWriter.BeginWriteArray(); + + // Act + this.arrayTagWriter.WriteFloatingPointValue(ref arrayState, value); + + // Assert + Assert.True(arrayState.WritePosition > 0); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void WriteBooleanValue_WritesBooleanValueToBuffer(bool value) + { + // Arrange + var arrayState = this.arrayTagWriter.BeginWriteArray(); + + // Act + this.arrayTagWriter.WriteBooleanValue(ref arrayState, value); + + // Assert + Assert.True(arrayState.WritePosition > 0); + } + + [Theory] + [InlineData("")] + [InlineData("test")] + public void WriteStringValue_WritesStringValueToBuffer(string value) + { + // Arrange + var arrayState = this.arrayTagWriter.BeginWriteArray(); + + // Act + this.arrayTagWriter.WriteStringValue(ref arrayState, value.AsSpan()); + + // Assert + Assert.True(arrayState.WritePosition > 0); + } + + [Fact] + public void TryResize_SucceedsInitially() + { + // Act + this.arrayTagWriter.BeginWriteArray(); + bool result = this.arrayTagWriter.TryResize(); + + // Assert + Assert.True(result); + } + + [Fact] + public void TryResize_RepeatedResizingStopsAtMaxBufferSize() + { + // Arrange + var arrayState = this.arrayTagWriter.BeginWriteArray(); + bool resizeResult = true; + + // Act: Repeatedly attempt to resize until reaching maximum buffer size + while (resizeResult) + { + resizeResult = this.arrayTagWriter.TryResize(); + } + + // Assert + Assert.False(resizeResult, "Buffer should not resize beyond the maximum allowed size."); + } + + [Fact] + public void SerializeLargeArrayExceeding2MB_TruncatesInOtlpSpan() + { + // Create a large array exceeding 2 MB + var largeArray = new string[512 * 1024]; + for (int i = 0; i < largeArray.Length; i++) + { + largeArray[i] = "1234"; + } + + var lessthat1MBArray = new string[256 * 4]; + for (int i = 0; i < lessthat1MBArray.Length; i++) + { + lessthat1MBArray[i] = "1234"; + } + + string?[] stringArray = ["12345"]; + var tags = new ActivityTagsCollection + { + new("lessthat1MBArray", lessthat1MBArray), + new("StringArray", stringArray), + new("LargeArray", largeArray), + }; + + using var activitySource = new ActivitySource(nameof(this.SerializeLargeArrayExceeding2MB_TruncatesInOtlpSpan)); + using var activity = activitySource.StartActivity("activity", ActivityKind.Server, default(ActivityContext), tags); + + Assert.NotNull(activity); + + var otlpSpan = ToOtlpSpanWithExtendedBuffer(new SdkLimitOptions(), activity); + + Assert.NotNull(otlpSpan); + Assert.Equal(3, otlpSpan.Attributes.Count); + var keyValue = otlpSpan.Attributes.FirstOrDefault(kvp => kvp.Key == "StringArray"); + Assert.NotNull(keyValue); + Assert.Equal("12345", keyValue.Value.ArrayValue.Values[0].StringValue); + + // The string is too large, hence not evaluating the content. + keyValue = otlpSpan.Attributes.FirstOrDefault(kvp => kvp.Key == "lessthat1MBArray"); + Assert.NotNull(keyValue); + + keyValue = otlpSpan.Attributes.FirstOrDefault(kvp => kvp.Key == "LargeArray"); + Assert.NotNull(keyValue); + Assert.Equal("TRUNCATED", keyValue.Value.StringValue); + } + + [Fact] + public void LargeArray_WithSmallBaseBuffer_ThrowsExceptionOnWriteSpan() + { + var lessthat1MBArray = new string[256 * 256]; + for (int i = 0; i < lessthat1MBArray.Length; i++) + { + lessthat1MBArray[i] = "1234"; + } + + var tags = new ActivityTagsCollection + { + new("lessthat1MBArray", lessthat1MBArray), + }; + + using var activitySource = new ActivitySource(nameof(this.LargeArray_WithSmallBaseBuffer_ThrowsExceptionOnWriteSpan)); + using var activity = activitySource.StartActivity("root", ActivityKind.Server, default(ActivityContext), tags); + + Assert.NotNull(activity); + Assert.Throws(() => ToOtlpSpan(new SdkLimitOptions(), activity)); + } + + [Fact] + public void LargeArray_WithSmallBaseBuffer_ExpandsOnTraceData() + { + var lessthat1MBArray = new string[256 * 256]; + for (int i = 0; i < lessthat1MBArray.Length; i++) + { + lessthat1MBArray[i] = "1234"; + } + + var tags = new ActivityTagsCollection + { + new("lessthat1MBArray", lessthat1MBArray), + }; + + using var activitySource = new ActivitySource(nameof(this.LargeArray_WithSmallBaseBuffer_ExpandsOnTraceData)); + using var activity = activitySource.StartActivity("root", ActivityKind.Server, default(ActivityContext), tags); + + Assert.NotNull(activity); + var batch = new Batch([activity], 1); + RunTest(new(), batch); + + void RunTest(SdkLimitOptions sdkOptions, Batch batch) + { + var buffer = new byte[4096]; + var writePosition = ProtobufOtlpTraceSerializer.WriteTraceData(ref buffer, 0, sdkOptions, ResourceBuilder.CreateEmpty().Build(), batch); + using var stream = new MemoryStream(buffer, 0, writePosition); + var tracesData = OtlpTrace.TracesData.Parser.ParseFrom(stream); + var request = new OtlpCollector.ExportTraceServiceRequest(); + request.ResourceSpans.Add(tracesData.ResourceSpans); + + // Buffer should be expanded to accommodate the large array. + Assert.True(buffer.Length > 4096); + + Assert.Single(request.ResourceSpans); + var scopeSpans = request.ResourceSpans.First().ScopeSpans; + Assert.Single(scopeSpans); + var otlpSpan = scopeSpans.First().Spans.First(); + Assert.NotNull(otlpSpan); + + // The string is too large, hence not evaluating the content. + var keyValue = otlpSpan.Attributes.FirstOrDefault(kvp => kvp.Key == "lessthat1MBArray"); + Assert.NotNull(keyValue); + } + } + + public void Dispose() + { + // Clean up the thread buffer after each test + ProtobufOtlpTagWriter.OtlpArrayTagWriter.ThreadBuffer = null; + this.activityListener.Dispose(); + } + + private static OtlpTrace.Span? ToOtlpSpan(SdkLimitOptions sdkOptions, Activity activity) + { + var buffer = new byte[4096]; + var writePosition = ProtobufOtlpTraceSerializer.WriteSpan(buffer, 0, sdkOptions, activity); + using var stream = new MemoryStream(buffer, 0, writePosition); + var scopeSpans = OtlpTrace.ScopeSpans.Parser.ParseFrom(stream); + return scopeSpans.Spans.FirstOrDefault(); + } + + private static OtlpTrace.Span? ToOtlpSpanWithExtendedBuffer(SdkLimitOptions sdkOptions, Activity activity) + { + var buffer = new byte[4194304]; + var writePosition = ProtobufOtlpTraceSerializer.WriteSpan(buffer, 0, sdkOptions, activity); + using var stream = new MemoryStream(buffer, 0, writePosition); + var scopeSpans = OtlpTrace.ScopeSpans.Parser.ParseFrom(stream); + return scopeSpans.Spans.FirstOrDefault(); + } +} diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/Serializer/ProtobufSerializerTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/Serializer/ProtobufSerializerTests.cs new file mode 100644 index 00000000000..6c202b528e7 --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/Serializer/ProtobufSerializerTests.cs @@ -0,0 +1,359 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Text; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; +using Xunit; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.Implementation.Serializer; + +public class ProtobufSerializerTests +{ + [Fact] + public void GetTagValue_ReturnsCorrectValue() + { + Assert.Equal(8u, ProtobufSerializer.GetTagValue(1, ProtobufWireType.VARINT)); + Assert.Equal(17u, ProtobufSerializer.GetTagValue(2, ProtobufWireType.I64)); + Assert.Equal(26u, ProtobufSerializer.GetTagValue(3, ProtobufWireType.LEN)); + } + + [Fact] + public void WriteTag_WritesCorrectly() + { + byte[] buffer = new byte[10]; + int position = ProtobufSerializer.WriteTag(buffer, 0, 1, ProtobufWireType.VARINT); + Assert.Equal(1, position); + Assert.Equal(8, buffer[0]); + } + + [Fact] + public void WriteLength_WritesCorrectly() + { + byte[] buffer = new byte[10]; + int position = ProtobufSerializer.WriteLength(buffer, 0, 300); + Assert.Equal(2, position); + Assert.Equal(0xAC, buffer[0]); + Assert.Equal(0x02, buffer[1]); + } + + [Fact] + public void WriteBoolWithTag_WritesCorrectly() + { + byte[] buffer = new byte[10]; + int position = ProtobufSerializer.WriteBoolWithTag(buffer, 0, 1, true); + Assert.Equal(2, position); + Assert.Equal(8, buffer[0]); + Assert.Equal(1, buffer[1]); + } + + [Fact] + public void WriteFixed32WithTag_WritesCorrectly() + { + byte[] buffer = new byte[10]; + int position = ProtobufSerializer.WriteFixed32WithTag(buffer, 0, 1, 0x12345678); + Assert.Equal(5, position); + Assert.Equal(13, buffer[0]); + Assert.Equal(0x78, buffer[1]); + Assert.Equal(0x56, buffer[2]); + Assert.Equal(0x34, buffer[3]); + Assert.Equal(0x12, buffer[4]); + } + + [Fact] + public void WriteFixed64WithTag_WritesCorrectly() + { + byte[] buffer = new byte[10]; + int position = ProtobufSerializer.WriteFixed64WithTag(buffer, 0, 1, 0x123456789ABCDEF0); + Assert.Equal(9, position); + Assert.Equal(9, buffer[0]); // Tag + Assert.Equal(0xF0, buffer[1]); + Assert.Equal(0xDE, buffer[2]); + Assert.Equal(0xBC, buffer[3]); + Assert.Equal(0x9A, buffer[4]); + Assert.Equal(0x78, buffer[5]); + Assert.Equal(0x56, buffer[6]); + Assert.Equal(0x34, buffer[7]); + Assert.Equal(0x12, buffer[8]); + } + + [Fact] + public void WriteStringWithTag_WritesCorrectly() + { + byte[] buffer = new byte[20]; + int position = ProtobufSerializer.WriteStringWithTag(buffer, 0, 1, "Hello"); + Assert.Equal(7, position); + Assert.Equal(10, buffer[0]); + Assert.Equal(5, buffer[1]); + Assert.Equal((byte)'H', buffer[2]); + Assert.Equal((byte)'e', buffer[3]); + Assert.Equal((byte)'l', buffer[4]); + Assert.Equal((byte)'l', buffer[5]); + Assert.Equal((byte)'o', buffer[6]); + } + + [Theory] + [InlineData(300, new byte[] { 0xAC, 0x82, 0x80, 0x00 })] // Normal case with 300 + [InlineData(127, new byte[] { 0xFF, 0x80, 0x80, 0x00 })] // Boundary case: max 1-byte value + [InlineData(128, new byte[] { 0x80, 0x81, 0x80, 0x00 })] // Boundary case: min 2-byte value + [InlineData(16383, new byte[] { 0xFF, 0xFF, 0x80, 0x00 })] // Max 2-byte value + [InlineData(16384, new byte[] { 0x80, 0x80, 0x81, 0x00 })] // Min 3-byte value + [InlineData(2097151, new byte[] { 0xFF, 0xFF, 0xFF, 0x00 })] // Max 3-byte value + [InlineData(2097152, new byte[] { 0x80, 0x80, 0x80, 0x01 })] // Min 4-byte value + [InlineData(268435455, new byte[] { 0xFF, 0xFF, 0xFF, 0x7F })] // Max 4-byte value + public void WriteReservedLength_WritesCorrectly(int length, byte[] expectedBytes) + { +#if NET + Assert.NotNull(expectedBytes); +#else + if (expectedBytes == null) + { + throw new ArgumentNullException(nameof(expectedBytes)); + } +#endif + byte[] buffer = new byte[10]; + ProtobufSerializer.WriteReservedLength(buffer, 0, length); + + for (int i = 0; i < expectedBytes.Length; i++) + { + Assert.Equal(expectedBytes[i], buffer[i]); + } + } + + [Fact] + public void WriteTagAndLength_WritesCorrectly() + { + byte[] buffer = new byte[10]; + int position = ProtobufSerializer.WriteTagAndLength(buffer, 0, 300, 1, ProtobufWireType.LEN); + Assert.Equal(3, position); + Assert.Equal(10, buffer[0]); // Tag + Assert.Equal(0xAC, buffer[1]); // Length (300 in varint encoding) + Assert.Equal(0x02, buffer[2]); + } + + [Fact] + public void WriteEnumWithTag_WritesCorrectly() + { + byte[] buffer = new byte[10]; + int position = ProtobufSerializer.WriteEnumWithTag(buffer, 0, 1, 5); + Assert.Equal(2, position); + Assert.Equal(8, buffer[0]); // Tag + Assert.Equal(5, buffer[1]); // Enum value + } + + [Fact] + public void WriteVarInt64_WritesCorrectly() + { + byte[] buffer = new byte[10]; + int position = ProtobufSerializer.WriteVarInt64(buffer, 0, 300); + Assert.Equal(2, position); + Assert.Equal(0xAC, buffer[0]); + Assert.Equal(0x02, buffer[1]); + } + + [Fact] + public void WriteInt64WithTag_WritesCorrectly() + { + byte[] buffer = new byte[10]; + int position = ProtobufSerializer.WriteInt64WithTag(buffer, 0, 1, 300); + Assert.Equal(3, position); + Assert.Equal(8, buffer[0]); // Tag + Assert.Equal(0xAC, buffer[1]); + Assert.Equal(0x02, buffer[2]); + } + + [Fact] + public void WriteDoubleWithTag_WritesCorrectly() + { + byte[] buffer = new byte[10]; + int position = ProtobufSerializer.WriteDoubleWithTag(buffer, 0, 1, 123.456); + Assert.Equal(9, position); + Assert.Equal(9, buffer[0]); // Tag + + // The next 8 bytes represent 123.456 in IEEE 754 double-precision format + Assert.Equal(0x77, buffer[1]); + Assert.Equal(0xBE, buffer[2]); + Assert.Equal(0x9F, buffer[3]); + Assert.Equal(0x1A, buffer[4]); + Assert.Equal(0x2F, buffer[5]); + Assert.Equal(0xDD, buffer[6]); + Assert.Equal(0x5E, buffer[7]); + Assert.Equal(0x40, buffer[8]); + } + + [Fact] + public void WriteSignedInt32_WritesCorrectly() + { + byte[] buffer = new byte[10]; + int position = ProtobufSerializer.WriteSInt32WithTag(buffer, 0, 1, 300); + Assert.Equal(3, position); + Assert.Equal(8, buffer[0]); // Tag + Assert.Equal(0xD8, buffer[1]); + Assert.Equal(0x04, buffer[2]); + + buffer = new byte[10]; + position = ProtobufSerializer.WriteSInt32WithTag(buffer, 0, 1, -300); + Assert.Equal(3, position); + Assert.Equal(8, buffer[0]); // Tag + Assert.Equal(0xD7, buffer[1]); + Assert.Equal(0x04, buffer[2]); + } + + [Fact] + public void WriteVarInt32_WritesCorrectly() + { + byte[] buffer = new byte[10]; + int position = ProtobufSerializer.WriteVarInt32(buffer, 0, 300); + Assert.Equal(2, position); + Assert.Equal(0xAC, buffer[0]); + Assert.Equal(0x02, buffer[1]); + } + + [Fact] + public void WriteVarInt32_MaxValue_WritesCorrectly() + { + byte[] buffer = new byte[10]; + int position = ProtobufSerializer.WriteVarInt32(buffer, 0, uint.MaxValue); + Assert.Equal(5, position); + Assert.Equal(0xFF, buffer[0]); + Assert.Equal(0xFF, buffer[1]); + Assert.Equal(0xFF, buffer[2]); + Assert.Equal(0xFF, buffer[3]); + Assert.Equal(0x0F, buffer[4]); + } + + [Fact] + public void WriteVarInt64_MaxValue_WritesCorrectly() + { + byte[] buffer = new byte[10]; + int position = ProtobufSerializer.WriteVarInt64(buffer, 0, ulong.MaxValue); + Assert.Equal(10, position); + for (int i = 0; i < 9; i++) + { + Assert.Equal(0xFF, buffer[i]); + } + + Assert.Equal(0x01, buffer[9]); + } + + [Fact] + public void WriteStringWithTag_EmptyString_WritesCorrectly() + { + byte[] buffer = new byte[10]; + int position = ProtobufSerializer.WriteStringWithTag(buffer, 0, 1, string.Empty); + Assert.Equal(2, position); + Assert.Equal(10, buffer[0]); // Tag + Assert.Equal(0, buffer[1]); // Length + } + + [Fact] + public void WriteStringWithTag_ASCIIString_WritesCorrectly() + { + byte[] buffer = new byte[20]; + int position = ProtobufSerializer.WriteStringWithTag(buffer, 0, 1, "Hello"); + Assert.Equal(7, position); + Assert.Equal(10, buffer[0]); // Tag + Assert.Equal(5, buffer[1]); // Length + + byte[] expectedContent = "Hello"u8.ToArray(); + byte[] actualContent = new byte[5]; + Array.Copy(buffer, 2, actualContent, 0, 5); + Assert.True(expectedContent.SequenceEqual(actualContent)); + } + + [Fact] + public void WriteStringWithTag_UnicodeString_WritesCorrectly() + { + byte[] buffer = new byte[20]; + string unicodeString = "\u3053\u3093\u306b\u3061\u306f"; // "Hello" in Japanese + int position = ProtobufSerializer.WriteStringWithTag(buffer, 0, 1, unicodeString); + Assert.Equal(17, position); + Assert.Equal(10, buffer[0]); // Tag + Assert.Equal(15, buffer[1]); // Length (3 bytes per character in UTF-8) + + byte[] expectedContent = Encoding.UTF8.GetBytes(unicodeString); + byte[] actualContent = new byte[15]; + Array.Copy(buffer, 2, actualContent, 0, 15); + Assert.True(expectedContent.SequenceEqual(actualContent)); + } + + [Fact] + public void WriteStringWithTag_LongString_WritesCorrectly() + { + string longString = new string('a', 1000); + byte[] buffer = new byte[1100]; + int position = ProtobufSerializer.WriteStringWithTag(buffer, 0, 1, longString); + Assert.Equal(1003, position); + Assert.Equal(10, buffer[0]); // Tag + Assert.Equal(0xE8, buffer[1]); // Length (1000 in varint encoding) + Assert.Equal(0x07, buffer[2]); + + byte[] expectedContent = Encoding.UTF8.GetBytes(longString); + byte[] actualContent = new byte[1000]; + Array.Copy(buffer, 3, actualContent, 0, 1000); + Assert.True(expectedContent.SequenceEqual(actualContent)); + } + + [Fact] + public void WriteStringWithTag_MixedEncodingString_WritesCorrectly() + { + byte[] buffer = new byte[30]; + string mixedString = "Hello\u4e16\u754c"; // "Hello World" with "World" in Chinese + int position = ProtobufSerializer.WriteStringWithTag(buffer, 0, 1, mixedString); + Assert.Equal(13, position); + Assert.Equal(10, buffer[0]); // Tag + Assert.Equal(11, buffer[1]); // Length (5 for "Hello" + 6 for Chinese "World" in UTF-8) + + byte[] expectedContent = Encoding.UTF8.GetBytes(mixedString); + byte[] actualContent = new byte[11]; + Array.Copy(buffer, 2, actualContent, 0, 11); + Assert.True(expectedContent.SequenceEqual(actualContent)); + } + + [Fact] + public void WriteStringWithTag_StringWithSpecialCharacters_WritesCorrectly() + { + byte[] buffer = new byte[30]; + string specialString = "Hello\n\t\"World\""; + int position = ProtobufSerializer.WriteStringWithTag(buffer, 0, 1, specialString); + Assert.Equal(16, position); + Assert.Equal(10, buffer[0]); // Tag + Assert.Equal(14, buffer[1]); // Length + + byte[] expectedContent = Encoding.UTF8.GetBytes(specialString); + byte[] actualContent = new byte[14]; + Array.Copy(buffer, 2, actualContent, 0, 14); + Assert.True(expectedContent.SequenceEqual(actualContent)); + } + + [Fact] + public void WriteStringWithTag_StringWithNullCharacters_WritesCorrectly() + { + byte[] buffer = new byte[20]; + string stringWithNull = "Hello\0World"; + int position = ProtobufSerializer.WriteStringWithTag(buffer, 0, 1, stringWithNull); + Assert.Equal(13, position); + Assert.Equal(10, buffer[0]); // Tag + Assert.Equal(11, buffer[1]); // Length + + byte[] expectedContent = Encoding.UTF8.GetBytes(stringWithNull); + byte[] actualContent = new byte[11]; + Array.Copy(buffer, 2, actualContent, 0, 11); + Assert.True(expectedContent.SequenceEqual(actualContent)); + } + + [Fact] + public void WriteStringWithTag_SurrogatePairs_WritesCorrectly() + { + byte[] buffer = new byte[20]; + string surrogatePairString = "\uD83D\uDCD6"; // Books emoji + int position = ProtobufSerializer.WriteStringWithTag(buffer, 0, 1, surrogatePairString); + Assert.Equal(6, position); + Assert.Equal(10, buffer[0]); // Tag + Assert.Equal(4, buffer[1]); // Length (4 bytes for the surrogate pair in UTF-8) + + byte[] expectedContent = Encoding.UTF8.GetBytes(surrogatePairString); + byte[] actualContent = new byte[4]; + Array.Copy(buffer, 2, actualContent, 0, 4); + Assert.True(expectedContent.SequenceEqual(actualContent)); + } +} diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/.gitignore b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/.gitignore index feada150421..b9e60522f66 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/.gitignore +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/.gitignore @@ -1,3 +1,9 @@ # Self-signed cert generated by integration test otel-collector.crt otel-collector.key +otel-client.crt +otel-client.key +otel-untrusted-collector.crt +otel-untrusted-collector.key +certs/* +certs diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/Dockerfile b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/Dockerfile index 691524a9d28..d878b3fa0b0 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/Dockerfile +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/Dockerfile @@ -2,18 +2,20 @@ # This should be run from the root of the repo: # docker build --file test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/Dockerfile -ARG BUILD_SDK_VERSION=8.0 -ARG TEST_SDK_VERSION=8.0 +ARG BUILD_SDK_VERSION=9.0 +ARG TEST_SDK_VERSION=9.0 +FROM mcr.microsoft.com/dotnet/sdk:8.0.414@sha256:3cef19377b2ef2a0171e930a24627677447c3e41b5f2eab84ff4895f1b15d254 AS dotnet-sdk-8.0 +FROM mcr.microsoft.com/dotnet/sdk:9.0.305@sha256:bb42ae2c058609d1746baf24fe6864ecab0686711dfca1f4b7a99e367ab17162 AS dotnet-sdk-9.0 -FROM mcr.microsoft.com/dotnet/sdk:${BUILD_SDK_VERSION} AS build +FROM dotnet-sdk-${BUILD_SDK_VERSION} AS build ARG PUBLISH_CONFIGURATION=Release -ARG PUBLISH_FRAMEWORK=net8.0 +ARG PUBLISH_FRAMEWORK=net9.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 -FROM mcr.microsoft.com/dotnet/sdk:${TEST_SDK_VERSION} AS final +FROM dotnet-sdk-${TEST_SDK_VERSION} AS final WORKDIR /test COPY --from=build /drop . diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs index 14b73571e35..d5d69cca093 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Diagnostics.Metrics; using System.Diagnostics.Tracing; +using System.Globalization; using Microsoft.Extensions.Logging; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; using OpenTelemetry.Logs; @@ -21,7 +22,7 @@ public sealed class IntegrationTests : IDisposable private const int ExportIntervalMilliseconds = 10000; private static readonly SdkLimitOptions DefaultSdkLimitOptions = new(); private static readonly ExperimentalOptions DefaultExperimentalOptions = new(); - private static readonly string CollectorHostname = SkipUnlessEnvVarFoundTheoryAttribute.GetEnvironmentVariable(CollectorHostnameEnvVarName); + private static readonly string? CollectorHostname = SkipUnlessEnvVarFoundTheoryAttribute.GetEnvironmentVariable(CollectorHostnameEnvVarName); private readonly OpenTelemetryEventListener openTelemetryEventListener; public IntegrationTests(ITestOutputHelper outputHelper) @@ -34,6 +35,7 @@ public void Dispose() this.openTelemetryEventListener.Dispose(); } +#pragma warning disable CS0618 // Suppressing gRPC obsolete warning [InlineData(OtlpExportProtocol.Grpc, ":4317", ExportProcessorType.Batch, false)] [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/traces", ExportProcessorType.Batch, false)] [InlineData(OtlpExportProtocol.Grpc, ":4317", ExportProcessorType.Batch, true)] @@ -44,6 +46,7 @@ public void Dispose() [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")] +#pragma warning restore CS0618 // Suppressing gRPC obsolete warning [Trait("CategoryName", "CollectorIntegrationTests")] [SkipUnlessEnvVarFoundTheory(CollectorHostnameEnvVarName)] public void TraceExportResultIsSuccess(OtlpExportProtocol protocol, string endpoint, ExportProcessorType exportProcessorType, bool forceFlush, string scheme = "http") @@ -61,7 +64,7 @@ public void TraceExportResultIsSuccess(OtlpExportProtocol protocol, string endpo }, }; - DelegatingExporter delegatingExporter = null; + DelegatingExporter? delegatingExporter = null; var exportResults = new List(); var activitySourceName = "otlp.collector.test"; @@ -118,6 +121,7 @@ public void TraceExportResultIsSuccess(OtlpExportProtocol protocol, string endpo } } +#pragma warning disable CS0618 // Suppressing gRPC obsolete warning [InlineData(OtlpExportProtocol.Grpc, ":4317", false, false)] [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/metrics", false, false)] [InlineData(OtlpExportProtocol.Grpc, ":4317", false, true)] @@ -128,6 +132,7 @@ public void TraceExportResultIsSuccess(OtlpExportProtocol protocol, string endpo [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/metrics", true, true)] [InlineData(OtlpExportProtocol.Grpc, ":5317", true, true, "https")] [InlineData(OtlpExportProtocol.HttpProtobuf, ":5318/v1/metrics", true, true, "https")] +#pragma warning restore CS0618 // Suppressing gRPC obsolete warning [Trait("CategoryName", "CollectorIntegrationTests")] [SkipUnlessEnvVarFoundTheory(CollectorHostnameEnvVarName)] public void MetricExportResultIsSuccess(OtlpExportProtocol protocol, string endpoint, bool useManualExport, bool forceFlush, string scheme = "http") @@ -140,7 +145,7 @@ public void MetricExportResultIsSuccess(OtlpExportProtocol protocol, string endp Protocol = protocol, }; - DelegatingExporter delegatingExporter = null; + DelegatingExporter? delegatingExporter = null; var exportResults = new List(); var meterName = "otlp.collector.test"; @@ -202,12 +207,14 @@ public void MetricExportResultIsSuccess(OtlpExportProtocol protocol, string endp } } +#pragma warning disable CS0618 // Suppressing gRPC obsolete warning [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")] +#pragma warning restore CS0618 // Suppressing gRPC obsolete warning [Trait("CategoryName", "CollectorIntegrationTests")] [SkipUnlessEnvVarFoundTheory(CollectorHostnameEnvVarName)] public void LogExportResultIsSuccess(OtlpExportProtocol protocol, string endpoint, ExportProcessorType exportProcessorType, string scheme = "http") @@ -220,7 +227,7 @@ public void LogExportResultIsSuccess(OtlpExportProtocol protocol, string endpoin Protocol = protocol, }; - DelegatingExporter delegatingExporter = null; + DelegatingExporter delegatingExporter; var exportResults = new List(); var processorOptions = new LogRecordExportProcessorOptions { @@ -259,7 +266,7 @@ public void LogExportResultIsSuccess(OtlpExportProtocol protocol, string endpoin }); var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); - logger.LogInformation("Hello from {name} {price}.", "tomato", 2.99); + logger.HelloFrom("tomato", 2.99); switch (processorOptions.ExportProcessorType) { @@ -298,10 +305,10 @@ protected override void OnEventSourceCreated(EventSource eventSource) protected override void OnEventWritten(EventWrittenEventArgs eventData) { - string message; - if (eventData.Message != null && (eventData.Payload?.Count ?? 0) > 0) + string? message; + if (eventData.Message != null && eventData.Payload != null && eventData.Payload.Count > 0) { - message = string.Format(eventData.Message, eventData.Payload.ToArray()); + message = string.Format(CultureInfo.InvariantCulture, eventData.Message, eventData.Payload.ToArray()); } else { diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/create-cert.sh b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/create-cert.sh deleted file mode 100755 index c0821abc468..00000000000 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/create-cert.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -set -e - -# 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/IntegrationTest/docker-compose.yml b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/docker-compose.yml index b6317ff5181..d6bc9f4aefc 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/docker-compose.yml +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/docker-compose.yml @@ -2,24 +2,32 @@ # This should be run from the root of the repo: # 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 + init-service: + image: otel-test-image + build: + context: . + dockerfile: ./test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/Dockerfile volumes: - ./test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest:/cfg - command: /cfg/create-cert.sh + command: > + sh -c " + mkdir -p /cfg/certs; + cp /test/*.pem /cfg/certs/; + chmod 644 /cfg/certs/*; + sleep 1000; + " otel-collector: - image: otel/opentelemetry-collector + image: otel/opentelemetry-collector:0.136.0@sha256:98fd3b410ae8a939be9588f1580c4b7c3da6ebba49f5363df4259a827aabb779 volumes: - ./test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest:/cfg command: --config=/cfg/otel-collector-config.yaml depends_on: - - create-cert + - init-service tests: + image: otel-test-image build: context: . dockerfile: ./test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/Dockerfile @@ -27,7 +35,7 @@ services: - ./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 + OTEL_COLLECTOR_HOSTNAME: otel-collector + OTEL_MOCK_COLLECTOR_HOSTNAME: mock-otel-collector depends_on: - otel-collector diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/otel-collector-config.yaml b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/otel-collector-config.yaml index f479ebe4ad8..9a71c67e4d6 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/otel-collector-config.yaml +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/otel-collector-config.yaml @@ -16,13 +16,39 @@ receivers: grpc: endpoint: 0.0.0.0:5317 tls: - cert_file: /cfg/otel-collector.crt - key_file: /cfg/otel-collector.key + cert_file: /cfg/certs/otel-test-server-cert.pem + key_file: /cfg/certs/otel-test-server-key.pem http: endpoint: 0.0.0.0:5318 tls: - cert_file: /cfg/otel-collector.crt - key_file: /cfg/otel-collector.key + cert_file: /cfg/certs/otel-test-server-cert.pem + key_file: /cfg/certs/otel-test-server-key.pem + otlp/untrustedtls: + protocols: + grpc: + endpoint: 0.0.0.0:6317 + tls: + cert_file: /cfg/certs/otel-untrusted-collector-cert.pem + key_file: /cfg/certs/otel-untrusted-collector-key.pem + http: + endpoint: 0.0.0.0:6318 + tls: + cert_file: /cfg/certs/otel-untrusted-collector-cert.pem + key_file: /cfg/certs/otel-untrusted-collector-key.pem + otlp/mtls: + protocols: + grpc: + endpoint: 0.0.0.0:7317 + tls: + cert_file: /cfg/certs/otel-test-server-cert.pem + key_file: /cfg/certs/otel-test-server-key.pem + client_ca_file: /cfg/certs/otel-test-ca-cert.pem + http: + endpoint: 0.0.0.0:7318 + tls: + cert_file: /cfg/certs/otel-test-server-cert.pem + key_file: /cfg/certs/otel-test-server-key.pem + client_ca_file: /cfg/certs/otel-test-ca-cert.pem exporters: debug: @@ -31,11 +57,11 @@ exporters: service: pipelines: traces: - receivers: [otlp, otlp/tls] + receivers: [otlp, otlp/tls, otlp/untrustedtls, otlp/mtls] exporters: [debug] metrics: - receivers: [otlp, otlp/tls] + receivers: [otlp, otlp/tls, otlp/untrustedtls, otlp/mtls] exporters: [debug] logs: - receivers: [otlp, otlp/tls] + receivers: [otlp, otlp/tls, otlp/untrustedtls, otlp/mtls] exporters: [debug] diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/run-test.sh b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/run-test.sh index d88a7f1aa5f..fbe9f076a95 100755 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/run-test.sh +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/run-test.sh @@ -1,8 +1,8 @@ #!/bin/bash set -e -# Trust the self-signed certificated used by the collector -cp /cfg/otel-collector.crt /usr/local/share/ca-certificates/ +# Trust the self-signed certificate used by the collector +cp /cfg/certs/otel-test-ca-cert.pem /usr/local/share/ca-certificates/otel-test-ca-cert.crt 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/LoggerExtensions.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/LoggerExtensions.cs new file mode 100644 index 00000000000..70834b83810 --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/LoggerExtensions.cs @@ -0,0 +1,48 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.Logging; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +internal static partial class LoggerExtensions +{ + [LoggerMessage(LogLevel.Information, "Hello from {Name} {Price}.")] + public static partial void HelloFrom(this ILogger logger, string name, double price); + + [LoggerMessage("Hello from {Name} {Price}.")] + public static partial void HelloFrom(this ILogger logger, LogLevel logLevel, string name, double price); + + [LoggerMessage(LogLevel.Information, EventId = 10, Message = "Hello from {Name} {Price}.")] + public static partial void HelloFromWithEventId(this ILogger logger, string name, double price); + + [LoggerMessage(LogLevel.Information, EventId = 10, EventName = "MyEvent10", Message = "Hello from {Name} {Price}.")] + public static partial void HelloFromWithEventIdAndEventName(this ILogger logger, string name, double price); + + [LoggerMessage(LogLevel.Information, "Log message")] + public static partial void LogMessage(this ILogger logger); + + [LoggerMessage(LogLevel.Information, "Log when there is no activity.")] + public static partial void LogWhenThereIsNoActivity(this ILogger logger); + + [LoggerMessage(LogLevel.Information, "Log within an activity.")] + public static partial void LogWithinAnActivity(this ILogger logger); + + [LoggerMessage(LogLevel.Information, "OpenTelemetry {Greeting} {Subject}!")] + public static partial void OpenTelemetryGreeting(this ILogger logger, string greeting, string subject); + + [LoggerMessage(LogLevel.Information, "OpenTelemetry {AttributeOne} {AttributeTwo} {AttributeThree}!")] + public static partial void OpenTelemetryWithAttributes(this ILogger logger, string attributeOne, string attributeTwo, string attributeThree); + + [LoggerMessage(LogLevel.Information, "Some log information message.")] + public static partial void SomeLogInformation(this ILogger logger); + + [LoggerMessage(LogLevel.Information, "Hello from red-tomato")] + public static partial void HelloFromRedTomato(this ILogger logger); + + [LoggerMessage(LogLevel.Information, "Hello from green-tomato")] + public static partial void HelloFromGreenTomato(this ILogger logger); + + [LoggerMessage(LogLevel.Information, "Exception Occurred")] + public static partial void ExceptionOccured(this ILogger logger, Exception exception); +} diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/MockCollectorIntegrationTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/MockCollectorIntegrationTests.cs index a5ce7a0a279..0f810a25d34 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/MockCollectorIntegrationTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/MockCollectorIntegrationTests.cs @@ -3,8 +3,9 @@ #if !NETFRAMEWORK using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Net; -using Google.Protobuf; using Grpc.Core; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -16,7 +17,6 @@ using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; -using OpenTelemetry.Metrics; using OpenTelemetry.PersistentStorage.Abstractions; using OpenTelemetry.Proto.Collector.Trace.V1; using OpenTelemetry.Tests; @@ -59,7 +59,7 @@ public async Task TestRecoveryAfterFailedExport() "/MockCollector/SetResponseCodes/{responseCodesCsv}", (MockCollectorState collectorState, string responseCodesCsv) => { - var codes = responseCodesCsv.Split(",").Select(x => int.Parse(x)).ToArray(); + var codes = responseCodesCsv.Split(",").Select(x => int.Parse(x, CultureInfo.InvariantCulture)).ToArray(); collectorState.SetStatusCodes(codes); }); @@ -71,13 +71,15 @@ public async Task TestRecoveryAfterFailedExport() using var httpClient = new HttpClient() { BaseAddress = new Uri($"http://localhost:{testHttpPort}") }; var codes = new[] { Grpc.Core.StatusCode.Unimplemented, Grpc.Core.StatusCode.OK }; - await httpClient.GetAsync($"/MockCollector/SetResponseCodes/{string.Join(",", codes.Select(x => (int)x))}"); + await httpClient.GetAsync(new Uri($"/MockCollector/SetResponseCodes/{string.Join(",", codes.Select(x => (int)x))}", UriKind.Relative)); var exportResults = new List(); - var otlpExporter = new OtlpTraceExporter(new OtlpExporterOptions() { Endpoint = new Uri($"http://localhost:{testGrpcPort}") }); + using var otlpExporter = new OtlpTraceExporter(new OtlpExporterOptions() { Endpoint = new Uri($"http://localhost:{testGrpcPort}") }); +#pragma warning disable CA2000 // Dispose objects before losing scope var delegatingExporter = new DelegatingExporter +#pragma warning disable CA2000 // Dispose objects before losing scope { - OnExportFunc = (batch) => + OnExportFunc = batch => { var result = otlpExporter.Export(batch); exportResults.Add(result); @@ -89,17 +91,19 @@ public async Task TestRecoveryAfterFailedExport() using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(activitySourceName) +#pragma warning disable CA2000 // Dispose objects before losing scope .AddProcessor(new SimpleActivityExportProcessor(delegatingExporter)) +#pragma warning restore CA2000 // Dispose objects before losing scope .Build(); using var source = new ActivitySource(activitySourceName); - source.StartActivity().Stop(); + source.StartActivity()?.Stop(); Assert.Single(exportResults); Assert.Equal(ExportResult.Failure, exportResults[0]); - source.StartActivity().Stop(); + source.StartActivity()?.Stop(); Assert.Equal(2, exportResults.Count); Assert.Equal(ExportResult.Success, exportResults[1]); @@ -159,7 +163,7 @@ public async Task GrpcRetryTests(bool useRetryTransmissionHandler, ExportResult "/MockCollector/SetResponseCodes/{responseCodesCsv}", (MockCollectorState collectorState, string responseCodesCsv) => { - var codes = responseCodesCsv.Split(",").Select(x => int.Parse(x)).ToArray(); + var codes = responseCodesCsv.Split(",").Select(x => int.Parse(x, CultureInfo.InvariantCulture)).ToArray(); collectorState.SetStatusCodes(codes); }); @@ -172,17 +176,20 @@ public async Task GrpcRetryTests(bool useRetryTransmissionHandler, ExportResult // First reply with failure and then Ok var codes = new[] { initialStatusCode, Grpc.Core.StatusCode.OK }; - await httpClient.GetAsync($"/MockCollector/SetResponseCodes/{string.Join(",", codes.Select(x => (int)x))}"); + await httpClient.GetAsync(new Uri($"/MockCollector/SetResponseCodes/{string.Join(",", codes.Select(x => (int)x))}", UriKind.Relative)); var endpoint = new Uri($"http://localhost:{testGrpcPort}"); var exporterOptions = new OtlpExporterOptions() { Endpoint = endpoint, TimeoutMilliseconds = 20000, Protocol = OtlpExportProtocol.Grpc }; var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { [ExperimentalOptions.OtlpRetryEnvVar] = useRetryTransmissionHandler ? "in_memory" : null }) - .Build(); + .AddInMemoryCollection(new Dictionary + { + [ExperimentalOptions.OtlpRetryEnvVar] = useRetryTransmissionHandler ? "in_memory" : null, + }) + .Build(); - var otlpExporter = new OtlpTraceExporter(exporterOptions, new SdkLimitOptions(), new ExperimentalOptions(configuration)); + using var otlpExporter = new OtlpTraceExporter(exporterOptions, new SdkLimitOptions(), new ExperimentalOptions(configuration)); var activitySourceName = "otel.grpc.retry.test"; using var source = new ActivitySource(activitySourceName); @@ -192,6 +199,7 @@ public async Task GrpcRetryTests(bool useRetryTransmissionHandler, ExportResult .Build(); using var activity = source.StartActivity("GrpcRetryTest"); + Assert.NotNull(activity); activity.Stop(); using var batch = new Batch([activity], 1); @@ -238,7 +246,7 @@ public async Task HttpRetryTests(bool useRetryTransmissionHandler, ExportResult "/MockCollector/SetResponseCodes/{responseCodesCsv}", (MockCollectorHttpState collectorState, string responseCodesCsv) => { - var codes = responseCodesCsv.Split(",").Select(x => int.Parse(x)).ToArray(); + var codes = responseCodesCsv.Split(",").Select(x => int.Parse(x, CultureInfo.InvariantCulture)).ToArray(); collectorState.SetStatusCodes(codes); }); @@ -256,17 +264,20 @@ public async Task HttpRetryTests(bool useRetryTransmissionHandler, ExportResult using var httpClient = new HttpClient() { BaseAddress = new Uri($"http://localhost:{testHttpPort}") }; var codes = new[] { initialHttpStatusCode, HttpStatusCode.OK }; - await httpClient.GetAsync($"/MockCollector/SetResponseCodes/{string.Join(",", codes.Select(x => (int)x))}"); + await httpClient.GetAsync(new Uri($"/MockCollector/SetResponseCodes/{string.Join(",", codes.Select(x => (int)x))}", UriKind.Relative)); var endpoint = new Uri($"http://localhost:{testHttpPort}/v1/traces"); var exporterOptions = new OtlpExporterOptions() { Endpoint = endpoint, TimeoutMilliseconds = 20000, Protocol = OtlpExportProtocol.HttpProtobuf }; var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { [ExperimentalOptions.OtlpRetryEnvVar] = useRetryTransmissionHandler ? "in_memory" : null }) - .Build(); + .AddInMemoryCollection(new Dictionary + { + [ExperimentalOptions.OtlpRetryEnvVar] = useRetryTransmissionHandler ? "in_memory" : null, + }) + .Build(); - var otlpExporter = new OtlpTraceExporter(exporterOptions, new SdkLimitOptions(), new ExperimentalOptions(configuration)); + using var otlpExporter = new OtlpTraceExporter(exporterOptions, new SdkLimitOptions(), new ExperimentalOptions(configuration)); var activitySourceName = "otel.http.retry.test"; using var source = new ActivitySource(activitySourceName); @@ -276,6 +287,7 @@ public async Task HttpRetryTests(bool useRetryTransmissionHandler, ExportResult .Build(); using var activity = source.StartActivity("HttpRetryTest"); + Assert.NotNull(activity); activity.Stop(); using var batch = new Batch([activity], 1); @@ -320,7 +332,7 @@ public async Task HttpPersistentStorageRetryTests(bool usePersistentStorageTrans "/MockCollector/SetResponseCodes/{responseCodesCsv}", (MockCollectorHttpState collectorState, string responseCodesCsv) => { - var codes = responseCodesCsv.Split(",").Select(x => int.Parse(x)).ToArray(); + var codes = responseCodesCsv.Split(",").Select(x => int.Parse(x, CultureInfo.InvariantCulture)).ToArray(); collectorState.SetStatusCodes(codes); }); @@ -338,37 +350,32 @@ public async Task HttpPersistentStorageRetryTests(bool usePersistentStorageTrans using var httpClient = new HttpClient() { BaseAddress = new Uri($"http://localhost:{testHttpPort}") }; var codes = new[] { initialHttpStatusCode, HttpStatusCode.OK }; - await httpClient.GetAsync($"/MockCollector/SetResponseCodes/{string.Join(",", codes.Select(x => (int)x))}"); + await httpClient.GetAsync(new Uri($"/MockCollector/SetResponseCodes/{string.Join(",", codes.Select(x => (int)x))}", UriKind.Relative)); var endpoint = new Uri($"http://localhost:{testHttpPort}/v1/traces"); var exporterOptions = new OtlpExporterOptions() { Endpoint = endpoint, TimeoutMilliseconds = 20000 }; - var exportClient = new OtlpHttpTraceExportClient(exporterOptions, new HttpClient()); + using var exporterHttpClient = new HttpClient(); + var exportClient = new OtlpHttpExportClient(exporterOptions, exporterHttpClient, "/v1/traces"); // TODO: update this to configure via experimental environment variable. - OtlpExporterTransmissionHandler transmissionHandler; - MockFileProvider mockProvider = null; + OtlpExporterTransmissionHandler transmissionHandler; + MockFileProvider? mockProvider = null; if (usePersistentStorageTransmissionHandler) { mockProvider = new MockFileProvider(); - transmissionHandler = new OtlpExporterPersistentStorageTransmissionHandler( + transmissionHandler = new OtlpExporterPersistentStorageTransmissionHandler( mockProvider, exportClient, - exporterOptions.TimeoutMilliseconds, - (byte[] data) => - { - var request = new ExportTraceServiceRequest(); - request.MergeFrom(data); - return request; - }); + exporterOptions.TimeoutMilliseconds); } else { - transmissionHandler = new OtlpExporterTransmissionHandler(exportClient, exporterOptions.TimeoutMilliseconds); + transmissionHandler = new OtlpExporterTransmissionHandler(exportClient, exporterOptions.TimeoutMilliseconds); } - var otlpExporter = new OtlpTraceExporter(exporterOptions, new(), new(), transmissionHandler); + using var otlpExporter = new OtlpTraceExporter(exporterOptions, new(), new(), transmissionHandler); var activitySourceName = "otel.http.persistent.storage.retry.test"; using var source = new ActivitySource(activitySourceName); @@ -378,6 +385,7 @@ public async Task HttpPersistentStorageRetryTests(bool usePersistentStorageTrans .Build(); using var activity = source.StartActivity("HttpPersistentStorageRetryTest"); + Assert.NotNull(activity); activity.Stop(); using var batch = new Batch([activity], 1); @@ -387,12 +395,13 @@ public async Task HttpPersistentStorageRetryTests(bool usePersistentStorageTrans if (usePersistentStorageTransmissionHandler) { + Assert.NotNull(mockProvider); if (exportResult == ExportResult.Success) { Assert.Single(mockProvider.TryGetBlobs()); // Force Retry - Assert.True((transmissionHandler as OtlpExporterPersistentStorageTransmissionHandler).InitiateAndWaitForRetryProcess(-1)); + Assert.True((transmissionHandler as OtlpExporterPersistentStorageTransmissionHandler)?.InitiateAndWaitForRetryProcess(-1)); Assert.False(mockProvider.TryGetBlob(out _)); } @@ -463,7 +472,7 @@ public async Task GrpcPersistentStorageRetryTests(bool usePersistentStorageTrans "/MockCollector/SetResponseCodes/{responseCodesCsv}", (MockCollectorState collectorState, string responseCodesCsv) => { - var codes = responseCodesCsv.Split(",").Select(x => int.Parse(x)).ToArray(); + var codes = responseCodesCsv.Split(",").Select(x => int.Parse(x, CultureInfo.InvariantCulture)).ToArray(); collectorState.SetStatusCodes(codes); }); @@ -475,37 +484,32 @@ public async Task GrpcPersistentStorageRetryTests(bool usePersistentStorageTrans using var httpClient = new HttpClient() { BaseAddress = new Uri($"http://localhost:{testHttpPort}") }; var codes = new[] { initialgrpcStatusCode, Grpc.Core.StatusCode.OK }; - await httpClient.GetAsync($"/MockCollector/SetResponseCodes/{string.Join(",", codes.Select(x => (int)x))}"); + await httpClient.GetAsync(new Uri($"/MockCollector/SetResponseCodes/{string.Join(",", codes.Select(x => (int)x))}", UriKind.Relative)); var endpoint = new Uri($"http://localhost:{testGrpcPort}"); var exporterOptions = new OtlpExporterOptions() { Endpoint = endpoint, TimeoutMilliseconds = 20000 }; - var exportClient = new OtlpGrpcTraceExportClient(exporterOptions); + using var exporterHttpClient = new HttpClient(); + var exportClient = new OtlpGrpcExportClient(exporterOptions, exporterHttpClient, "opentelemetry.proto.collector.trace.v1.TraceService/Export"); // TODO: update this to configure via experimental environment variable. - OtlpExporterTransmissionHandler transmissionHandler; - MockFileProvider mockProvider = null; + OtlpExporterTransmissionHandler transmissionHandler; + MockFileProvider? mockProvider = null; if (usePersistentStorageTransmissionHandler) { mockProvider = new MockFileProvider(); - transmissionHandler = new OtlpExporterPersistentStorageTransmissionHandler( + transmissionHandler = new OtlpExporterPersistentStorageTransmissionHandler( mockProvider, exportClient, - exporterOptions.TimeoutMilliseconds, - (byte[] data) => - { - var request = new ExportTraceServiceRequest(); - request.MergeFrom(data); - return request; - }); + exporterOptions.TimeoutMilliseconds); } else { - transmissionHandler = new OtlpExporterTransmissionHandler(exportClient, exporterOptions.TimeoutMilliseconds); + transmissionHandler = new OtlpExporterTransmissionHandler(exportClient, exporterOptions.TimeoutMilliseconds); } - var otlpExporter = new OtlpTraceExporter(exporterOptions, new(), new(), transmissionHandler); + using var otlpExporter = new OtlpTraceExporter(exporterOptions, new(), new(), transmissionHandler); var activitySourceName = "otel.grpc.persistent.storage.retry.test"; using var source = new ActivitySource(activitySourceName); @@ -515,6 +519,7 @@ public async Task GrpcPersistentStorageRetryTests(bool usePersistentStorageTrans .Build(); using var activity = source.StartActivity("GrpcPersistentStorageRetryTest"); + Assert.NotNull(activity); activity.Stop(); using var batch = new Batch([activity], 1); @@ -524,12 +529,13 @@ public async Task GrpcPersistentStorageRetryTests(bool usePersistentStorageTrans if (usePersistentStorageTransmissionHandler) { + Assert.NotNull(mockProvider); if (exportResult == ExportResult.Success) { Assert.Single(mockProvider.TryGetBlobs()); // Force Retry - Assert.True((transmissionHandler as OtlpExporterPersistentStorageTransmissionHandler).InitiateAndWaitForRetryProcess(-1)); + Assert.True((transmissionHandler as OtlpExporterPersistentStorageTransmissionHandler)?.InitiateAndWaitForRetryProcess(-1)); Assert.False(mockProvider.TryGetBlob(out _)); } @@ -548,10 +554,10 @@ public async Task GrpcPersistentStorageRetryTests(bool usePersistentStorageTrans transmissionHandler.Dispose(); } - private class MockCollectorState + private sealed class MockCollectorState { - private Grpc.Core.StatusCode[] statusCodes = { }; - private int statusCodeIndex = 0; + private Grpc.Core.StatusCode[] statusCodes = []; + private int statusCodeIndex; public void SetStatusCodes(int[] statusCodes) { @@ -567,10 +573,10 @@ public Grpc.Core.StatusCode NextStatus() } } - private class MockCollectorHttpState + private sealed class MockCollectorHttpState { - private HttpStatusCode[] statusCodes = { }; - private int statusCodeIndex = 0; + private HttpStatusCode[] statusCodes = []; + private int statusCodeIndex; public void SetStatusCodes(int[] statusCodes) { @@ -586,7 +592,9 @@ public HttpStatusCode NextStatus() } } - private class MockTraceService : TraceService.TraceServiceBase +#pragma warning disable CA1812 // Avoid uninstantiated internal classes + private sealed class MockTraceService : TraceService.TraceServiceBase +#pragma warning restore CA1812 // Avoid uninstantiated internal classes { private readonly MockCollectorState state; @@ -607,9 +615,9 @@ public override Task Export(ExportTraceServiceReques } } - private class MockFileProvider : PersistentBlobProvider + private sealed class MockFileProvider : PersistentBlobProvider { - private readonly List mockStorage = new(); + private readonly List mockStorage = []; public IEnumerable TryGetBlobs() => this.mockStorage.AsEnumerable(); @@ -630,7 +638,7 @@ protected override bool OnTryCreateBlob(byte[] buffer, out PersistentBlob blob) return blob.TryWrite(buffer); } - protected override bool OnTryGetBlob(out PersistentBlob blob) + protected override bool OnTryGetBlob([NotNullWhen(true)] out PersistentBlob? blob) { blob = this.GetBlobs().FirstOrDefault(); @@ -638,11 +646,11 @@ protected override bool OnTryGetBlob(out PersistentBlob blob) } } - private class MockFileBlob : PersistentBlob + private sealed class MockFileBlob : PersistentBlob { private readonly List mockStorage; - private byte[] buffer = Array.Empty(); + private byte[] buffer = []; public MockFileBlob(List mockStorage) { 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 ad60a2385a2..5541cabe5c6 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.csproj +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.csproj @@ -1,21 +1,39 @@ - + $(TargetFrameworksForTests) - - disable + + false + + + + + + + + + + + + + + + + + - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - @@ -28,10 +46,15 @@ - + + + $(RepoRoot)\src\Shared\Proto + + + diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpAttributeTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpAttributeTests.cs index af31f32c795..e260a4544d9 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpAttributeTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpAttributeTests.cs @@ -1,8 +1,9 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using Google.Protobuf.Collections; -using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; using Xunit; using OtlpCommon = OpenTelemetry.Proto.Common.V1; @@ -13,19 +14,25 @@ public class OtlpAttributeTests [Fact] public void NullValueAttribute() { - var kvp = new KeyValuePair("key", null); - Assert.False(TryTransformTag(kvp, out var _)); + var kvp = new KeyValuePair("key", null); + Assert.True(TryTransformTag(kvp, out var attribute)); + Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.None, attribute.Value.ValueCase); + Assert.False(attribute.Value.HasBoolValue); + Assert.False(attribute.Value.HasBytesValue); + Assert.False(attribute.Value.HasDoubleValue); + Assert.False(attribute.Value.HasIntValue); + Assert.False(attribute.Value.HasStringValue); } [Fact] public void EmptyArrays() { - var kvp = new KeyValuePair("key", Array.Empty()); + var kvp = new KeyValuePair("key", Array.Empty()); Assert.True(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()); + kvp = new KeyValuePair("key", Array.Empty()); Assert.True(TryTransformTag(kvp, out attribute)); Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.ArrayValue, attribute.Value.ValueCase); Assert.Empty(attribute.Value.ArrayValue.Values); @@ -48,24 +55,33 @@ public void EmptyArrays() [InlineData(new long[] { 1, 2, 3 })] public void IntegralTypesSupported(object value) { - var kvp = new KeyValuePair("key", value); + var kvp = new KeyValuePair("key", value); Assert.True(TryTransformTag(kvp, out var attribute)); 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++) + if (value.GetType() == typeof(byte[])) + { + Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.BytesValue, attribute.Value.ValueCase); + Assert.Equal((byte[])value, attribute.Value.BytesValue.ToByteArray()); + } + else { - expectedArray[i] = Convert.ToInt64(array.GetValue(i)); + 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), CultureInfo.InvariantCulture); + } + + Assert.Equal(expectedArray, attribute.Value.ArrayValue.Values.Select(x => x.IntValue)); } - 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); + Assert.Equal(Convert.ToInt64(value, CultureInfo.InvariantCulture), attribute.Value.IntValue); break; } } @@ -77,7 +93,7 @@ public void IntegralTypesSupported(object value) [InlineData(new double[] { 1, 2, 3 })] public void FloatingPointTypesSupported(object value) { - var kvp = new KeyValuePair("key", value); + var kvp = new KeyValuePair("key", value); Assert.True(TryTransformTag(kvp, out var attribute)); switch (value) @@ -87,14 +103,14 @@ public void FloatingPointTypesSupported(object value) var expectedArray = new double[array.Length]; for (var i = 0; i < array.Length; i++) { - expectedArray[i] = Convert.ToDouble(array.GetValue(i)); + expectedArray[i] = Convert.ToDouble(array.GetValue(i), CultureInfo.InvariantCulture); } 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); + Assert.Equal(Convert.ToDouble(value, CultureInfo.InvariantCulture), attribute.Value.DoubleValue); break; } } @@ -104,7 +120,7 @@ public void FloatingPointTypesSupported(object value) [InlineData(new bool[] { true, false, true })] public void BooleanTypeSupported(object value) { - var kvp = new KeyValuePair("key", value); + var kvp = new KeyValuePair("key", value); Assert.True(TryTransformTag(kvp, out var attribute)); switch (value) @@ -114,14 +130,14 @@ public void BooleanTypeSupported(object value) var expectedArray = new bool[array.Length]; for (var i = 0; i < array.Length; i++) { - expectedArray[i] = Convert.ToBoolean(array.GetValue(i)); + expectedArray[i] = Convert.ToBoolean(array.GetValue(i), CultureInfo.InvariantCulture); } 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); + Assert.Equal(Convert.ToBoolean(value, CultureInfo.InvariantCulture), attribute.Value.BoolValue); break; } } @@ -131,19 +147,19 @@ public void BooleanTypeSupported(object value) [InlineData("string")] public void StringTypesSupported(object value) { - var kvp = new KeyValuePair("key", value); + var kvp = new KeyValuePair("key", value); Assert.True(TryTransformTag(kvp, out var attribute)); Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.StringValue, attribute.Value.ValueCase); - Assert.Equal(Convert.ToString(value), attribute.Value.StringValue); + Assert.Equal(Convert.ToString(value, CultureInfo.InvariantCulture), attribute.Value.StringValue); } [Fact] public void ObjectArrayTypesSupported() { var obj = new object(); - var objectArray = new object[] { null, "a", 'b', true, int.MaxValue, long.MaxValue, float.MaxValue, double.MaxValue, obj }; + var objectArray = new object?[] { null, "a", 'b', true, int.MaxValue, long.MaxValue, float.MaxValue, double.MaxValue, obj }; - var kvp = new KeyValuePair("key", objectArray); + var kvp = new KeyValuePair("key", objectArray); Assert.True(TryTransformTag(kvp, out var attribute)); Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.ArrayValue, attribute.Value.ValueCase); @@ -179,14 +195,14 @@ public void ObjectArrayTypesSupported() public void StringArrayTypesSupported() { var charArray = new char[] { 'a', 'b', 'c' }; - var stringArray = new string[] { "a", "b", "c", string.Empty, null }; + var stringArray = new string?[] { "a", "b", "c", string.Empty, null }; - var kvp = new KeyValuePair("key", charArray); + var kvp = new KeyValuePair("key", charArray); Assert.True(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); + kvp = new KeyValuePair("key", stringArray); Assert.True(TryTransformTag(kvp, out attribute)); Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.ArrayValue, attribute.Value.ValueCase); @@ -213,7 +229,7 @@ public void ToStringIsCalledForAllOtherTypes() (nint)int.MaxValue, (nuint)uint.MaxValue, decimal.MaxValue, - new object(), + new(), }; var testArrayValues = new object[] @@ -221,12 +237,12 @@ public void ToStringIsCalledForAllOtherTypes() new nint[] { 1, 2, 3 }, new nuint[] { 1, 2, 3 }, new decimal[] { 1, 2, 3 }, - new object[] { new object[3], new object(), null }, + new object?[] { new object[3], new(), null }, }; foreach (var value in testValues) { - var kvp = new KeyValuePair("key", value); + var kvp = new KeyValuePair("key", value); Assert.True(TryTransformTag(kvp, out var attribute)); Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.StringValue, attribute.Value.ValueCase); Assert.Equal(value.ToString(), attribute.Value.StringValue); @@ -234,11 +250,12 @@ public void ToStringIsCalledForAllOtherTypes() foreach (var value in testArrayValues) { - var kvp = new KeyValuePair("key", value); + var kvp = new KeyValuePair("key", value); Assert.True(TryTransformTag(kvp, out var attribute)); Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.ArrayValue, attribute.Value.ValueCase); var array = value as Array; + Assert.NotNull(array); for (var i = 0; i < attribute.Value.ArrayValue.Values.Count; ++i) { var expectedValue = array.GetValue(i)?.ToString(); @@ -249,7 +266,7 @@ public void ToStringIsCalledForAllOtherTypes() 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); } } } @@ -258,21 +275,31 @@ public void ToStringIsCalledForAllOtherTypes() [Fact] public void ExceptionInToStringIsCaught() { - var kvp = new KeyValuePair("key", new MyToStringMethodThrowsAnException()); - Assert.False(TryTransformTag(kvp, out var _)); + var kvp = new KeyValuePair("key", new MyToStringMethodThrowsAnException()); + Assert.False(TryTransformTag(kvp, out _)); - kvp = new KeyValuePair("key", new object[] { 1, false, new MyToStringMethodThrowsAnException() }); - Assert.False(TryTransformTag(kvp, out var _)); + kvp = new KeyValuePair("key", new object[] { 1, false, new MyToStringMethodThrowsAnException() }); + Assert.False(TryTransformTag(kvp, out _)); } - private static bool TryTransformTag(KeyValuePair tag, out OtlpCommon.KeyValue attribute) + private static bool TryTransformTag(KeyValuePair tag, [NotNullWhen(true)] out OtlpCommon.KeyValue? attribute) { - var destination = new RepeatedField(); + ProtobufOtlpTagWriter.OtlpTagWriterState otlpTagWriterState = new ProtobufOtlpTagWriter.OtlpTagWriterState + { + Buffer = new byte[1024], + WritePosition = 0, + }; - if (OtlpTagWriter.Instance.TryWriteTag(ref destination, tag)) + if (ProtobufOtlpTagWriter.Instance.TryWriteTag(ref otlpTagWriterState, tag)) { - Assert.NotEmpty(destination); - attribute = destination[0]; + // Deserialize the ResourceSpans and validate the attributes. + using (var stream = new MemoryStream(otlpTagWriterState.Buffer, 0, otlpTagWriterState.WritePosition)) + { + var keyValue = OtlpCommon.KeyValue.Parser.ParseFrom(stream); + Assert.NotNull(keyValue); + attribute = keyValue; + } + return true; } @@ -280,11 +307,11 @@ private static bool TryTransformTag(KeyValuePair tag, out OtlpCo return false; } - private class MyToStringMethodThrowsAnException + private sealed class MyToStringMethodThrowsAnException { public override string ToString() { - throw new Exception("Nope."); + throw new InvalidOperationException("Nope."); } } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExportProtocolParserTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExportProtocolParserTests.cs index b4b2917491b..c2fdc81eb2e 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExportProtocolParserTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExportProtocolParserTests.cs @@ -8,7 +8,9 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; public class OtlpExportProtocolParserTests { [Theory] +#pragma warning disable CS0618 // Suppressing gRPC obsolete warning [InlineData("grpc", true, OtlpExportProtocol.Grpc)] +#pragma warning restore CS0618 // Suppressing gRPC obsolete warning [InlineData("http/protobuf", true, OtlpExportProtocol.HttpProtobuf)] [InlineData("unsupported", false, default(OtlpExportProtocol))] public void TryParse_Protocol_MapsToCorrectValue(string protocol, bool expectedResult, OtlpExportProtocol expectedExportProtocol) diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterHelperExtensionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterHelperExtensionsTests.cs new file mode 100644 index 00000000000..4dcd98eb22d --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterHelperExtensionsTests.cs @@ -0,0 +1,138 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NET462_OR_GREATER || NETSTANDARD2_0 +#pragma warning disable CS0618 // Suppressing gRPC obsolete warning +using System.Net.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +public class OtlpExporterHelperExtensionsTests +{ + [Fact] + public void OtlpExporter_Throws_OnGrpcWithDefaultFactory_ForTracing() + { + var services = new ServiceCollection(); + services.AddOpenTelemetry() + .WithTracing(tracing => tracing.AddOtlpExporter(options => options.Protocol = OtlpExportProtocol.Grpc)); + + using var sp = services.BuildServiceProvider(); + + Assert.Throws(() => sp.GetRequiredService()); + + var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder() + .AddOtlpExporter(o => o.Protocol = OtlpExportProtocol.Grpc); + + Assert.Throws(() => tracerProviderBuilder.Build()); + } + + [Fact] + public void OtlpExporter_Throws_OnGrpcWithDefaultFactory_ForMetrics() + { + var services = new ServiceCollection(); + + services.AddOpenTelemetry() + .WithMetrics(metrics => metrics.AddOtlpExporter(options => options.Protocol = OtlpExportProtocol.Grpc)); + + using var sp = services.BuildServiceProvider(); + + Assert.Throws(() => sp.GetRequiredService()); + + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .AddOtlpExporter(o => o.Protocol = OtlpExportProtocol.Grpc); + + Assert.Throws(() => meterProviderBuilder.Build()); + } + + [Fact] + public void OtlpExporter_Throws_OnGrpcWithDefaultFactory_ForLogging() + { + var services = new ServiceCollection(); + + services.AddOpenTelemetry() + .WithLogging(builder => builder.AddOtlpExporter(options => options.Protocol = OtlpExportProtocol.Grpc)); + + using var sp = services.BuildServiceProvider(); + + Assert.Throws(() => sp.GetRequiredService()); + + Assert.Throws(() => LoggerFactory.Create(builder => + { + builder.AddOpenTelemetry(logging => + { + logging.AddOtlpExporter(o => o.Protocol = OtlpExportProtocol.Grpc); + }); + })); + } + + [Fact] + public void OtlpExporter_DoesNotThrow_WhenCustomHttpClientFactoryIsSet_ForTraces() + { + var services = new ServiceCollection(); + + services.AddOpenTelemetry() + .WithTracing(builder => + { + builder.AddOtlpExporter(options => + { + options.Protocol = OtlpExportProtocol.Grpc; + options.HttpClientFactory = () => new HttpClient(); + }); + }); + + using var sp = services.BuildServiceProvider(); + + var tracerProvider = sp.GetRequiredService(); + Assert.NotNull(tracerProvider); + } + + [Fact] + public void OtlpExporter_DoesNotThrow_WhenCustomHttpClientFactoryIsSet_ForMetrics() + { + var services = new ServiceCollection(); + + services.AddOpenTelemetry() + .WithMetrics(builder => + { + builder.AddOtlpExporter(options => + { + options.Protocol = OtlpExportProtocol.Grpc; + options.HttpClientFactory = () => new HttpClient(); + }); + }); + + using var sp = services.BuildServiceProvider(); + + var meterProvider = sp.GetRequiredService(); + Assert.NotNull(meterProvider); + } + + [Fact] + public void OtlpExporter_DoesNotThrow_WhenCustomHttpClientFactoryIsSet_ForLogging() + { + var services = new ServiceCollection(); + + services.AddOpenTelemetry() + .WithLogging(builder => + { + builder.AddOtlpExporter(options => + { + options.Protocol = OtlpExportProtocol.Grpc; + options.HttpClientFactory = () => new HttpClient(); + }); + }); + + using var sp = services.BuildServiceProvider(); + + var loggerProvider = sp.GetRequiredService(); + Assert.NotNull(loggerProvider); + } +} +#pragma warning restore CS0618 // Suppressing gRPC obsolete warning +#endif diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs index 2b7810dfadf..8e0c24601a4 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs @@ -9,90 +9,62 @@ using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; using Xunit; -using Xunit.Sdk; namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; public class OtlpExporterOptionsExtensionsTests { [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) + [InlineData("")] + [InlineData(null)] + public void GetHeaders_NoOptionHeaders_ReturnsStandardHeaders(string? optionHeaders) { var options = new OtlpExporterOptions { - Headers = headers, + Headers = optionHeaders, }; - var metadata = options.GetMetadataFromHeaders(); - Assert.Equal(OtlpExporterOptions.StandardHeaders.Length + keys.Length, metadata.Count); + var headers = options.GetHeaders>((d, k, v) => d.Add(k, v)); - for (int i = 0; i < keys.Length; i++) - { - Assert.Contains(metadata, entry => entry.Key == keys[i] && entry.Value == values[i]); - } + Assert.Equal(OtlpExporterOptions.StandardHeaders.Length, headers.Count); 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); + Assert.Contains(headers, entry => entry.Key == OtlpExporterOptions.StandardHeaders[i].Key && entry.Value == OtlpExporterOptions.StandardHeaders[i].Value); } } [Theory] - [InlineData("headers")] - [InlineData("key,value")] - public void GetMetadataFromHeadersThrowsExceptionOnInvalidFormat(string headers) + [InlineData(" ")] + [InlineData(",key1=value1,key2=value2,")] + [InlineData(",,key1=value1,,key2=value2,,")] + [InlineData("key1")] + public void GetHeaders_InvalidOptionHeaders_ThrowsArgumentException(string inputOptionHeaders) { - 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"); + VerifyHeaders(inputOptionHeaders, string.Empty, true); } [Theory] - [InlineData("")] - [InlineData(null)] - public void GetHeaders_NoOptionHeaders_ReturnsStandardHeaders(string optionHeaders) + [InlineData("", "")] + [InlineData("key1=value1", "key1=value1")] + [InlineData("key1=value1,key2=value2", "key1=value1,key2=value2")] + [InlineData("key1=value1,key2=value2,key3=value3", "key1=value1,key2=value2,key3=value3")] + [InlineData(" key1 = value1 , key2=value2 ", "key1=value1,key2=value2")] + [InlineData("key1= value with spaces ,key2=another value", "key1=value with spaces,key2=another value")] + [InlineData("=value1", "=value1")] + [InlineData("key1=", "key1=")] + [InlineData("key1=value1%2Ckey2=value2", "key1=value1,key2=value2")] + [InlineData("key1=value1%2Ckey2=value2%2Ckey3=value3", "key1=value1,key2=value2,key3=value3")] + public void GetHeaders_ValidAndUrlEncodedHeaders_ReturnsCorrectHeaders(string inputOptionHeaders, string expectedNormalizedOptional) { - var options = new OtlpExporterOptions - { - Headers = optionHeaders, - }; - - 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); - } + VerifyHeaders(inputOptionHeaders, expectedNormalizedOptional); } [Theory] - [InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcTraceExportClient))] - [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpTraceExportClient))] +#pragma warning disable CS0618 // Suppressing gRPC obsolete warning + [InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcExportClient))] +#pragma warning restore CS0618 // Suppressing gRPC obsolete warning + [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpExportClient))] public void GetTraceExportClient_SupportedProtocol_ReturnsCorrectExportClient(OtlpExportProtocol protocol, Type expectedExportClientType) { var options = new OtlpExporterOptions @@ -100,7 +72,7 @@ public void GetTraceExportClient_SupportedProtocol_ReturnsCorrectExportClient(Ot Protocol = protocol, }; - var exportClient = options.GetTraceExportClient(); + var exportClient = options.GetExportClient(OtlpSignalType.Traces); Assert.Equal(expectedExportClientType, exportClient.GetType()); } @@ -113,7 +85,7 @@ public void GetTraceExportClient_UnsupportedProtocol_Throws() Protocol = (OtlpExportProtocol)123, }; - Assert.Throws(() => options.GetTraceExportClient()); + Assert.Throws(() => options.GetExportClient(OtlpSignalType.Traces)); } [Theory] @@ -121,95 +93,109 @@ public void GetTraceExportClient_UnsupportedProtocol_Throws() [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) + public void AppendPathIfNotPresent_TracesPath_AppendsCorrectly(string input, string expected) { - var uri = new Uri(inputUri, UriKind.Absolute); + var uri = new Uri(input, UriKind.Absolute); var resultUri = uri.AppendPathIfNotPresent("v1/traces"); - Assert.Equal(expectedUri, resultUri.AbsoluteUri); + Assert.Equal(expected, resultUri.AbsoluteUri); } [Theory] - [InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcTraceExportClient), false, 10000, null)] - [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpTraceExportClient), false, 10000, null)] - [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpTraceExportClient), true, 8000, null)] - [InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcMetricsExportClient), false, 10000, null)] - [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpMetricsExportClient), false, 10000, null)] - [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpMetricsExportClient), true, 8000, null)] - [InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcLogExportClient), false, 10000, null)] - [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpLogExportClient), false, 10000, null)] - [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpLogExportClient), true, 8000, null)] - [InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcTraceExportClient), false, 10000, "in_memory")] - [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpTraceExportClient), false, 10000, "in_memory")] - [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpTraceExportClient), true, 8000, "in_memory")] - [InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcMetricsExportClient), false, 10000, "in_memory")] - [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpMetricsExportClient), false, 10000, "in_memory")] - [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpMetricsExportClient), true, 8000, "in_memory")] - [InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcLogExportClient), false, 10000, "in_memory")] - [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpLogExportClient), false, 10000, "in_memory")] - [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpLogExportClient), true, 8000, "in_memory")] - [InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcTraceExportClient), false, 10000, "disk")] - [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpTraceExportClient), false, 10000, "disk")] - [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpTraceExportClient), true, 8000, "disk")] - [InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcMetricsExportClient), false, 10000, "disk")] - [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpMetricsExportClient), false, 10000, "disk")] - [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpMetricsExportClient), true, 8000, "disk")] - [InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcLogExportClient), false, 10000, "disk")] - [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpLogExportClient), false, 10000, "disk")] - [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpLogExportClient), true, 8000, "disk")] - public void GetTransmissionHandler_InitializesCorrectHandlerExportClientAndTimeoutValue(OtlpExportProtocol protocol, Type exportClientType, bool customHttpClient, int expectedTimeoutMilliseconds, string retryStrategy) +#pragma warning disable CS0618 // Suppressing gRPC obsolete warning + [InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcExportClient), false, 10000, null)] + [InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcExportClient), false, 10000, "in_memory")] + [InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcExportClient), false, 10000, "disk")] +#pragma warning restore CS0618 // Suppressing gRPC obsolete warning + [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpExportClient), false, 10000, null)] + [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpExportClient), true, 8000, null)] + [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpExportClient), false, 10000, "in_memory")] + [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpExportClient), true, 8000, "in_memory")] + [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpExportClient), false, 10000, "disk")] + [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpExportClient), true, 8000, "disk")] + public void GetTransmissionHandler_InitializesCorrectHandlerExportClientAndTimeoutValue(OtlpExportProtocol protocol, Type exportClientType, bool customHttpClient, int expectedTimeoutMilliseconds, string? retryStrategy) { var exporterOptions = new OtlpExporterOptions() { Protocol = protocol }; if (customHttpClient) { exporterOptions.HttpClientFactory = () => { - return new HttpClient() { Timeout = TimeSpan.FromMilliseconds(expectedTimeoutMilliseconds) }; + return new HttpClient { Timeout = TimeSpan.FromMilliseconds(expectedTimeoutMilliseconds) }; }; } var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { [ExperimentalOptions.OtlpRetryEnvVar] = retryStrategy }) + .AddInMemoryCollection(new Dictionary { [ExperimentalOptions.OtlpRetryEnvVar] = retryStrategy }) .Build(); - if (exportClientType == typeof(OtlpGrpcTraceExportClient) || exportClientType == typeof(OtlpHttpTraceExportClient)) - { - var transmissionHandler = exporterOptions.GetTraceExportTransmissionHandler(new ExperimentalOptions(configuration)); + var transmissionHandler = exporterOptions.GetExportTransmissionHandler(new ExperimentalOptions(configuration), OtlpSignalType.Traces); + AssertTransmissionHandler(transmissionHandler, exportClientType, expectedTimeoutMilliseconds, retryStrategy); + } - AssertTransmissionHandler(transmissionHandler, exportClientType, expectedTimeoutMilliseconds, retryStrategy); + private static void AssertTransmissionHandler(OtlpExporterTransmissionHandler transmissionHandler, Type exportClientType, int expectedTimeoutMilliseconds, string? retryStrategy) + { + if (retryStrategy == "in_memory") + { + Assert.True(transmissionHandler is OtlpExporterRetryTransmissionHandler); } - else if (exportClientType == typeof(OtlpGrpcMetricsExportClient) || exportClientType == typeof(OtlpHttpMetricsExportClient)) + else if (retryStrategy == "disk") { - var transmissionHandler = exporterOptions.GetMetricsExportTransmissionHandler(new ExperimentalOptions(configuration)); - - AssertTransmissionHandler(transmissionHandler, exportClientType, expectedTimeoutMilliseconds, retryStrategy); + Assert.True(transmissionHandler is OtlpExporterPersistentStorageTransmissionHandler); } else { - var transmissionHandler = exporterOptions.GetLogsExportTransmissionHandler(new ExperimentalOptions(configuration)); - - AssertTransmissionHandler(transmissionHandler, exportClientType, expectedTimeoutMilliseconds, retryStrategy); + Assert.True(transmissionHandler is OtlpExporterTransmissionHandler); } + + Assert.Equal(exportClientType, transmissionHandler.ExportClient.GetType()); + + Assert.Equal(expectedTimeoutMilliseconds, transmissionHandler.TimeoutMilliseconds); } - private static void AssertTransmissionHandler(OtlpExporterTransmissionHandler transmissionHandler, Type exportClientType, int expectedTimeoutMilliseconds, string retryStrategy) + /// + /// Validates whether the `Headers` property in `OtlpExporterOptions` is correctly processed and parsed. + /// It also verifies that the extracted headers match the expected values and checks for expected exceptions. + /// + /// The raw header string assigned to `OtlpExporterOptions`. + /// The format should be "key1=value1,key2=value2" (comma-separated key-value pairs). + /// A string representing expected additional headers. + /// This will be parsed into a dictionary and compared with the actual extracted headers. + /// If `true`, the method expects `GetHeaders` to throw an `ArgumentException` + /// when processing `inputOptionHeaders`. + private static void VerifyHeaders(string inputOptionHeaders, string expectedNormalizedOptional, bool expectException = false) { - if (retryStrategy == "in_memory") + var options = new OtlpExporterOptions { Headers = inputOptionHeaders }; + + if (expectException) { - Assert.True(transmissionHandler is OtlpExporterRetryTransmissionHandler); + Assert.Throws(() => + options.GetHeaders>((d, k, v) => d.Add(k, v))); + return; } - else if (retryStrategy == "disk") + + var headers = options.GetHeaders>((d, k, v) => d.Add(k, v)); + var expectedOptional = new Dictionary(); + + if (!string.IsNullOrEmpty(expectedNormalizedOptional)) { - Assert.True(transmissionHandler is OtlpExporterPersistentStorageTransmissionHandler); + foreach (var segment in expectedNormalizedOptional.Split([','], StringSplitOptions.RemoveEmptyEntries)) + { + var parts = segment.Split(['='], 2); + expectedOptional.Add(parts[0].Trim(), parts[1].Trim()); + } } - else + + Assert.Equal(OtlpExporterOptions.StandardHeaders.Length + expectedOptional.Count, headers.Count); + + foreach (var kvp in expectedOptional) { - Assert.True(transmissionHandler is OtlpExporterTransmissionHandler); + Assert.Contains(headers, h => h.Key == kvp.Key && h.Value == kvp.Value); } - Assert.Equal(exportClientType, transmissionHandler.ExportClient.GetType()); - - Assert.Equal(expectedTimeoutMilliseconds, transmissionHandler.TimeoutMilliseconds); + foreach (var std in OtlpExporterOptions.StandardHeaders) + { + Assert.Contains(headers, h => h.Key == std.Key && h.Value == std.Value); + } } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs index dc79c95c07b..9536d283cf5 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs @@ -7,7 +7,7 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; [Collection("EnvVars")] -public class OtlpExporterOptionsTests : IDisposable +public sealed class OtlpExporterOptionsTests : IDisposable { public OtlpExporterOptionsTests() { @@ -24,7 +24,12 @@ public void OtlpExporterOptions_Defaults() { var options = new OtlpExporterOptions(); +#if NET462_OR_GREATER || NETSTANDARD2_0 + Assert.Equal(new Uri(OtlpExporterOptions.DefaultHttpEndpoint), options.Endpoint); +#else Assert.Equal(new Uri(OtlpExporterOptions.DefaultGrpcEndpoint), options.Endpoint); +#endif + Assert.Null(options.Headers); Assert.Equal(10000, options.TimeoutMilliseconds); Assert.Equal(OtlpExporterOptions.DefaultOtlpExportProtocol, options.Protocol); @@ -47,6 +52,14 @@ public void OtlpExporterOptions_DefaultsForHttpProtobuf() [ClassData(typeof(OtlpSpecConfigDefinitionTests))] public void OtlpExporterOptions_EnvironmentVariableOverride(object testDataObject) { +#if NET + Assert.NotNull(testDataObject); +#else + if (testDataObject == null) + { + throw new ArgumentNullException(nameof(testDataObject)); + } +#endif var testData = testDataObject as OtlpSpecConfigDefinitionTests.TestData; Assert.NotNull(testData); @@ -61,6 +74,14 @@ public void OtlpExporterOptions_EnvironmentVariableOverride(object testDataObjec [ClassData(typeof(OtlpSpecConfigDefinitionTests))] public void OtlpExporterOptions_UsingIConfiguration(object testDataObject) { +#if NET + Assert.NotNull(testDataObject); +#else + if (testDataObject == null) + { + throw new ArgumentNullException(nameof(testDataObject)); + } +#endif var testData = testDataObject as OtlpSpecConfigDefinitionTests.TestData; Assert.NotNull(testData); @@ -74,7 +95,7 @@ public void OtlpExporterOptions_UsingIConfiguration(object testDataObject) [Fact] public void OtlpExporterOptions_InvalidEnvironmentVariableOverride() { - var values = new Dictionary() + var values = new Dictionary { ["EndpointWithInvalidValue"] = "invalid", ["TimeoutWithInvalidValue"] = "invalid", @@ -95,7 +116,12 @@ public void OtlpExporterOptions_InvalidEnvironmentVariableOverride() "NoopHeaders", "TimeoutWithInvalidValue"); +#if NET462_OR_GREATER || NETSTANDARD2_0 + Assert.Equal(new Uri(OtlpExporterOptions.DefaultHttpEndpoint), options.Endpoint); +#else Assert.Equal(new Uri(OtlpExporterOptions.DefaultGrpcEndpoint), options.Endpoint); +#endif + Assert.Equal(10000, options.TimeoutMilliseconds); Assert.Equal(OtlpExporterOptions.DefaultOtlpExportProtocol, options.Protocol); Assert.Null(options.Headers); @@ -104,7 +130,7 @@ public void OtlpExporterOptions_InvalidEnvironmentVariableOverride() [Fact] public void OtlpExporterOptions_SetterOverridesEnvironmentVariable() { - var values = new Dictionary() + var values = new Dictionary { ["Endpoint"] = "http://test:8888", ["Timeout"] = "2000", @@ -143,14 +169,20 @@ public void OtlpExporterOptions_EndpointGetterUsesProtocolWhenNull() { var options = new OtlpExporterOptions(); +#if NET462_OR_GREATER || NETSTANDARD2_0 + Assert.Equal(new Uri(OtlpExporterOptions.DefaultHttpEndpoint), options.Endpoint); +#else Assert.Equal(new Uri(OtlpExporterOptions.DefaultGrpcEndpoint), options.Endpoint); +#endif + Assert.Equal(OtlpExporterOptions.DefaultOtlpExportProtocol, options.Protocol); options.Protocol = OtlpExportProtocol.HttpProtobuf; Assert.Equal(new Uri(OtlpExporterOptions.DefaultHttpEndpoint), options.Endpoint); - +#pragma warning disable CS0618 // Suppressing gRPC obsolete warning options.Protocol = OtlpExportProtocol.Grpc; +#pragma warning restore CS0618 // Suppressing gRPC obsolete warning Assert.Equal(new Uri(OtlpExporterOptions.DefaultGrpcEndpoint), options.Endpoint); } @@ -158,10 +190,12 @@ public void OtlpExporterOptions_EndpointGetterUsesProtocolWhenNull() [Fact] public void OtlpExporterOptions_EndpointThrowsWhenSetToNull() { +#pragma warning disable CS0618 // Suppressing gRPC obsolete warning 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); +#pragma warning restore CS0618 // Suppressing gRPC obsolete warning } [Fact] @@ -169,7 +203,7 @@ public void OtlpExporterOptions_SettingEndpointToNullResetsAppendSignalPathToEnd { var options = new OtlpExporterOptions(OtlpExporterOptionsConfigurationType.Default); - Assert.Throws(() => options.Endpoint = null); + Assert.Throws(() => options.Endpoint = null!); } [Fact] @@ -177,7 +211,7 @@ public void OtlpExporterOptions_HttpClientFactoryThrowsWhenSetToNull() { var options = new OtlpExporterOptions(OtlpExporterOptionsConfigurationType.Default); - Assert.Throws(() => options.HttpClientFactory = null); + Assert.Throws(() => options.HttpClientFactory = null!); } [Fact] @@ -210,7 +244,9 @@ public void OtlpExporterOptions_ApplyDefaultsTest() var targetOptionsWithData = new OtlpExporterOptions { Endpoint = new Uri("http://metrics_endpoint/"), +#pragma warning disable CS0618 // Suppressing gRPC obsolete warning Protocol = OtlpExportProtocol.Grpc, +#pragma warning restore CS0618 // Suppressing gRPC obsolete warning Headers = "key2=value2", TimeoutMilliseconds = 1800, HttpClientFactory = () => throw new NotImplementedException(), diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/BaseOtlpHttpExportClientTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpHttpExportClientTests.cs similarity index 66% rename from test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/BaseOtlpHttpExportClientTests.cs rename to test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpHttpExportClientTests.cs index a846af33a4e..5de79944839 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/BaseOtlpHttpExportClientTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpHttpExportClientTests.cs @@ -9,7 +9,7 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; -public class BaseOtlpHttpExportClientTests +public class OtlpHttpExportClientTests { [Theory] [InlineData(null, null, "http://localhost:4318/signal/path")] @@ -17,7 +17,7 @@ public class BaseOtlpHttpExportClientTests [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) + public void ValidateOtlpHttpExportClientEndpoint(string? optionEndpoint, string? endpointEnvVar, string expectedExporterEndpoint) { try { @@ -30,7 +30,8 @@ public void ValidateOtlpHttpExportClientEndpoint(string optionEndpoint, string e options.Endpoint = new Uri(optionEndpoint); } - var exporterClient = new TestOtlpHttpExportClient(options, new HttpClient()); + using var httpClient = new HttpClient(); + var exporterClient = new OtlpHttpExportClient(options, httpClient, "signal/path"); Assert.Equal(new Uri(expectedExporterEndpoint), exporterClient.Endpoint); } finally @@ -38,17 +39,4 @@ public void ValidateOtlpHttpExportClientEndpoint(string optionEndpoint, string e Environment.SetEnvironmentVariable(OtlpSpecConfigDefinitions.DefaultEndpointEnvVarName, null); } } - - internal class TestOtlpHttpExportClient : BaseOtlpHttpExportClient - { - public TestOtlpHttpExportClient(OtlpExporterOptions options, HttpClient httpClient) - : base(options, httpClient, "signal/path") - { - } - - protected override HttpContent CreateHttpContent(string exportRequest) - { - throw new NotImplementedException(); - } - } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs index f494a2683c6..c6f7f32f6d0 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs @@ -3,15 +3,17 @@ using System.Collections.ObjectModel; using System.Diagnostics; +using System.Globalization; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; using OpenTelemetry.Internal; using OpenTelemetry.Logs; +using OpenTelemetry.Proto.Trace.V1; using OpenTelemetry.Resources; using OpenTelemetry.Tests; using OpenTelemetry.Trace; @@ -100,7 +102,7 @@ public void UserHttpFactoryCalledWhenUsingHttpProtobuf() Assert.Equal(2, invocations); } - options.HttpClientFactory = () => null; + options.HttpClientFactory = () => null!; Assert.Throws(() => { using var exporter = new OtlpLogExporter(options); @@ -147,7 +149,7 @@ public void AddOtlpLogExporterReceivesAttributesWithParseStateValueSetToFalse(bo Assert.True(optionsValidated); var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); - logger.LogInformation("Hello from {name} {price}.", "tomato", 2.99); + logger.HelloFrom("tomato", 2.99); Assert.Single(logRecords); var logRecord = logRecords[0]; #pragma warning disable CS0618 // Type or member is obsolete @@ -218,6 +220,7 @@ public void AddOtlpLogExporterParseStateValueCanBeTurnedOffHosting(bool parseSta var host = hostBuilder.Build(); var loggerFactory = host.Services.GetService(); + Assert.NotNull(loggerFactory); var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); logger.Log(LogLevel.Information, default, new { propertyA = "valueA" }, null, (s, e) => "Custom state log message"); Assert.Single(logRecords); @@ -259,14 +262,11 @@ public void OtlpLogRecordTestWhenStateValuesArePopulated() }); var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); - logger.LogInformation("Hello from {name} {price}.", "tomato", 2.99); - + logger.HelloFrom("tomato", 2.99); Assert.Single(logRecords); - var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - var logRecord = logRecords[0]; - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + OtlpLogs.LogRecord? otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); Assert.NotNull(otlpLogRecord); Assert.Equal("Hello from tomato 2.99.", otlpLogRecord.Body.StringValue); @@ -274,23 +274,23 @@ public void OtlpLogRecordTestWhenStateValuesArePopulated() var index = 0; var attribute = otlpLogRecord.Attributes[index]; - Assert.Equal("name", attribute.Key); + Assert.Equal("Name", attribute.Key); Assert.Equal("tomato", attribute.Value.StringValue); attribute = otlpLogRecord.Attributes[++index]; - Assert.Equal("price", attribute.Key); + 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); + Assert.Equal("Hello from {Name} {Price}.", attribute.Value.StringValue); } [Theory] [InlineData("true")] [InlineData("false")] [InlineData(null)] - public void CheckToOtlpLogRecordEventId(string emitLogEventAttributes) + public void CheckToOtlpLogRecordEventId(string? emitLogEventAttributes) { var logRecords = new List(); using var loggerFactory = LoggerFactory.Create(builder => @@ -305,18 +305,15 @@ public void CheckToOtlpLogRecordEventId(string emitLogEventAttributes) }); var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); - logger.LogInformation(new EventId(10, null), "Hello from {name} {price}.", "tomato", 2.99); + logger.HelloFromWithEventId("tomato", 2.99); Assert.Single(logRecords); var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { [ExperimentalOptions.EmitLogEventEnvVar] = emitLogEventAttributes }) + .AddInMemoryCollection(new Dictionary { [ExperimentalOptions.EmitLogEventEnvVar] = emitLogEventAttributes }) .Build(); - var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new(configuration)); - var logRecord = logRecords[0]; - - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + OtlpLogs.LogRecord? otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new(configuration), logRecord); Assert.NotNull(otlpLogRecord); Assert.Equal("Hello from tomato 2.99.", otlpLogRecord.Body.StringValue); @@ -325,37 +322,38 @@ public void CheckToOtlpLogRecordEventId(string emitLogEventAttributes) var otlpLogRecordAttributes = otlpLogRecord.Attributes.ToString(); if (emitLogEventAttributes == "true") { - Assert.Contains(ExperimentalOptions.LogRecordEventIdAttribute, otlpLogRecordAttributes); - Assert.Contains("10", otlpLogRecordAttributes); + Assert.Contains(ExperimentalOptions.LogRecordEventIdAttribute, otlpLogRecordAttributes, StringComparison.Ordinal); + Assert.Contains("10", otlpLogRecordAttributes, StringComparison.Ordinal); } else { - Assert.DoesNotContain(ExperimentalOptions.LogRecordEventIdAttribute, otlpLogRecordAttributes); + Assert.DoesNotContain(ExperimentalOptions.LogRecordEventIdAttribute, otlpLogRecordAttributes, StringComparison.Ordinal); } logRecords.Clear(); - logger.LogInformation(new EventId(10, "MyEvent10"), "Hello from {name} {price}.", "tomato", 2.99); + logger.HelloFromWithEventIdAndEventName("tomato", 2.99); Assert.Single(logRecords); logRecord = logRecords[0]; - otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + + otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new(configuration), logRecord); + Assert.NotNull(otlpLogRecord); Assert.Equal("Hello from tomato 2.99.", otlpLogRecord.Body.StringValue); + Assert.Equal("MyEvent10", otlpLogRecord.EventName); + // Event otlpLogRecordAttributes = otlpLogRecord.Attributes.ToString(); if (emitLogEventAttributes == "true") { - Assert.Contains(ExperimentalOptions.LogRecordEventIdAttribute, otlpLogRecordAttributes); - Assert.Contains("10", otlpLogRecordAttributes); - Assert.Contains(ExperimentalOptions.LogRecordEventNameAttribute, otlpLogRecordAttributes); - Assert.Contains("MyEvent10", otlpLogRecordAttributes); + Assert.Contains(ExperimentalOptions.LogRecordEventIdAttribute, otlpLogRecordAttributes, StringComparison.Ordinal); + Assert.Contains("10", otlpLogRecordAttributes, StringComparison.Ordinal); } else { - Assert.DoesNotContain(ExperimentalOptions.LogRecordEventIdAttribute, otlpLogRecordAttributes); - Assert.DoesNotContain(ExperimentalOptions.LogRecordEventNameAttribute, otlpLogRecordAttributes); + Assert.DoesNotContain(ExperimentalOptions.LogRecordEventIdAttribute, otlpLogRecordAttributes, StringComparison.Ordinal); } } @@ -369,12 +367,12 @@ public void CheckToOtlpLogRecordTimestamps() }); var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); - logger.LogInformation("Log message"); - var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); + logger.LogMessage(); var logRecord = logRecords[0]; - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + OtlpLogs.LogRecord? otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + Assert.NotNull(otlpLogRecord); Assert.True(otlpLogRecord.TimeUnixNano > 0); Assert.True(otlpLogRecord.ObservedTimeUnixNano > 0); } @@ -389,17 +387,16 @@ public void CheckToOtlpLogRecordTraceIdSpanIdFlagWithNoActivity() }); var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); - logger.LogInformation("Log when there is no activity."); - - var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); + logger.LogWhenThereIsNoActivity(); var logRecord = logRecords[0]; - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + OtlpLogs.LogRecord? otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); Assert.Null(Activity.Current); + Assert.NotNull(otlpLogRecord); Assert.True(otlpLogRecord.TraceId.IsEmpty); Assert.True(otlpLogRecord.SpanId.IsEmpty); - Assert.True(otlpLogRecord.Flags == 0); + Assert.Equal(0u, otlpLogRecord.Flags); } [Fact] @@ -415,19 +412,20 @@ public void CheckToOtlpLogRecordSpanIdTraceIdAndFlag() ActivityTraceId expectedTraceId = default; ActivitySpanId expectedSpanId = default; - using (var activity = new Activity(Utils.GetCurrentMethodName()).Start()) + using (var activity = new Activity(Utils.GetCurrentMethodName())) { - logger.LogInformation("Log within an activity."); + activity.Start(); + logger.LogWithinAnActivity(); expectedTraceId = activity.TraceId; expectedSpanId = activity.SpanId; } - var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - var logRecord = logRecords[0]; - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + OtlpLogs.LogRecord? otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + + Assert.NotNull(otlpLogRecord); 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); @@ -453,18 +451,17 @@ public void CheckToOtlpLogRecordSeverityLevelAndText(LogLevel logLevel) }); var logger = loggerFactory.CreateLogger("CheckToOtlpLogRecordSeverityLevelAndText"); - logger.Log(logLevel, "Hello from {name} {price}.", "tomato", 2.99); + logger.HelloFrom(logLevel, "tomato", 2.99); Assert.Single(logRecords); - var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - var logRecord = logRecords[0]; - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + OtlpLogs.LogRecord? otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); Assert.NotNull(otlpLogRecord); #pragma warning disable CS0618 // Type or member is obsolete Assert.Equal(logRecord.LogLevel.ToString(), otlpLogRecord.SeverityText); #pragma warning restore CS0618 // Type or member is obsolete + Assert.NotNull(logRecord.Severity); Assert.Equal((int)logRecord.Severity, (int)otlpLogRecord.SeverityNumber); switch (logLevel) { @@ -509,13 +506,11 @@ public void CheckToOtlpLogRecordBodyIsPopulated(bool includeFormattedMessage) var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); // Scenario 1 - Using ExtensionMethods on ILogger.Log - logger.LogInformation("OpenTelemetry {Greeting} {Subject}!", "Hello", "World"); + logger.OpenTelemetryGreeting("Hello", "World"); Assert.Single(logRecords); - var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - var logRecord = logRecords[0]; - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + OtlpLogs.LogRecord? otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); Assert.NotNull(otlpLogRecord); if (includeFormattedMessage) @@ -534,7 +529,7 @@ public void CheckToOtlpLogRecordBodyIsPopulated(bool includeFormattedMessage) Assert.Single(logRecords); logRecord = logRecords[0]; - otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); Assert.NotNull(otlpLogRecord); @@ -546,11 +541,11 @@ public void CheckToOtlpLogRecordBodyIsPopulated(bool includeFormattedMessage) // Scenario 3 - Using the raw ILogger.Log Method, but with null // formatter. - logger.Log(LogLevel.Information, default, "state", exception: null, formatter: null); + logger.Log(LogLevel.Information, default, "state", exception: null, formatter: null!); Assert.Single(logRecords); logRecord = logRecords[0]; - otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); Assert.NotNull(otlpLogRecord); @@ -586,10 +581,9 @@ public void LogRecordBodyIsExportedWhenUsingBridgeApi(bool isBodySet) Assert.Equal(2, logRecords.Count); - var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecords[0]); + var otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecords[0]); + Assert.NotNull(otlpLogRecord); if (isBodySet) { Assert.Equal("Hello world", otlpLogRecord.Body?.StringValue); @@ -599,8 +593,9 @@ public void LogRecordBodyIsExportedWhenUsingBridgeApi(bool isBodySet) Assert.Null(otlpLogRecord.Body); } - otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecords[1]); + otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecords[1]); + Assert.NotNull(otlpLogRecord); Assert.Equal(2, otlpLogRecord.Attributes.Count); var index = 0; @@ -625,26 +620,25 @@ public void CheckToOtlpLogRecordExceptionAttributes() }); var logger = loggerFactory.CreateLogger("OtlpLogExporterTests"); - logger.LogInformation(new Exception("Exception Message"), "Exception Occurred"); + logger.ExceptionOccured(new InvalidOperationException("Exception Message")); var logRecord = logRecords[0]; var loggedException = logRecord.Exception; - var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + OtlpLogs.LogRecord? otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); Assert.NotNull(otlpLogRecord); var otlpLogRecordAttributes = otlpLogRecord.Attributes.ToString(); - Assert.Contains(SemanticConventions.AttributeExceptionType, otlpLogRecordAttributes); - Assert.Contains(logRecord.Exception.GetType().Name, otlpLogRecordAttributes); + Assert.Contains(SemanticConventions.AttributeExceptionType, otlpLogRecordAttributes, StringComparison.Ordinal); + Assert.NotNull(logRecord.Exception); + Assert.Contains(logRecord.Exception.GetType().Name, otlpLogRecordAttributes, StringComparison.Ordinal); - Assert.Contains(SemanticConventions.AttributeExceptionMessage, otlpLogRecordAttributes); - Assert.Contains(logRecord.Exception.Message, otlpLogRecordAttributes); + Assert.Contains(SemanticConventions.AttributeExceptionMessage, otlpLogRecordAttributes, StringComparison.Ordinal); + Assert.Contains(logRecord.Exception.Message, otlpLogRecordAttributes, StringComparison.Ordinal); - Assert.Contains(SemanticConventions.AttributeExceptionStacktrace, otlpLogRecordAttributes); - Assert.Contains(logRecord.Exception.ToInvariantString(), otlpLogRecordAttributes); + Assert.Contains(SemanticConventions.AttributeExceptionStacktrace, otlpLogRecordAttributes, StringComparison.Ordinal); + Assert.Contains(logRecord.Exception.ToInvariantString(), otlpLogRecordAttributes, StringComparison.Ordinal); } [Fact] @@ -665,12 +659,10 @@ public void CheckToOtlpLogRecordRespectsAttributeLimits() }); var logger = loggerFactory.CreateLogger(string.Empty); - logger.LogInformation("OpenTelemetry {AttributeOne} {AttributeTwo} {AttributeThree}!", "I'm an attribute", "I too am an attribute", "I get dropped :("); - - var otlpLogRecordTransformer = new OtlpLogRecordTransformer(sdkLimitOptions, new()); + logger.OpenTelemetryWithAttributes("I'm an attribute", "I too am an attribute", "I get dropped :("); var logRecord = logRecords[0]; - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + OtlpLogs.LogRecord? otlpLogRecord = ToOtlpLogs(sdkLimitOptions, new(), logRecord); Assert.NotNull(otlpLogRecord); Assert.Equal(1u, otlpLogRecord.DroppedAttributesCount); @@ -694,12 +686,12 @@ public void CheckToOtlpLogRecordRespectsAttributeLimits() public void Export_WhenExportClientIsProvidedInCtor_UsesProvidedExportClient() { // Arrange. - var testExportClient = new TestExportClient(); + var testExportClient = new TestExportClient(); var exporterOptions = new OtlpExporterOptions(); - var transmissionHandler = new OtlpExporterTransmissionHandler(testExportClient, exporterOptions.TimeoutMilliseconds); + using var transmissionHandler = new OtlpExporterTransmissionHandler(testExportClient, exporterOptions.TimeoutMilliseconds); var emptyLogRecords = Array.Empty(); var emptyBatch = new Batch(emptyLogRecords, emptyLogRecords.Length); - var sut = new OtlpLogExporter( + using var sut = new OtlpLogExporter( exporterOptions, new SdkLimitOptions(), new ExperimentalOptions(), @@ -716,12 +708,12 @@ public void Export_WhenExportClientIsProvidedInCtor_UsesProvidedExportClient() public void Export_WhenExportClientThrowsException_ReturnsExportResultFailure() { // Arrange. - var testExportClient = new TestExportClient(throwException: true); + var testExportClient = new TestExportClient(throwException: true); var exporterOptions = new OtlpExporterOptions(); - var transmissionHandler = new OtlpExporterTransmissionHandler(testExportClient, exporterOptions.TimeoutMilliseconds); + using var transmissionHandler = new OtlpExporterTransmissionHandler(testExportClient, exporterOptions.TimeoutMilliseconds); var emptyLogRecords = Array.Empty(); var emptyBatch = new Batch(emptyLogRecords, emptyLogRecords.Length); - var sut = new OtlpLogExporter( + using var sut = new OtlpLogExporter( exporterOptions, new SdkLimitOptions(), new ExperimentalOptions(), @@ -738,12 +730,12 @@ public void Export_WhenExportClientThrowsException_ReturnsExportResultFailure() public void Export_WhenExportIsSuccessful_ReturnsExportResultSuccess() { // Arrange. - var testExportClient = new TestExportClient(); + var testExportClient = new TestExportClient(); var exporterOptions = new OtlpExporterOptions(); - var transmissionHandler = new OtlpExporterTransmissionHandler(testExportClient, exporterOptions.TimeoutMilliseconds); + using var transmissionHandler = new OtlpExporterTransmissionHandler(testExportClient, exporterOptions.TimeoutMilliseconds); var emptyLogRecords = Array.Empty(); var emptyBatch = new Batch(emptyLogRecords, emptyLogRecords.Length); - var sut = new OtlpLogExporter( + using var sut = new OtlpLogExporter( exporterOptions, new SdkLimitOptions(), new ExperimentalOptions(), @@ -775,16 +767,17 @@ public void ToOtlpLog_WhenOptionsIncludeScopesIsFalse_DoesNotContainScopeAttribu // Act. using (logger.BeginScope(new List> { - new KeyValuePair(expectedScopeKey, expectedScopeValue), + new(expectedScopeKey, expectedScopeValue), })) { - logger.LogInformation("Some log information message."); + logger.SomeLogInformation(); } // Assert. var logRecord = logRecords.Single(); - var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + OtlpLogs.LogRecord? otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + + Assert.NotNull(otlpLogRecord); var actualScope = TryGetAttribute(otlpLogRecord, expectedScopeKey); Assert.Null(actualScope); } @@ -794,6 +787,15 @@ public void ToOtlpLog_WhenOptionsIncludeScopesIsFalse_DoesNotContainScopeAttribu [InlineData('a')] public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_ContainsScopeAttributeStringValue(object scopeValue) { +#if NET + Assert.NotNull(scopeValue); +#else + if (scopeValue == null) + { + throw new ArgumentNullException(nameof(scopeValue)); + } +#endif + // Arrange. var logRecords = new List(1); using var loggerFactory = LoggerFactory.Create(builder => @@ -809,16 +811,17 @@ public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_ContainsScopeAttributeStrin // Act. using (logger.BeginScope(new List> { - new KeyValuePair(scopeKey, scopeValue), + new(scopeKey, scopeValue), })) { - logger.LogInformation("Some log information message."); + logger.SomeLogInformation(); } // Assert. var logRecord = logRecords.Single(); - var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + var otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + + Assert.NotNull(otlpLogRecord); Assert.Single(otlpLogRecord.Attributes); var actualScope = TryGetAttribute(otlpLogRecord, scopeKey); Assert.NotNull(actualScope); @@ -847,16 +850,17 @@ public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_ContainsScopeAttributeBoolV // Act. using (logger.BeginScope(new List> { - new KeyValuePair(scopeKey, scopeValue), + new(scopeKey, scopeValue), })) { - logger.LogInformation("Some log information message."); + logger.SomeLogInformation(); } // Assert. var logRecord = logRecords.Single(); - var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + var otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + + Assert.NotNull(otlpLogRecord); Assert.Single(otlpLogRecord.Attributes); var actualScope = TryGetAttribute(otlpLogRecord, scopeKey); Assert.NotNull(actualScope); @@ -882,6 +886,15 @@ public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_ContainsScopeAttributeBoolV [InlineData(long.MaxValue)] public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_ContainsScopeAttributeIntValue(object scopeValue) { +#if NET + Assert.NotNull(scopeValue); +#else + if (scopeValue == null) + { + throw new ArgumentNullException(nameof(scopeValue)); + } +#endif + // Arrange. var logRecords = new List(1); using var loggerFactory = LoggerFactory.Create(builder => @@ -897,22 +910,23 @@ public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_ContainsScopeAttributeIntVa // Act. using (logger.BeginScope(new List> { - new KeyValuePair(scopeKey, scopeValue), + new(scopeKey, scopeValue), })) { - logger.LogInformation("Some log information message."); + logger.SomeLogInformation(); } // Assert. var logRecord = logRecords.Single(); - var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + var otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + + Assert.NotNull(otlpLogRecord); 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()); + Assert.Equal(scopeValue.ToString(), actualScope.Value.IntValue.ToString(CultureInfo.InvariantCulture)); } [Theory] @@ -935,22 +949,24 @@ public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_ContainsScopeAttributeDoubl // Act. using (logger.BeginScope(new List> { - new KeyValuePair(scopeKey, scopeValue), + new(scopeKey, scopeValue), })) { - logger.LogInformation("Some log information message."); + logger.SomeLogInformation(); } // Assert. var logRecord = logRecords.Single(); - var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + + var otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + + Assert.NotNull(otlpLogRecord); 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()); + Assert.Equal(scopeValue, actualScope.Value.DoubleValue); } [Theory] @@ -973,21 +989,22 @@ public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_ContainsScopeAttributeDoubl // Act. using (logger.BeginScope(new List> { - new KeyValuePair(scopeKey, scopeValue), + new(scopeKey, scopeValue), })) { - logger.LogInformation("Some log information message."); + logger.SomeLogInformation(); } // Assert. var logRecord = logRecords.Single(); - var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + var otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + + Assert.NotNull(otlpLogRecord); 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()); + Assert.Equal(scopeValue, actualScope.Value.DoubleValue); } [Fact] @@ -1008,13 +1025,13 @@ public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_AndScopeStateIsOfTypeString // Act. using (logger.BeginScope(scopeState)) { - logger.LogInformation("Some log information message."); + logger.SomeLogInformation(); } // Assert. var logRecord = logRecords.Single(); - var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + var otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + Assert.NotNull(otlpLogRecord); Assert.Empty(otlpLogRecord.Attributes); } @@ -1038,17 +1055,18 @@ public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_AndScopeStateIsOfPrimitiveT var logger = loggerFactory.CreateLogger(nameof(OtlpLogExporterTests)); var scopeState = Activator.CreateInstance(typeOfScopeState); + Assert.NotNull(scopeState); // Act. using (logger.BeginScope(scopeState)) { - logger.LogInformation("Some log information message."); + logger.SomeLogInformation(); } // Assert. var logRecord = logRecords.Single(); - var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + var otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + Assert.NotNull(otlpLogRecord); Assert.Empty(otlpLogRecord.Attributes); } @@ -1073,13 +1091,14 @@ public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_AndScopeStateIsOfDictionary // Act. using (logger.BeginScope(scopeState)) { - logger.LogInformation("Some log information message."); + logger.SomeLogInformation(); } // Assert. var logRecord = logRecords.Single(); - var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + var otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + + Assert.NotNull(otlpLogRecord); Assert.Single(otlpLogRecord.Attributes); var actualScope = TryGetAttribute(otlpLogRecord, scopeKey); Assert.NotNull(actualScope); @@ -1105,19 +1124,21 @@ public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_AndScopeStateIsOfEnumerable const string scopeKey = "Some scope key"; const string scopeValue = "Some scope value"; - var scopeValues = new List> { new KeyValuePair(scopeKey, scopeValue) }; + var scopeValues = new List> { new(scopeKey, scopeValue) }; var scopeState = Activator.CreateInstance(typeOfScopeState, scopeValues) as ICollection>; // Act. + Assert.NotNull(scopeState); using (logger.BeginScope(scopeState)) { - logger.LogInformation("Some log information message."); + logger.SomeLogInformation(); } // Assert. var logRecord = logRecords.Single(); - var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + var otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + + Assert.NotNull(otlpLogRecord); Assert.Single(otlpLogRecord.Attributes); var actualScope = TryGetAttribute(otlpLogRecord, scopeKey); Assert.NotNull(actualScope); @@ -1146,17 +1167,18 @@ public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_AndMultipleScopesAreAdded_C // Act. using (logger.BeginScope(new List> { - new KeyValuePair(scopeKey1, scopeValue1), - new KeyValuePair(scopeKey2, scopeValue2), + new(scopeKey1, scopeValue1), + new(scopeKey2, scopeValue2), })) { - logger.LogInformation("Some log information message."); + logger.SomeLogInformation(); } // Assert. var logRecord = logRecords.Single(); - var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + var otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + + Assert.NotNull(otlpLogRecord); var allScopeValues = otlpLogRecord.Attributes .Where(_ => _.Key == scopeKey1 || _.Key == scopeKey2) .Select(_ => _.Value.StringValue); @@ -1185,18 +1207,19 @@ public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_AndMultipleScopeLevelsAreAd const string scopeValue2 = "Some other scope value"; // Act. - using (logger.BeginScope(new List> { new KeyValuePair(scopeKey1, scopeValue1) })) + using (logger.BeginScope(new List> { new(scopeKey1, scopeValue1) })) { - using (logger.BeginScope(new List> { new KeyValuePair(scopeKey2, scopeValue2) })) + using (logger.BeginScope(new List> { new(scopeKey2, scopeValue2) })) { - logger.LogInformation("Some log information message."); + logger.SomeLogInformation(); } } // Assert. var logRecord = logRecords.Single(); - var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + var otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + + Assert.NotNull(otlpLogRecord); var allScopeValues = otlpLogRecord.Attributes .Where(_ => _.Key == scopeKey1 || _.Key == scopeKey2) .Select(_ => _.Value.StringValue); @@ -1227,21 +1250,22 @@ public void ToOtlpLog_WhenOptionsIncludeScopesIsTrue_AndScopeIsUsedInLogMethod_C // Act. using (logger.BeginScope(new List> { - new KeyValuePair(scopeKey1, scopeValue1), + new(scopeKey1, scopeValue1), })) { logger.Log( LogLevel.Error, new EventId(1), - new List> { new KeyValuePair(scopeKey2, scopeValue2) }, - exception: new Exception("Some exception message"), + new List> { new(scopeKey2, scopeValue2) }, + exception: new InvalidOperationException("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 otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + + Assert.NotNull(otlpLogRecord); var allScopeValues = otlpLogRecord.Attributes .Where(_ => _.Key == scopeKey1 || _.Key == scopeKey2) .Select(_ => _.Value.StringValue); @@ -1314,9 +1338,9 @@ public void AddOtlpLogExporterLogRecordProcessorOptionsTest(ExportProcessorType } else { - var simpleProcesor = processor as SimpleLogRecordExportProcessor; + var simpleProcessor = processor as SimpleLogRecordExportProcessor; - Assert.NotNull(simpleProcesor); + Assert.NotNull(simpleProcessor); } } @@ -1330,20 +1354,18 @@ public void ValidateInstrumentationScope() }); var logger1 = loggerFactory.CreateLogger("OtlpLogExporterTests-A"); - logger1.LogInformation("Hello from red-tomato"); + logger1.HelloFromRedTomato(); var logger2 = loggerFactory.CreateLogger("OtlpLogExporterTests-B"); - logger2.LogInformation("Hello from green-tomato"); + logger2.HelloFromGreenTomato(); 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 processResource = CreateResourceSpans(resourceBuilder.Build()); - var request = logRecordTransformer.BuildExportRequest(processResource, batch); + OtlpCollector.ExportLogsServiceRequest request = CreateLogsExportRequest(DefaultSdkLimitOptions, new ExperimentalOptions(), batch, resourceBuilder.Build()); Assert.Single(request.ResourceLogs); @@ -1363,17 +1385,9 @@ public void ValidateInstrumentationScope() 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); + request = CreateLogsExportRequest(DefaultSdkLimitOptions, new ExperimentalOptions(), batch, resourceBuilder.Build()); Assert.Single(request.ResourceLogs); - - // ScopeLogs will be reused. - Assert.Empty(OtlpLogRecordTransformer.LogListPool); } [Theory] @@ -1381,7 +1395,7 @@ public void ValidateInstrumentationScope() [InlineData("logging", false)] [InlineData(null, true)] [InlineData("logging", true)] - public void VerifyEnvironmentVariablesTakenFromIConfigurationWhenUsingLoggerFactoryCreate(string optionsName, bool callUseOpenTelemetry) + public void VerifyEnvironmentVariablesTakenFromIConfigurationWhenUsingLoggerFactoryCreate(string? optionsName, bool callUseOpenTelemetry) { RunVerifyEnvironmentVariablesTakenFromIConfigurationTest( optionsName, @@ -1406,7 +1420,7 @@ public void VerifyEnvironmentVariablesTakenFromIConfigurationWhenUsingLoggerFact [InlineData("logging", false)] [InlineData(null, true)] [InlineData("logging", true)] - public void VerifyEnvironmentVariablesTakenFromIConfigurationWhenUsingLoggingBuilder(string optionsName, bool callUseOpenTelemetry) + public void VerifyEnvironmentVariablesTakenFromIConfigurationWhenUsingLoggingBuilder(string? optionsName, bool callUseOpenTelemetry) { RunVerifyEnvironmentVariablesTakenFromIConfigurationTest( optionsName, @@ -1433,7 +1447,7 @@ public void VerifyEnvironmentVariablesTakenFromIConfigurationWhenUsingLoggingBui [Theory] [InlineData("my_instrumentation_scope_name", "my_instrumentation_scope_name")] [InlineData(null, "")] - public void LogRecordLoggerNameIsExportedWhenUsingBridgeApi(string loggerName, string expectedScopeName) + public void LogRecordLoggerNameIsExportedWhenUsingBridgeApi(string? loggerName, string expectedScopeName) { LogRecordAttributeList attributes = default; attributes.Add("name", "tomato"); @@ -1453,26 +1467,102 @@ public void LogRecordLoggerNameIsExportedWhenUsingBridgeApi(string loggerName, s Assert.Single(logRecords); - var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); + var batch = new Batch([logRecords[0]], 1); + OtlpCollector.ExportLogsServiceRequest request = CreateLogsExportRequest(DefaultSdkLimitOptions, new ExperimentalOptions(), batch, ResourceBuilder.CreateEmpty().Build()); - var batch = new Batch(new[] { logRecords[0] }, 1); + Assert.NotNull(request); + Assert.Single(request.ResourceLogs); + Assert.Single(request.ResourceLogs[0].ScopeLogs); - var request = otlpLogRecordTransformer.BuildExportRequest( - new Proto.Resource.V1.Resource(), - batch); + Assert.Equal(expectedScopeName, request.ResourceLogs[0].ScopeLogs[0].Scope?.Name); + } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void LogRecordEventNameIsExportedWhenUsingBridgeApi(bool emitEventName) + { + 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(); + + logger.EmitLog(new LogRecordData + { + Body = "test body", + EventName = emitEventName ? "test event" : null, + }); + } + + Assert.Single(logRecords); + var logRecord = logRecords[0]; + + OtlpLogs.LogRecord? otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + + Assert.NotNull(otlpLogRecord); + Assert.Equal("test body", otlpLogRecord.Body.StringValue); + + if (!emitEventName) + { + Assert.Empty(otlpLogRecord.EventName); + } + else + { + Assert.Equal("test event", otlpLogRecord.EventName); + } + } + + [Fact] + public void LogSerialization_ExpandsBufferForLogsAndSerializes() + { + 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("MyLogger"); + + logger.EmitLog(new LogRecordData()); + } + + Assert.Single(logRecords); + + var batch = new Batch([logRecords[0]], 1); + + var buffer = new byte[50]; + var writePosition = ProtobufOtlpLogSerializer.WriteLogsData(ref buffer, 0, DefaultSdkLimitOptions, new(), ResourceBuilder.CreateEmpty().Build(), batch); + using var stream = new MemoryStream(buffer, 0, writePosition); + var logsData = OtlpLogs.LogsData.Parser.ParseFrom(stream); + var request = new OtlpCollector.ExportLogsServiceRequest(); + request.ResourceLogs.Add(logsData.ResourceLogs); + + Assert.True(buffer.Length > 50); Assert.NotNull(request); Assert.Single(request.ResourceLogs); Assert.Single(request.ResourceLogs[0].ScopeLogs); - Assert.Equal(expectedScopeName, request.ResourceLogs[0].ScopeLogs[0].Scope?.Name); + Assert.Equal("MyLogger", request.ResourceLogs[0].ScopeLogs[0].Scope?.Name); } private static void RunVerifyEnvironmentVariablesTakenFromIConfigurationTest( - string optionsName, + string? optionsName, Func, (IDisposable Container, ILoggerFactory LoggerFactory)> createLoggerFactoryFunc) { - var values = new Dictionary() + var values = new Dictionary { [OtlpSpecConfigDefinitions.DefaultEndpointEnvVarName] = "http://test:8888", }; @@ -1542,7 +1632,7 @@ private static void RunVerifyEnvironmentVariablesTakenFromIConfigurationTest( Assert.True(allConfigureDelegateCalled); } - private static OtlpCommon.KeyValue TryGetAttribute(OtlpLogs.LogRecord record, string key) + private static OtlpCommon.KeyValue? TryGetAttribute(OtlpLogs.LogRecord record, string key) { return record.Attributes.FirstOrDefault(att => att.Key == key); } @@ -1550,11 +1640,11 @@ private static OtlpCommon.KeyValue TryGetAttribute(OtlpLogs.LogRecord record, st private static void ConfigureOtlpExporter( ILoggingBuilder builder, bool callUseOpenTelemetry, - string name = null, - Action configureExporter = null, - Action configureExporterAndProcessor = null, - Action configureOptions = null, - List logRecords = null) + string? name = null, + Action? configureExporter = null, + Action? configureExporterAndProcessor = null, + Action? configureOptions = null, + List? logRecords = null) { if (callUseOpenTelemetry) { @@ -1605,22 +1695,37 @@ private static void ConfigureOtlpExporter( } } - private sealed class TestOptionsMonitor : IOptionsMonitor + private static OtlpCollector.ExportLogsServiceRequest CreateLogsExportRequest(SdkLimitOptions sdkOptions, ExperimentalOptions experimentalOptions, in Batch batch, Resource resource) { - private readonly T instance; - - public TestOptionsMonitor(T instance) - { - this.instance = instance; - } + var buffer = new byte[4096]; + var writePosition = ProtobufOtlpLogSerializer.WriteLogsData(ref buffer, 0, sdkOptions, experimentalOptions, resource, batch); + using var stream = new MemoryStream(buffer, 0, writePosition); + var logsData = OtlpLogs.LogsData.Parser.ParseFrom(stream); + var request = new OtlpCollector.ExportLogsServiceRequest(); + request.ResourceLogs.Add(logsData.ResourceLogs); + return request; + } - public T CurrentValue => this.instance; + private static OtlpLogs.LogRecord? ToOtlpLogs(SdkLimitOptions sdkOptions, ExperimentalOptions experimentalOptions, LogRecord logRecord) + { + var buffer = new byte[4096]; + var writePosition = ProtobufOtlpLogSerializer.WriteLogRecord(buffer, 0, sdkOptions, experimentalOptions, logRecord); + using var stream = new MemoryStream(buffer, 0, writePosition); + var scopeLogs = OtlpLogs.ScopeLogs.Parser.ParseFrom(stream); + return scopeLogs.LogRecords.FirstOrDefault(); + } - public T Get(string name) => this.instance; + private static ResourceSpans CreateResourceSpans(Resource resource) + { + byte[] buffer = new byte[1024]; + var writePosition = ProtobufOtlpResourceSerializer.WriteResource(buffer, 0, resource); - public IDisposable OnChange(Action listener) + ResourceSpans? resourceSpans; + using (var stream = new MemoryStream(buffer, 0, writePosition)) { - throw new NotImplementedException(); + resourceSpans = ResourceSpans.Parser.ParseFrom(stream); } + + return resourceSpans; } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs index 07124e63a85..52499046070 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs @@ -7,7 +7,7 @@ using Google.Protobuf; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Tests; @@ -19,13 +19,13 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; [Collection("EnvVars")] -public class OtlpMetricsExporterTests : IDisposable +public sealed class OtlpMetricsExporterTests : IDisposable { - private static readonly KeyValuePair[] KeyValues = new KeyValuePair[] - { - new KeyValuePair("key1", "value1"), - new KeyValuePair("key2", 123), - }; + private static readonly KeyValuePair[] KeyValues = + [ + new("key1", "value1"), + new("key2", 123), + ]; public OtlpMetricsExporterTests() { @@ -57,14 +57,14 @@ void CheckMetricReaderDefaults() var metricReader = typeof(MetricReader) .Assembly - .GetType("OpenTelemetry.Metrics.MeterProviderSdk") - .GetField("reader", bindingFlags) + .GetType("OpenTelemetry.Metrics.MeterProviderSdk")? + .GetField("reader", bindingFlags)? .GetValue(meterProvider) as PeriodicExportingMetricReader; Assert.NotNull(metricReader); - var exportIntervalMilliseconds = (int)typeof(PeriodicExportingMetricReader) - .GetField("ExportIntervalMilliseconds", bindingFlags) + var exportIntervalMilliseconds = (int?)typeof(PeriodicExportingMetricReader)? + .GetField("ExportIntervalMilliseconds", bindingFlags)? .GetValue(metricReader); Assert.Equal(60000, exportIntervalMilliseconds); @@ -129,8 +129,8 @@ public void UserHttpFactoryCalled() Assert.Equal(2, invocations); } - options.HttpClientFactory = () => null; - Assert.Throws(() => + options.HttpClientFactory = () => throw new NotSupportedException(); + Assert.Throws(() => { using var exporter = new OtlpMetricExporter(options); }); @@ -175,7 +175,13 @@ public void ToOtlpResourceMetricsTest(bool includeServiceNameInResource) var metrics = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{includeServiceNameInResource}", "0.0.1"); + var meterTags = new KeyValuePair[] + { + new("key1", "value1"), + new("key2", "value2"), + }; + + using var meter = new Meter(name: $"{Utils.GetCurrentMethodName()}.{includeServiceNameInResource}", version: "0.0.1", tags: meterTags); using var provider = Sdk.CreateMeterProviderBuilder() .SetResourceBuilder(resourceBuilder) .AddMeter(meter.Name) @@ -188,9 +194,7 @@ public void ToOtlpResourceMetricsTest(bool includeServiceNameInResource) provider.ForceFlush(); var batch = new Batch(metrics.ToArray(), metrics.Count); - - var request = new OtlpCollector.ExportMetricsServiceRequest(); - request.AddMetrics(resourceBuilder.Build().ToOtlpResource(), batch); + var request = CreateMetricExportRequest(batch, resourceBuilder.Build()); Assert.Single(request.ResourceMetrics); var resourceMetric = request.ResourceMetrics.First(); @@ -203,7 +207,7 @@ public void ToOtlpResourceMetricsTest(bool includeServiceNameInResource) } else { - Assert.Contains(otlpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.ToString().Contains("unknown_service:")); + Assert.DoesNotContain(otlpResource.Attributes, kvp => kvp.Key == ResourceSemanticConventions.AttributeServiceName); } Assert.Single(resourceMetric.ScopeMetrics); @@ -211,6 +215,10 @@ public void ToOtlpResourceMetricsTest(bool includeServiceNameInResource) Assert.Equal(string.Empty, instrumentationLibraryMetrics.SchemaUrl); Assert.Equal(meter.Name, instrumentationLibraryMetrics.Scope.Name); Assert.Equal("0.0.1", instrumentationLibraryMetrics.Scope.Version); + + Assert.Equal(2, instrumentationLibraryMetrics.Scope.Attributes.Count); + Assert.Contains(instrumentationLibraryMetrics.Scope.Attributes, (kvp) => kvp.Key == "key1" && kvp.Value.StringValue == "value1"); + Assert.Contains(instrumentationLibraryMetrics.Scope.Attributes, (kvp) => kvp.Key == "key2" && kvp.Value.StringValue == "value2"); } [Theory] @@ -219,7 +227,7 @@ public void ToOtlpResourceMetricsTest(bool includeServiceNameInResource) [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) + public void TestGaugeToOtlpMetric(string name, string? description, string? unit, long? longValue, double? doubleValue, bool enableExemplars = false) { var metrics = new List(); @@ -236,15 +244,14 @@ public void TestGaugeToOtlpMetric(string name, string description, string unit, } else { - meter.CreateObservableGauge(name, () => doubleValue.Value, unit, description); + meter.CreateObservableGauge(name, () => doubleValue!.Value, unit, description); } provider.ForceFlush(); var batch = new Batch(metrics.ToArray(), metrics.Count); - var request = new OtlpCollector.ExportMetricsServiceRequest(); - request.AddMetrics(ResourceBuilder.CreateEmpty().Build().ToOtlpResource(), batch); + var request = CreateMetricExportRequest(batch, ResourceBuilder.CreateEmpty().Build()); var resourceMetric = request.ResourceMetrics.Single(); var scopeMetrics = resourceMetric.ScopeMetrics.Single(); @@ -294,7 +301,7 @@ public void TestGaugeToOtlpMetric(string name, string description, string unit, [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) + 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(); @@ -308,7 +315,7 @@ public void TestCounterToOtlpMetric(string name, string description, string unit }) .Build(); - var attributes = enableKeyValues ? KeyValues : Array.Empty>(); + var attributes = enableKeyValues ? KeyValues : Array.Empty>(); if (longValue.HasValue) { var counter = meter.CreateCounter(name, unit, description); @@ -317,15 +324,13 @@ public void TestCounterToOtlpMetric(string name, string description, string unit else { var counter = meter.CreateCounter(name, unit, description); - counter.Add(doubleValue.Value, attributes); + counter.Add(doubleValue!.Value, attributes); } provider.ForceFlush(); var batch = new Batch(metrics.ToArray(), metrics.Count); - - var request = new OtlpCollector.ExportMetricsServiceRequest(); - request.AddMetrics(ResourceBuilder.CreateEmpty().Build().ToOtlpResource(), batch); + var request = CreateMetricExportRequest(batch, ResourceBuilder.CreateEmpty().Build()); var resourceMetric = request.ResourceMetrics.Single(); var scopeMetrics = resourceMetric.ScopeMetrics.Single(); @@ -391,7 +396,7 @@ public void TestCounterToOtlpMetric(string name, string description, string unit [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) + 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(); @@ -405,7 +410,7 @@ public void TestUpDownCounterToOtlpMetric(string name, string description, strin }) .Build(); - var attributes = enableKeyValues ? KeyValues : Array.Empty>(); + var attributes = enableKeyValues ? KeyValues : Array.Empty>(); if (longValue.HasValue) { var counter = meter.CreateUpDownCounter(name, unit, description); @@ -414,15 +419,14 @@ public void TestUpDownCounterToOtlpMetric(string name, string description, strin else { var counter = meter.CreateUpDownCounter(name, unit, description); - counter.Add(doubleValue.Value, attributes); + counter.Add(doubleValue!.Value, attributes); } provider.ForceFlush(); var batch = new Batch(metrics.ToArray(), metrics.Count); - var request = new OtlpCollector.ExportMetricsServiceRequest(); - request.AddMetrics(ResourceBuilder.CreateEmpty().Build().ToOtlpResource(), batch); + var request = CreateMetricExportRequest(batch, ResourceBuilder.CreateEmpty().Build()); var resourceMetric = request.ResourceMetrics.Single(); var scopeMetrics = resourceMetric.ScopeMetrics.Single(); @@ -488,7 +492,7 @@ public void TestUpDownCounterToOtlpMetric(string name, string description, strin [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) + 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(); @@ -506,7 +510,7 @@ public void TestExponentialHistogramToOtlpMetric(string name, string description }) .Build(); - var attributes = enableKeyValues ? KeyValues : Array.Empty>(); + var attributes = enableKeyValues ? KeyValues : Array.Empty>(); if (longValue.HasValue) { var histogram = meter.CreateHistogram(name, unit, description); @@ -516,16 +520,14 @@ public void TestExponentialHistogramToOtlpMetric(string name, string description else { var histogram = meter.CreateHistogram(name, unit, description); - histogram.Record(doubleValue.Value, attributes); + histogram.Record(doubleValue!.Value, attributes); histogram.Record(0, attributes); } provider.ForceFlush(); var batch = new Batch(metrics.ToArray(), metrics.Count); - - var request = new OtlpCollector.ExportMetricsServiceRequest(); - request.AddMetrics(ResourceBuilder.CreateEmpty().Build().ToOtlpResource(), batch); + var request = CreateMetricExportRequest(batch, ResourceBuilder.CreateEmpty().Build()); var resourceMetric = request.ResourceMetrics.Single(); var scopeMetrics = resourceMetric.ScopeMetrics.Single(); @@ -577,7 +579,7 @@ public void TestExponentialHistogramToOtlpMetric(string name, string description { Assert.Equal(0, dataPoint.Sum); Assert.Null(dataPoint.Negative); - Assert.True(dataPoint.Positive.Offset == 0); + Assert.Equal(0, dataPoint.Positive.Offset); Assert.Empty(dataPoint.Positive.BucketCounts); } } @@ -594,7 +596,7 @@ public void TestExponentialHistogramToOtlpMetric(string name, string description { Assert.Equal(0, dataPoint.Sum); Assert.Null(dataPoint.Negative); - Assert.True(dataPoint.Positive.Offset == 0); + Assert.Equal(0, dataPoint.Positive.Offset); Assert.Empty(dataPoint.Positive.BucketCounts); } } @@ -628,7 +630,7 @@ public void TestExponentialHistogramToOtlpMetric(string name, string description [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) + 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(); @@ -642,7 +644,7 @@ public void TestHistogramToOtlpMetric(string name, string description, string un }) .Build(); - var attributes = enableKeyValues ? KeyValues : Array.Empty>(); + var attributes = enableKeyValues ? KeyValues : Array.Empty>(); if (longValue.HasValue) { var histogram = meter.CreateHistogram(name, unit, description); @@ -651,15 +653,13 @@ public void TestHistogramToOtlpMetric(string name, string description, string un else { var histogram = meter.CreateHistogram(name, unit, description); - histogram.Record(doubleValue.Value, attributes); + histogram.Record(doubleValue!.Value, attributes); } provider.ForceFlush(); var batch = new Batch(metrics.ToArray(), metrics.Count); - - var request = new OtlpCollector.ExportMetricsServiceRequest(); - request.AddMetrics(ResourceBuilder.CreateEmpty().Build().ToOtlpResource(), batch); + var request = CreateMetricExportRequest(batch, ResourceBuilder.CreateEmpty().Build()); var resourceMetric = request.ResourceMetrics.Single(); var scopeMetrics = resourceMetric.ScopeMetrics.Single(); @@ -732,7 +732,7 @@ public void TestTemporalityPreferenceUsingConfiguration(string configValue, Metr { var testExecuted = false; - var configData = new Dictionary { [OtlpSpecConfigDefinitionTests.MetricsData.TemporalityKeyName] = configValue }; + var configData = new Dictionary { [OtlpSpecConfigDefinitionTests.MetricsData.TemporalityKeyName] = configValue }; var configuration = new ConfigurationBuilder() .AddInMemoryCollection(configData) .Build(); @@ -786,9 +786,9 @@ public void TestTemporalityPreferenceUsingEnvVar(string configValue, MetricReade [InlineData(true, true)] public void ToOtlpExemplarTests(bool enableTagFiltering, bool enableTracing) { - ActivitySource activitySource = null; - Activity activity = null; - TracerProvider tracerProvider = null; + ActivitySource? activitySource = null; + Activity? activity = null; + TracerProvider? tracerProvider = null; using var meter = new Meter(Utils.GetCurrentMethodName()); @@ -823,42 +823,39 @@ public void ToOtlpExemplarTests(bool enableTagFiltering, bool enableTracing) var counterDouble = meter.CreateCounter("testCounterDouble"); var counterLong = meter.CreateCounter("testCounterLong"); - counterDouble.Add(1.18D, new KeyValuePair("key1", "value1")); - counterLong.Add(18L, new KeyValuePair("key1", "value1")); + counterDouble.Add(1.18D, new KeyValuePair("key1", "value1")); + counterLong.Add(18L, new KeyValuePair("key1", "value1")); meterProvider.ForceFlush(); - var counterDoubleMetric = exportedItems.FirstOrDefault(m => m.Name == counterDouble.Name); - var counterLongMetric = exportedItems.FirstOrDefault(m => m.Name == counterLong.Name); + var batch = new Batch(exportedItems.ToArray(), exportedItems.Count); + var request = CreateMetricExportRequest(batch, ResourceBuilder.CreateEmpty().Build()); - Assert.NotNull(counterDoubleMetric); - Assert.NotNull(counterLongMetric); + Assert.Single(request.ResourceMetrics); + var resourceMetric = request.ResourceMetrics.First(); + var otlpResource = resourceMetric.Resource; - AssertExemplars(1.18D, counterDoubleMetric); - AssertExemplars(18L, counterLongMetric); + Assert.Single(resourceMetric.ScopeMetrics); + var instrumentationLibraryMetrics = resourceMetric.ScopeMetrics.First(); + Assert.Equal(meter.Name, instrumentationLibraryMetrics.Scope.Name); + + var scopeMetrics = resourceMetric.ScopeMetrics.Single(); + var otlpCounterDoubleMetric = scopeMetrics.Metrics.Single(m => m.Name == counterDouble.Name); + var otlpCounterLongMetric = scopeMetrics.Metrics.Single(m => m.Name == counterLong.Name); + + AssertExemplars(1.18D, otlpCounterDoubleMetric); + AssertExemplars(18L, otlpCounterLongMetric); activity?.Dispose(); tracerProvider?.Dispose(); activitySource?.Dispose(); - void AssertExemplars(T value, Metric metric) + void AssertExemplars(T value, OtlpMetrics.Metric metric) where T : struct { - var metricPointEnumerator = metric.GetMetricPoints().GetEnumerator(); - Assert.True(metricPointEnumerator.MoveNext()); - - 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.Single(metric.Sum.DataPoints); + var dataPoint = metric.Sum.DataPoints.First(); + var otlpExemplar = dataPoint.Exemplars.First(); Assert.NotEqual(default, otlpExemplar.TimeUnixNano); if (!enableTracing) @@ -869,6 +866,7 @@ void AssertExemplars(T value, Metric metric) else { byte[] traceIdBytes = new byte[16]; + Assert.NotNull(activity); activity.TraceId.CopyTo(traceIdBytes); byte[] spanIdBytes = new byte[8]; @@ -880,41 +878,90 @@ void AssertExemplars(T value, Metric metric) if (typeof(T) == typeof(long)) { - Assert.Equal((long)(object)value, exemplar.LongValue); + Assert.Equal((long)(object)value, otlpExemplar.AsInt); } else if (typeof(T) == typeof(double)) { - Assert.Equal((double)(object)value, exemplar.DoubleValue); + Assert.Equal((double)(object)value, otlpExemplar.AsDouble); } else { - Debug.Fail("Unexpected type"); + Assert.Fail("Unexpected type"); } if (!enableTagFiltering) { - var tagEnumerator = exemplar.FilteredTags.GetEnumerator(); + var tagEnumerator = otlpExemplar.FilteredAttributes.GetEnumerator(); Assert.False(tagEnumerator.MoveNext()); } else { - var tagEnumerator = exemplar.FilteredTags.GetEnumerator(); + var tagEnumerator = otlpExemplar.FilteredAttributes.GetEnumerator(); Assert.True(tagEnumerator.MoveNext()); var tag = tagEnumerator.Current; Assert.Equal("key1", tag.Key); - Assert.Equal("value1", tag.Value); + Assert.Equal("value1", tag.Value.StringValue); } } } + [Fact] + public void MetricsSerialization_ExpandsBufferForMetricsAndSerializes() + { + var metrics = new List(); + + var meterTags = new KeyValuePair[] + { + new("key1", "value1"), + new("key2", "value2"), + }; + + using var meter = new Meter(name: Utils.GetCurrentMethodName(), version: "0.0.1", tags: meterTags); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + var counter = meter.CreateCounter("counter"); + counter.Add(100); + + provider.ForceFlush(); + + var batch = new Batch(metrics.ToArray(), metrics.Count); + + var buffer = new byte[50]; + var writePosition = ProtobufOtlpMetricSerializer.WriteMetricsData(ref buffer, 0, ResourceBuilder.CreateEmpty().Build(), in batch); + using var stream = new MemoryStream(buffer, 0, writePosition); + + var metricsData = OtlpMetrics.MetricsData.Parser.ParseFrom(stream); + + var request = new OtlpCollector.ExportMetricsServiceRequest(); + request.ResourceMetrics.Add(metricsData.ResourceMetrics); + + Assert.True(buffer.Length > 50); + + Assert.Single(request.ResourceMetrics); + var resourceMetric = request.ResourceMetrics.First(); + + 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); + + Assert.Equal(2, instrumentationLibraryMetrics.Scope.Attributes.Count); + Assert.Contains(instrumentationLibraryMetrics.Scope.Attributes, (kvp) => kvp.Key == "key1" && kvp.Value.StringValue == "value1"); + Assert.Contains(instrumentationLibraryMetrics.Scope.Attributes, (kvp) => kvp.Key == "key2" && kvp.Value.StringValue == "value2"); + } + public void Dispose() { OtlpSpecConfigDefinitionTests.ClearEnvVars(); GC.SuppressFinalize(this); } - private static void VerifyExemplars(long? longValue, double? doubleValue, bool enableExemplars, Func getExemplarFunc, T state) + private static void VerifyExemplars(long? longValue, double? doubleValue, bool enableExemplars, Func getExemplarFunc, T state) { var exemplar = getExemplarFunc(state); @@ -928,6 +975,7 @@ private static void VerifyExemplars(long? longValue, double? doubleValue, boo } else { + Assert.NotNull(doubleValue); Assert.Equal(doubleValue.Value, exemplar.AsDouble); } } @@ -936,4 +984,17 @@ private static void VerifyExemplars(long? longValue, double? doubleValue, boo Assert.Null(exemplar); } } + + private static OtlpCollector.ExportMetricsServiceRequest CreateMetricExportRequest(in Batch batch, Resource resource) + { + var buffer = new byte[4096]; + var writePosition = ProtobufOtlpMetricSerializer.WriteMetricsData(ref buffer, 0, resource, in batch); + using var stream = new MemoryStream(buffer, 0, writePosition); + + var metricsData = OtlpMetrics.MetricsData.Parser.ParseFrom(stream); + + var request = new OtlpCollector.ExportMetricsServiceRequest(); + request.ResourceMetrics.Add(metricsData.ResourceMetrics); + return request; + } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpResourceTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpResourceTests.cs index 4a82310103f..5bc0c3601ae 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpResourceTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpResourceTests.cs @@ -1,7 +1,8 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; +using OpenTelemetry.Proto.Trace.V1; using OpenTelemetry.Resources; using Xunit; @@ -23,7 +24,18 @@ public void ToOtlpResourceTest(bool includeServiceNameInResource) } var resource = resourceBuilder.Build(); - var otlpResource = resource.ToOtlpResource(); + Proto.Resource.V1.Resource otlpResource; + + byte[] buffer = new byte[1024]; + var writePosition = ProtobufOtlpResourceSerializer.WriteResource(buffer, 0, resource); + + // Deserialize the ResourceSpans and validate the attributes. + using (var stream = new MemoryStream(buffer, 0, writePosition)) + { + var resourceSpans = ResourceSpans.Parser.ParseFrom(stream); + otlpResource = resourceSpans.Resource; + } + if (includeServiceNameInResource) { Assert.Contains(otlpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.StringValue == "service-name"); @@ -31,7 +43,7 @@ public void ToOtlpResourceTest(bool includeServiceNameInResource) } else { - Assert.Contains(otlpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.ToString().Contains("unknown_service:")); + Assert.DoesNotContain(otlpResource.Attributes, kvp => kvp.Key == ResourceSemanticConventions.AttributeServiceName); } } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpRetryTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpRetryTests.cs index 25acb30e3e6..9e3a266a749 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpRetryTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpRetryTests.cs @@ -1,41 +1,39 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - -using System.Net; -using System.Net.Http.Headers; -#if NETFRAMEWORK -using System.Net.Http; -#endif -using Google.Protobuf; -using Google.Protobuf.WellKnownTypes; -using Grpc.Core; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient.Grpc; using Xunit; namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient.Tests; public class OtlpRetryTests { - public static IEnumerable GrpcRetryTestData => GrpcRetryTestCase.GetGrpcTestCases(); + public static TheoryData GrpcRetryTestData => GrpcRetryTestCase.GetGrpcTestCases(); - public static IEnumerable HttpRetryTestData => HttpRetryTestCase.GetHttpTestCases(); + public static TheoryData HttpRetryTestData => HttpRetryTestCase.GetHttpTestCases(); [Theory] [MemberData(nameof(GrpcRetryTestData))] public void TryGetGrpcRetryResultTest(GrpcRetryTestCase testCase) { +#if NET + Assert.NotNull(testCase); +#else + if (testCase == null) + { + throw new ArgumentNullException(nameof(testCase)); + } +#endif var attempts = 0; var nextRetryDelayMilliseconds = OtlpRetry.InitialBackoffMilliseconds; foreach (var retryAttempt in testCase.RetryAttempts) { ++attempts; - var rpcException = retryAttempt.Response.Exception as RpcException; - Assert.NotNull(rpcException); - var statusCode = rpcException.StatusCode; + Assert.NotNull(retryAttempt.Response.Status); + var statusCode = retryAttempt.Response.Status.Value.StatusCode; var deadline = retryAttempt.Response.DeadlineUtc; - var trailers = rpcException.Trailers; + var trailers = retryAttempt.Response.GrpcStatusDetailsHeader; var success = OtlpRetry.TryGetGrpcRetryResult(retryAttempt.Response, nextRetryDelayMilliseconds, out var retryResult); Assert.Equal(retryAttempt.ExpectedSuccess, success); @@ -48,7 +46,7 @@ public void TryGetGrpcRetryResultTest(GrpcRetryTestCase testCase) if (retryResult.Throttled) { - Assert.Equal(retryAttempt.ThrottleDelay, retryResult.RetryDelay); + Assert.Equal(GrpcStatusDeserializer.TryGetGrpcRetryDelay(retryAttempt.ThrottleDelay), retryResult.RetryDelay); } else { @@ -68,6 +66,14 @@ public void TryGetGrpcRetryResultTest(GrpcRetryTestCase testCase) [MemberData(nameof(HttpRetryTestData))] public void TryGetHttpRetryResultTest(HttpRetryTestCase testCase) { +#if NET + Assert.NotNull(testCase); +#else + if (testCase == null) + { + throw new ArgumentNullException(nameof(testCase)); + } +#endif var attempts = 0; var nextRetryDelayMilliseconds = OtlpRetry.InitialBackoffMilliseconds; @@ -104,244 +110,4 @@ public void TryGetHttpRetryResultTest(HttpRetryTestCase testCase) 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 GetGrpcTestCases() - { - 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), - }; - } - - 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 TimeSpan? ThrottleDelay; - public int? ExpectedNextRetryDelayMilliseconds; - public bool ExpectedSuccess; - internal ExportClientGrpcResponse Response; - - public GrpcRetryAttempt( - StatusCode statusCode, - bool deadlineExceeded = false, - Duration? throttleDelay = null, - int expectedNextRetryDelayMilliseconds = 1500, - bool expectedSuccess = true) - { - var status = new Status(statusCode, "Error"); - var rpcException = throttleDelay != null - ? new RpcException(status, GenerateTrailers(throttleDelay)) - : new RpcException(status); - - // Using arbitrary +1 hr for deadline for test purposes. - var deadlineUtc = deadlineExceeded ? DateTime.UtcNow.AddSeconds(-1) : DateTime.UtcNow.AddHours(1); - - this.ThrottleDelay = throttleDelay != null ? throttleDelay.ToTimeSpan() : null; - - this.Response = new ExportClientGrpcResponse(expectedSuccess, deadlineUtc, rpcException); - - this.ExpectedNextRetryDelayMilliseconds = expectedNextRetryDelayMilliseconds; - - this.ExpectedSuccess = expectedSuccess; - } - } - } - - public class HttpRetryTestCase - { - public int ExpectedRetryAttempts; - internal HttpRetryAttempt[] RetryAttempts; - - private string testRunnerName; - - private HttpRetryTestCase(string testRunnerName, HttpRetryAttempt[] retryAttempts, int expectedRetryAttempts = 1) - { - this.ExpectedRetryAttempts = expectedRetryAttempts; - this.RetryAttempts = retryAttempts; - this.testRunnerName = testRunnerName; - } - - public static IEnumerable GetHttpTestCases() - { - yield return new[] { new HttpRetryTestCase("NetworkError", [new(statusCode: null)]) }; - yield return new[] { new HttpRetryTestCase("GatewayTimeout", [new(statusCode: HttpStatusCode.GatewayTimeout, throttleDelay: TimeSpan.FromSeconds(1))]) }; -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - yield return new[] { new HttpRetryTestCase("ServiceUnavailable", [new(statusCode: HttpStatusCode.TooManyRequests, throttleDelay: TimeSpan.FromSeconds(1))]) }; -#endif - - yield return new[] - { - new HttpRetryTestCase( - "Exponential Backoff", - new HttpRetryAttempt[] - { - new(statusCode: null, expectedNextRetryDelayMilliseconds: 1500), - new(statusCode: null, expectedNextRetryDelayMilliseconds: 2250), - new(statusCode: null, expectedNextRetryDelayMilliseconds: 3375), - new(statusCode: null, expectedNextRetryDelayMilliseconds: 5000), - new(statusCode: null, expectedNextRetryDelayMilliseconds: 5000), - }, - expectedRetryAttempts: 5), - }; - - yield return new[] - { - new HttpRetryTestCase( - "Retry until non-retryable status code encountered", - new HttpRetryAttempt[] - { - new(statusCode: HttpStatusCode.ServiceUnavailable, expectedNextRetryDelayMilliseconds: 1500), - new(statusCode: HttpStatusCode.ServiceUnavailable, expectedNextRetryDelayMilliseconds: 2250), - new(statusCode: HttpStatusCode.ServiceUnavailable, expectedNextRetryDelayMilliseconds: 3375), - new(statusCode: HttpStatusCode.BadRequest, expectedSuccess: false), - new(statusCode: HttpStatusCode.ServiceUnavailable, expectedNextRetryDelayMilliseconds: 5000), - }, - expectedRetryAttempts: 4), - }; - - yield return new[] { new HttpRetryTestCase("Expired deadline", new HttpRetryAttempt[] { new(statusCode: HttpStatusCode.ServiceUnavailable, isDeadlineExceeded: true, expectedSuccess: false) }) }; - - // TODO: Add more cases. - } - - public override string ToString() - { - return this.testRunnerName; - } - - internal class HttpRetryAttempt - { - public ExportClientHttpResponse Response; - public TimeSpan? ThrottleDelay; - public int? ExpectedNextRetryDelayMilliseconds; - public bool ExpectedSuccess; - - internal HttpRetryAttempt( - HttpStatusCode? statusCode, - TimeSpan? throttleDelay = null, - bool isDeadlineExceeded = false, - int expectedNextRetryDelayMilliseconds = 1500, - bool expectedSuccess = true) - { - this.ThrottleDelay = throttleDelay; - - HttpResponseMessage? responseMessage = null; - if (statusCode != null) - { - responseMessage = new HttpResponseMessage(); - - if (throttleDelay != null) - { - responseMessage.Headers.RetryAfter = new RetryConditionHeaderValue(throttleDelay.Value); - } - - responseMessage.StatusCode = (HttpStatusCode)statusCode; - } - - // Using arbitrary +1 hr for deadline for test purposes. - var deadlineUtc = isDeadlineExceeded ? DateTime.UtcNow.AddMilliseconds(-1) : DateTime.UtcNow.AddHours(1); - this.Response = new ExportClientHttpResponse(expectedSuccess, deadlineUtc, responseMessage, new HttpRequestException()); - this.ExpectedNextRetryDelayMilliseconds = expectedNextRetryDelayMilliseconds; - this.ExpectedSuccess = expectedSuccess; - } - } - } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionTests.cs index 4e4e5469183..c5d1b0717a4 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionTests.cs @@ -1,9 +1,8 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Collections; +using System.Globalization; using Microsoft.Extensions.Configuration; using OpenTelemetry.Metrics; using Xunit; @@ -192,12 +191,13 @@ public IConfiguration ToConfiguration() public ConfigurationBuilder AddToConfiguration(ConfigurationBuilder configurationBuilder) { - Dictionary dictionary = new(); - - dictionary[this.EndpointKeyName] = this.EndpointValue; - dictionary[this.HeadersKeyName] = this.HeadersValue; - dictionary[this.TimeoutKeyName] = this.TimeoutValue; - dictionary[this.ProtocolKeyName] = this.ProtocolValue; + Dictionary dictionary = new() + { + [this.EndpointKeyName] = this.EndpointValue, + [this.HeadersKeyName] = this.HeadersValue, + [this.TimeoutKeyName] = this.TimeoutValue, + [this.ProtocolKeyName] = this.ProtocolValue, + }; this.OnAddToDictionary(dictionary); @@ -230,7 +230,7 @@ public void AssertMatches(IOtlpExporterOptions otlpExporterOptions) { Assert.Equal(new Uri(this.EndpointValue), otlpExporterOptions.Endpoint); Assert.Equal(this.HeadersValue, otlpExporterOptions.Headers); - Assert.Equal(int.Parse(this.TimeoutValue), otlpExporterOptions.TimeoutMilliseconds); + Assert.Equal(int.Parse(this.TimeoutValue, CultureInfo.InvariantCulture), otlpExporterOptions.TimeoutMilliseconds); if (!OtlpExportProtocolParser.TryParse(this.ProtocolValue, out var protocol)) { @@ -293,7 +293,11 @@ public MetricsTestData( public void AssertMatches(MetricReaderOptions metricReaderOptions) { +#if NET + Assert.Equal(Enum.Parse(this.TemporalityValue), metricReaderOptions.TemporalityPreference); +#else Assert.Equal(Enum.Parse(typeof(MetricReaderTemporalityPreference), this.TemporalityValue), metricReaderOptions.TemporalityPreference); +#endif } protected override void OnSetEnvVars() diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTestHelpers.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTestHelpers.cs index f6011fed01c..7cc6b10336e 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTestHelpers.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTestHelpers.cs @@ -1,6 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Globalization; using Google.Protobuf.Collections; using Xunit; using OtlpCommon = OpenTelemetry.Proto.Common.V1; @@ -10,7 +11,7 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; internal static class OtlpTestHelpers { public static void AssertOtlpAttributes( - IEnumerable> expected, + IEnumerable> expected, RepeatedField actual) { var expectedAttributes = expected.ToList(); @@ -19,6 +20,7 @@ public static void AssertOtlpAttributes( { var current = expectedAttributes[i].Value; Assert.Equal(expectedAttributes[i].Key, actual[i].Key); + Assert.NotNull(current); if (current.GetType().IsArray) { @@ -90,7 +92,7 @@ public static void AssertOtlpAttributes( Assert.Equal(expectedSize, actual.Count); } - private static void AssertOtlpAttributeValue(object expected, OtlpCommon.AnyValue actual) + private static void AssertOtlpAttributeValue(object? expected, OtlpCommon.AnyValue actual) { switch (expected) { @@ -110,7 +112,7 @@ private static void AssertOtlpAttributeValue(object expected, OtlpCommon.AnyValu Assert.Equal(i, actual.IntValue); break; default: - Assert.Equal(expected.ToString(), actual.StringValue); + Assert.Equal(Convert.ToString(expected, CultureInfo.InvariantCulture), actual.StringValue); break; } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs index 18434043bc7..d9e1fdb9a39 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs @@ -5,6 +5,7 @@ using Google.Protobuf.Collections; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; @@ -14,29 +15,39 @@ using OtlpCollector = OpenTelemetry.Proto.Collector.Trace.V1; using OtlpCommon = OpenTelemetry.Proto.Common.V1; using OtlpTrace = OpenTelemetry.Proto.Trace.V1; -using Status = OpenTelemetry.Trace.Status; namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; [Collection("xUnitCollectionPreventingTestsThatDependOnSdkConfigurationFromRunningInParallel")] -public class OtlpTraceExporterTests +public sealed class OtlpTraceExporterTests : IDisposable { private static readonly SdkLimitOptions DefaultSdkLimitOptions = new(); - private static readonly ExperimentalOptions DefaultExperimentalOptions = new(); + private readonly ActivityListener activityListener; + static OtlpTraceExporterTests() { Activity.DefaultIdFormat = ActivityIdFormat.W3C; Activity.ForceDefaultIdFormat = true; + } - var listener = new ActivityListener + public OtlpTraceExporterTests() + { + this.activityListener = new ActivityListener { ShouldListenTo = _ => true, - Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, + Sample = (ref ActivityCreationOptions options) => options.Parent.TraceFlags.HasFlag(ActivityTraceFlags.Recorded) + ? ActivitySamplingResult.AllDataAndRecorded + : ActivitySamplingResult.AllData, }; - ActivitySource.AddActivityListener(listener); + ActivitySource.AddActivityListener(this.activityListener); + } + + public void Dispose() + { + this.activityListener.Dispose(); } [Fact] @@ -63,8 +74,8 @@ public void AddOtlpTraceExporterNamedOptionsSupported() [Fact] public void OtlpExporter_BadArgs() { - TracerProviderBuilder builder = null; - Assert.Throws(() => builder.AddOtlpExporter()); + TracerProviderBuilder? builder = null; + Assert.Throws(() => builder!.AddOtlpExporter()); } [Fact] @@ -98,7 +109,7 @@ public void UserHttpFactoryCalled() Assert.Equal(2, invocations); } - options.HttpClientFactory = () => null; + options.HttpClientFactory = () => null!; Assert.Throws(() => { using var exporter = new OtlpTraceExporter(options); @@ -131,8 +142,8 @@ public void ServiceProviderHttpClientFactoryInvoked() [InlineData(false)] public void ToOtlpResourceSpansTest(bool includeServiceNameInResource) { - var evenTags = new[] { new KeyValuePair("k0", "v0") }; - var oddTags = new[] { new KeyValuePair("k1", "v1") }; + var evenTags = new[] { new KeyValuePair("k0", "v0") }; + var oddTags = new[] { new KeyValuePair("k1", "v1") }; var sources = new[] { new ActivitySource("even", "2.4.6"), @@ -150,7 +161,9 @@ public void ToOtlpResourceSpansTest(bool includeServiceNameInResource) .SetResourceBuilder(resourceBuilder) .AddSource(sources[0].Name) .AddSource(sources[1].Name) +#pragma warning disable CA2000 // Dispose objects before losing scope .AddProcessor(new SimpleActivityExportProcessor(new InMemoryExporter(exportedItems))); +#pragma warning restore CA2000 // Dispose objects before losing scope using var openTelemetrySdk = builder.Build(); @@ -163,7 +176,7 @@ public void ToOtlpResourceSpansTest(bool includeServiceNameInResource) var activityKind = isEven ? ActivityKind.Client : ActivityKind.Server; var activityTags = isEven ? evenTags : oddTags; - using Activity activity = source.StartActivity($"span-{i}", activityKind, parentContext: default, activityTags); + using Activity? activity = source.StartActivity($"span-{i}", activityKind, parentContext: default, activityTags); } Assert.Equal(10, exportedItems.Count); @@ -172,9 +185,7 @@ public void ToOtlpResourceSpansTest(bool includeServiceNameInResource) void RunTest(SdkLimitOptions sdkOptions, Batch batch) { - var request = new OtlpCollector.ExportTraceServiceRequest(); - - request.AddBatch(sdkOptions, resourceBuilder.Build().ToOtlpResource(), batch); + var request = CreateTraceExportRequest(sdkOptions, batch, resourceBuilder.Build()); Assert.Single(request.ResourceSpans); var otlpResource = request.ResourceSpans.First().Resource; @@ -185,7 +196,7 @@ void RunTest(SdkLimitOptions sdkOptions, Batch batch) } else { - Assert.Contains(otlpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.ToString().Contains("unknown_service:")); + Assert.DoesNotContain(otlpResource.Attributes, kvp => kvp.Key == ResourceSemanticConventions.AttributeServiceName); } var scopeSpans = request.ResourceSpans.First().ScopeSpans; @@ -221,6 +232,148 @@ void RunTest(SdkLimitOptions sdkOptions, Batch batch) } } + [Fact] + public void ScopeAttributesRemainConsistentAcrossMultipleBatches() + { + var activitySourceTags = new TagList + { + new("k0", "v0"), + }; + + using var activitySourceWithTags = new ActivitySource($"{nameof(this.ScopeAttributesRemainConsistentAcrossMultipleBatches)}_WithTags", "1.1.1.3", activitySourceTags); + using var activitySourceWithoutTags = new ActivitySource($"{nameof(this.ScopeAttributesRemainConsistentAcrossMultipleBatches)}_WithoutTags", "1.1.1.4"); + + var resourceBuilder = ResourceBuilder.CreateDefault(); + + var exportedItems = new List(); + var builder = Sdk.CreateTracerProviderBuilder() + .SetResourceBuilder(resourceBuilder) + .AddSource(activitySourceWithTags.Name) + .AddSource(activitySourceWithoutTags.Name) +#pragma warning disable CA2000 // Dispose objects before losing scope + .AddProcessor(new SimpleActivityExportProcessor(new InMemoryExporter(exportedItems))); +#pragma warning restore CA2000 // Dispose objects before losing scope + + using var openTelemetrySdk = builder.Build(); + + var parentActivity = activitySourceWithTags.StartActivity("parent", ActivityKind.Server, default(ActivityContext)); + var nestedChildActivity = activitySourceWithTags.StartActivity("nested-child", ActivityKind.Client); + parentActivity?.Dispose(); + nestedChildActivity?.Dispose(); + + Assert.Equal(2, exportedItems.Count); + var batch = new Batch(exportedItems.ToArray(), exportedItems.Count); + RunTest(DefaultSdkLimitOptions, batch, activitySourceWithTags); + + exportedItems.Clear(); + + var parentActivityNoTags = activitySourceWithoutTags.StartActivity("parent", ActivityKind.Server, default(ActivityContext)); + parentActivityNoTags?.Dispose(); + + Assert.Single(exportedItems); + batch = new Batch(exportedItems.ToArray(), exportedItems.Count); + RunTest(DefaultSdkLimitOptions, batch, activitySourceWithoutTags); + + void RunTest(SdkLimitOptions sdkOptions, Batch batch, ActivitySource activitySource) + { + var request = CreateTraceExportRequest(sdkOptions, batch, resourceBuilder.Build()); + + var resourceSpans = request.ResourceSpans.First(); + Assert.NotNull(request.ResourceSpans.First()); + + var scopeSpans = resourceSpans.ScopeSpans.First(); + Assert.NotNull(scopeSpans); + + var scope = scopeSpans.Scope; + Assert.NotNull(scope); + + Assert.Equal(activitySource.Name, scope.Name); + Assert.Equal(activitySource.Version, scope.Version); + Assert.Equal(activitySource.Tags?.Count() ?? 0, scope.Attributes.Count); + + foreach (var tag in activitySource.Tags ?? []) + { + Assert.Contains(scope.Attributes, (kvp) => kvp.Key == tag.Key && kvp.Value.StringValue == (string?)tag.Value); + } + + // Return and re-add batch to simulate reuse + request = CreateTraceExportRequest(DefaultSdkLimitOptions, batch, ResourceBuilder.CreateDefault().Build()); + + resourceSpans = request.ResourceSpans.First(); + scopeSpans = resourceSpans.ScopeSpans.First(); + scope = scopeSpans.Scope; + + Assert.Equal(activitySource.Name, scope.Name); + Assert.Equal(activitySource.Version, scope.Version); + Assert.Equal(activitySource.Tags?.Count() ?? 0, scope.Attributes.Count); + + foreach (var tag in activitySource.Tags ?? []) + { + Assert.Contains(scope.Attributes, (kvp) => kvp.Key == tag.Key && kvp.Value.StringValue == (string?)tag.Value); + } + } + } + + [Fact] + public void ScopeAttributesLimitsTest() + { + var sdkOptions = new SdkLimitOptions() + { + AttributeValueLengthLimit = 4, + AttributeCountLimit = 3, + }; + + // ActivitySource Tags are sorted in .NET. + var activitySourceTags = new TagList + { + new("1_TruncatedSourceTag", "12345"), + new("2_TruncatedSourceStringArray", new string?[] { "12345", "1234", string.Empty, null }), + new("3_TruncatedSourceObjectTag", new object()), + new("4_OneSourceTagTooMany", 1), + }; + + var resourceBuilder = ResourceBuilder.CreateDefault(); + + using var activitySource = new ActivitySource(name: nameof(this.ScopeAttributesLimitsTest), tags: activitySourceTags); + + var exportedItems = new List(); + var builder = Sdk.CreateTracerProviderBuilder() + .SetResourceBuilder(resourceBuilder) + .AddSource(activitySource.Name) +#pragma warning disable CA2000 // Dispose objects before losing scope + .AddProcessor(new SimpleActivityExportProcessor(new InMemoryExporter(exportedItems))); +#pragma warning restore CA2000 // Dispose objects before losing scope + + using var openTelemetrySdk = builder.Build(); + + var activity = activitySource.StartActivity("parent", ActivityKind.Server, default(ActivityContext)); + activity?.Dispose(); + + Assert.Single(exportedItems); + var batch = new Batch(exportedItems.ToArray(), exportedItems.Count); + RunTest(sdkOptions, batch); + + void RunTest(SdkLimitOptions sdkOptions, Batch batch) + { + var request = CreateTraceExportRequest(sdkOptions, batch, resourceBuilder.Build()); + + var resourceSpans = request.ResourceSpans.First(); + Assert.NotNull(request.ResourceSpans.First()); + + var scopeSpans = resourceSpans.ScopeSpans.First(); + Assert.NotNull(scopeSpans); + + var scope = scopeSpans.Scope; + Assert.NotNull(scope); + + Assert.Equal(3, scope.Attributes.Count); + Assert.Equal(1u, scope.DroppedAttributesCount); + Assert.Equal("1234", scope.Attributes[0].Value.StringValue); + ArrayValueAsserts(scope.Attributes[1].Value.ArrayValue.Values); + Assert.Equal(new object().ToString()!.Substring(0, 4), scope.Attributes[2].Value.StringValue); + } + } + [Fact] public void SpanLimitsTest() { @@ -232,12 +385,12 @@ public void SpanLimitsTest() SpanLinkCountLimit = 1, }; - var tags = new ActivityTagsCollection() + 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), + new("TruncatedTag", "12345"), + new("TruncatedStringArray", new string?[] { "12345", "1234", string.Empty, null }), + new("TruncatedObjectTag", new object()), + new("OneTagTooMany", 1), }; var links = new[] @@ -249,20 +402,22 @@ public void SpanLimitsTest() using var activitySource = new ActivitySource(nameof(this.SpanLimitsTest)); using var activity = activitySource.StartActivity("root", ActivityKind.Server, default(ActivityContext), tags, links); + Assert.NotNull(activity); + 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); + var otlpSpan = ToOtlpSpan(sdkOptions, activity); 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.Equal(new object().ToString()!.Substring(0, 4), otlpSpan.Attributes[2].Value.StringValue); Assert.Single(otlpSpan.Events); Assert.Equal(1u, otlpSpan.DroppedEventsCount); @@ -270,7 +425,7 @@ public void SpanLimitsTest() 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.Equal(new object().ToString()!.Substring(0, 4), otlpSpan.Events[0].Attributes[2].Value.StringValue); Assert.Single(otlpSpan.Links); Assert.Equal(1u, otlpSpan.DroppedLinksCount); @@ -278,26 +433,7 @@ public void SpanLimitsTest() 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 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(new object().ToString()!.Substring(0, 4), otlpSpan.Links[0].Attributes[2].Value.StringValue); } [Fact] @@ -307,21 +443,27 @@ public void 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" }), - new KeyValuePair("datetime_array", new DateTime[] { DateTime.UtcNow, DateTime.Now }), + bool[] boolArray = [true, false]; + int[] intArray = [1, 2]; + double[] doubleArray = [1.0, 2.09]; + string[] stringArray = ["a", "b"]; + + var attributes = new List> + { + new("bool", true), + new("long", 1L), + new("string", "text"), + new("double", 3.14), + new("int", 1), + new("datetime", DateTime.UtcNow), + new("bool_array", boolArray), + new("int_array", intArray), + new("double_array", doubleArray), + new("string_array", stringArray), + new("datetime_array", new DateTime[] { DateTime.UtcNow, DateTime.Now }), }; + Assert.NotNull(rootActivity); foreach (var kvp in attributes) { rootActivity.SetTag(kvp.Key, kvp.Value); @@ -342,7 +484,7 @@ public void ToOtlpSpanTest() rootActivity.TraceId.CopyTo(traceIdSpan); var traceId = traceIdSpan.ToArray(); - var otlpSpan = rootActivity.ToOtlpSpan(DefaultSdkLimitOptions); + var otlpSpan = ToOtlpSpan(DefaultSdkLimitOptions, rootActivity); Assert.NotNull(otlpSpan); Assert.Equal("root", otlpSpan.Name); @@ -359,16 +501,18 @@ public void ToOtlpSpanTest() var expectedEndTimeUnixNano = expectedStartTimeUnixNano + (duration.TotalMilliseconds * 1_000_000); Assert.Equal(expectedEndTimeUnixNano, otlpSpan.EndTimeUnixNano); - var childLinks = new List { new ActivityLink(rootActivity.Context, new ActivityTagsCollection(attributes)) }; + var childLinks = new List { new(rootActivity.Context, new ActivityTagsCollection(attributes)) }; var childActivity = activitySource.StartActivity( "child", ActivityKind.Client, rootActivity.Context, links: childLinks); - childActivity.SetStatus(Status.Error); + Assert.NotNull(childActivity); - var childEvents = new List { new ActivityEvent("e0"), new ActivityEvent("e1", default, new ActivityTagsCollection(attributes)) }; + childActivity.SetStatus(ActivityStatusCode.Error, new string('a', 150)); + + var childEvents = new List { new("e0"), new("e1", default, new ActivityTagsCollection(attributes)) }; childActivity.AddEvent(childEvents[0]); childActivity.AddEvent(childEvents[1]); @@ -376,7 +520,7 @@ public void ToOtlpSpanTest() rootActivity.Context.SpanId.CopyTo(parentIdSpan); var parentId = parentIdSpan.ToArray(); - otlpSpan = childActivity.ToOtlpSpan(DefaultSdkLimitOptions); + otlpSpan = ToOtlpSpan(DefaultSdkLimitOptions, childActivity); Assert.NotNull(otlpSpan); Assert.Equal("child", otlpSpan.Name); @@ -384,9 +528,10 @@ public void ToOtlpSpanTest() Assert.Equal(traceId, otlpSpan.TraceId); Assert.Equal(parentId, otlpSpan.ParentSpanId); - // Assert.Equal(OtlpTrace.Status.Types.StatusCode.NotFound, otlpSpan.Status.Code); + Assert.NotNull(otlpSpan.Status); + Assert.Equal(OtlpTrace.Status.Types.StatusCode.Error, otlpSpan.Status.Code); - Assert.Equal(Status.Error.Description ?? string.Empty, otlpSpan.Status.Message); + Assert.Equal(childActivity.StatusDescription ?? string.Empty, otlpSpan.Status.Message); Assert.Empty(otlpSpan.Attributes); Assert.Equal(childEvents.Count, otlpSpan.Events.Count); @@ -400,7 +545,9 @@ public void ToOtlpSpanTest() 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); + var tags = childLinks[i].Tags; + Assert.NotNull(tags); + OtlpTestHelpers.AssertOtlpAttributes(tags, otlpSpan.Links[i].Attributes); } var flags = (OtlpTrace.SpanFlags)otlpSpan.Flags; @@ -416,10 +563,10 @@ public void ToOtlpSpanActivitiesWithNullArrayTest() using var rootActivity = activitySource.StartActivity("root", ActivityKind.Client); Assert.NotNull(rootActivity); - var stringArr = new string[] { "test", string.Empty, null }; + var stringArr = new string?[] { "test", string.Empty, null }; rootActivity.SetTag("stringArray", stringArr); - var otlpSpan = rootActivity.ToOtlpSpan(DefaultSdkLimitOptions); + var otlpSpan = ToOtlpSpan(DefaultSdkLimitOptions, rootActivity); Assert.NotNull(otlpSpan); @@ -435,14 +582,16 @@ public void ToOtlpSpanActivitiesWithNullArrayTest() [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.")] + [InlineData(ActivityStatusCode.Error, "150 Character String - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] public void ToOtlpSpanNativeActivityStatusTest(ActivityStatusCode expectedStatusCode, string statusDescription) { using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); using var activity = activitySource.StartActivity("Name"); + Assert.NotNull(activity); activity.SetStatus(expectedStatusCode, statusDescription); - var otlpSpan = activity.ToOtlpSpan(DefaultSdkLimitOptions); - + var otlpSpan = ToOtlpSpan(DefaultSdkLimitOptions, activity); + Assert.NotNull(otlpSpan); if (expectedStatusCode == ActivityStatusCode.Unset) { Assert.Null(otlpSpan.Status); @@ -463,19 +612,63 @@ public void ToOtlpSpanNativeActivityStatusTest(ActivityStatusCode expectedStatus } } + [Fact] + public void TracesSerialization_ExpandsBufferForTracesAndSerializes() + { + var tags = new ActivityTagsCollection + { + new("Tagkey", "Tagvalue"), + }; + + using var activitySource = new ActivitySource(nameof(this.TracesSerialization_ExpandsBufferForTracesAndSerializes)); + using var activity = activitySource.StartActivity("root", ActivityKind.Server, default(ActivityContext), tags); + + Assert.NotNull(activity); + var batch = new Batch([activity], 1); + RunTest(new(), batch); + + void RunTest(SdkLimitOptions sdkOptions, Batch batch) + { + var buffer = new byte[50]; + var writePosition = ProtobufOtlpTraceSerializer.WriteTraceData(ref buffer, 0, sdkOptions, ResourceBuilder.CreateEmpty().Build(), batch); + using var stream = new MemoryStream(buffer, 0, writePosition); + var tracesData = OtlpTrace.TracesData.Parser.ParseFrom(stream); + var request = new OtlpCollector.ExportTraceServiceRequest(); + request.ResourceSpans.Add(tracesData.ResourceSpans); + + // Buffer should be expanded to accommodate the large array. + Assert.True(buffer.Length > 50); + + Assert.Single(request.ResourceSpans); + var scopeSpans = request.ResourceSpans.First().ScopeSpans; + Assert.Single(scopeSpans); + var otlpSpan = scopeSpans.First().Spans.First(); + Assert.NotNull(otlpSpan); + + // The string is too large, hence not evaluating the content. + var keyValue = otlpSpan.Attributes.FirstOrDefault(kvp => kvp.Key == "Tagkey"); + Assert.NotNull(keyValue); + Assert.Equal("Tagvalue", keyValue.Value.StringValue); + } + } + [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.")] + [InlineData(StatusCode.Error, "Error", "150 Character String - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] + [Obsolete("Remove when ActivityExtensions status APIs are removed")] public void ToOtlpSpanStatusTagTest(StatusCode expectedStatusCode, string statusCodeTagValue, string statusDescription) { using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); using var activity = activitySource.StartActivity("Name"); + Assert.NotNull(activity); activity.SetTag(SpanAttributeConstants.StatusCodeKey, statusCodeTagValue); activity.SetTag(SpanAttributeConstants.StatusDescriptionKey, statusDescription); - var otlpSpan = activity.ToOtlpSpan(DefaultSdkLimitOptions); + var otlpSpan = ToOtlpSpan(DefaultSdkLimitOptions, activity); + Assert.NotNull(otlpSpan); Assert.NotNull(otlpSpan.Status); Assert.Equal((int)expectedStatusCode, (int)otlpSpan.Status.Code); @@ -493,49 +686,58 @@ public void ToOtlpSpanStatusTagTest(StatusCode expectedStatusCode, string status [InlineData(StatusCode.Unset, "uNsET")] [InlineData(StatusCode.Ok, "oK")] [InlineData(StatusCode.Error, "ERROR")] + [Obsolete("Remove when ActivityExtensions status APIs are removed")] public void ToOtlpSpanStatusTagIsCaseInsensitiveTest(StatusCode expectedStatusCode, string statusCodeTagValue) { using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); using var activity = activitySource.StartActivity("Name"); + Assert.NotNull(activity); activity.SetTag(SpanAttributeConstants.StatusCodeKey, statusCodeTagValue); - var otlpSpan = activity.ToOtlpSpan(DefaultSdkLimitOptions); + var otlpSpan = ToOtlpSpan(DefaultSdkLimitOptions, activity); + Assert.NotNull(otlpSpan); Assert.NotNull(otlpSpan.Status); Assert.Equal((int)expectedStatusCode, (int)otlpSpan.Status.Code); } [Fact] + [Obsolete("Remove when ActivityExtensions status APIs are removed")] 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."; + const string tagDescriptionOnError = "Description when TagStatusCode is Error."; + Assert.NotNull(activity); activity.SetStatus(ActivityStatusCode.Ok); activity.SetTag(SpanAttributeConstants.StatusCodeKey, "ERROR"); - activity.SetTag(SpanAttributeConstants.StatusDescriptionKey, TagDescriptionOnError); + activity.SetTag(SpanAttributeConstants.StatusDescriptionKey, tagDescriptionOnError); - var otlpSpan = activity.ToOtlpSpan(DefaultSdkLimitOptions); + var otlpSpan = ToOtlpSpan(DefaultSdkLimitOptions, activity); + Assert.NotNull(otlpSpan); Assert.NotNull(otlpSpan.Status); Assert.Equal((int)ActivityStatusCode.Ok, (int)otlpSpan.Status.Code); Assert.Empty(otlpSpan.Status.Message); } [Fact] + [Obsolete("Remove when ActivityExtensions status APIs are removed")] 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); + const string statusDescriptionOnError = "Description when ActivityStatusCode is Error."; + Assert.NotNull(activity); + activity.SetStatus(ActivityStatusCode.Error, statusDescriptionOnError); activity.SetTag(SpanAttributeConstants.StatusCodeKey, "OK"); - var otlpSpan = activity.ToOtlpSpan(DefaultSdkLimitOptions); + var otlpSpan = ToOtlpSpan(DefaultSdkLimitOptions, activity); + Assert.NotNull(otlpSpan); Assert.NotNull(otlpSpan.Status); Assert.Equal((int)ActivityStatusCode.Error, (int)otlpSpan.Status.Code); - Assert.Equal(StatusDescriptionOnError, otlpSpan.Status.Message); + Assert.Equal(statusDescriptionOnError, otlpSpan.Status.Message); } [Theory] @@ -545,13 +747,15 @@ public void ToOtlpSpanTraceStateTest(bool traceStateWasSet) { using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); using var activity = activitySource.StartActivity("Name"); + Assert.NotNull(activity); string tracestate = "a=b;c=d"; if (traceStateWasSet) { activity.TraceStateString = tracestate; } - var otlpSpan = activity.ToOtlpSpan(DefaultSdkLimitOptions); + var otlpSpan = ToOtlpSpan(DefaultSdkLimitOptions, activity); + Assert.NotNull(otlpSpan); if (traceStateWasSet) { @@ -564,30 +768,13 @@ public void ToOtlpSpanTraceStateTest(bool traceStateWasSet) } } - [Fact] - public void ToOtlpSpanPeerServiceTest() - { - using var activitySource = new ActivitySource(nameof(this.ToOtlpSpanTest)); - - using var rootActivity = activitySource.StartActivity("root", ActivityKind.Client); - - rootActivity.SetTag(SemanticConventions.AttributeHttpHost, "opentelemetry.io"); - - var otlpSpan = rootActivity.ToOtlpSpan(DefaultSdkLimitOptions); - - Assert.NotNull(otlpSpan); - - var peerService = otlpSpan.Attributes.FirstOrDefault(kvp => kvp.Key == SemanticConventions.AttributePeerService); - - Assert.NotNull(peerService); - Assert.Equal("opentelemetry.io", peerService.Value.StringValue); - } - [Fact] public void UseOpenTelemetryProtocolActivityExporterWithCustomActivityProcessor() { const string ActivitySourceName = "otlp.test"; +#pragma warning disable CA2000 // Dispose objects before losing scope TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); +#pragma warning restore CA2000 // Dispose objects before losing scope bool startCalled = false; bool endCalled = false; @@ -621,12 +808,12 @@ public void UseOpenTelemetryProtocolActivityExporterWithCustomActivityProcessor( [Fact] public void Shutdown_ClientShutdownIsCalled() { - var exportClientMock = new TestExportClient(); + var exportClientMock = new TestExportClient(); var exporterOptions = new OtlpExporterOptions(); - var transmissionHandler = new OtlpExporterTransmissionHandler(exportClientMock, exporterOptions.TimeoutMilliseconds); + using var transmissionHandler = new OtlpExporterTransmissionHandler(exportClientMock, exporterOptions.TimeoutMilliseconds); - var exporter = new OtlpTraceExporter(new OtlpExporterOptions(), DefaultSdkLimitOptions, DefaultExperimentalOptions, transmissionHandler); + using var exporter = new OtlpTraceExporter(new OtlpExporterOptions(), DefaultSdkLimitOptions, DefaultExperimentalOptions, transmissionHandler); exporter.Shutdown(); Assert.True(exportClientMock.ShutdownCalled); @@ -641,7 +828,7 @@ public void Null_BatchExportProcessorOptions_SupportedTest() { o.Protocol = OtlpExportProtocol.HttpProtobuf; o.ExportProcessorType = ExportProcessorType.Batch; - o.BatchExportProcessorOptions = null; + o.BatchExportProcessorOptions = null!; }); } @@ -650,8 +837,8 @@ public void NonnamedOptionsMutateSharedInstanceTest() { var testOptionsInstance = new OtlpExporterOptions(); - OtlpExporterOptions tracerOptions = null; - OtlpExporterOptions meterOptions = null; + OtlpExporterOptions? tracerOptions = null; + OtlpExporterOptions? meterOptions = null; var services = new ServiceCollection(); @@ -695,8 +882,8 @@ public void NonnamedOptionsMutateSharedInstanceTest() [Fact] public void NamedOptionsMutateSeparateInstancesTest() { - OtlpExporterOptions tracerOptions = null; - OtlpExporterOptions meterOptions = null; + OtlpExporterOptions? tracerOptions = null; + OtlpExporterOptions? meterOptions = null; var services = new ServiceCollection(); @@ -753,9 +940,11 @@ public void SpanFlagsTest(bool isRecorded, bool isRemote) isRemote: isRemote); using var rootActivity = activitySource.StartActivity("root", ActivityKind.Server, ctx); + Assert.NotNull(rootActivity); - var otlpSpan = rootActivity.ToOtlpSpan(DefaultSdkLimitOptions); + var otlpSpan = ToOtlpSpan(DefaultSdkLimitOptions, rootActivity); + Assert.NotNull(otlpSpan); var flags = (OtlpTrace.SpanFlags)otlpSpan.Flags; ActivityTraceFlags traceFlags = (ActivityTraceFlags)(flags & OtlpTrace.SpanFlags.TraceFlagsMask); @@ -802,9 +991,11 @@ public void SpanLinkFlagsTest(bool isRecorded, bool isRemote) }; using var rootActivity = activitySource.StartActivity("root", ActivityKind.Server, default(ActivityContext), links: links); + Assert.NotNull(rootActivity); - var otlpSpan = rootActivity.ToOtlpSpan(DefaultSdkLimitOptions); + var otlpSpan = ToOtlpSpan(DefaultSdkLimitOptions, rootActivity); + Assert.NotNull(otlpSpan); var spanLink = Assert.Single(otlpSpan.Links); var flags = (OtlpTrace.SpanFlags)spanLink.Flags; @@ -831,4 +1022,43 @@ public void SpanLinkFlagsTest(bool isRecorded, bool isRemote) Assert.False(flags.HasFlag(OtlpTrace.SpanFlags.ContextIsRemoteMask)); } } + + private static OtlpTrace.Span? ToOtlpSpan(SdkLimitOptions sdkOptions, Activity activity) + { + var buffer = new byte[4096]; + var writePosition = ProtobufOtlpTraceSerializer.WriteSpan(buffer, 0, sdkOptions, activity); + using var stream = new MemoryStream(buffer, 0, writePosition); + var scopeSpans = OtlpTrace.ScopeSpans.Parser.ParseFrom(stream); + return scopeSpans.Spans.FirstOrDefault(); + } + + private static OtlpCollector.ExportTraceServiceRequest CreateTraceExportRequest(SdkLimitOptions sdkOptions, in Batch batch, Resource resource) + { + var buffer = new byte[4096]; + var writePosition = ProtobufOtlpTraceSerializer.WriteTraceData(ref buffer, 0, sdkOptions, resource, batch); + using var stream = new MemoryStream(buffer, 0, writePosition); + var tracesData = OtlpTrace.TracesData.Parser.ParseFrom(stream); + var request = new OtlpCollector.ExportTraceServiceRequest(); + request.ResourceSpans.Add(tracesData.ResourceSpans); + return request; + } + + private static void ArrayValueAsserts(RepeatedField values) + { + 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) + { + Assert.Equal(expectedValue, actual.StringValue); + } + } + } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/SdkLimitOptionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/SdkLimitOptionsTests.cs index dc36def4665..0a737ff1438 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/SdkLimitOptionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/SdkLimitOptionsTests.cs @@ -135,7 +135,7 @@ public void SpanAttributeCountLimitFallback() [Fact] public void SdkLimitOptionsUsingIConfiguration() { - var values = new Dictionary + var values = new Dictionary { ["OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT"] = "23", ["OTEL_ATTRIBUTE_COUNT_LIMIT"] = "24", diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestExportClient.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestExportClient.cs index eab9178db49..7141ffd8c7d 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestExportClient.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestExportClient.cs @@ -5,7 +5,7 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; -internal class TestExportClient(bool throwException = false) : IExportClient +internal sealed class TestExportClient(bool throwException = false) : IExportClient { public bool SendExportRequestCalled { get; private set; } @@ -13,11 +13,11 @@ internal class TestExportClient(bool throwException = false) : IExportClient< public bool ThrowException { get; set; } = throwException; - public ExportClientResponse SendExportRequest(T request, DateTime deadlineUtc, CancellationToken cancellationToken = default) + public ExportClientResponse SendExportRequest(byte[] buffer, int contentLength, DateTime deadlineUtc, CancellationToken cancellationToken = default) { if (this.ThrowException) { - throw new Exception("Exception thrown from SendExportRequest"); + throw new InvalidOperationException("Exception thrown from SendExportRequest"); } this.SendExportRequestCalled = true; @@ -30,9 +30,9 @@ public bool Shutdown(int timeoutMilliseconds) return true; } - private class TestExportClientResponse : ExportClientResponse + private sealed class TestExportClientResponse : ExportClientResponse { - public TestExportClientResponse(bool success, DateTime deadline, Exception exception) + 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 index a15775cb1ac..980d17bad14 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestHttpMessageHandler.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestHttpMessageHandler.cs @@ -1,26 +1,30 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if !NET6_0_OR_GREATER +#if !NET using System.Net.Http; #endif namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; -internal class TestHttpMessageHandler : HttpMessageHandler +internal sealed class TestHttpMessageHandler : HttpMessageHandler { - public HttpRequestMessage HttpRequestMessage { get; private set; } + public HttpRequestMessage? HttpRequestMessage { get; private set; } - public byte[] HttpRequestContent { get; private set; } + public byte[]? HttpRequestContent { get; private set; } - public virtual HttpResponseMessage InternalSend(HttpRequestMessage request, CancellationToken cancellationToken) + public HttpResponseMessage InternalSend(HttpRequestMessage request, CancellationToken cancellationToken) { this.HttpRequestMessage = request; - this.HttpRequestContent = request.Content.ReadAsByteArrayAsync().Result; +#if NET + this.HttpRequestContent = request.Content!.ReadAsByteArrayAsync(cancellationToken).Result; +#else + this.HttpRequestContent = request.Content!.ReadAsByteArrayAsync().Result; +#endif return new HttpResponseMessage(); } -#if NET6_0_OR_GREATER +#if NET protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) { return this.InternalSend(request, cancellationToken); diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/UseOtlpExporterExtensionTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/UseOtlpExporterExtensionTests.cs index 8a03a8e47b7..2bb8245b657 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/UseOtlpExporterExtensionTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/UseOtlpExporterExtensionTests.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -16,7 +14,7 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; [Collection("EnvVars")] -public class UseOtlpExporterExtensionTests : IDisposable +public sealed class UseOtlpExporterExtensionTests : IDisposable { public UseOtlpExporterExtensionTests() { @@ -40,7 +38,12 @@ public void UseOtlpExporterDefaultTest() var exporterOptions = sp.GetRequiredService>().CurrentValue; +#if NET462_OR_GREATER || NETSTANDARD2_0 + Assert.Equal(new Uri(OtlpExporterOptions.DefaultHttpEndpoint), exporterOptions.DefaultOptions.Endpoint); +#else Assert.Equal(new Uri(OtlpExporterOptions.DefaultGrpcEndpoint), exporterOptions.DefaultOptions.Endpoint); +#endif + Assert.Equal(OtlpExporterOptions.DefaultOtlpExportProtocol, exporterOptions.DefaultOptions.Protocol); Assert.False(((OtlpExporterOptions)exporterOptions.DefaultOptions).HasData); @@ -50,7 +53,9 @@ public void UseOtlpExporterDefaultTest() } [Theory] +#pragma warning disable CS0618 // Suppressing gRPC obsolete warning [InlineData(OtlpExportProtocol.Grpc)] +#pragma warning restore CS0618 // Suppressing gRPC obsolete warning [InlineData(OtlpExportProtocol.HttpProtobuf)] public void UseOtlpExporterSetEndpointAndProtocolTest(OtlpExportProtocol protocol) { diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/gen_test_cert.ps1 b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/gen_test_cert.ps1 new file mode 100644 index 00000000000..d9443ca85fa --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/gen_test_cert.ps1 @@ -0,0 +1,90 @@ +using namespace System.Security.Cryptography; +using namespace System.Security.Cryptography.X509Certificates; + +param ( + [string] $OutDir +) + +function Write-Certificate { + param ( + [X509Certificate2] $Cert, + [string] $Name, + [string] $Dir + ) + + # write cert content + $certPem = $Cert.ExportCertificatePem(); + $certPemPath = Join-Path $Dir -ChildPath "$Name-cert.pem"; + [System.IO.File]::WriteAllText($certPemPath, $certPem); + + # write pkey + [AsymmetricAlgorithm] $pkey = [RSACertificateExtensions]::GetRSAPrivateKey($Cert); + [string] $pkeyPem = $null; + + if ($null -ne $pkey) { + $pkeyPem = $pkey.ExportRSAPrivateKeyPem(); + } + + if ($null -eq $pkey) { + $pkey = [ECDsaCertificateExtensions]::GetECDsaPrivateKey($Cert); + $pkeyPem = $pkey.ExportECPrivateKeyPem(); + } + + if ($null -eq $pkeyPem) { + return; + } + + + $pKeyPath = Join-Path $Dir -ChildPath "$Name-key.pem"; + [System.IO.File]::WriteAllText($pKeyPath, $pkeyPem); +} + +$ca = New-SelfSignedCertificate -CertStoreLocation 'Cert:\CurrentUser\My' ` + -DnsName "otel-test-ca" ` + -NotAfter (Get-Date).AddYears(20) ` + -FriendlyName "otel-test-ca" ` + -KeyAlgorithm ECDSA_nistP256 ` + -KeyExportPolicy Exportable ` + -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature; + + +try { + Write-Certificate -Cert $ca -Name "otel-test-ca" -Dir $OutDir; + $serverCert = New-SelfSignedCertificate -CertStoreLocation 'Cert:\CurrentUser\My' ` + -DnsName "otel-collector" ` + -Signer $ca ` + -NotAfter (Get-Date).AddYears(20) ` + -FriendlyName "otel-test-server" ` + -KeyAlgorithm ECDSA_nistP256 ` + -KeyUsageProperty All ` + -KeyExportPolicy Exportable ` + -KeyUsage CertSign, CRLSign, DigitalSignature ` + -TextExtension @("2.5.29.19={text}CA=1&pathlength=1", "2.5.29.37={text}1.3.6.1.5.5.7.3.1"); + + try { + Write-Certificate -Cert $serverCert -Name "otel-test-server" -Dir $OutDir; + + $clientCert = New-SelfSignedCertificate -CertStoreLocation 'Cert:\CurrentUser\My' ` + -DnsName "otel-test-client" ` + -Signer $ca ` + -NotAfter (Get-Date).AddYears(20) ` + -FriendlyName "otel-test-client" ` + -KeyAlgorithm ECDSA_nistP256 ` + -KeyUsageProperty All ` + -KeyExportPolicy Exportable ` + -KeyUsage CertSign, CRLSign, DigitalSignature ` + -TextExtension @("2.5.29.19={text}CA=1&pathlength=1", "2.5.29.37={text}1.3.6.1.5.5.7.3.2"); + try { + Write-Certificate -Cert $clientCert -Name "otel-test-client" -Dir $OutDir; + } + finally { + Get-Item -Path "Cert:\CurrentUser\My\$($clientCert.Thumbprint)" | Remove-Item; + } + } + finally { + Get-Item -Path "Cert:\CurrentUser\My\$($serverCert.Thumbprint)" | Remove-Item; + } +} +finally { + Get-Item -Path "Cert:\CurrentUser\My\$($ca.Thumbprint)" | Remove-Item; +} diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/gen_test_cert.sh b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/gen_test_cert.sh new file mode 100644 index 00000000000..bd129a4b838 --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/gen_test_cert.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +# Set output directory, default is the current directory +OUT_DIR=${1:-"."} + +# Create output directory if it doesn't exist +mkdir -p "$OUT_DIR" + +# Generate CA certificate (Certificate Authority) +openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 \ + -subj "/CN=otel-test-ca" \ + -keyout "$OUT_DIR/otel-test-ca-key.pem" -out "$OUT_DIR/otel-test-ca-cert.pem" + +# Create the extension configuration file for the server certificate +cat > "$OUT_DIR/server_cert_ext.cnf" < "$OUT_DIR/client_cert_ext.cnf" < + Unit test project for Prometheus Exporter AspNetCore for OpenTelemetry $(TargetFrameworksForAspNetCoreTests) $(DefineConstants);PROMETHEUS_ASPNETCORE - - - disable - - - - runtime; build; native; contentfiles; analyzers - + + + + - + + + + + - + @@ -32,4 +33,5 @@ + diff --git a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs index 19b22e47be5..98d503e8e92 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests/PrometheusExporterMiddlewareTests.cs @@ -3,6 +3,7 @@ #if !NETFRAMEWORK using System.Diagnostics.Metrics; +using System.Globalization; using System.Net; using System.Text.RegularExpressions; using Microsoft.AspNetCore.Builder; @@ -110,9 +111,9 @@ public Task PrometheusExporterMiddlewareIntegration_MixedPredicateAndPath() 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 = []; } Assert.Equal("true", headers.FirstOrDefault()); @@ -137,9 +138,9 @@ public Task PrometheusExporterMiddlewareIntegration_MixedPath() 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 = []; } Assert.Equal("true", headers.FirstOrDefault()); @@ -249,24 +250,97 @@ public Task PrometheusExporterMiddlewareIntegration_UseOpenMetricsVersionHeader( } [Fact] - public async Task PrometheusExporterMiddlewareIntegration_CanServeOpenMetricsAndPlainFormats() + public Task PrometheusExporterMiddlewareIntegration_TextPlainResponse_WithMeterTags() + { + var meterTags = new KeyValuePair[] + { + new("meterKey1", "value1"), + new("meterKey2", "value2"), + }; + + return RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics", + app => app.UseOpenTelemetryPrometheusScrapingEndpoint(), + acceptHeader: "text/plain", + meterTags: meterTags); + } + + [Fact] + public Task PrometheusExporterMiddlewareIntegration_UseOpenMetricsVersionHeader_WithMeterTags() + { + var meterTags = new KeyValuePair[] + { + new("meterKey1", "value1"), + new("meterKey2", "value2"), + }; + + return RunPrometheusExporterMiddlewareIntegrationTest( + "/metrics", + app => app.UseOpenTelemetryPrometheusScrapingEndpoint(), + acceptHeader: "application/openmetrics-text; version=1.0.0", + meterTags: meterTags); + } + + [Fact] + public async Task PrometheusExporterMiddlewareIntegration_CanServeOpenMetricsAndPlainFormats_NoMeterTags() + { + await RunPrometheusExporterMiddlewareIntegrationTestWithBothFormats(); + } + + [Fact] + public async Task PrometheusExporterMiddlewareIntegration_CanServeOpenMetricsAndPlainFormats_WithMeterTags() + { + var meterTags = new KeyValuePair[] + { + new("meterKey1", "value1"), + new("meterKey2", "value2"), + }; + + await RunPrometheusExporterMiddlewareIntegrationTestWithBothFormats(meterTags); + } + + [Fact] + public async Task PrometheusExporterMiddlewareIntegration_TestBufferSizeIncrease_With_LotOfMetrics() { using var host = await StartTestHostAsync( app => app.UseOpenTelemetryPrometheusScrapingEndpoint()); - var tags = new KeyValuePair[] + using var meter = new Meter(MeterName, MeterVersion); + + for (var x = 0; x < 1000; x++) { - new KeyValuePair("key1", "value1"), - new KeyValuePair("key2", "value2"), + var counter = meter.CreateCounter("counter_double_" + x, unit: "By"); + counter.Add(1); + } + + using var client = host.GetTestClient(); + + using var response = await client.GetAsync(new Uri("/metrics", UriKind.Relative)); + var text = await response.Content.ReadAsStringAsync(); + + Assert.NotEmpty(text); + + await host.StopAsync(); + } + + private static async Task RunPrometheusExporterMiddlewareIntegrationTestWithBothFormats(KeyValuePair[]? meterTags = null) + { + using var host = await StartTestHostAsync( + app => app.UseOpenTelemetryPrometheusScrapingEndpoint()); + + var counterTags = new KeyValuePair[] + { + new("key1", "value1"), + new("key2", "value2"), }; - using var meter = new Meter(MeterName, MeterVersion); + using var meter = new Meter(MeterName, MeterVersion, meterTags); var beginTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); var counter = meter.CreateCounter("counter_double", unit: "By"); - counter.Add(100.18D, tags); - counter.Add(0.99D, tags); + counter.Add(100.18D, counterTags); + counter.Add(0.99D, counterTags); var testCases = new bool[] { true, false, true, true, false }; @@ -282,7 +356,7 @@ public async Task PrometheusExporterMiddlewareIntegration_CanServeOpenMetricsAnd }; using var response = await client.SendAsync(request); var endTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - await VerifyAsync(beginTimestamp, endTimestamp, response, testCase); + await VerifyAsync(beginTimestamp, endTimestamp, response, testCase, meterTags); } await host.StopAsync(); @@ -291,32 +365,33 @@ public async Task PrometheusExporterMiddlewareIntegration_CanServeOpenMetricsAnd private static async Task RunPrometheusExporterMiddlewareIntegrationTest( string path, Action configure, - Action configureServices = null, - Action validateResponse = null, + Action? configureServices = null, + Action? validateResponse = null, bool registerMeterProvider = true, - Action configureOptions = null, + Action? configureOptions = null, bool skipMetrics = false, - string acceptHeader = "application/openmetrics-text") + string acceptHeader = "application/openmetrics-text", + KeyValuePair[]? meterTags = null) { - var requestOpenMetrics = acceptHeader.StartsWith("application/openmetrics-text"); + var requestOpenMetrics = acceptHeader.StartsWith("application/openmetrics-text", StringComparison.Ordinal); using var host = await StartTestHostAsync(configure, configureServices, registerMeterProvider, configureOptions); - var tags = new KeyValuePair[] + var counterTags = new KeyValuePair[] { - new KeyValuePair("key1", "value1"), - new KeyValuePair("key2", "value2"), + new("key1", "value1"), + new("key2", "value2"), }; - using var meter = new Meter(MeterName, MeterVersion); + using var meter = new Meter(MeterName, MeterVersion, meterTags); var beginTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); var counter = meter.CreateCounter("counter_double", unit: "By"); if (!skipMetrics) { - counter.Add(100.18D, tags); - counter.Add(0.99D, tags); + counter.Add(100.18D, counterTags); + counter.Add(0.99D, counterTags); } using var client = host.GetTestClient(); @@ -326,13 +401,13 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( client.DefaultRequestHeaders.Add("Accept", acceptHeader); } - using var response = await client.GetAsync(path); + using var response = await client.GetAsync(new Uri(path, UriKind.Relative)); var endTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); if (!skipMetrics) { - await VerifyAsync(beginTimestamp, endTimestamp, response, requestOpenMetrics); + await VerifyAsync(beginTimestamp, endTimestamp, response, requestOpenMetrics, meterTags); } else { @@ -344,20 +419,24 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest( await host.StopAsync(); } - private static async Task VerifyAsync(long beginTimestamp, long endTimestamp, HttpResponseMessage response, bool requestOpenMetrics) + private static async Task VerifyAsync(long beginTimestamp, long endTimestamp, HttpResponseMessage response, bool requestOpenMetrics, KeyValuePair[]? meterTags) { 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()); + Assert.Equal("application/openmetrics-text; version=1.0.0; charset=utf-8", response.Content.Headers.ContentType!.ToString()); } else { - Assert.Equal("text/plain; charset=utf-8; version=0.0.4", response.Content.Headers.ContentType.ToString()); + Assert.Equal("text/plain; charset=utf-8; version=0.0.4", response.Content.Headers.ContentType!.ToString()); } + var additionalTags = meterTags is { Length: > 0 } + ? $"{string.Join(",", meterTags.Select(x => $"{x.Key}=\"{x.Value}\""))}," + : string.Empty; + string content = (await response.Content.ReadAsStringAsync()).ReplaceLineEndings(); string expected = requestOpenMetrics @@ -370,14 +449,14 @@ private static async Task VerifyAsync(long beginTimestamp, long endTimestamp, Ht otel_scope_info{otel_scope_name="{{MeterName}}"} 1 # TYPE counter_double_bytes counter # UNIT counter_double_bytes bytes - counter_double_bytes_total{otel_scope_name="{{MeterName}}",otel_scope_version="{{MeterVersion}}",key1="value1",key2="value2"} 101.17 (\d+\.\d{3}) + counter_double_bytes_total{otel_scope_name="{{MeterName}}",otel_scope_version="{{MeterVersion}}",{{additionalTags}}key1="value1",key2="value2"} 101.17 (\d+\.\d{3}) # EOF """.ReplaceLineEndings() : $$""" # TYPE counter_double_bytes_total counter # UNIT counter_double_bytes_total bytes - counter_double_bytes_total{otel_scope_name="{{MeterName}}",otel_scope_version="{{MeterVersion}}",key1="value1",key2="value2"} 101.17 (\d+) + counter_double_bytes_total{otel_scope_name="{{MeterName}}",otel_scope_version="{{MeterVersion}}",{{additionalTags}}key1="value1",key2="value2"} 101.17 (\d+) # EOF """.ReplaceLineEndings(); @@ -386,16 +465,16 @@ private static async Task VerifyAsync(long beginTimestamp, long endTimestamp, Ht Assert.True(matches.Count == 1, content); - var timestamp = long.Parse(matches[0].Groups[1].Value.Replace(".", string.Empty)); + var timestamp = long.Parse(matches[0].Groups[1].Value.Replace(".", string.Empty, StringComparison.Ordinal), CultureInfo.InvariantCulture); Assert.True(beginTimestamp <= timestamp && timestamp <= endTimestamp, $"{beginTimestamp} {timestamp} {endTimestamp}"); } private static Task StartTestHostAsync( Action configure, - Action configureServices = null, + Action? configureServices = null, bool registerMeterProvider = true, - Action configureOptions = null) + Action? configureOptions = null) { return new HostBuilder() .ConfigureWebHost(webBuilder => webBuilder diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/EventSourceTest.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/EventSourceTests.cs similarity index 92% rename from test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/EventSourceTest.cs rename to test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/EventSourceTests.cs index baf8dc43377..b1aed407d63 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/EventSourceTest.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/EventSourceTests.cs @@ -6,7 +6,7 @@ namespace OpenTelemetry.Exporter.Prometheus.Tests; -public class EventSourceTest +public class EventSourceTests { [Fact] public void EventSourceTest_PrometheusExporterEventSource() 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 7ab05160d72..55d40b0afe9 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,26 +1,25 @@ + Unit test project for Prometheus Exporter HttpListener for OpenTelemetry $(TargetFrameworksForTests) $(DefineConstants);PROMETHEUS_HTTP_LISTENER - - - disable + + false - - - - runtime; build; native; contentfiles; analyzers - + - - + + + + + diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusCollectionManagerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusCollectionManagerTests.cs index 32ee87e3de5..75e3261b71a 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusCollectionManagerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusCollectionManagerTests.cs @@ -31,7 +31,9 @@ public async Task EnterExitCollectTest(int scrapeResponseCacheDurationMillisecon #endif .Build()) { - if (!provider.TryFindExporter(out PrometheusExporter exporter)) +#pragma warning disable CA2000 // Dispose objects before losing scope + if (!provider.TryFindExporter(out PrometheusExporter? exporter)) +#pragma warning restore CA2000 // Dispose objects before losing scope { throw new InvalidOperationException("PrometheusExporter could not be found on MeterProvider."); } @@ -40,7 +42,7 @@ public async Task EnterExitCollectTest(int scrapeResponseCacheDurationMillisecon var collectFunc = exporter.Collect; exporter.Collect = (timeout) => { - bool result = collectFunc(timeout); + bool result = collectFunc!(timeout); runningCollectCount++; Thread.Sleep(5000); return result; @@ -60,7 +62,7 @@ public async Task EnterExitCollectTest(int scrapeResponseCacheDurationMillisecon return new Response { CollectionResponse = response, - ViewPayload = openMetricsRequested ? response.OpenMetricsView.ToArray() : response.PlainTextView.ToArray(), + ViewPayload = openMetricsRequested ? [.. response.OpenMetricsView] : [.. response.PlainTextView], }; } finally @@ -110,7 +112,10 @@ public async Task EnterExitCollectTest(int scrapeResponseCacheDurationMillisecon exporter.CollectionManager.ExitCollect(); } +#pragma warning disable CA1849 // 'Thread.Sleep(int)' synchronously blocks. Use await instead. + // Changing to await Task.Delay leads to test instability. Thread.Sleep(exporter.ScrapeResponseCacheDurationMilliseconds); +#pragma warning restore CA1849 // 'Thread.Sleep(int)' synchronously blocks. Use await instead. counter.Add(100); @@ -118,13 +123,13 @@ public async Task EnterExitCollectTest(int scrapeResponseCacheDurationMillisecon { collectTasks[i] = Task.Run(async () => { - var response = await exporter.CollectionManager.EnterCollect(openMetricsRequested); + var collectionResponse = await exporter.CollectionManager.EnterCollect(openMetricsRequested); try { return new Response { - CollectionResponse = response, - ViewPayload = openMetricsRequested ? response.OpenMetricsView.ToArray() : response.PlainTextView.ToArray(), + CollectionResponse = collectionResponse, + ViewPayload = openMetricsRequested ? [.. collectionResponse.OpenMetricsView] : [.. collectionResponse.PlainTextView], }; } finally @@ -152,10 +157,10 @@ public async Task EnterExitCollectTest(int scrapeResponseCacheDurationMillisecon } } - private class Response + private sealed class Response { public PrometheusCollectionManager.CollectionResponse CollectionResponse; - public byte[] ViewPayload; + public byte[]? ViewPayload; } } diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs index 9ef62de196f..7ff58940692 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusHttpListenerTests.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Metrics; +using System.Globalization; using System.Net; #if NETFRAMEWORK using System.Net.Http; @@ -38,7 +39,7 @@ public void UriPrefixesNull() { Assert.Throws(() => { - TestPrometheusHttpListenerUriPrefixOptions(null); + TestPrometheusHttpListenerUriPrefixOptions(null!); }); } @@ -47,7 +48,7 @@ public void UriPrefixesEmptyList() { Assert.Throws(() => { - TestPrometheusHttpListenerUriPrefixOptions(new string[] { }); + TestPrometheusHttpListenerUriPrefixOptions([]); }); } @@ -56,32 +57,56 @@ public void UriPrefixesInvalid() { Assert.Throws(() => { - TestPrometheusHttpListenerUriPrefixOptions(new string[] { "ftp://example.com" }); + TestPrometheusHttpListenerUriPrefixOptions(["ftp://example.com"]); }); } [Fact] public async Task PrometheusExporterHttpServerIntegration() { - await this.RunPrometheusExporterHttpServerIntegrationTest(); + await RunPrometheusExporterHttpServerIntegrationTest(); } [Fact] public async Task PrometheusExporterHttpServerIntegration_NoMetrics() { - await this.RunPrometheusExporterHttpServerIntegrationTest(skipMetrics: true); + await RunPrometheusExporterHttpServerIntegrationTest(skipMetrics: true); } [Fact] public async Task PrometheusExporterHttpServerIntegration_NoOpenMetrics() { - await this.RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: string.Empty); + await RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: string.Empty); } [Fact] public async Task PrometheusExporterHttpServerIntegration_UseOpenMetricsVersionHeader() { - await this.RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: "application/openmetrics-text; version=1.0.0"); + await RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: "application/openmetrics-text; version=1.0.0"); + } + + [Fact] + public async Task PrometheusExporterHttpServerIntegration_NoOpenMetrics_WithMeterTags() + { + var tags = new KeyValuePair[] + { + new("meter1", "value1"), + new("meter2", "value2"), + }; + + await RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: string.Empty, meterTags: tags); + } + + [Fact] + public async Task PrometheusExporterHttpServerIntegration_OpenMetrics_WithMeterTags() + { + var tags = new KeyValuePair[] + { + new("meter1", "value1"), + new("meter2", "value2"), + }; + + await RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: "application/openmetrics-text; version=1.0.0", meterTags: tags); } [Fact] @@ -89,16 +114,17 @@ public void PrometheusHttpListenerThrowsOnStart() { Random random = new Random(); int retryAttempts = 5; - int port = 0; - string address = null; + string? address = null; - PrometheusExporter exporter = null; - PrometheusHttpListener listener = null; + PrometheusExporter? exporter = null; + PrometheusHttpListener? listener = null; // Step 1: Start a listener on a random port. while (retryAttempts-- != 0) { - port = random.Next(2000, 5000); +#pragma warning disable CA5394 // Do not use insecure randomness + int port = random.Next(2000, 5000); +#pragma warning restore CA5394 // Do not use insecure randomness address = $"http://localhost:{port}/"; try @@ -108,7 +134,7 @@ public void PrometheusHttpListenerThrowsOnStart() exporter, new() { - UriPrefixes = new string[] { address }, + UriPrefixes = [address], }); listener.Start(); @@ -134,7 +160,7 @@ public void PrometheusHttpListenerThrowsOnStart() exporter, new() { - UriPrefixes = new string[] { address }, + UriPrefixes = [address!], }); listener.Start(); @@ -144,6 +170,45 @@ public void PrometheusHttpListenerThrowsOnStart() listener?.Dispose(); } + [Theory] + [InlineData("application/openmetrics-text")] + [InlineData("")] + public async Task PrometheusExporterHttpServerIntegration_TestBufferSizeIncrease_With_LargePayload(string acceptHeader) + { + using var meter = new Meter(MeterName, MeterVersion); + + var attributes = new List>(); + var oneKb = new string('A', 1024); + for (var x = 0; x < 8500; x++) + { + attributes.Add(new KeyValuePair(x.ToString(CultureInfo.InvariantCulture), oneKb)); + } + + var provider = BuildMeterProvider(meter, attributes, out var address); + + for (var x = 0; x < 1000; x++) + { + var counter = meter.CreateCounter("counter_double_" + x, unit: "By"); + counter.Add(1); + } + + using HttpClient client = new HttpClient(); + + if (!string.IsNullOrEmpty(acceptHeader)) + { + client.DefaultRequestHeaders.Add("Accept", acceptHeader); + } + + using var response = await client.GetAsync(new Uri($"{address}metrics")); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("counter_double_999", content, StringComparison.Ordinal); + Assert.DoesNotContain('\0', content); + + provider.Dispose(); + } + private static void TestPrometheusHttpListenerUriPrefixOptions(string[] uriPrefixes) { using var exporter = new PrometheusExporter(new()); @@ -155,31 +220,28 @@ private static void TestPrometheusHttpListenerUriPrefixOptions(string[] uriPrefi }); } - private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetrics = false, string acceptHeader = "application/openmetrics-text") + private static MeterProvider BuildMeterProvider(Meter meter, IEnumerable> attributes, out string address) { - 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); + string? generatedAddress = null; + MeterProvider? provider = null; while (retryAttempts-- != 0) { - port = random.Next(2000, 5000); - address = $"http://localhost:{port}/"; +#pragma warning disable CA5394 // Do not use insecure randomness + int port = random.Next(2000, 5000); +#pragma warning restore CA5394 // Do not use insecure randomness + generatedAddress = $"http://localhost:{port}/"; try { provider = Sdk.CreateMeterProviderBuilder() .AddMeter(meter.Name) - .ConfigureResource(x => x.Clear().AddService("my_service", serviceInstanceId: "id1")) + .ConfigureResource(x => x.Clear().AddService("my_service", serviceInstanceId: "id1").AddAttributes(attributes)) .AddPrometheusHttpListener(options => { - options.UriPrefixes = new string[] { address }; + options.UriPrefixes = [generatedAddress]; }) .Build(); @@ -191,22 +253,30 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri } } - if (provider == null) - { - throw new InvalidOperationException("HttpListener could not be started"); - } + address = generatedAddress!; + + return provider ?? throw new InvalidOperationException("HttpListener could not be started"); + } + + private static async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetrics = false, string acceptHeader = "application/openmetrics-text", KeyValuePair[]? meterTags = null) + { + var requestOpenMetrics = acceptHeader.StartsWith("application/openmetrics-text", StringComparison.Ordinal); - var tags = new KeyValuePair[] + using var meter = new Meter(MeterName, MeterVersion, meterTags); + + var provider = BuildMeterProvider(meter, [], out var address); + + var counterTags = new KeyValuePair[] { - new KeyValuePair("key1", "value1"), - new KeyValuePair("key2", "value2"), + new("key1", "value1"), + new("key2", "value2"), }; var counter = meter.CreateCounter("counter_double", unit: "By"); if (!skipMetrics) { - counter.Add(100.18D, tags); - counter.Add(0.99D, tags); + counter.Add(100.18D, counterTags); + counter.Add(0.99D, counterTags); } using HttpClient client = new HttpClient(); @@ -216,7 +286,7 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri client.DefaultRequestHeaders.Add("Accept", acceptHeader); } - using var response = await client.GetAsync($"{address}metrics"); + using var response = await client.GetAsync(new Uri($"{address}metrics")); if (!skipMetrics) { @@ -225,13 +295,17 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri if (requestOpenMetrics) { - Assert.Equal("application/openmetrics-text; version=1.0.0; charset=utf-8", response.Content.Headers.ContentType.ToString()); + Assert.Equal("application/openmetrics-text; version=1.0.0; charset=utf-8", response.Content.Headers.ContentType!.ToString()); } else { - Assert.Equal("text/plain; charset=utf-8; version=0.0.4", response.Content.Headers.ContentType.ToString()); + Assert.Equal("text/plain; charset=utf-8; version=0.0.4", response.Content.Headers.ContentType!.ToString()); } + var additionalTags = meterTags is { Length: > 0 } + ? $"{string.Join(",", meterTags.Select(x => $"{x.Key}='{x.Value}'"))}," + : string.Empty; + var content = await response.Content.ReadAsStringAsync(); var expected = requestOpenMetrics @@ -243,11 +317,11 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri + $"otel_scope_info{{otel_scope_name='{MeterName}'}} 1\n" + "# TYPE counter_double_bytes counter\n" + "# UNIT counter_double_bytes bytes\n" - + $"counter_double_bytes_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+\\.\\d{{3}})\n" + + $"counter_double_bytes_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',{additionalTags}key1='value1',key2='value2'}} 101.17 (\\d+\\.\\d{{3}})\n" + "# EOF\n" : "# TYPE counter_double_bytes_total counter\n" + "# UNIT counter_double_bytes_total bytes\n" - + $"counter_double_bytes_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+)\n" + + $"counter_double_bytes_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',{additionalTags}key1='value1',key2='value2'}} 101.17 (\\d+)\n" + "# EOF\n"; Assert.Matches(("^" + expected + "$").Replace('\'', '"'), content); diff --git a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs index 7c4a95b05f4..2864fe59b5c 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.HttpListener.Tests/PrometheusSerializerTests.cs @@ -129,7 +129,7 @@ public void GaugeOneDimension() meter.CreateObservableGauge( "test_gauge", - () => new Measurement(123, new KeyValuePair("tagKey", "tagValue"))); + () => new Measurement(123, new KeyValuePair("tagKey", "tagValue"))); provider.ForceFlush(); @@ -156,7 +156,7 @@ public void GaugeBoolDimension() meter.CreateObservableGauge( "test_gauge", - () => new Measurement(123, new KeyValuePair("tagKey", true))); + () => new Measurement(123, new KeyValuePair("tagKey", true))); provider.ForceFlush(); @@ -312,8 +312,8 @@ public void HistogramOneDimension() .Build(); var histogram = meter.CreateHistogram("test_histogram"); - histogram.Record(18, new KeyValuePair("x", "1")); - histogram.Record(100, new KeyValuePair("x", "1")); + histogram.Record(18, new KeyValuePair("x", "1")); + histogram.Record(100, new KeyValuePair("x", "1")); provider.ForceFlush(); @@ -539,8 +539,8 @@ public void HistogramOneDimensionWithOpenMetricsFormat() .Build(); var histogram = meter.CreateHistogram("test_histogram"); - histogram.Record(18, new KeyValuePair("x", "1")); - histogram.Record(100, new KeyValuePair("x", "1")); + histogram.Record(18, new KeyValuePair("x", "1")); + histogram.Record(100, new KeyValuePair("x", "1")); provider.ForceFlush(); @@ -622,8 +622,8 @@ public void HistogramOneDimensionWithScopeVersion() .Build(); var histogram = meter.CreateHistogram("test_histogram"); - histogram.Record(18, new KeyValuePair("x", "1")); - histogram.Record(100, new KeyValuePair("x", "1")); + histogram.Record(18, new KeyValuePair("x", "1")); + histogram.Record(100, new KeyValuePair("x", "1")); provider.ForceFlush(); diff --git a/test/OpenTelemetry.Exporter.Zipkin.Tests/EventSourceTest.cs b/test/OpenTelemetry.Exporter.Zipkin.Tests/EventSourceTests.cs similarity index 93% rename from test/OpenTelemetry.Exporter.Zipkin.Tests/EventSourceTest.cs rename to test/OpenTelemetry.Exporter.Zipkin.Tests/EventSourceTests.cs index 0394d1a142d..2e7ef5fab46 100644 --- a/test/OpenTelemetry.Exporter.Zipkin.Tests/EventSourceTest.cs +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/EventSourceTests.cs @@ -7,7 +7,7 @@ namespace OpenTelemetry.Exporter.Zipkin.Tests; -public class EventSourceTest +public class EventSourceTests { [Fact] public void EventSourceTest_ZipkinExporterEventSource() diff --git a/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/RemoteEndpointPriorityTestCase.cs b/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/RemoteEndpointPriorityTestCase.cs new file mode 100644 index 00000000000..527961bc816 --- /dev/null +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/RemoteEndpointPriorityTestCase.cs @@ -0,0 +1,160 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Exporter.Zipkin.Implementation.Tests; + +#pragma warning disable CA1515 // Consider making public types internal +public class RemoteEndpointPriorityTestCase +#pragma warning restore CA1515 // Consider making public types internal +{ + public static TheoryData TestCases => + [ + new() + { + Name = "Rank 1: Only peer.service provided", + ExpectedResult = "PeerService", + RemoteEndpointAttributes = new Dictionary + { + [SemanticConventions.AttributePeerService] = "PeerService", + }, + }, + new() + { + Name = "Rank 2: Only server.address provided", + ExpectedResult = "ServerAddress", + RemoteEndpointAttributes = new Dictionary + { + [SemanticConventions.AttributeServerAddress] = "ServerAddress", + }, + }, + new() + { + Name = "Rank 3: Only net.peer.name provided", + ExpectedResult = "NetPeerName", + RemoteEndpointAttributes = new Dictionary + { + [SemanticConventions.AttributeNetPeerName] = "NetPeerName", + }, + }, + new() + { + Name = "Rank 4: network.peer.address and network.peer.port provided", + ExpectedResult = "1.2.3.4:5678", + RemoteEndpointAttributes = new Dictionary + { + [SemanticConventions.AttributeNetworkPeerAddress] = "1.2.3.4", + [SemanticConventions.AttributeNetworkPeerPort] = "5678", + }, + }, + new() + { + Name = "Rank 4: Only network.peer.address provided", + ExpectedResult = "1.2.3.4", + RemoteEndpointAttributes = new Dictionary + { + [SemanticConventions.AttributeNetworkPeerAddress] = "1.2.3.4", + }, + }, + new() + { + Name = "Rank 5: Only server.socket.domain provided", + ExpectedResult = "SocketDomain", + RemoteEndpointAttributes = new Dictionary + { + [SemanticConventions.AttributeServerSocketDomain] = "SocketDomain", + }, + }, + new() + { + Name = "Rank 6: server.socket.address and server.socket.port provided", + ExpectedResult = "SocketAddress:4321", + RemoteEndpointAttributes = new Dictionary + { + [SemanticConventions.AttributeServerSocketAddress] = "SocketAddress", + [SemanticConventions.AttributeServerSocketPort] = "4321", + }, + }, + new() + { + Name = "Rank 7: Only net.sock.peer.name provided", + ExpectedResult = "NetSockPeerName", + RemoteEndpointAttributes = new Dictionary + { + [SemanticConventions.AttributeNetSockPeerName] = "NetSockPeerName", + }, + }, + new() + { + Name = "Rank 8: net.sock.peer.addr and net.sock.peer.port provided", + ExpectedResult = "5.6.7.8:8765", + RemoteEndpointAttributes = new Dictionary + { + [SemanticConventions.AttributeNetSockPeerAddr] = "5.6.7.8", + [SemanticConventions.AttributeNetSockPeerPort] = "8765", + }, + }, + new() + { + Name = "Rank 9: Only peer.hostname provided", + ExpectedResult = "PeerHostname", + RemoteEndpointAttributes = new Dictionary + { + [SemanticConventions.AttributePeerHostname] = "PeerHostname", + }, + }, + new() + { + Name = "Rank 10: Only peer.address provided", + ExpectedResult = "PeerAddress", + RemoteEndpointAttributes = new Dictionary + { + [SemanticConventions.AttributePeerAddress] = "PeerAddress", + }, + }, + new() + { + Name = "Rank 11: Only db.name provided", + ExpectedResult = "DbName", + RemoteEndpointAttributes = new Dictionary + { + [SemanticConventions.AttributeDbName] = "DbName", + }, + }, + new() + { + Name = "Multiple attributes: highest rank wins", + ExpectedResult = "PeerService", + RemoteEndpointAttributes = new Dictionary + { + [SemanticConventions.AttributeDbName] = "DbName", + [SemanticConventions.AttributePeerAddress] = "PeerAddress", + [SemanticConventions.AttributePeerHostname] = "PeerHostname", + [SemanticConventions.AttributeNetSockPeerAddr] = "5.6.7.8", + [SemanticConventions.AttributeNetSockPeerPort] = "8765", + [SemanticConventions.AttributeNetSockPeerName] = "NetSockPeerName", + [SemanticConventions.AttributeServerSocketAddress] = "SocketAddress", + [SemanticConventions.AttributeServerSocketPort] = "4321", + [SemanticConventions.AttributeServerSocketDomain] = "SocketDomain", + [SemanticConventions.AttributeNetworkPeerAddress] = "1.2.3.4", + [SemanticConventions.AttributeNetworkPeerPort] = "5678", + [SemanticConventions.AttributeNetPeerName] = "NetPeerName", + [SemanticConventions.AttributeServerAddress] = "ServerAddress", + [SemanticConventions.AttributePeerService] = "PeerService", + }, + }, + ]; + + public string? Name { get; private set; } + + public string? ExpectedResult { get; private set; } + + public Dictionary? RemoteEndpointAttributes { get; private set; } + + public override string? ToString() + { + return this.Name; + } +} diff --git a/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityConversionExtensionsTest.cs b/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityConversionExtensionsTest.cs deleted file mode 100644 index 6e28b18d88c..00000000000 --- a/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityConversionExtensionsTest.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright The OpenTelemetry Authors -// 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; - -public class ZipkinActivityConversionExtensionsTest -{ - [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 - { - Tags = PooledList>.Create(), - }; - - using var activity = new Activity("TestActivity"); - activity.SetTag(key, value); - - attributeEnumerationState.EnumerateTags(activity); - - 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) - { - var attributeEnumerationState = new TagEnumerationState - { - Tags = PooledList>.Create(), - }; - - using var activity = new Activity("TestActivity"); - activity.SetTag(key, value); - - attributeEnumerationState.EnumerateTags(activity); - - Assert.Empty(attributeEnumerationState.Tags); - } -} diff --git a/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityConversionExtensionsTests.cs b/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityConversionExtensionsTests.cs new file mode 100644 index 00000000000..fbb958ceb74 --- /dev/null +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityConversionExtensionsTests.cs @@ -0,0 +1,56 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using OpenTelemetry.Internal; +using Xunit; + +namespace OpenTelemetry.Exporter.Zipkin.Implementation.Tests; + +public class ZipkinActivityConversionExtensionsTests +{ + [Theory] + [InlineData("int", 1)] + [InlineData("string", "s")] + [InlineData("bool", true)] + [InlineData("double", 1.0)] + public void CheckProcessTag(string key, object value) + { + using var activity = new Activity("TestActivity"); + activity.SetTag(key, value); + + var tags = PooledList>.Create(); + ExtractTags(activity, ref tags); + + var tag = Assert.Single(tags); + Assert.Equal(key, tag.Key); + Assert.Equal(value, tag.Value); + } + + [Theory] + [InlineData("int", null)] + [InlineData("string", null)] + [InlineData("bool", null)] + [InlineData("double", null)] + public void CheckNullValueProcessTag(string key, object? value) + { + using var activity = new Activity("TestActivity"); + activity.SetTag(key, value); + + var tags = PooledList>.Create(); + ExtractTags(activity, ref tags); + + Assert.Empty(tags); + } + + private static void ExtractTags(Activity activity, ref PooledList> tags) + { + foreach (var tag in activity.TagObjects) + { + if (tag.Value != null) + { + PooledList>.Add(ref tags, new KeyValuePair(tag.Key, tag.Value)); + } + } + } +} diff --git a/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityConversionTest.cs b/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityConversionTests.cs similarity index 74% rename from test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityConversionTest.cs rename to test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityConversionTests.cs index da6cdcbfcbb..723e113da71 100644 --- a/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityConversionTest.cs +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityConversionTests.cs @@ -9,7 +9,7 @@ namespace OpenTelemetry.Exporter.Zipkin.Implementation.Tests; -public class ZipkinActivityConversionTest +public class ZipkinActivityConversionTests { private const string ZipkinSpanName = "Name"; private static readonly ZipkinEndpoint DefaultZipkinEndpoint = new("TestService"); @@ -18,7 +18,7 @@ public class ZipkinActivityConversionTest public void ToZipkinSpan_AllPropertiesSet() { // Arrange - using var activity = ZipkinExporterTests.CreateTestActivity(); + using var activity = ZipkinActivitySource.CreateTestActivity(); // Act & Assert var zipkinSpan = activity.ToZipkinSpan(DefaultZipkinEndpoint); @@ -51,7 +51,7 @@ public void ToZipkinSpan_AllPropertiesSet() public void ToZipkinSpan_NoEvents() { // Arrange - using var activity = ZipkinExporterTests.CreateTestActivity(addEvents: false); + using var activity = ZipkinActivitySource.CreateTestActivity(addEvents: false); // Act & Assert var zipkinSpan = activity.ToZipkinSpan(DefaultZipkinEndpoint); @@ -76,13 +76,14 @@ public void ToZipkinSpan_NoEvents() [Theory] [InlineData(StatusCode.Unset, "unset")] - [InlineData(StatusCode.Ok, "Ok")] + [InlineData(StatusCode.Ok, "OK")] [InlineData(StatusCode.Error, "ERROR")] [InlineData(StatusCode.Unset, "iNvAlId")] + [Obsolete("Remove when ActivityExtensions status APIs are removed")] public void ToZipkinSpan_Status_ErrorFlagTest(StatusCode expectedStatusCode, string statusCodeTagValue) { // Arrange - using var activity = ZipkinExporterTests.CreateTestActivity(); + using var activity = ZipkinActivitySource.CreateTestActivity(); activity.SetTag(SpanAttributeConstants.StatusCodeKey, statusCodeTagValue); // Act @@ -105,11 +106,11 @@ public void ToZipkinSpan_Status_ErrorFlagTest(StatusCode expectedStatusCode, str if (expectedStatusCode == StatusCode.Error) { - Assert.Contains(zipkinSpan.Tags, t => t.Key == "error" && (string)t.Value == string.Empty); + Assert.Contains(zipkinSpan.Tags, t => t.Key == ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName && ((string?)t.Value)?.Length == 0); } else { - Assert.DoesNotContain(zipkinSpan.Tags, t => t.Key == "error"); + Assert.DoesNotContain(zipkinSpan.Tags, t => t.Key == ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName); } } @@ -121,7 +122,7 @@ public void ToZipkinSpan_Activity_Status_And_StatusDescription_is_Set(ActivitySt { // Arrange. const string description = "Description when ActivityStatusCode is Error."; - using var activity = ZipkinExporterTests.CreateTestActivity(); + using var activity = ZipkinActivitySource.CreateTestActivity(); activity.SetStatus(expectedStatusCode, description); // Act. @@ -148,7 +149,7 @@ public void ToZipkinSpan_Activity_Status_And_StatusDescription_is_Set(ActivitySt Assert.Contains( zipkinSpan.Tags, t => t.Key == ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName && - (string)t.Value == description); + (string?)t.Value == description); } else { @@ -158,11 +159,51 @@ public void ToZipkinSpan_Activity_Status_And_StatusDescription_is_Set(ActivitySt } } + [Theory] + [InlineData("FromStatusDescription", null, null, "FromStatusDescription")] + [InlineData(null, "FromStatusDescriptionKeyTag", null, "FromStatusDescriptionKeyTag")] + [InlineData(null, null, "FromZipkinErrorFlagTag", "FromZipkinErrorFlagTag")] + [InlineData(null, null, null, "")] + public void ToZipkinSpan_StatusDescription_ErrorTagTest( + string? statusDescription, + string? statusDescriptionTag, + string? zipkinErrorTag, + string expectedErrorTagValue) + { + // Arrange + using var activity = ZipkinActivitySource.CreateTestActivity(); + activity.SetStatus(ActivityStatusCode.Error); + + if (statusDescription != null) + { + activity.SetStatus(ActivityStatusCode.Error, statusDescription); + } + + if (statusDescriptionTag != null) + { + activity.SetTag(SpanAttributeConstants.StatusDescriptionKey, statusDescriptionTag); + } + + if (zipkinErrorTag != null) + { + activity.SetTag(ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName, zipkinErrorTag); + } + + // Act + var zipkinSpan = activity.ToZipkinSpan(DefaultZipkinEndpoint); + + // Assert + var errorTag = zipkinSpan.Tags.FirstOrDefault(t => t.Key == ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName); + + Assert.Equal(expectedErrorTagValue, errorTag.Value); + } + [Fact] + [Obsolete("Remove when ActivityExtensions status APIs are removed")] public void ActivityStatus_Takes_precedence_Over_Status_Tags_ActivityStatusCodeIsOk() { // Arrange. - using var activity = ZipkinExporterTests.CreateTestActivity(); + using var activity = ZipkinActivitySource.CreateTestActivity(); activity.SetStatus(ActivityStatusCode.Ok); activity.SetTag(SpanAttributeConstants.StatusCodeKey, "ERROR"); @@ -175,19 +216,20 @@ public void ActivityStatus_Takes_precedence_Over_Status_Tags_ActivityStatusCodeI // Assert. Assert.Equal("OK", 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"); + 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"); // Ensure additional Activity tags were being converted. - Assert.Contains(zipkinSpan.Tags, t => t.Key == "myCustomTag" && (string)t.Value == "myCustomTagValue"); + Assert.Contains(zipkinSpan.Tags, t => t.Key == "myCustomTag" && (string?)t.Value == "myCustomTagValue"); Assert.DoesNotContain(zipkinSpan.Tags, t => t.Key == ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName); } [Fact] + [Obsolete("Remove when ActivityExtensions status APIs are removed")] public void ActivityStatus_Takes_precedence_Over_Status_Tags_ActivityStatusCodeIsError() { // Arrange. - using var activity = ZipkinExporterTests.CreateTestActivity(); + using var activity = ZipkinActivitySource.CreateTestActivity(); const string StatusDescriptionOnError = "Description when ActivityStatusCode is Error."; const string TagDescriptionOnError = "Description when TagStatusCode is Error."; @@ -208,21 +250,22 @@ public void ActivityStatus_Takes_precedence_Over_Status_Tags_ActivityStatusCodeI Assert.Contains( zipkinSpan.Tags, t => t.Key == ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName && - (string)t.Value == StatusDescriptionOnError); + (string?)t.Value == StatusDescriptionOnError); Assert.DoesNotContain( zipkinSpan.Tags, t => t.Key == ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName && - (string)t.Value == TagDescriptionOnError); + (string?)t.Value == TagDescriptionOnError); // Ensure additional Activity tags were being converted. - Assert.Contains(zipkinSpan.Tags, t => t.Key == "myCustomTag" && (string)t.Value == "myCustomTagValue"); + Assert.Contains(zipkinSpan.Tags, t => t.Key == "myCustomTag" && (string?)t.Value == "myCustomTagValue"); } [Fact] + [Obsolete("Remove when ActivityExtensions status APIs are removed")] public void ActivityStatus_Takes_precedence_Over_Status_Tags_ActivityStatusCodeIsError_SettingTagFirst() { // Arrange. - using var activity = ZipkinExporterTests.CreateTestActivity(); + using var activity = ZipkinActivitySource.CreateTestActivity(); const string StatusDescriptionOnError = "Description when ActivityStatusCode is Error."; const string TagDescriptionOnError = "Description when TagStatusCode is Error."; @@ -243,13 +286,13 @@ public void ActivityStatus_Takes_precedence_Over_Status_Tags_ActivityStatusCodeI Assert.Contains( zipkinSpan.Tags, t => t.Key == ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName && - (string)t.Value == StatusDescriptionOnError); + (string?)t.Value == StatusDescriptionOnError); Assert.DoesNotContain( zipkinSpan.Tags, t => t.Key == ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName && - (string)t.Value == TagDescriptionOnError); + (string?)t.Value == TagDescriptionOnError); // Ensure additional Activity tags were being converted. - Assert.Contains(zipkinSpan.Tags, t => t.Key == "myCustomTag" && (string)t.Value == "myCustomTagValue"); + 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 ddbd059c0cf..06b17f954f0 100644 --- a/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityExporterRemoteEndpointTests.cs +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinActivityExporterRemoteEndpointTests.cs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 using OpenTelemetry.Exporter.Zipkin.Tests; -using OpenTelemetry.Trace; using Xunit; namespace OpenTelemetry.Exporter.Zipkin.Implementation.Tests; @@ -11,41 +10,22 @@ public class ZipkinActivityExporterRemoteEndpointTests { private static readonly ZipkinEndpoint DefaultZipkinEndpoint = new("TestService"); - [Fact] - public void GenerateActivity_RemoteEndpointOmittedByDefault() - { - // Arrange - using var activity = ZipkinExporterTests.CreateTestActivity(); - - // Act & Assert - var zipkinSpan = ZipkinActivityConversionExtensions.ToZipkinSpan(activity, DefaultZipkinEndpoint); - - Assert.NotNull(zipkinSpan.RemoteEndpoint); - } - - [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); - - Assert.NotNull(zipkinSpan.RemoteEndpoint); - Assert.Equal("RemoteServiceName", zipkinSpan.RemoteEndpoint.ServiceName); - } - [Theory] - [MemberData(nameof(RemoteEndpointPriorityTestCase.GetTestCases), MemberType = typeof(RemoteEndpointPriorityTestCase))] + [MemberData(nameof(RemoteEndpointPriorityTestCase.TestCases), MemberType = typeof(RemoteEndpointPriorityTestCase))] public void GenerateActivity_RemoteEndpointResolutionPriority(RemoteEndpointPriorityTestCase testCase) { +#if NET + Assert.NotNull(testCase); +#else + if (testCase == null) + { + throw new ArgumentNullException(nameof(testCase)); + } +#endif + // Arrange - using var activity = ZipkinExporterTests.CreateTestActivity(additionalAttributes: testCase.RemoteEndpointAttributes); + using var activity = + ZipkinActivitySource.CreateTestActivity(additionalAttributes: testCase.RemoteEndpointAttributes!); // Act & Assert var zipkinSpan = ZipkinActivityConversionExtensions.ToZipkinSpan(activity, DefaultZipkinEndpoint); @@ -53,130 +33,4 @@ public void GenerateActivity_RemoteEndpointResolutionPriority(RemoteEndpointPrio Assert.NotNull(zipkinSpan.RemoteEndpoint); Assert.Equal(testCase.ExpectedResult, zipkinSpan.RemoteEndpoint.ServiceName); } - - 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; - } - } } 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 f5a72a2f25f..7970f2d56ee 100644 --- a/test/OpenTelemetry.Exporter.Zipkin.Tests/OpenTelemetry.Exporter.Zipkin.Tests.csproj +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/OpenTelemetry.Exporter.Zipkin.Tests.csproj @@ -1,9 +1,10 @@ + Unit test project for Zipkin Exporter for OpenTelemetry $(TargetFrameworksForTests) - - disable + + false @@ -14,14 +15,9 @@ - - + - - - runtime; build; native; contentfiles; analyzers - diff --git a/test/OpenTelemetry.Tests/Internal/PooledListTest.cs b/test/OpenTelemetry.Exporter.Zipkin.Tests/PooledListTests.cs similarity index 84% rename from test/OpenTelemetry.Tests/Internal/PooledListTest.cs rename to test/OpenTelemetry.Exporter.Zipkin.Tests/PooledListTests.cs index eccbef450ad..19b7e3f0378 100644 --- a/test/OpenTelemetry.Tests/Internal/PooledListTest.cs +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/PooledListTests.cs @@ -2,13 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 using System.Collections; -using System.Reflection; using Xunit; namespace OpenTelemetry.Internal.Tests; -public class PooledListTest +public class PooledListTests { [Fact] public void Verify_ICollectionExplicitProperties() @@ -44,17 +43,9 @@ public void Verify_CreateAddClear() [Fact] public void Verify_AllocatedSize() { - 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); + var size = PooledList.LastAllocatedSize; Assert.Equal(64, size); // The Add() method has a condition to double the size of the buffer @@ -65,7 +56,7 @@ int GetLastAllocatedSize(PooledList pooledList) PooledList.Add(ref pooledList, i); } - size = GetLastAllocatedSize(pooledList); + size = PooledList.LastAllocatedSize; Assert.Equal(128, size); } diff --git a/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinActivitySource.cs b/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinActivitySource.cs new file mode 100644 index 00000000000..f638a99f52b --- /dev/null +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinActivitySource.cs @@ -0,0 +1,128 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using Xunit; + +namespace OpenTelemetry.Exporter.Zipkin.Tests; + +internal static class ZipkinActivitySource +{ + private static readonly ActivitySource ActivitySource = new(nameof(ZipkinActivitySource)); + + private static readonly bool[] BoolArray = [true, false]; + + internal static Activity CreateTestActivity( + bool isRootSpan = false, + bool setAttributes = true, + Dictionary? additionalAttributes = null, + bool addEvents = true, + bool addLinks = true, + ActivityKind kind = ActivityKind.Client, + ActivityStatusCode statusCode = ActivityStatusCode.Unset, + string? statusDescription = null, + DateTime? dateTime = null) + { + using var activityListener = new ActivityListener + { + ShouldListenTo = _ => true, + Sample = (ref ActivityCreationOptions options) => options.Parent.TraceFlags.HasFlag(ActivityTraceFlags.Recorded) + ? ActivitySamplingResult.AllDataAndRecorded + : ActivitySamplingResult.AllData, + }; + + ActivitySource.AddActivityListener(activityListener); + + var startTimestamp = DateTime.UtcNow; + var endTimestamp = startTimestamp.AddSeconds(60); + var eventTimestamp = DateTime.UtcNow; + var traceId = ActivityTraceId.CreateFromString("e8ea7e9ac72de94e91fabc613f9686b2".AsSpan()); + + dateTime ??= DateTime.UtcNow; + + var parentSpanId = isRootSpan ? default : ActivitySpanId.CreateFromBytes([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", BoolArray }, + { "http.host", "http://localhost:44312/" }, // simulating instrumentation tag adding http.host + { "dateTimeKey", dateTime.Value }, + { "dateTimeArrayKey", new DateTime[] { dateTime.Value } }, + }; + if (additionalAttributes != null) + { + foreach (var attribute in additionalAttributes) + { + if (!attributes.ContainsKey(attribute.Key)) + { + attributes.Add(attribute.Key, attribute.Value); + } + } + } + + var events = new List + { + new( + "Event1", + eventTimestamp, + new(new Dictionary + { + { "key", "value" }, + })), + new( + "Event2", + eventTimestamp, + new(new Dictionary + { + { "key", "value" }, + })), + }; + + var linkedSpanId = ActivitySpanId.CreateFromString("888915b6286b9c41".AsSpan()); + + var tags = setAttributes ? + attributes.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value)) + : null; + var links = addLinks ? + new[] + { + new ActivityLink(new( + traceId, + linkedSpanId, + ActivityTraceFlags.Recorded)), + } + : null; + + var activity = ActivitySource.StartActivity( + "Name", + kind, + parentContext: new(traceId, parentSpanId, ActivityTraceFlags.Recorded), + tags, + links, + startTime: startTimestamp)!; + + Assert.NotNull(activity); + + if (addEvents) + { + foreach (var evnt in events) + { + activity.AddEvent(evnt); + } + } + + activity.SetStatus(statusCode, statusDescription); + + activity.SetEndTime(endTimestamp); + activity.Stop(); + + return activity; + } +} diff --git a/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinExporterTests.cs b/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinExporterTests.cs index 308f2e6715a..f4b3f113ac7 100644 --- a/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinExporterTests.cs +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinExporterTests.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Diagnostics; +using System.Globalization; using System.Net; #if NETFRAMEWORK using System.Net.Http; @@ -18,7 +19,7 @@ namespace OpenTelemetry.Exporter.Zipkin.Tests; -public class ZipkinExporterTests : IDisposable +public sealed class ZipkinExporterTests : IDisposable { private const string TraceId = "e8ea7e9ac72de94e91fabc613f9686b2"; private static readonly ConcurrentDictionary Responses = new(); @@ -31,14 +32,6 @@ static ZipkinExporterTests() { Activity.DefaultIdFormat = ActivityIdFormat.W3C; Activity.ForceDefaultIdFormat = true; - - var listener = new ActivityListener - { - ShouldListenTo = _ => true, - Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, - }; - - ActivitySource.AddActivityListener(listener); } public ZipkinExporterTests() @@ -52,12 +45,12 @@ static void ProcessServerRequest(HttpListenerContext context) { context.Response.StatusCode = 200; - using StreamReader readStream = new StreamReader(context.Request.InputStream); + using StreamReader readStream = new(context.Request.InputStream); string requestContent = readStream.ReadToEnd(); Responses.TryAdd( - Guid.Parse(context.Request.QueryString["requestId"]), + Guid.Parse(context.Request.QueryString["requestId"]!), requestContent); context.Response.OutputStream.Close(); @@ -67,7 +60,6 @@ static void ProcessServerRequest(HttpListenerContext context) public void Dispose() { this.testServer.Dispose(); - GC.SuppressFinalize(this); } [Fact] @@ -94,16 +86,18 @@ public void AddAddZipkinExporterNamedOptionsSupported() [Fact] public void BadArgs() { - TracerProviderBuilder builder = null; - Assert.Throws(() => builder.AddZipkinExporter()); + TracerProviderBuilder? builder = null; + Assert.Throws(() => builder!.AddZipkinExporter()); } [Fact] public void SuppressesInstrumentation() { - const string ActivitySourceName = "zipkin.test"; + const string activitySourceName = "zipkin.test"; Guid requestId = Guid.NewGuid(); - TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); +#pragma warning disable CA2000 // Dispose objects before losing scope + TestActivityProcessor testActivityProcessor = new(); +#pragma warning restore CA2000 // Dispose objects before losing scope int endCalledCount = 0; @@ -115,19 +109,19 @@ public void SuppressesInstrumentation() var exporterOptions = new ZipkinExporterOptions { - Endpoint = new Uri($"http://{this.testServerHost}:{this.testServerPort}/api/v2/spans?requestId={requestId}"), + Endpoint = new($"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) + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) .AddProcessor(testActivityProcessor) .AddProcessor(exportActivityProcessor) .AddHttpClientInstrumentation() .Build(); - using var source = new ActivitySource(ActivitySourceName); + using var source = new ActivitySource(activitySourceName); using var activity = source.StartActivity("Test Zipkin Activity"); activity?.Stop(); @@ -150,7 +144,7 @@ public void EndpointConfigurationUsingEnvironmentVariable() var exporterOptions = new ZipkinExporterOptions(); - Assert.Equal(new Uri(Environment.GetEnvironmentVariable(ZipkinExporterOptions.ZipkinEndpointEnvVar)).AbsoluteUri, exporterOptions.Endpoint.AbsoluteUri); + Assert.Equal(new Uri(Environment.GetEnvironmentVariable(ZipkinExporterOptions.ZipkinEndpointEnvVar)!).AbsoluteUri, exporterOptions.Endpoint.AbsoluteUri); } finally { @@ -167,7 +161,7 @@ public void IncodeEndpointConfigTakesPrecedenceOverEnvironmentVariable() var exporterOptions = new ZipkinExporterOptions { - Endpoint = new Uri("http://urifromcode"), + Endpoint = new("http://urifromcode"), }; Assert.Equal(new Uri("http://urifromcode").AbsoluteUri, exporterOptions.Endpoint.AbsoluteUri); @@ -178,7 +172,7 @@ public void IncodeEndpointConfigTakesPrecedenceOverEnvironmentVariable() } } - [Fact(Skip = "https://github.com/open-telemetry/opentelemetry-dotnet/issues/3690")] + [Fact] public void ErrorGettingUriFromEnvVarSetsDefaultEndpointValue() { try @@ -187,7 +181,7 @@ public void ErrorGettingUriFromEnvVarSetsDefaultEndpointValue() var options = new ZipkinExporterOptions(); - Assert.Equal(new Uri(ZipkinExporterOptions.DefaultZipkinEndpoint), options.Endpoint); + Assert.Equal(new(ZipkinExporterOptions.DefaultZipkinEndpoint), options.Endpoint); } finally { @@ -204,18 +198,18 @@ public void EndpointConfigurationUsingIConfiguration() }; var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(values) + .AddInMemoryCollection(values!) .Build(); var options = new ZipkinExporterOptions(configuration, new()); - Assert.Equal(new Uri("http://custom-endpoint:12345"), options.Endpoint); + Assert.Equal(new("http://custom-endpoint:12345"), options.Endpoint); } [Fact] public void UserHttpFactoryCalled() { - ZipkinExporterOptions options = new ZipkinExporterOptions(); + ZipkinExporterOptions options = new(); var defaultFactory = options.HttpClientFactory; @@ -246,13 +240,13 @@ public void UserHttpFactoryCalled() Assert.Equal(2, invocations); } - options.HttpClientFactory = null; + options.HttpClientFactory = null!; Assert.Throws(() => { using var exporter = new ZipkinExporter(options); }); - options.HttpClientFactory = () => null; + options.HttpClientFactory = () => null!; Assert.Throws(() => { using var exporter = new ZipkinExporter(options); @@ -283,11 +277,11 @@ public void ServiceProviderHttpClientFactoryInvoked() [Fact] public void UpdatesServiceNameFromDefaultResource() { - var zipkinExporter = new ZipkinExporter(new ZipkinExporterOptions()); + using var zipkinExporter = new ZipkinExporter(new()); zipkinExporter.SetLocalEndpointFromResource(Resource.Empty); - Assert.StartsWith("unknown_service:", zipkinExporter.LocalEndpoint.ServiceName); + Assert.StartsWith("unknown_service:", zipkinExporter.LocalEndpoint!.ServiceName, StringComparison.Ordinal); } [Fact] @@ -302,18 +296,20 @@ public void UpdatesServiceNameFromIConfiguration() }; services.AddSingleton( - new ConfigurationBuilder().AddInMemoryCollection(configuration).Build()); + new ConfigurationBuilder().AddInMemoryCollection(configuration!).Build()); }); - var zipkinExporter = new ZipkinExporter(new ZipkinExporterOptions()); +#pragma warning disable CA2000 // Dispose objects before losing scope + var zipkinExporter = new ZipkinExporter(new()); tracerProviderBuilder.AddProcessor(new BatchActivityExportProcessor(zipkinExporter)); +#pragma warning restore CA2000 // Dispose objects before losing scope using var provider = tracerProviderBuilder.Build(); zipkinExporter.SetLocalEndpointFromResource(Resource.Empty); - Assert.Equal("myservicename", zipkinExporter.LocalEndpoint.ServiceName); + Assert.Equal("myservicename", zipkinExporter.LocalEndpoint!.ServiceName); } [Theory] @@ -321,44 +317,33 @@ public void UpdatesServiceNameFromIConfiguration() [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")] + [InlineData(false, false, false, ActivityStatusCode.Ok)] + [InlineData(false, false, false, ActivityStatusCode.Ok, null, true)] + [InlineData(false, false, false, ActivityStatusCode.Error)] + [InlineData(false, false, false, ActivityStatusCode.Error, "Error description")] public void IntegrationTest( bool useShortTraceIds, bool useTestResource, bool isRootSpan, - StatusCode statusCode = StatusCode.Unset, - string statusDescription = null, + ActivityStatusCode statusCode = ActivityStatusCode.Unset, + string? statusDescription = null, bool addErrorTag = false) { - var status = statusCode switch - { - StatusCode.Unset => Status.Unset, - StatusCode.Ok => Status.Ok, - StatusCode.Error => Status.Error, - _ => throw new InvalidOperationException(), - }; - - if (!string.IsNullOrEmpty(statusDescription)) - { - status = status.WithDescription(statusDescription); - } - Guid requestId = Guid.NewGuid(); - ZipkinExporter exporter = new ZipkinExporter( - new ZipkinExporterOptions +#pragma warning disable CA2000 // Dispose objects before losing scope + ZipkinExporter exporter = new( + new() { - Endpoint = new Uri($"http://{this.testServerHost}:{this.testServerPort}/api/v2/spans?requestId={requestId}"), + Endpoint = new($"http://{this.testServerHost}:{this.testServerPort}/api/v2/spans?requestId={requestId}"), UseShortTraceIds = useShortTraceIds, }); +#pragma warning restore CA2000 // Dispose objects before losing scope - var serviceName = (string)exporter.ParentProvider.GetDefaultResource().Attributes - .Where(pair => pair.Key == ResourceSemanticConventions.AttributeServiceName).FirstOrDefault().Value; + var serviceName = (string)exporter.ParentProvider.GetDefaultResource().Attributes.FirstOrDefault(pair => pair.Key == ResourceSemanticConventions.AttributeServiceName).Value; var resourceTags = string.Empty; - var activity = CreateTestActivity(isRootSpan: isRootSpan, status: status); + var dateTime = DateTime.UtcNow; + using var activity = ZipkinActivitySource.CreateTestActivity(isRootSpan: isRootSpan, statusCode: statusCode, statusDescription: statusDescription, dateTime: dateTime); if (useTestResource) { serviceName = "MyService"; @@ -374,12 +359,14 @@ public void IntegrationTest( exporter.SetLocalEndpointFromResource(Resource.Empty); } + activity.SetTag(SemanticConventions.AttributePeerService, "http://localhost:44312/"); + if (addErrorTag) { activity.SetTag(ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName, "This should be removed."); } - var processor = new SimpleActivityExportProcessor(exporter); + using var processor = new SimpleActivityExportProcessor(exporter); processor.OnEnd(activity); @@ -388,15 +375,23 @@ public void IntegrationTest( var timestamp = activity.StartTimeUtc.ToEpochMicroseconds(); var eventTimestamp = activity.Events.First().Timestamp.ToEpochMicroseconds(); - StringBuilder ipInformation = new StringBuilder(); - if (!string.IsNullOrEmpty(exporter.LocalEndpoint.Ipv4)) + StringBuilder ipInformation = new(); + if (!string.IsNullOrEmpty(exporter.LocalEndpoint!.Ipv4)) { +#if NET + ipInformation.Append(CultureInfo.InvariantCulture, $@",""ipv4"":""{exporter.LocalEndpoint.Ipv4}"""); +#else ipInformation.Append($@",""ipv4"":""{exporter.LocalEndpoint.Ipv4}"""); +#endif } if (!string.IsNullOrEmpty(exporter.LocalEndpoint.Ipv6)) { +#if NET + ipInformation.Append(CultureInfo.InvariantCulture, $@",""ipv6"":""{exporter.LocalEndpoint.Ipv6}"""); +#else ipInformation.Append($@",""ipv6"":""{exporter.LocalEndpoint.Ipv6}"""); +#endif } var parentId = isRootSpan ? string.Empty : $@"""parentId"":""{ZipkinActivityConversionExtensions.EncodeSpanId(activity.ParentSpanId)}"","; @@ -407,13 +402,13 @@ public void IntegrationTest( string errorTag = string.Empty; switch (statusCode) { - case StatusCode.Ok: + case ActivityStatusCode.Ok: statusTag = $@"""{SpanAttributeConstants.StatusCodeKey}"":""OK"","; break; - case StatusCode.Unset: + case ActivityStatusCode.Unset: statusTag = string.Empty; break; - case StatusCode.Error: + case ActivityStatusCode.Error: statusTag = $@"""{SpanAttributeConstants.StatusCodeKey}"":""ERROR"","; errorTag = $@"""{ZipkinActivityConversionExtensions.ZipkinErrorFlagTagName}"":""{statusDescription}"","; break; @@ -422,109 +417,35 @@ public void IntegrationTest( } 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.scope.name"":""CreateTestActivity"",""otel.library.name"":""CreateTestActivity"",""peer.service"":""http://localhost:44312/""}}}}]", + $@"[{{""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/""," + + $@"""dateTimeKey"":""{Convert.ToString(dateTime, CultureInfo.InvariantCulture)}""," + + $@"""dateTimeArrayKey"":""[\u0022{Convert.ToString(dateTime, CultureInfo.InvariantCulture)}\u0022]""," + + $@"""peer.service"":""http://localhost:44312/""," + + statusTag + + errorTag + + @"""otel.scope.name"":""ZipkinActivitySource""," + + @"""otel.library.name"":""ZipkinActivitySource""" + + "}}]", Responses[requestId]); } - - 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 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) - { - if (!attributes.ContainsKey(attribute.Key)) - { - 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 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) - { - activity.AddEvent(evnt); - } - } - - 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/EventSourceTests.cs similarity index 93% rename from test/OpenTelemetry.Extensions.Hosting.Tests/EventSourceTest.cs rename to test/OpenTelemetry.Extensions.Hosting.Tests/EventSourceTests.cs index 83d475bfec0..ee622b185e1 100644 --- a/test/OpenTelemetry.Extensions.Hosting.Tests/EventSourceTest.cs +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/EventSourceTests.cs @@ -7,7 +7,7 @@ namespace OpenTelemetry.Extensions.Hosting.Tests; -public class EventSourceTest +public class EventSourceTests { [Fact] public void EventSourceTest_HostingExtensionsEventSource() diff --git a/test/OpenTelemetry.Extensions.Hosting.Tests/InMemoryExporterMetricsExtensionsTests.cs b/test/OpenTelemetry.Extensions.Hosting.Tests/InMemoryExporterMetricsExtensionsTests.cs index 99db85423b4..62b5078d79a 100644 --- a/test/OpenTelemetry.Extensions.Hosting.Tests/InMemoryExporterMetricsExtensionsTests.cs +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/InMemoryExporterMetricsExtensionsTests.cs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if NET6_0_OR_GREATER +#if NET using System.Diagnostics.Metrics; using System.Net; @@ -85,7 +85,7 @@ private static async Task RunMetricsTest(Action configure, }))) .StartAsync(); - using var response = await host.GetTestClient().GetAsync($"/{nameof(RunMetricsTest)}"); + using var response = await host.GetTestClient().GetAsync(new Uri($"/{nameof(RunMetricsTest)}", UriKind.Relative)); Assert.Equal(HttpStatusCode.OK, response.StatusCode); 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 1ea17a421de..57ec3b2fbb5 100644 --- a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetry.Extensions.Hosting.Tests.csproj +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetry.Extensions.Hosting.Tests.csproj @@ -1,9 +1,8 @@ - + + Unit test project for OpenTelemetry .NET Core hosting library $(TargetFrameworksForTests) - - disable $(DefineConstants);BUILDING_HOSTING_TESTS @@ -14,7 +13,6 @@ - @@ -23,9 +21,11 @@ - + + + - + @@ -36,10 +36,6 @@ - - - - runtime; build; native; contentfiles; analyzers - + diff --git a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryBuilderTests.cs b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryBuilderTests.cs index e5160e3b79e..7c0bfae5711 100644 --- a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryBuilderTests.cs +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryBuilderTests.cs @@ -91,7 +91,9 @@ public void ConfigureResourceServiceProviderTest() Assert.Single(loggerProvider.Resource.Attributes, kvp => kvp.Key == "key1" && (string)kvp.Value == "value1"); } +#pragma warning disable CA1812 // Avoid uninstantiated internal classes private sealed class TestResourceDetector : IResourceDetector +#pragma warning restore CA1812 // Avoid uninstantiated internal classes { public Resource Detect() => ResourceBuilder.CreateEmpty().AddAttributes( new Dictionary diff --git a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryMetricsBuilderExtensionsTests.cs b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryMetricsBuilderExtensionsTests.cs index d34dc060f1e..c990d14a336 100644 --- a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryMetricsBuilderExtensionsTests.cs +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryMetricsBuilderExtensionsTests.cs @@ -25,7 +25,7 @@ public class OpenTelemetryMetricsBuilderExtensionsTests public void EnableMetricsTest(bool useWithMetricsStyle) { using var meter = new Meter(Utils.GetCurrentMethodName()); - List exportedItems = new(); + List exportedItems = []; using (var host = MetricTestsBase.BuildHost( useWithMetricsStyle, @@ -45,7 +45,7 @@ public void EnableMetricsTest(bool useWithMetricsStyle) public void EnableMetricsWithAddMeterTest(bool useWithMetricsStyle) { using var meter = new Meter(Utils.GetCurrentMethodName()); - List exportedItems = new(); + List exportedItems = []; using (var host = MetricTestsBase.BuildHost( useWithMetricsStyle, @@ -71,11 +71,11 @@ public void ReloadOfMetricsViaIConfigurationWithExportCleanupTest(bool useWithMe using var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log); using var meter = new Meter(Utils.GetCurrentMethodName()); - List exportedItems = new(); + List exportedItems = []; var source = new MemoryConfigurationSource(); var memory = new MemoryConfigurationProvider(source); - var configuration = new ConfigurationRoot(new[] { memory }); + using var configuration = new ConfigurationRoot([memory]); using var host = MetricTestsBase.BuildHost( useWithMetricsStyle, @@ -162,12 +162,12 @@ public void ReloadOfMetricsViaIConfigurationWithoutExportCleanupTest(bool useWit using var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log); using var meter = new Meter(Utils.GetCurrentMethodName()); - List exportedItems = new(); + List exportedItems = []; 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 configuration = new ConfigurationRoot([memory]); using var host = MetricTestsBase.BuildHost( useWithMetricsStyle, @@ -232,7 +232,7 @@ private static void AssertSingleMetricWithLongSum(List exportedItems, lo private static void AssertMetricWithLongSum(Metric metric, long expectedValue = 1) { - List metricPoints = new(); + List metricPoints = []; foreach (ref readonly var mp in metric.GetMetricPoints()) { metricPoints.Add(mp); diff --git a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryServicesExtensionsTests.cs b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryServicesExtensionsTests.cs index 0753220b6cc..85b85dfd998 100644 --- a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryServicesExtensionsTests.cs +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryServicesExtensionsTests.cs @@ -129,7 +129,7 @@ public async Task AddOpenTelemetry_WithTracing_HostConfigurationHonoredTest() var builder = new HostBuilder() .ConfigureAppConfiguration(builder => { - builder.AddInMemoryCollection(new Dictionary + builder.AddInMemoryCollection(new Dictionary { ["TEST_KEY"] = "TEST_KEY_VALUE", }); @@ -147,7 +147,7 @@ public async Task AddOpenTelemetry_WithTracing_HostConfigurationHonoredTest() var configuration = sp.GetRequiredService(); - var testKeyValue = configuration.GetValue("TEST_KEY", null); + var testKeyValue = configuration.GetValue("TEST_KEY", null); Assert.Equal("TEST_KEY_VALUE", testKeyValue); }); @@ -252,7 +252,7 @@ public async Task AddOpenTelemetry_WithMetrics_HostConfigurationHonoredTest() var builder = new HostBuilder() .ConfigureAppConfiguration(builder => { - builder.AddInMemoryCollection(new Dictionary + builder.AddInMemoryCollection(new Dictionary { ["TEST_KEY"] = "TEST_KEY_VALUE", }); @@ -270,7 +270,7 @@ public async Task AddOpenTelemetry_WithMetrics_HostConfigurationHonoredTest() var configuration = sp.GetRequiredService(); - var testKeyValue = configuration.GetValue("TEST_KEY", null); + var testKeyValue = configuration.GetValue("TEST_KEY", null); Assert.Equal("TEST_KEY_VALUE", testKeyValue); }); @@ -375,7 +375,7 @@ public void AddOpenTelemetry_WithLogging_HostConfigurationHonoredTest() var builder = new HostBuilder() .ConfigureAppConfiguration(builder => { - builder.AddInMemoryCollection(new Dictionary + builder.AddInMemoryCollection(new Dictionary { ["TEST_KEY"] = "TEST_KEY_VALUE", }); @@ -393,7 +393,7 @@ public void AddOpenTelemetry_WithLogging_HostConfigurationHonoredTest() var configuration = sp.GetRequiredService(); - var testKeyValue = configuration.GetValue("TEST_KEY", null); + var testKeyValue = configuration.GetValue("TEST_KEY", null); Assert.Equal("TEST_KEY_VALUE", testKeyValue); }); @@ -457,15 +457,25 @@ public async Task AddOpenTelemetry_HostedServiceOrder_DoesNotMatter() Assert.Single(exportedItems); } +#pragma warning disable CA1812 // Avoid uninstantiated internal classes private sealed class MySampler : Sampler +#pragma warning restore CA1812 // Avoid uninstantiated internal classes { public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) => new(SamplingDecision.RecordAndSample); } +#pragma warning disable CA1812 // Avoid uninstantiated internal classes private sealed class TestHostedService : BackgroundService +#pragma warning restore CA1812 // Avoid uninstantiated internal classes { - private readonly ActivitySource activitySource = new ActivitySource(nameof(TestHostedService)); + private readonly ActivitySource activitySource = new(nameof(TestHostedService)); + + public override void Dispose() + { + this.activitySource.Dispose(); + base.Dispose(); + } protected override Task ExecuteAsync(CancellationToken stoppingToken) { diff --git a/test/OpenTelemetry.Extensions.Propagators.Tests/B3PropagatorTest.cs b/test/OpenTelemetry.Extensions.Propagators.Tests/B3PropagatorTests.cs similarity index 95% rename from test/OpenTelemetry.Extensions.Propagators.Tests/B3PropagatorTest.cs rename to test/OpenTelemetry.Extensions.Propagators.Tests/B3PropagatorTests.cs index 5373573ad6c..b390e7d1d36 100644 --- a/test/OpenTelemetry.Extensions.Propagators.Tests/B3PropagatorTest.cs +++ b/test/OpenTelemetry.Extensions.Propagators.Tests/B3PropagatorTests.cs @@ -8,7 +8,7 @@ namespace OpenTelemetry.Extensions.Propagators.Tests; -public class B3PropagatorTest +public class B3PropagatorTests { private const string TraceIdBase16 = "ff000000000000000000000000000041"; private const string TraceIdBase16EightBytes = "0000000000000041"; @@ -25,8 +25,12 @@ public class B3PropagatorTest private static readonly Func, string, IEnumerable> Getter = (d, k) => { - d.TryGetValue(k, out var v); - return new string[] { v }; + if (d.TryGetValue(k, out var v)) + { + return [v]; + } + + return []; }; private readonly B3Propagator b3propagator = new(); @@ -34,7 +38,7 @@ public class B3PropagatorTest private readonly ITestOutputHelper output; - public B3PropagatorTest(ITestOutputHelper output) + public B3PropagatorTests(ITestOutputHelper output) { this.output = output; } @@ -354,9 +358,10 @@ public void ParseMissingSpanId_SingleHeader() [Fact] public void Fields_list() { + Assert.Equivalent(this.b3propagator.Fields, new List { B3Propagator.XB3TraceId, B3Propagator.XB3SpanId, B3Propagator.XB3ParentSpanId, B3Propagator.XB3Sampled, B3Propagator.XB3Flags, B3Propagator.XB3Flags }); ContainsExactly( this.b3propagator.Fields, - new List { B3Propagator.XB3TraceId, B3Propagator.XB3SpanId, B3Propagator.XB3ParentSpanId, B3Propagator.XB3Sampled, B3Propagator.XB3Flags }); + [B3Propagator.XB3TraceId, B3Propagator.XB3SpanId, B3Propagator.XB3ParentSpanId, B3Propagator.XB3Sampled, B3Propagator.XB3Flags]); } private static void ContainsExactly(ISet list, List items) @@ -368,7 +373,7 @@ private static void ContainsExactly(ISet list, List items) } } - private void ContainsExactly(IDictionary dict, IDictionary items) + private void ContainsExactly(Dictionary dict, Dictionary items) { foreach (var d in dict) { diff --git a/test/OpenTelemetry.Extensions.Propagators.Tests/EventSourceTest.cs b/test/OpenTelemetry.Extensions.Propagators.Tests/EventSourceTests.cs similarity index 93% rename from test/OpenTelemetry.Extensions.Propagators.Tests/EventSourceTest.cs rename to test/OpenTelemetry.Extensions.Propagators.Tests/EventSourceTests.cs index ee3b570c437..5f8c0c9978c 100644 --- a/test/OpenTelemetry.Extensions.Propagators.Tests/EventSourceTest.cs +++ b/test/OpenTelemetry.Extensions.Propagators.Tests/EventSourceTests.cs @@ -7,7 +7,7 @@ namespace OpenTelemetry.Extensions.Propagators.Tests; -public class EventSourceTest +public class EventSourceTests { [Fact] public void EventSourceTest_PropagatorsEventSource() diff --git a/test/OpenTelemetry.Extensions.Propagators.Tests/JaegerPropagatorTest.cs b/test/OpenTelemetry.Extensions.Propagators.Tests/JaegerPropagatorTests.cs similarity index 93% rename from test/OpenTelemetry.Extensions.Propagators.Tests/JaegerPropagatorTest.cs rename to test/OpenTelemetry.Extensions.Propagators.Tests/JaegerPropagatorTests.cs index 9e040746dd3..765ea3b3db5 100644 --- a/test/OpenTelemetry.Extensions.Propagators.Tests/JaegerPropagatorTest.cs +++ b/test/OpenTelemetry.Extensions.Propagators.Tests/JaegerPropagatorTests.cs @@ -7,7 +7,7 @@ namespace OpenTelemetry.Extensions.Propagators.Tests; -public class JaegerPropagatorTest +public class JaegerPropagatorTests { private const string JaegerHeader = "uber-trace-id"; private const string JaegerDelimiter = ":"; @@ -23,12 +23,7 @@ public class JaegerPropagatorTest private static readonly Func, string, IEnumerable> Getter = (headers, name) => { - if (headers.TryGetValue(name, out var value)) - { - return value; - } - - return Array.Empty(); + return headers.TryGetValue(name, out var value) ? value : []; }; private static readonly Action, string, string> Setter = (carrier, name, value) => @@ -77,7 +72,7 @@ public void ExtractReturnsOriginalContextIfGetterIsNull() var headers = new Dictionary(); // act - var result = new JaegerPropagator().Extract(propagationContext, headers, null); + var result = new JaegerPropagator().Extract(propagationContext, headers, null!); // assert Assert.Equal(propagationContext, result); @@ -104,7 +99,7 @@ public void ExtractReturnsOriginalContextIfHeaderIsNotValid(string traceId, stri parentSpanId, flags); - var headers = new Dictionary { { JaegerHeader, new[] { formattedHeader } } }; + var headers = new Dictionary { { JaegerHeader, [formattedHeader] } }; // act var result = new JaegerPropagator().Extract(propagationContext, headers, Getter); @@ -118,6 +113,21 @@ public void ExtractReturnsOriginalContextIfHeaderIsNotValid(string traceId, stri [InlineData(TraceIdShort, SpanIdShort, ParentSpanId, FlagNotSampled, JaegerDelimiterEncoded)] public void ExtractReturnsNewContextIfHeaderIsValid(string traceId, string spanId, string parentSpanId, string flags, string delimiter) { +#if NET + Assert.NotNull(traceId); + Assert.NotNull(spanId); +#else + if (traceId == null) + { + throw new ArgumentNullException(nameof(traceId)); + } + + if (spanId == null) + { + throw new ArgumentNullException(nameof(traceId)); + } +#endif + // arrange var propagationContext = default(PropagationContext); @@ -128,7 +138,7 @@ public void ExtractReturnsNewContextIfHeaderIsValid(string traceId, string spanI parentSpanId, flags); - var headers = new Dictionary { { JaegerHeader, new[] { formattedHeader } } }; + var headers = new Dictionary { { JaegerHeader, [formattedHeader] } }; // act var result = new JaegerPropagator().Extract(propagationContext, headers, Getter); @@ -183,7 +193,7 @@ public void InjectDoesNoopIfSetterIsNull() var headers = new Dictionary(); // act - new JaegerPropagator().Inject(propagationContext, headers, null); + new JaegerPropagator().Inject(propagationContext, headers, null!); // assert Assert.Empty(headers); 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 77eaa8a1f68..86e28192880 100644 --- a/test/OpenTelemetry.Extensions.Propagators.Tests/OpenTelemetry.Extensions.Propagators.Tests.csproj +++ b/test/OpenTelemetry.Extensions.Propagators.Tests/OpenTelemetry.Extensions.Propagators.Tests.csproj @@ -2,8 +2,6 @@ $(TargetFrameworksForTests) - - disable @@ -11,14 +9,6 @@ - - - - - runtime; build; native; contentfiles; analyzers - - - diff --git a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/Dockerfile b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/Dockerfile index 9723867f4f3..b52fb55795e 100644 --- a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/Dockerfile +++ b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/Dockerfile @@ -2,31 +2,35 @@ # This should be run from the root of the repo: # docker build --file test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/Dockerfile . -ARG BUILD_SDK_VERSION=8.0 -ARG TEST_SDK_VERSION=8.0 +ARG BUILD_SDK_VERSION=9.0 +ARG TEST_SDK_VERSION=9.0 -FROM ubuntu AS w3c +FROM ubuntu:24.04@sha256:353675e2a41babd526e2b837d7ec780c2a05bca0164f7ea5dbbd433d21d166fc AS w3c #Install git WORKDIR /w3c RUN apt-get update && apt-get install -y 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 +FROM mcr.microsoft.com/dotnet/sdk:8.0.414@sha256:3cef19377b2ef2a0171e930a24627677447c3e41b5f2eab84ff4895f1b15d254 AS dotnet-sdk-8.0 +FROM mcr.microsoft.com/dotnet/sdk:9.0.305@sha256:bb42ae2c058609d1746baf24fe6864ecab0686711dfca1f4b7a99e367ab17162 AS dotnet-sdk-9.0 + +FROM dotnet-sdk-${BUILD_SDK_VERSION} AS build ARG PUBLISH_CONFIGURATION=Release -ARG PUBLISH_FRAMEWORK=net8.0 +ARG PUBLISH_FRAMEWORK=net9.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 -FROM mcr.microsoft.com/dotnet/sdk:${TEST_SDK_VERSION} AS final +FROM dotnet-sdk-${TEST_SDK_VERSION} AS final WORKDIR /test +COPY /test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/requirements.txt . COPY --from=build /drop . COPY --from=w3c /w3c . RUN apt-get update \ && apt-get install -y python3-pip python3-dev \ && cd /usr/local/bin \ - && ln -s /usr/bin/python3 python \ - && pip3 install --upgrade pip \ - && pip3 install aiohttp + && ln -s /usr/bin/python3 python + +RUN pip3 install --requirement requirements.txt --break-system-packages ENTRYPOINT ["dotnet", "vstest", "OpenTelemetry.Instrumentation.W3cTraceContext.Tests.dll", "--logger:console;verbosity=detailed"] 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 8d9e8800c51..300d53feacb 100644 --- a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/OpenTelemetry.Instrumentation.W3cTraceContext.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/OpenTelemetry.Instrumentation.W3cTraceContext.Tests.csproj @@ -3,17 +3,10 @@ Unit test project for OpenTelemetry ASP.NET Core instrumentation for W3C Trace Context Trace $(TargetFrameworksForAspNetCoreTests) - - disable - - - - runtime; build; native; contentfiles; analyzers - diff --git a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/W3CTraceContextTests.cs b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/W3CTraceContextTests.cs index 323b35ecba9..16a45886aaa 100644 --- a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/W3CTraceContextTests.cs +++ b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/W3CTraceContextTests.cs @@ -14,14 +14,13 @@ namespace OpenTelemetry.Instrumentation.W3cTraceContext.Tests; -public class W3CTraceContextTests : IDisposable +public sealed 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 const string W3CTraceContextEnvVarName = "OTEL_W3CTRACECONTEXT"; private readonly HttpClient httpClient = new(); private readonly ITestOutputHelper output; @@ -31,15 +30,15 @@ public W3CTraceContextTests(ITestOutputHelper output) } [Trait("CategoryName", "W3CTraceContextTests")] - [SkipUnlessEnvVarFoundTheory(W3cTraceContextEnvVarName)] + [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) + public async Task W3CTraceContextTestSuiteAsync(string value) { // configure SDK - using var tracerprovider = Sdk.CreateTracerProviderBuilder() - .AddAspNetCoreInstrumentation() - .Build(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .Build(); var builder = WebApplication.CreateBuilder(); using var app = builder.Build(); @@ -51,13 +50,11 @@ public void W3CTraceContextTestSuiteAsync(string value) { foreach (var argument in data) { - using var request = new HttpRequestMessage(HttpMethod.Post, argument.Url) - { - Content = new StringContent( - JsonSerializer.Serialize(argument.Arguments), - Encoding.UTF8, - "application/json"), - }; + using var request = new HttpRequestMessage(HttpMethod.Post, argument.Url); + request.Content = new StringContent( + JsonSerializer.Serialize(argument.Arguments), + Encoding.UTF8, + "application/json"); await this.httpClient.SendAsync(request); } } @@ -69,29 +66,19 @@ public void W3CTraceContextTestSuiteAsync(string value) return result; }); - app.RunAsync(); + _ = app.RunAsync("http://localhost:5000/"); - string result = RunCommand("python", "trace-context/test/test.py http://localhost:5000/"); + (var stdout, var stderr) = await RunCommand("python", "-W ignore trace-context/test/test.py http://localhost:5000/"); // Assert - string lastLine = ParseLastLine(result); + // TODO: after W3C Trace Context test suite passes, it might go in standard output + string lastLine = ParseLastLine(stderr); - this.output.WriteLine("result:" + result); + this.output.WriteLine("[stderr]" + stderr); + this.output.WriteLine("[stdout]" + stdout); // 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). - - if (AspNetCoreHostingVersion.Major <= 6) - { - Assert.StartsWith("FAILED (failures=3)", lastLine); - } - else - { - Assert.StartsWith("OK", lastLine); - } + Assert.StartsWith("OK", lastLine, StringComparison.Ordinal); } public void Dispose() @@ -99,9 +86,9 @@ public void Dispose() this.httpClient.Dispose(); } - private static string RunCommand(string command, string args) + private static async Task<(string StdOut, string StdErr)> RunCommand(string command, string args) { - using var proc = new Process + using var process = new Process { StartInfo = new ProcessStartInfo { @@ -114,12 +101,43 @@ private static string RunCommand(string command, string args) WorkingDirectory = ".", }, }; - proc.Start(); + process.Start(); - // TODO: after W3C Trace Context test suite passes, it might go in standard output - var results = proc.StandardError.ReadToEnd(); - proc.WaitForExit(); - return results; + // See https://stackoverflow.com/a/16326426/1064169 and + // https://learn.microsoft.com/dotnet/api/system.diagnostics.processstartinfo.redirectstandardoutput. + using var outputTokenSource = new CancellationTokenSource(); + + var readOutput = ReadOutputAsync(process, outputTokenSource.Token); + + try + { + await process.WaitForExitAsync(); + } + catch (OperationCanceledException) + { + try + { + process.Kill(entireProcessTree: true); + } + catch (Exception) + { + // Ignore + } + } + finally + { + await outputTokenSource.CancelAsync(); + } + + try + { + return await readOutput; + } + finally + { + process.Dispose(); + outputTokenSource.Dispose(); + } } private static string ParseLastLine(string output) @@ -134,12 +152,67 @@ private static string ParseLastLine(string output) return output.Substring(lastNewLineCharacterPos + 1); } - public class Data + private static async Task<(string Output, string Error)> ReadOutputAsync( + Process process, + CancellationToken cancellationToken) + { + var processErrors = ConsumeStreamAsync(process.StandardError, process.StartInfo.RedirectStandardError, cancellationToken); + var processOutput = ConsumeStreamAsync(process.StandardOutput, process.StartInfo.RedirectStandardOutput, cancellationToken); + + await Task.WhenAll(processErrors, processOutput); + + string error = string.Empty; + string output = string.Empty; + + if (processErrors.Status == TaskStatus.RanToCompletion) + { + error = (await processErrors).ToString(); + } + + if (processOutput.Status == TaskStatus.RanToCompletion) + { + output = (await processOutput).ToString(); + } + + return (output, error); + } + + private static Task ConsumeStreamAsync( + StreamReader reader, + bool isRedirected, + CancellationToken cancellationToken) + { + return isRedirected ? + Task.Run(() => ProcessStream(reader, cancellationToken), cancellationToken) : + Task.FromResult(new StringBuilder(0)); + + static async Task ProcessStream( + StreamReader reader, + CancellationToken cancellationToken) + { + var builder = new StringBuilder(); + + try + { + builder.Append(await reader.ReadToEndAsync(cancellationToken)); + } + catch (OperationCanceledException) + { + // Ignore + } + + return builder; + } + } + +#pragma warning disable CA1812 // Avoid uninstantiated internal classes + internal sealed class Data +#pragma warning restore CA1812 // Avoid uninstantiated internal classes { [JsonPropertyName("url")] - public string Url { get; set; } + public string? Url { get; set; } [JsonPropertyName("arguments")] - public Data[] Arguments { get; set; } + 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 b777fc67447..4f59a64c6aa 100644 --- a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/docker-compose.yml +++ b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/docker-compose.yml @@ -1,7 +1,6 @@ # Start a container and then run OpenTelemetry W3C Trace Context tests. # This should be run 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 -version: '3.7' services: tests: diff --git a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/requirements.txt b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/requirements.txt new file mode 100644 index 00000000000..8a0383d4197 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/requirements.txt @@ -0,0 +1 @@ +aiohttp == 3.12.15 diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/IntegrationTests.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/IntegrationTests.cs index d8f93b2df0c..a5ad1d33244 100644 --- a/test/OpenTelemetry.Shims.OpenTracing.Tests/IntegrationTests.cs +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/IntegrationTests.cs @@ -55,7 +55,7 @@ public void WithActivities( b => b.AddSource(ChildActivitySource)) .Build(); - ITracer otTracer = new TracerShim( + var otTracer = new TracerShim( tracerProvider, Propagators.DefaultTextMapPropagator); @@ -76,7 +76,7 @@ public void WithActivities( } } - var expectedExportedSpans = new string[] + var expectedExportedSpans = new string?[] { childActivitySamplingDecision == SamplingDecision.RecordAndSample ? ChildActivityName : null, shimSamplingDecision == SamplingDecision.RecordAndSample ? ShimActivityName : null, @@ -99,7 +99,7 @@ public void WithActivities( } } - private class TestSampler : Sampler + private sealed class TestSampler : Sampler { private readonly Func shouldSampleDelegate; diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/ListenAndSampleAllActivitySources.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/ListenAndSampleAllActivitySources.cs index eccc55b95eb..fee708dcf00 100644 --- a/test/OpenTelemetry.Shims.OpenTracing.Tests/ListenAndSampleAllActivitySources.cs +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/ListenAndSampleAllActivitySources.cs @@ -1,35 +1,11 @@ // 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(); - } - } -} +#pragma warning disable CA1515 // Consider making public types internal +public sealed class ListenAndSampleAllActivitySources : ICollectionFixture; +#pragma warning restore CA1515 // Consider making public types internal diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/ListenAndSampleAllActivitySourcesFixture.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/ListenAndSampleAllActivitySourcesFixture.cs new file mode 100644 index 00000000000..5240385d83b --- /dev/null +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/ListenAndSampleAllActivitySourcesFixture.cs @@ -0,0 +1,32 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; + +namespace OpenTelemetry.Shims.OpenTracing.Tests; + +#pragma warning disable CA1515 // Consider making public types internal +public sealed class ListenAndSampleAllActivitySourcesFixture : IDisposable +#pragma warning restore CA1515 // Consider making public types internal +{ + private readonly ActivityListener listener; + + public ListenAndSampleAllActivitySourcesFixture() + { + Activity.DefaultIdFormat = ActivityIdFormat.W3C; + Activity.ForceDefaultIdFormat = true; + + this.listener = new ActivityListener + { + ShouldListenTo = _ => true, + Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllDataAndRecorded, + }; + + 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 7e08df554f3..eae8ca329de 100644 --- a/test/OpenTelemetry.Shims.OpenTracing.Tests/OpenTelemetry.Shims.OpenTracing.Tests.csproj +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/OpenTelemetry.Shims.OpenTracing.Tests.csproj @@ -1,22 +1,25 @@ + Unit test project for OpenTelemetry.Shims.OpenTracing $(TargetFrameworksForTests) - - disable + $(DefineConstants);BUILDING_USING_PROJECTS - - - - runtime; build; native; contentfiles; analyzers - + - - + + + + + + + + + diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/ScopeManagerShimTests.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/ScopeManagerShimTests.cs index 6f4ca876c55..0e24b2b1673 100644 --- a/test/OpenTelemetry.Shims.OpenTracing.Tests/ScopeManagerShimTests.cs +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/ScopeManagerShimTests.cs @@ -33,6 +33,7 @@ public void Active_IsNotNull() Assert.NotNull(scope); var activeScope = shim.Active; + Assert.NotNull(activeScope); Assert.Equal(scope.Span.Context.SpanId, activeScope.Span.Context.SpanId); openTracingSpan.Finish(); } @@ -64,6 +65,7 @@ public void Activate() #endif spanShim.Finish(); + Assert.NotNull(spanShim.Span.Activity); 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 ce546c868bf..5e8b2adc011 100644 --- a/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanBuilderShimTests.cs +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanBuilderShimTests.cs @@ -18,8 +18,8 @@ public class SpanBuilderShimTests public void CtorArgumentValidation() { var tracer = TracerProvider.Default.GetTracer(TracerName); - Assert.Throws(() => new SpanBuilderShim(null, "foo")); - Assert.Throws(() => new SpanBuilderShim(tracer, null)); + Assert.Throws(() => new SpanBuilderShim(null!, "foo")); + Assert.Throws(() => new SpanBuilderShim(tracer, null!)); } [Fact] @@ -36,7 +36,7 @@ public void IgnoreActiveSpan() // build var spanShim = (SpanShim)shim.Start(); - + Assert.NotNull(spanShim.Span.Activity); Assert.Equal("foo", spanShim.Span.Activity.OperationName); } @@ -51,7 +51,7 @@ public void StartWithExplicitTimestamp() // build var spanShim = (SpanShim)shim.Start(); - + Assert.NotNull(spanShim.Span.Activity); Assert.Equal(startTimestamp, spanShim.Span.Activity.StartTimeUtc); } @@ -62,11 +62,12 @@ public void AsChildOf_WithNullSpan() var shim = new SpanBuilderShim(tracer, "foo"); // Add a null parent - shim.AsChildOf((global::OpenTracing.ISpan)null); + shim.AsChildOf((global::OpenTracing.ISpan?)null); // build var spanShim = (SpanShim)shim.Start(); + Assert.NotNull(spanShim.Span.Activity); Assert.Equal("foo", spanShim.Span.Activity.OperationName); Assert.Null(spanShim.Span.Activity.Parent); } @@ -84,6 +85,7 @@ public void AsChildOf_WithSpan() // build var spanShim = (SpanShim)shim.Start(); + Assert.NotNull(spanShim.Span.Activity); Assert.Equal("foo", spanShim.Span.Activity.OperationName); Assert.NotNull(spanShim.Span.Activity.ParentId); } @@ -92,8 +94,8 @@ public void AsChildOf_WithSpan() public void Start_ActivityOperationRootSpanChecks() { // Create an activity - using var activity = new Activity("foo") - .SetIdFormat(ActivityIdFormat.W3C) + using var activity = new Activity("foo"); + activity.SetIdFormat(ActivityIdFormat.W3C) .Start(); // matching root operation name @@ -101,12 +103,14 @@ public void Start_ActivityOperationRootSpanChecks() var shim = new SpanBuilderShim(tracer, "foo"); var spanShim1 = (SpanShim)shim.Start(); + Assert.NotNull(spanShim1.Span.Activity); Assert.Equal("foo", spanShim1.Span.Activity.OperationName); // mis-matched root operation name shim = new SpanBuilderShim(tracer, "foo"); var spanShim2 = (SpanShim)shim.Start(); + Assert.NotNull(spanShim2.Span.Activity); Assert.Equal("foo", spanShim2.Span.Activity.OperationName); Assert.Equal(spanShim1.Context.TraceId, spanShim2.Context.TraceId); } @@ -126,8 +130,9 @@ public void AsChildOf_MultipleCallsWithSpan() // build var spanShim = (SpanShim)shim.Start(); + Assert.NotNull(spanShim.Span.Activity); Assert.Equal("foo", spanShim.Span.Activity.OperationName); - Assert.Contains(spanShim.Context.TraceId, spanShim.Span.Activity.TraceId.ToHexString()); + Assert.Contains(spanShim.Context.TraceId, spanShim.Span.Activity.TraceId.ToHexString(), StringComparison.Ordinal); // TODO: Check for multi level parenting } @@ -139,12 +144,13 @@ public void AsChildOf_WithNullSpanContext() var shim = new SpanBuilderShim(tracer, "foo"); // Add a null parent - shim.AsChildOf((global::OpenTracing.ISpanContext)null); + shim.AsChildOf((global::OpenTracing.ISpanContext?)null); // build var spanShim = (SpanShim)shim.Start(); // should be no parent. + Assert.NotNull(spanShim.Span.Activity); Assert.Null(spanShim.Span.Activity.Parent); } @@ -161,6 +167,7 @@ public void AsChildOfWithSpanContext() // build var spanShim = (SpanShim)shim.Start(); + Assert.NotNull(spanShim.Span.Activity); Assert.NotNull(spanShim.Span.Activity.ParentId); } @@ -182,9 +189,9 @@ public void AsChildOf_MultipleCallsWithSpanContext() // build var spanShim = (SpanShim)shim.Start(); - + Assert.NotNull(spanShim.Span.Activity); Assert.Equal("foo", spanShim.Span.Activity.OperationName); - Assert.Contains(spanContext1.TraceId, spanShim.Span.Activity.ParentId); + Assert.Contains(spanContext1.TraceId, spanShim.Span.Activity.ParentId, StringComparison.Ordinal); Assert.Equal(spanContext2.SpanId, spanShim.Span.Activity.Links.First().Context.SpanId.ToHexString()); } @@ -200,6 +207,7 @@ public void WithTag_KeyIsSpanKindStringValue() var spanShim = (SpanShim)shim.Start(); // Not an attribute + Assert.NotNull(spanShim.Span.Activity); Assert.Empty(spanShim.Span.Activity.Tags); Assert.Equal("foo", spanShim.Span.Activity.OperationName); Assert.Equal(ActivityKind.Client, spanShim.Span.Activity.Kind); @@ -216,8 +224,19 @@ public void WithTag_KeyIsErrorStringValue() // build var spanShim = (SpanShim)shim.Start(); - // Span status should be set - Assert.Equal(Status.Error, spanShim.Span.Activity.GetStatus()); + // Legacy span status tag should be set + Assert.NotNull(spanShim.Span.Activity); + Assert.Equal("ERROR", spanShim.Span.Activity.GetTagValue(SpanAttributeConstants.StatusCodeKey)); + + if (VersionHelper.IsApiVersionGreaterThanOrEqualTo(1, 10)) + { + // Activity status code should also be set + Assert.Equal(ActivityStatusCode.Error, spanShim.Span.Activity.Status); + } + else + { + Assert.Equal(ActivityStatusCode.Unset, spanShim.Span.Activity.Status); + } } [Fact] @@ -226,17 +245,18 @@ public void WithTag_KeyIsNullStringValue() var tracer = TracerProvider.Default.GetTracer(TracerName); var shim = new SpanBuilderShim(tracer, "foo"); - shim.WithTag((string)null, "unused"); + shim.WithTag((string)null!, "unused"); // build var spanShim = (SpanShim)shim.Start(); // Null key was ignored + Assert.NotNull(spanShim.Span.Activity); Assert.Empty(spanShim.Span.Activity.Tags); } [Fact] - public void WithTag_ValueIsNullStringValue() + public void WithTag_ValueIsIgnoredWhenNull() { var tracer = TracerProvider.Default.GetTracer(TracerName); var shim = new SpanBuilderShim(tracer, "foo"); @@ -247,8 +267,8 @@ public void WithTag_ValueIsNullStringValue() 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); + Assert.NotNull(spanShim.Span.Activity); + Assert.Empty(spanShim.Span.Activity.TagObjects); } [Fact] @@ -262,8 +282,18 @@ public void WithTag_KeyIsErrorBoolValue() // build var spanShim = (SpanShim)shim.Start(); - // Span status should be set - Assert.Equal(Status.Error, spanShim.Span.Activity.GetStatus()); + // Legacy span status tag should be set + Assert.NotNull(spanShim.Span.Activity); + Assert.Equal("ERROR", spanShim.Span.Activity.GetTagValue(SpanAttributeConstants.StatusCodeKey)); + if (VersionHelper.IsApiVersionGreaterThanOrEqualTo(1, 10)) + { + // Activity status code should also be set + Assert.Equal(ActivityStatusCode.Error, spanShim.Span.Activity.Status); + } + else + { + Assert.Equal(ActivityStatusCode.Unset, spanShim.Span.Activity.Status); + } } [Fact] @@ -284,7 +314,8 @@ public void WithTag_VariousValueTypes() var spanShim = (SpanShim)shim.Start(); // Just verify the count - Assert.Equal(7, spanShim.Span.Activity.Tags.Count()); + Assert.NotNull(spanShim.Span.Activity); + Assert.Equal(7, spanShim.Span.Activity.TagObjects.Count()); } [Fact] @@ -299,6 +330,7 @@ public void Start() // 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.NotNull(span.Span.Activity); Assert.Equal("foo", span.Span.Activity.OperationName); } @@ -317,6 +349,7 @@ public void Start_UnderAspNetCoreInstrumentation() Assert.NotNull(spanShim); var telemetrySpan = spanShim.Span; + Assert.NotNull(telemetrySpan.Activity); Assert.Same(telemetrySpan.Activity, Activity.Current); Assert.Same(parentSpan, telemetrySpan.Activity.Parent); diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanShimTests.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanShimTests.cs index 80178f405be..66e8b63652b 100644 --- a/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanShimTests.cs +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanShimTests.cs @@ -1,6 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics; using OpenTelemetry.Trace; using OpenTracing.Tag; using Xunit; @@ -16,7 +17,7 @@ public class SpanShimTests [Fact] public void CtorArgumentValidation() { - Assert.Throws(() => new SpanShim(null)); + Assert.Throws(() => new SpanShim(null!)); } [Fact] @@ -34,9 +35,9 @@ public void FinishSpan() { var tracer = TracerProvider.Default.GetTracer(TracerName); var shim = new SpanShim(tracer.StartSpan(SpanName)); - shim.Finish(); + Assert.NotNull(shim.Span.Activity); Assert.NotEqual(default, shim.Span.Activity.Duration); } @@ -57,7 +58,7 @@ public void FinishSpanUsingSpecificTimestamp() var endTime = DateTimeOffset.UtcNow; shim.Finish(endTime); - Assert.Equal(endTime - shim.Span.Activity.StartTimeUtc, shim.Span.Activity.Duration); + Assert.Equal(endTime - shim.Span.Activity!.StartTimeUtc, shim.Span.Activity.Duration); } [Fact] @@ -67,10 +68,10 @@ public void SetOperationName() var shim = new SpanShim(tracer.StartSpan(SpanName)); // parameter validation - Assert.Throws(() => shim.SetOperationName(null)); + Assert.Throws(() => shim.SetOperationName(null!)); shim.SetOperationName("bar"); - Assert.Equal("bar", shim.Span.Activity.DisplayName); + Assert.Equal("bar", shim.Span.Activity!.DisplayName); } [Fact] @@ -80,7 +81,7 @@ public void GetBaggageItem() var shim = new SpanShim(tracer.StartSpan(SpanName)); // parameter validation - Assert.Throws(() => shim.GetBaggageItem(null)); + Assert.Throws(() => shim.GetBaggageItem(null!)); // TODO - Method not implemented } @@ -93,8 +94,8 @@ public void Log() shim.Log("foo"); - Assert.Single(shim.Span.Activity.Events); - var first = shim.Span.Activity.Events.First(); + Assert.NotNull(shim.Span.Activity); + var first = Assert.Single(shim.Span.Activity.Events); Assert.Equal("foo", first.Name); Assert.False(first.Tags.Any()); } @@ -108,8 +109,8 @@ public void LogWithExplicitTimestamp() var now = DateTimeOffset.UtcNow; shim.Log(now, "foo"); - Assert.Single(shim.Span.Activity.Events); - var first = shim.Span.Activity.Events.First(); + Assert.NotNull(shim.Span.Activity); + var first = Assert.Single(shim.Span.Activity.Events); Assert.Equal("foo", first.Name); Assert.Equal(now, first.Timestamp); Assert.False(first.Tags.Any()); @@ -121,19 +122,21 @@ public void LogUsingFields() var tracer = TracerProvider.Default.GetTracer(TracerName); var shim = new SpanShim(tracer.StartSpan(SpanName)); - Assert.Throws(() => shim.Log((IEnumerable>)null)); + Assert.Throws(() => shim.Log((IEnumerable>)null!)); shim.Log(new List> { - new KeyValuePair("foo", "bar"), + new("foo", "bar"), }); // "event" is a special event name shim.Log(new List> { - new KeyValuePair("event", "foo"), + new("event", "foo"), }); + Assert.NotNull(shim.Span.Activity); + var first = shim.Span.Activity.Events.FirstOrDefault(); var last = shim.Span.Activity.Events.LastOrDefault(); @@ -152,20 +155,21 @@ public void LogUsingFieldsWithExplicitTimestamp() var tracer = TracerProvider.Default.GetTracer(TracerName); var shim = new SpanShim(tracer.StartSpan(SpanName)); - Assert.Throws(() => shim.Log((IEnumerable>)null)); + Assert.Throws(() => shim.Log((IEnumerable>)null!)); var now = DateTimeOffset.UtcNow; shim.Log(now, new List> { - new KeyValuePair("foo", "bar"), + new("foo", "bar"), }); // "event" is a special event name shim.Log(now, new List> { - new KeyValuePair("event", "foo"), + new("event", "foo"), }); + Assert.NotNull(shim.Span.Activity); Assert.Equal(2, shim.Span.Activity.Events.Count()); var first = shim.Span.Activity.Events.First(); var last = shim.Span.Activity.Events.Last(); @@ -185,13 +189,14 @@ public void SetTagStringValue() var tracer = TracerProvider.Default.GetTracer(TracerName); var shim = new SpanShim(tracer.StartSpan(SpanName)); - Assert.Throws(() => shim.SetTag((string)null, "foo")); + Assert.Throws(() => shim.SetTag((string)null!, "foo")); shim.SetTag("foo", "bar"); - 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.NotNull(shim.Span.Activity); + var first = Assert.Single(shim.Span.Activity.Tags); + Assert.Equal("foo", first.Key); + Assert.Equal("bar", first.Value); } [Fact] @@ -200,19 +205,46 @@ public void SetTagBoolValue() 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!, true)); shim.SetTag("foo", true); shim.SetTag(Tags.Error.Key, true); - Assert.Equal("foo", shim.Span.Activity.TagObjects.First().Key); - Assert.True((bool)shim.Span.Activity.TagObjects.First().Value); + Assert.NotNull(shim.Span.Activity); + var first = shim.Span.Activity.TagObjects.First(); + Assert.Equal("foo", first.Key); + Assert.NotNull(first.Value); + Assert.True((bool)first.Value); // A boolean tag named "error" is a special case that must be checked - Assert.Equal(Status.Error, shim.Span.Activity.GetStatus()); + + // Legacy span status tag should be set + Assert.Equal("ERROR", shim.Span.Activity.GetTagValue(SpanAttributeConstants.StatusCodeKey)); + + if (VersionHelper.IsApiVersionGreaterThanOrEqualTo(1, 10)) + { + // Activity status code should also be set + Assert.Equal(ActivityStatusCode.Error, shim.Span.Activity.Status); + } + else + { + Assert.Equal(ActivityStatusCode.Unset, shim.Span.Activity.Status); + } shim.SetTag(Tags.Error.Key, false); - Assert.Equal(Status.Ok, shim.Span.Activity.GetStatus()); + + // Legacy span status tag should be set + Assert.Equal("OK", shim.Span.Activity.GetTagValue(SpanAttributeConstants.StatusCodeKey)); + + if (VersionHelper.IsApiVersionGreaterThanOrEqualTo(1, 10)) + { + // Activity status code should also be set + Assert.Equal(ActivityStatusCode.Ok, shim.Span.Activity.Status); + } + else + { + Assert.Equal(ActivityStatusCode.Unset, shim.Span.Activity.Status); + } } [Fact] @@ -221,13 +253,14 @@ public void SetTagIntValue() var tracer = TracerProvider.Default.GetTracer(TracerName); var shim = new SpanShim(tracer.StartSpan(SpanName)); - Assert.Throws(() => shim.SetTag((string)null, 1)); + Assert.Throws(() => shim.SetTag((string)null!, 1)); shim.SetTag("foo", 1); + Assert.NotNull(shim.Span.Activity); 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.Equal(1L, (int)shim.Span.Activity.TagObjects.First().Value!); } [Fact] @@ -236,34 +269,15 @@ public void SetTagDoubleValue() var tracer = TracerProvider.Default.GetTracer(TracerName); var shim = new SpanShim(tracer.StartSpan(SpanName)); - Assert.Throws(() => shim.SetTag(null, 1D)); + Assert.Throws(() => shim.SetTag(null!, 1D)); shim.SetTag("foo", 1D); - 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); - } - - [Fact] - public void SetTagBooleanTagValue() - { - var tracer = TracerProvider.Default.GetTracer(TracerName); - var shim = new SpanShim(tracer.StartSpan(SpanName)); - - Assert.Throws(() => shim.SetTag((BooleanTag)null, true)); - - shim.SetTag(new BooleanTag("foo"), true); - shim.SetTag(new BooleanTag(Tags.Error.Key), true); - - Assert.Equal("foo", shim.Span.Activity.TagObjects.First().Key); - Assert.True((bool)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()); - - shim.SetTag(Tags.Error.Key, false); - Assert.Equal(Status.Ok, shim.Span.Activity.GetStatus()); + Assert.NotNull(shim.Span.Activity); + var first = Assert.Single(shim.Span.Activity.TagObjects); + Assert.Equal("foo", first.Key); + Assert.NotNull(first.Value); + Assert.Equal(1, (double)first.Value); } [Fact] @@ -272,13 +286,14 @@ public void SetTagStringTagValue() var tracer = TracerProvider.Default.GetTracer(TracerName); var shim = new SpanShim(tracer.StartSpan(SpanName)); - Assert.Throws(() => shim.SetTag((StringTag)null, "foo")); + Assert.Throws(() => shim.SetTag((StringTag)null!, "foo")); shim.SetTag(new StringTag("foo"), "bar"); - 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.NotNull(shim.Span.Activity); + var first = Assert.Single(shim.Span.Activity.Tags); + Assert.Equal("foo", first.Key); + Assert.Equal("bar", first.Value); } [Fact] @@ -287,13 +302,15 @@ public void SetTagIntTagValue() var tracer = TracerProvider.Default.GetTracer(TracerName); var shim = new SpanShim(tracer.StartSpan(SpanName)); - Assert.Throws(() => shim.SetTag((IntTag)null, 1)); + 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); + Assert.NotNull(shim.Span.Activity); + var first = Assert.Single(shim.Span.Activity.TagObjects); + Assert.Equal("foo", first.Key); + Assert.NotNull(first.Value); + Assert.Equal(1L, (int)first.Value); } [Fact] @@ -302,17 +319,21 @@ 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"); + Assert.NotNull(shim.Span.Activity); 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); + var first = shim.Span.Activity.TagObjects.First(); + Assert.Equal("foo", first.Key); + Assert.NotNull(first.Value); + Assert.Equal(1L, (int)first.Value); - Assert.Equal("bar", shim.Span.Activity.TagObjects.Last().Key); - Assert.Equal("baz", shim.Span.Activity.TagObjects.Last().Value); + var second = shim.Span.Activity.TagObjects.Last(); + Assert.Equal("bar", second.Key); + Assert.Equal("baz", second.Value); } } diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/TestFormatTextMap.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/TestFormatTextMap.cs index 83d04095313..c1f7c7e3d08 100644 --- a/test/OpenTelemetry.Shims.OpenTracing.Tests/TestFormatTextMap.cs +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/TestFormatTextMap.cs @@ -5,6 +5,6 @@ namespace OpenTelemetry.Shims.OpenTracing.Tests; -internal class TestFormatTextMap : IFormat +internal sealed class TestFormatTextMap : IFormat { } diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/TestSpan.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/TestSpan.cs index f1e836f76b7..8cfb51c457a 100644 --- a/test/OpenTelemetry.Shims.OpenTracing.Tests/TestSpan.cs +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/TestSpan.cs @@ -6,7 +6,7 @@ namespace OpenTelemetry.Shims.OpenTracing.Tests; -internal class TestSpan : ISpan +internal sealed class TestSpan : ISpan { public ISpanContext Context => throw new NotImplementedException(); diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/TestSpanContext.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/TestSpanContext.cs index d0b5af69991..80f207e4a94 100644 --- a/test/OpenTelemetry.Shims.OpenTracing.Tests/TestSpanContext.cs +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/TestSpanContext.cs @@ -5,7 +5,7 @@ namespace OpenTelemetry.Shims.OpenTracing.Tests; -internal class TestSpanContext : ISpanContext +internal sealed class TestSpanContext : ISpanContext { public string TraceId => throw new NotImplementedException(); diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/TestTextMap.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/TestTextMap.cs index 7396b5543e6..39e5893fbe3 100644 --- a/test/OpenTelemetry.Shims.OpenTracing.Tests/TestTextMap.cs +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/TestTextMap.cs @@ -6,7 +6,7 @@ namespace OpenTelemetry.Shims.OpenTracing.Tests; -internal class TestTextMap : ITextMap +internal sealed class TestTextMap : ITextMap { public bool GetEnumeratorCalled { get; private set; } diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/TracerShimTests.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/TracerShimTests.cs index 578ed8ccbf3..0fb2c90b1ba 100644 --- a/test/OpenTelemetry.Shims.OpenTracing.Tests/TracerShimTests.cs +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/TracerShimTests.cs @@ -17,10 +17,10 @@ public class TracerShimTests public void CtorArgumentValidation() { // null tracer provider and text format - Assert.Throws(() => new TracerShim(null, null)); + Assert.Throws(() => new TracerShim(null!, null)); // null tracer provider - Assert.Throws(() => new TracerShim(null, new TraceContextPropagator())); + Assert.Throws(() => new TracerShim(null!, new TraceContextPropagator())); } [Fact] @@ -50,10 +50,10 @@ public void Inject_ArgumentValidation() var testFormat = new TestFormatTextMap(); var testCarrier = new TestTextMap(); - Assert.Throws(() => shim.Inject(null, testFormat, testCarrier)); + 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)); + Assert.Throws(() => shim.Inject(spanContextShim, null!, testCarrier)); + Assert.Throws(() => shim.Inject(spanContextShim, testFormat!, null)); } [Fact] @@ -76,8 +76,8 @@ public void Extract_ArgumentValidation() { var shim = new TracerShim(TracerProvider.Default, new TraceContextPropagator()); - Assert.Throws(() => shim.Extract(null, new TestTextMap())); - Assert.Throws(() => shim.Extract(new TestFormatTextMap(), null)); + Assert.Throws(() => shim.Extract(null!, new TestTextMap())); + Assert.Throws(() => shim.Extract(new TestFormatTextMap()!, null)); } [Fact] @@ -129,10 +129,10 @@ public void InjectExtract_TextMap_Ok() // then extract var extractedSpanContext = shim.Extract(BuiltinFormats.TextMap, carrier); - AssertOpenTracerSpanContextEqual(spanContextShim, extractedSpanContext); + AssertOpenTracerSpanContextEqual(spanContextShim, extractedSpanContext!); } - private static void AssertOpenTracerSpanContextEqual(ISpanContext source, ISpanContext target) + private static void AssertOpenTracerSpanContextEqual(SpanContextShim source, ISpanContext target) { Assert.Equal(source.TraceId, target.TraceId); Assert.Equal(source.SpanId, target.SpanId); @@ -144,7 +144,7 @@ private static void AssertOpenTracerSpanContextEqual(ISpanContext source, ISpanC /// Simple ITextMap implementation used for the inject/extract tests. /// /// - private class TextMapCarrier : ITextMap + private sealed class TextMapCarrier : ITextMap { private readonly Dictionary map = new(); @@ -159,22 +159,4 @@ public void Set(string key, string value) IEnumerator IEnumerable.GetEnumerator() => this.map.GetEnumerator(); } - - /// - /// Simple IBinary implementation used for the inject/extract tests. - /// - /// - private class BinaryCarrier : IBinary - { - 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); - } - } } diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/VersionHelper.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/VersionHelper.cs new file mode 100644 index 00000000000..65c96a2e504 --- /dev/null +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/VersionHelper.cs @@ -0,0 +1,40 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using NuGet.Versioning; +using OpenTelemetry.Internal; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Shims.OpenTracing.Tests; + +internal static class VersionHelper +{ +#if BUILDING_USING_PROJECTS + private static NuGetVersion? apiVersion = new(100, 0, 0); +#else + private static NuGetVersion? apiVersion; +#endif + + public static NuGetVersion ApiVersion + { + get + { + return apiVersion ??= ResolveApiVersion(); + + static NuGetVersion ResolveApiVersion() + { + if (!typeof(TracerProvider).Assembly.TryGetPackageVersion(out var packageVersion)) + { + throw new InvalidOperationException("OpenTelemetry.Api package version could not be resolved"); + } + + return NuGetVersion.Parse(packageVersion); + } + } + } + + public static bool IsApiVersionGreaterThanOrEqualTo(int major, int minor) + { + return ApiVersion >= new NuGetVersion(major, minor, 0); + } +} diff --git a/test/OpenTelemetry.Tests.Stress.Logs/DummyProcessor.cs b/test/OpenTelemetry.Tests.Stress.Logs/DummyProcessor.cs index aeb1963ec74..56e0629be10 100644 --- a/test/OpenTelemetry.Tests.Stress.Logs/DummyProcessor.cs +++ b/test/OpenTelemetry.Tests.Stress.Logs/DummyProcessor.cs @@ -5,7 +5,7 @@ namespace OpenTelemetry.Tests.Stress; -internal class DummyProcessor : BaseProcessor +internal sealed class DummyProcessor : BaseProcessor { public override void OnEnd(LogRecord record) { diff --git a/test/OpenTelemetry.Tests.Stress.Logs/LoggerExtensions.cs b/test/OpenTelemetry.Tests.Stress.Logs/LoggerExtensions.cs new file mode 100644 index 00000000000..ae3c1436663 --- /dev/null +++ b/test/OpenTelemetry.Tests.Stress.Logs/LoggerExtensions.cs @@ -0,0 +1,18 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.Logging; + +namespace OpenTelemetry.Tests.Stress; + +internal static partial class LoggerExtensions +{ + [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/test/OpenTelemetry.Tests.Stress.Logs/Payload.cs b/test/OpenTelemetry.Tests.Stress.Logs/Payload.cs deleted file mode 100644 index fa8041eeb56..00000000000 --- a/test/OpenTelemetry.Tests.Stress.Logs/Payload.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -namespace OpenTelemetry.Tests.Stress; - -internal class Payload -{ - public int Field001 = 0; - public float Field002 = 2.718281828f; - public double Field003 = 3.141592653589793d; - public string Field004 = "Hello, World!"; - public bool Field005 = true; - public int Field006 = 6; - public int Field007 = 7; - public int Field008 = 8; - public int Field009 = 9; - public int Field010 = 10; - public int Field011 = 11; - public int Field012 = 12; - public int Field013 = 13; - public int Field014 = 14; - public int Field015 = 15; - public int Field016 = 16; - public int Field017 = 17; - public int Field018 = 18; - public int Field019 = 19; - public int Field020 = 20; - public int Field021 = 21; - public int Field022 = 22; - public int Field023 = 23; - public int Field024 = 24; - public int Field025 = 25; - public int Field026 = 26; - public int Field027 = 27; - public int Field028 = 28; - public int Field029 = 29; - public int Field030 = 30; - public int Field031 = 31; - public int Field032 = 32; - public int Field033 = 33; - public int Field034 = 34; - public int Field035 = 35; - public int Field036 = 36; - public int Field037 = 37; - public int Field038 = 38; - public int Field039 = 39; - public int Field040 = 40; - public int Field041 = 41; - public int Field042 = 42; - public int Field043 = 43; - public int Field044 = 44; - public int Field045 = 45; - public int Field046 = 46; - public int Field047 = 47; - public int Field048 = 48; - public int Field049 = 49; - public int Field050 = 50; - public int Field051 = 51; - public int Field052 = 52; - public int Field053 = 53; - public int Field054 = 54; - public int Field055 = 55; - public int Field056 = 56; - public int Field057 = 57; - public int Field058 = 58; - public int Field059 = 59; - public int Field060 = 60; - public int Field061 = 61; - public int Field062 = 62; - public int Field063 = 63; - public int Field064 = 64; - public int Field065 = 65; - public int Field066 = 66; - public int Field067 = 67; - public int Field068 = 68; - public int Field069 = 69; - public int Field070 = 70; - public int Field071 = 71; - public int Field072 = 72; - public int Field073 = 73; - public int Field074 = 74; - public int Field075 = 75; - public int Field076 = 76; - public int Field077 = 77; - public int Field078 = 78; - public int Field079 = 79; - public int Field080 = 80; - public int Field081 = 81; - public int Field082 = 82; - public int Field083 = 83; - public int Field084 = 84; - public int Field085 = 85; - public int Field086 = 86; - public int Field087 = 87; - public int Field088 = 88; - public int Field089 = 89; - public int Field090 = 90; - public int Field091 = 91; - public int Field092 = 92; - public int Field093 = 93; - public int Field094 = 94; - public int Field095 = 95; - public int Field096 = 96; - public int Field097 = 97; - public int Field098 = 98; - public int Field099 = 99; -} diff --git a/test/OpenTelemetry.Tests.Stress.Logs/Program.cs b/test/OpenTelemetry.Tests.Stress.Logs/Program.cs index c8b640aea37..773dac95ca7 100644 --- a/test/OpenTelemetry.Tests.Stress.Logs/Program.cs +++ b/test/OpenTelemetry.Tests.Stress.Logs/Program.cs @@ -5,16 +5,17 @@ namespace OpenTelemetry.Tests.Stress; -public static class Program +internal static class Program { public static int Main(string[] args) { return StressTestFactory.RunSynchronously(args); } - private sealed class LogsStressTest : StressTest +#pragma warning disable CA1812 // Avoid uninstantiated internal classes + private sealed class LogsStressTest : StressTests +#pragma warning restore CA1812 // Avoid uninstantiated internal classes { - private static readonly Payload Payload = new(); private readonly ILoggerFactory loggerFactory; private readonly ILogger logger; @@ -32,24 +33,24 @@ public LogsStressTest(StressTestOptions options) this.logger = this.loggerFactory.CreateLogger(); } - protected override void RunWorkItemInParallel() - { - this.logger.Log( - logLevel: LogLevel.Information, - eventId: 2, - state: Payload, - exception: null, - formatter: (state, ex) => string.Empty); - } - - protected override void Dispose(bool isDisposing) + protected override void Dispose(bool disposing) { - if (isDisposing) + if (disposing) { this.loggerFactory.Dispose(); } - base.Dispose(isDisposing); + base.Dispose(disposing); + } + + protected override void RunWorkItemInParallel() + { + this.logger.FoodRecallNotice( + 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/test/OpenTelemetry.Tests.Stress.Metrics/Program.cs b/test/OpenTelemetry.Tests.Stress.Metrics/Program.cs index 17360444c8e..dd46a5aa853 100644 --- a/test/OpenTelemetry.Tests.Stress.Metrics/Program.cs +++ b/test/OpenTelemetry.Tests.Stress.Metrics/Program.cs @@ -8,7 +8,7 @@ namespace OpenTelemetry.Tests.Stress; -public static class Program +internal static class Program { private enum MetricsStressTestType { @@ -24,7 +24,9 @@ public static int Main(string[] args) return StressTestFactory.RunSynchronously(args); } - private sealed class MetricsStressTest : StressTest +#pragma warning disable CA1812 // Avoid uninstantiated internal classes + private sealed class MetricsStressTest : StressTests +#pragma warning restore CA1812 // Avoid uninstantiated internal classes { private const int ArraySize = 10; private const int MaxHistogramMeasurement = 1000; @@ -51,7 +53,7 @@ public MetricsStressTest(MetricsStressTestOptions options) if (options.PrometheusTestMetricsPort != 0) { - builder.AddPrometheusHttpListener(o => o.UriPrefixes = new string[] { $"http://localhost:{options.PrometheusTestMetricsPort}/" }); + builder.AddPrometheusHttpListener(o => o.UriPrefixes = [$"http://localhost:{options.PrometheusTestMetricsPort}/"]); } if (options.EnableExemplars) @@ -62,8 +64,8 @@ public MetricsStressTest(MetricsStressTestOptions options) if (options.AddViewToFilterTags) { builder - .AddView("TestCounter", new MetricStreamConfiguration { TagKeys = new string[] { "DimName1" } }) - .AddView("TestHistogram", new MetricStreamConfiguration { TagKeys = new string[] { "DimName1" } }); + .AddView("TestCounter", new MetricStreamConfiguration { TagKeys = ["DimName1"] }) + .AddView("TestHistogram", new MetricStreamConfiguration { TagKeys = ["DimName1"] }); } if (options.AddOtlpExporter) @@ -77,6 +79,16 @@ public MetricsStressTest(MetricsStressTestOptions options) this.meterProvider = builder.Build(); } + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.meterProvider.Dispose(); + } + + base.Dispose(disposing); + } + protected override void WriteRunInformationToConsole() { if (this.Options.PrometheusTestMetricsPort != 0) @@ -91,6 +103,7 @@ protected override void RunWorkItemInParallel() if (this.Options.TestType == MetricsStressTestType.Histogram) { TestHistogram.Record( +#pragma warning disable CA5394 // Do not use random number generators in secure applications random.Next(MaxHistogramMeasurement), new("DimName1", DimensionValues[random.Next(0, ArraySize)]), new("DimName2", DimensionValues[random.Next(0, ArraySize)]), @@ -103,21 +116,14 @@ protected override void RunWorkItemInParallel() new("DimName1", DimensionValues[random.Next(0, ArraySize)]), new("DimName2", DimensionValues[random.Next(0, ArraySize)]), new("DimName3", DimensionValues[random.Next(0, ArraySize)])); +#pragma warning restore CA5394 // Do not use random number generators in secure applications } } - - protected override void Dispose(bool isDisposing) - { - if (isDisposing) - { - this.meterProvider.Dispose(); - } - - base.Dispose(isDisposing); - } } +#pragma warning disable CA1812 // Avoid uninstantiated internal classes private sealed class MetricsStressTestOptions : StressTestOptions +#pragma warning restore CA1812 // Avoid uninstantiated internal classes { [JsonConverter(typeof(JsonStringEnumConverter))] [Option('t', "type", HelpText = "The metrics stress test type to run. Valid values: [Histogram, Counter]. Default value: Histogram.", Required = false)] diff --git a/test/OpenTelemetry.Tests.Stress.Traces/Program.cs b/test/OpenTelemetry.Tests.Stress.Traces/Program.cs index 422a44a99ef..4861e39aef7 100644 --- a/test/OpenTelemetry.Tests.Stress.Traces/Program.cs +++ b/test/OpenTelemetry.Tests.Stress.Traces/Program.cs @@ -6,14 +6,16 @@ namespace OpenTelemetry.Tests.Stress; -public static class Program +internal static class Program { public static int Main(string[] args) { return StressTestFactory.RunSynchronously(args); } - private sealed class TracesStressTest : StressTest +#pragma warning disable CA1812 // Avoid uninstantiated internal classes + private sealed class TracesStressTest : StressTests +#pragma warning restore CA1812 // Avoid uninstantiated internal classes { private static readonly ActivitySource ActivitySource = new("OpenTelemetry.Tests.Stress"); private readonly TracerProvider tracerProvider; @@ -26,21 +28,21 @@ public TracesStressTest(StressTestOptions options) .Build(); } - protected override void RunWorkItemInParallel() + protected override void Dispose(bool disposing) { - using var activity = ActivitySource.StartActivity("test"); + if (disposing) + { + this.tracerProvider.Dispose(); + } - activity?.SetTag("foo", "value"); + base.Dispose(disposing); } - protected override void Dispose(bool isDisposing) + protected override void RunWorkItemInParallel() { - if (isDisposing) - { - this.tracerProvider.Dispose(); - } + using var activity = ActivitySource.StartActivity("test"); - base.Dispose(isDisposing); + activity?.SetTag("foo", "value"); } } } diff --git a/test/OpenTelemetry.Tests.Stress/OpenTelemetry.Tests.Stress.csproj b/test/OpenTelemetry.Tests.Stress/OpenTelemetry.Tests.Stress.csproj index 01af1c993ae..9e6464cf484 100644 --- a/test/OpenTelemetry.Tests.Stress/OpenTelemetry.Tests.Stress.csproj +++ b/test/OpenTelemetry.Tests.Stress/OpenTelemetry.Tests.Stress.csproj @@ -1,17 +1,19 @@ + Exe $(TargetFrameworksForTests) + true - + diff --git a/test/OpenTelemetry.Tests.Stress/Program.cs b/test/OpenTelemetry.Tests.Stress/Program.cs index a5f6fb8975e..ce67d1c8511 100644 --- a/test/OpenTelemetry.Tests.Stress/Program.cs +++ b/test/OpenTelemetry.Tests.Stress/Program.cs @@ -3,14 +3,16 @@ namespace OpenTelemetry.Tests.Stress; -public static class Program +internal static class Program { public static int Main(string[] args) { return StressTestFactory.RunSynchronously(args); } - private sealed class DemoStressTest : StressTest +#pragma warning disable CA1812 // Avoid uninstantiated internal classes + private sealed class DemoStressTest : StressTests +#pragma warning restore CA1812 // Avoid uninstantiated internal classes { public DemoStressTest(StressTestOptions options) : base(options) diff --git a/test/OpenTelemetry.Tests.Stress/StressTestFactory.cs b/test/OpenTelemetry.Tests.Stress/StressTestFactory.cs index 6f3e7ff9ea7..aa67a66e670 100644 --- a/test/OpenTelemetry.Tests.Stress/StressTestFactory.cs +++ b/test/OpenTelemetry.Tests.Stress/StressTestFactory.cs @@ -5,16 +5,18 @@ namespace OpenTelemetry.Tests.Stress; +#pragma warning disable CA1515 // Consider making public types internal public static class StressTestFactory +#pragma warning restore CA1515 // Consider making public types internal { public static int RunSynchronously(string[] commandLineArguments) - where TStressTest : StressTest + where TStressTest : StressTests { return RunSynchronously(commandLineArguments); } public static int RunSynchronously(string[] commandLineArguments) - where TStressTest : StressTest + where TStressTest : StressTests where TStressTestOptions : StressTestOptions { return Parser.Default.ParseArguments(commandLineArguments) diff --git a/test/OpenTelemetry.Tests.Stress/StressTestNativeMethods.cs b/test/OpenTelemetry.Tests.Stress/StressTestNativeMethods.cs index da3df1c2864..8222c9dc9fc 100644 --- a/test/OpenTelemetry.Tests.Stress/StressTestNativeMethods.cs +++ b/test/OpenTelemetry.Tests.Stress/StressTestNativeMethods.cs @@ -23,6 +23,7 @@ public static ulong GetCpuCycles() } [DllImport("kernel32.dll")] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] [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 index 2dcb2b2e47c..4e5f9387355 100644 --- a/test/OpenTelemetry.Tests.Stress/StressTestOptions.cs +++ b/test/OpenTelemetry.Tests.Stress/StressTestOptions.cs @@ -5,7 +5,9 @@ namespace OpenTelemetry.Tests.Stress; +#pragma warning disable CA1515 // Consider making public types internal public class StressTestOptions +#pragma warning restore CA1515 // Consider making public types internal { [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; } diff --git a/test/OpenTelemetry.Tests.Stress/StressTest.cs b/test/OpenTelemetry.Tests.Stress/StressTests.cs similarity index 83% rename from test/OpenTelemetry.Tests.Stress/StressTest.cs rename to test/OpenTelemetry.Tests.Stress/StressTests.cs index ae19c7f8ece..4a39deb0427 100644 --- a/test/OpenTelemetry.Tests.Stress/StressTest.cs +++ b/test/OpenTelemetry.Tests.Stress/StressTests.cs @@ -3,19 +3,20 @@ using System.Diagnostics; using System.Diagnostics.Metrics; +using System.Globalization; using System.Runtime.InteropServices; using System.Text.Json; using OpenTelemetry.Metrics; namespace OpenTelemetry.Tests.Stress; -public abstract class StressTest : IDisposable +public abstract class StressTests : IDisposable where T : StressTestOptions { private volatile bool bContinue = true; private volatile string output = "Test results not available yet."; - protected StressTest(T options) + protected StressTests(T options) { this.Options = options ?? throw new ArgumentNullException(nameof(options)); } @@ -71,10 +72,10 @@ public void RunSynchronously() using var meterProvider = options.PrometheusInternalMetricsPort != 0 ? Sdk.CreateMeterProviderBuilder() .AddMeter(meter.Name) .AddRuntimeInstrumentation() - .AddPrometheusHttpListener(o => o.UriPrefixes = new string[] { $"http://localhost:{options.PrometheusInternalMetricsPort}/" }) + .AddPrometheusHttpListener(o => o.UriPrefixes = [$"http://localhost:{options.PrometheusInternalMetricsPort}/"]) .Build() : null; - var statistics = new long[options.Concurrency]; + var statistics = new MeasurementData[options.Concurrency]; var watchForTotal = Stopwatch.StartNew(); TimeSpan? duration = options.DurationSeconds > 0 @@ -111,7 +112,7 @@ public void RunSynchronously() switch (key) { case ConsoleKey.Enter: - Console.WriteLine(string.Format("{0} {1}", DateTime.UtcNow.ToString("O"), this.output)); + Console.WriteLine(string.Format(CultureInfo.InvariantCulture, "{0} {1}", DateTime.UtcNow.ToString("O"), this.output)); break; case ConsoleKey.Escape: this.bContinue = false; @@ -133,14 +134,14 @@ public void RunSynchronously() Console.SetCursorPosition(tempCursorLeft, tempCursorTop); } - var cntLoopsOld = (ulong)statistics.Sum(); + var cntLoopsOld = (ulong)statistics.Select(data => data.Count).Sum(); var cntCpuCyclesOld = StressTestNativeMethods.GetCpuCycles(); watch.Restart(); Thread.Sleep(200); watch.Stop(); - cntLoopsTotal = (ulong)statistics.Sum(); + cntLoopsTotal = (ulong)statistics.Select(data => data.Count).Sum(); var cntCpuCyclesNew = StressTestNativeMethods.GetCpuCycles(); var nLoops = cntLoopsTotal - cntLoopsOld; @@ -172,18 +173,18 @@ public void RunSynchronously() { Parallel.For(0, options.Concurrency, (i) => { - ref var count = ref statistics[i]; + ref var item = ref statistics[i]; while (this.bContinue) { this.RunWorkItemInParallel(); - count++; + item.Count++; } }); }); watchForTotal.Stop(); - cntLoopsTotal = (ulong)statistics.Sum(); + cntLoopsTotal = (ulong)statistics.Select(data => data.Count).Sum(); var totalLoopsPerSecond = (double)cntLoopsTotal / ((double)watchForTotal.ElapsedMilliseconds / 1000.0); var cntCpuCyclesTotal = StressTestNativeMethods.GetCpuCycles(); var cpuCyclesPerLoopTotal = cntLoopsTotal == 0 ? 0 : cntCpuCyclesTotal / cntLoopsTotal; @@ -197,13 +198,36 @@ public void RunSynchronously() #endif } + protected virtual void Dispose(bool disposing) + { + } + protected virtual void WriteRunInformationToConsole() { } protected abstract void RunWorkItemInParallel(); - protected virtual void Dispose(bool isDisposing) + // Padding to avoid false sharing. + // For most systems, the cache line size should be less than or equal to 128 bytes. + private struct MeasurementData { + public long Count; + + public long Padding1; + public long Padding2; + public long Padding3; + public long Padding4; + public long Padding5; + public long Padding6; + public long Padding7; + public long Padding8; + public long Padding9; + public long Padding10; + public long Padding11; + public long Padding12; + public long Padding13; + public long Padding14; + public long Padding15; } } diff --git a/test/OpenTelemetry.Tests/BaseExporterTest.cs b/test/OpenTelemetry.Tests/BaseExporterTests.cs similarity index 65% rename from test/OpenTelemetry.Tests/BaseExporterTest.cs rename to test/OpenTelemetry.Tests/BaseExporterTests.cs index 39095636996..0bd99875d01 100644 --- a/test/OpenTelemetry.Tests/BaseExporterTest.cs +++ b/test/OpenTelemetry.Tests/BaseExporterTests.cs @@ -5,19 +5,19 @@ namespace OpenTelemetry.Tests; -public class BaseExporterTest +public class BaseExporterTests { [Fact] public void Verify_ForceFlush_HandlesException() { // By default, ForceFlush should return true. - var testExporter = new DelegatingExporter(); + using var testExporter = new DelegatingExporter(); Assert.True(testExporter.ForceFlush()); // BaseExporter should catch any exceptions and return false. - var exceptionTestExporter = new DelegatingExporter + using var exceptionTestExporter = new DelegatingExporter { - OnForceFlushFunc = (timeout) => throw new Exception("test exception"), + OnForceFlushFunc = _ => throw new InvalidOperationException("test exception"), }; Assert.False(exceptionTestExporter.ForceFlush()); } @@ -26,7 +26,7 @@ public void Verify_ForceFlush_HandlesException() public void Verify_Shutdown_HandlesSecond() { // By default, ForceFlush should return true. - var testExporter = new DelegatingExporter(); + using var testExporter = new DelegatingExporter(); Assert.True(testExporter.Shutdown()); // A second Shutdown should return false. @@ -37,9 +37,9 @@ public void Verify_Shutdown_HandlesSecond() public void Verify_Shutdown_HandlesException() { // BaseExporter should catch any exceptions and return false. - var exceptionTestExporter = new DelegatingExporter + using var exceptionTestExporter = new DelegatingExporter { - OnShutdownFunc = (timeout) => throw new Exception("test exception"), + OnShutdownFunc = _ => throw new InvalidOperationException("test exception"), }; Assert.False(exceptionTestExporter.Shutdown()); } diff --git a/test/OpenTelemetry.Tests/BaseProcessorTest.cs b/test/OpenTelemetry.Tests/BaseProcessorTests.cs similarity index 69% rename from test/OpenTelemetry.Tests/BaseProcessorTest.cs rename to test/OpenTelemetry.Tests/BaseProcessorTests.cs index 61516290135..b5d89766b28 100644 --- a/test/OpenTelemetry.Tests/BaseProcessorTest.cs +++ b/test/OpenTelemetry.Tests/BaseProcessorTests.cs @@ -5,17 +5,17 @@ namespace OpenTelemetry.Tests; -public class BaseProcessorTest +public class BaseProcessorTests { [Fact] public void Verify_ForceFlush_HandlesException() { // By default, ForceFlush should return true. - var testProcessor = new DelegatingProcessor(); + using var testProcessor = new DelegatingProcessor(); Assert.True(testProcessor.ForceFlush()); // BaseExporter should catch any exceptions and return false. - testProcessor.OnForceFlushFunc = (timeout) => throw new Exception("test exception"); + testProcessor.OnForceFlushFunc = _ => throw new InvalidOperationException("test exception"); Assert.False(testProcessor.ForceFlush()); } @@ -23,7 +23,7 @@ public void Verify_ForceFlush_HandlesException() public void Verify_Shutdown_HandlesSecond() { // By default, Shutdown should return true. - var testProcessor = new DelegatingProcessor(); + using var testProcessor = new DelegatingProcessor(); Assert.True(testProcessor.Shutdown()); // A second Shutdown should return false. @@ -34,9 +34,9 @@ public void Verify_Shutdown_HandlesSecond() public void Verify_Shutdown_HandlesException() { // BaseExporter should catch any exceptions and return false. - var exceptionTestProcessor = new DelegatingProcessor + using var exceptionTestProcessor = new DelegatingProcessor { - OnShutdownFunc = (timeout) => throw new Exception("test exception"), + OnShutdownFunc = _ => throw new InvalidOperationException("test exception"), }; Assert.False(exceptionTestProcessor.Shutdown()); } @@ -44,7 +44,7 @@ public void Verify_Shutdown_HandlesException() [Fact] public void NoOp() { - var testProcessor = new DelegatingProcessor(); + using var testProcessor = new DelegatingProcessor(); // These two methods are no-op, but account for 7% of the test coverage. testProcessor.OnStart(new object()); diff --git a/test/OpenTelemetry.Tests/Concurrency/MetricsConcurrencyTests.cs b/test/OpenTelemetry.Tests/Concurrency/MetricsConcurrencyTests.cs index e20be44ee87..8f4c4ca1900 100644 --- a/test/OpenTelemetry.Tests/Concurrency/MetricsConcurrencyTests.cs +++ b/test/OpenTelemetry.Tests/Concurrency/MetricsConcurrencyTests.cs @@ -28,7 +28,7 @@ public void MultithreadedLongHistogramTestConcurrencyTest() .WithTestingIterations(100) .WithMemoryAccessRaceCheckingEnabled(true); - var test = TestingEngine.Create(config, this.aggregatorTests.MultiThreadedHistogramUpdateAndSnapShotTest); + using var test = TestingEngine.Create(config, this.aggregatorTests.MultiThreadedHistogramUpdateAndSnapShotTest); test.Run(); diff --git a/test/OpenTelemetry.Tests/EventSourceTest.cs b/test/OpenTelemetry.Tests/EventSourceTests.cs similarity index 92% rename from test/OpenTelemetry.Tests/EventSourceTest.cs rename to test/OpenTelemetry.Tests/EventSourceTests.cs index 5e7d3e2f258..f9680c5a6fb 100644 --- a/test/OpenTelemetry.Tests/EventSourceTest.cs +++ b/test/OpenTelemetry.Tests/EventSourceTests.cs @@ -6,7 +6,7 @@ namespace OpenTelemetry.Tests; -public class EventSourceTest +public class EventSourceTests { [Fact] public void EventSourceTest_OpenTelemetrySdkEventSource() diff --git a/test/OpenTelemetry.Tests/Internal/AssemblyVersionExtensionsTests.cs b/test/OpenTelemetry.Tests/Internal/AssemblyVersionExtensionsTests.cs index 22cf81be0ee..fd8f682af1e 100644 --- a/test/OpenTelemetry.Tests/Internal/AssemblyVersionExtensionsTests.cs +++ b/test/OpenTelemetry.Tests/Internal/AssemblyVersionExtensionsTests.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Reflection; using Xunit; @@ -27,7 +25,7 @@ public void ParseAssemblyInformationalVersionTests(string informationalVersion, Assert.Equal(expectedVersion, actualVersion); } - private class TestAssembly(string informationalVersion) : Assembly + private sealed class TestAssembly(string informationalVersion) : Assembly { public override object[] GetCustomAttributes(Type attributeType, bool inherit) { diff --git a/test/OpenTelemetry.Tests/Internal/CircularBufferTest.cs b/test/OpenTelemetry.Tests/Internal/CircularBufferTests.cs similarity index 94% rename from test/OpenTelemetry.Tests/Internal/CircularBufferTest.cs rename to test/OpenTelemetry.Tests/Internal/CircularBufferTests.cs index 2e0e4225526..bb45f1707f0 100644 --- a/test/OpenTelemetry.Tests/Internal/CircularBufferTest.cs +++ b/test/OpenTelemetry.Tests/Internal/CircularBufferTests.cs @@ -5,7 +5,7 @@ namespace OpenTelemetry.Internal.Tests; -public class CircularBufferTest +public class CircularBufferTests { [Fact] public void CheckInvalidArgument() @@ -22,14 +22,6 @@ public void CheckCapacity() 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 CheckValueWhenAdding() { diff --git a/test/OpenTelemetry.Tests/Internal/JsonStringArrayTagWriterTests.cs b/test/OpenTelemetry.Tests/Internal/JsonStringArrayTagWriterTests.cs index 0e7a8d45fa7..ac67d026c19 100644 --- a/test/OpenTelemetry.Tests/Internal/JsonStringArrayTagWriterTests.cs +++ b/test/OpenTelemetry.Tests/Internal/JsonStringArrayTagWriterTests.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Text; using Xunit; @@ -195,6 +193,13 @@ protected override void OnUnsupportedTagDropped(string tagKey, string tagValueTy { } + protected override bool TryWriteEmptyTag(ref Tag state, string key, object? value) + { + throw new NotImplementedException(); + } + + protected override bool TryWriteByteArrayTag(ref Tag consoleTag, string key, ReadOnlySpan value) => false; + public struct Tag { public string? Key; diff --git a/test/OpenTelemetry.Tests/Internal/MathHelperTest.cs b/test/OpenTelemetry.Tests/Internal/MathHelperTests.cs similarity index 99% rename from test/OpenTelemetry.Tests/Internal/MathHelperTest.cs rename to test/OpenTelemetry.Tests/Internal/MathHelperTests.cs index a0d1c6fa9fc..b838dd78fb1 100644 --- a/test/OpenTelemetry.Tests/Internal/MathHelperTest.cs +++ b/test/OpenTelemetry.Tests/Internal/MathHelperTests.cs @@ -5,7 +5,7 @@ namespace OpenTelemetry.Internal.Tests; -public class MathHelperTest +public class MathHelperTests { [Theory] [InlineData(0b0000_0000, 8)] diff --git a/test/OpenTelemetry.Tests/Internal/PeriodicExportingMetricReaderHelperTests.cs b/test/OpenTelemetry.Tests/Internal/PeriodicExportingMetricReaderHelperTests.cs index d0da994e018..236cbe1e727 100644 --- a/test/OpenTelemetry.Tests/Internal/PeriodicExportingMetricReaderHelperTests.cs +++ b/test/OpenTelemetry.Tests/Internal/PeriodicExportingMetricReaderHelperTests.cs @@ -1,8 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - +using System.Globalization; using Microsoft.Extensions.Configuration; using OpenTelemetry.Exporter; using OpenTelemetry.Metrics; @@ -25,7 +24,9 @@ public void Dispose() [Fact] public void CreatePeriodicExportingMetricReader_Defaults() { +#pragma warning disable CA2000 // Dispose objects before losing scope var reader = CreatePeriodicExportingMetricReader(); +#pragma warning restore CA2000 // Dispose objects before losing scope Assert.Equal(60000, reader.ExportIntervalMilliseconds); Assert.Equal(30000, reader.ExportTimeoutMilliseconds); @@ -36,7 +37,7 @@ public void CreatePeriodicExportingMetricReader_Defaults() public void CreatePeriodicExportingMetricReader_TemporalityPreference_FromOptions() { var value = MetricReaderTemporalityPreference.Delta; - var reader = CreatePeriodicExportingMetricReader(new() + using var reader = CreatePeriodicExportingMetricReader(new() { TemporalityPreference = value, }); @@ -49,7 +50,7 @@ public void CreatePeriodicExportingMetricReader_ExportIntervalMilliseconds_FromO { Environment.SetEnvironmentVariable(PeriodicExportingMetricReaderOptions.OTelMetricExportIntervalEnvVarKey, "88888"); // should be ignored, as value set via options has higher priority var value = 123; - var reader = CreatePeriodicExportingMetricReader(new() + using var reader = CreatePeriodicExportingMetricReader(new() { PeriodicExportingMetricReaderOptions = new() { @@ -65,7 +66,7 @@ public void CreatePeriodicExportingMetricReader_ExportTimeoutMilliseconds_FromOp { Environment.SetEnvironmentVariable(PeriodicExportingMetricReaderOptions.OTelMetricExportTimeoutEnvVarKey, "99999"); // should be ignored, as value set via options has higher priority var value = 456; - var reader = CreatePeriodicExportingMetricReader(new() + using var reader = CreatePeriodicExportingMetricReader(new() { PeriodicExportingMetricReaderOptions = new() { @@ -80,8 +81,8 @@ public void CreatePeriodicExportingMetricReader_ExportTimeoutMilliseconds_FromOp public void CreatePeriodicExportingMetricReader_ExportIntervalMilliseconds_FromEnvVar() { var value = 789; - Environment.SetEnvironmentVariable(PeriodicExportingMetricReaderOptions.OTelMetricExportIntervalEnvVarKey, value.ToString()); - var reader = CreatePeriodicExportingMetricReader(); + Environment.SetEnvironmentVariable(PeriodicExportingMetricReaderOptions.OTelMetricExportIntervalEnvVarKey, value.ToString(CultureInfo.InvariantCulture)); + using var reader = CreatePeriodicExportingMetricReader(); Assert.Equal(value, reader.ExportIntervalMilliseconds); } @@ -90,8 +91,8 @@ public void CreatePeriodicExportingMetricReader_ExportIntervalMilliseconds_FromE public void CreatePeriodicExportingMetricReader_ExportTimeoutMilliseconds_FromEnvVar() { var value = 246; - Environment.SetEnvironmentVariable(PeriodicExportingMetricReaderOptions.OTelMetricExportTimeoutEnvVarKey, value.ToString()); - var reader = CreatePeriodicExportingMetricReader(); + Environment.SetEnvironmentVariable(PeriodicExportingMetricReaderOptions.OTelMetricExportTimeoutEnvVarKey, value.ToString(CultureInfo.InvariantCulture)); + using var reader = CreatePeriodicExportingMetricReader(); Assert.Equal(value, reader.ExportTimeoutMilliseconds); } @@ -133,7 +134,9 @@ private static PeriodicExportingMetricReader CreatePeriodicExportingMetricReader { options ??= new(); +#pragma warning disable CA2000 // Dispose objects before losing scope var dummyMetricExporter = new InMemoryExporter(Array.Empty()); +#pragma warning restore CA2000 // Dispose objects before losing scope return PeriodicExportingMetricReaderHelper.CreatePeriodicExportingMetricReader(dummyMetricExporter, options); } } diff --git a/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigParserTest.cs b/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigParserTest.cs deleted file mode 100644 index 9249728690d..00000000000 --- a/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigParserTest.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using Xunit; - -namespace OpenTelemetry.Internal.Tests; - -public class SelfDiagnosticsConfigParserTest -{ - [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); - } - - [Fact] - public void SelfDiagnosticsConfigParser_TryParseFilePath_MissingField() - { - string configJson = @"{ - ""path"": ""Diagnostics"", - ""FileSize"": 1024 - }"; - Assert.False(SelfDiagnosticsConfigParser.TryParseLogDirectory(configJson, out _)); - } - - [Fact] - public void SelfDiagnosticsConfigParser_TryParseFileSize() - { - string configJson = @"{ - ""LogDirectory"": ""Diagnostics"", - ""FileSize"": 1024 - }"; - Assert.True(SelfDiagnosticsConfigParser.TryParseFileSize(configJson, out int fileSize)); - Assert.Equal(1024, fileSize); - } - - [Fact] - public void SelfDiagnosticsConfigParser_TryParseFileSize_CaseInsensitive() - { - string configJson = @"{ - ""LogDirectory"": ""Diagnostics"", - ""fileSize"" : - 2048 - }"; - Assert.True(SelfDiagnosticsConfigParser.TryParseFileSize(configJson, out int fileSize)); - Assert.Equal(2048, fileSize); - } - - [Fact] - public void SelfDiagnosticsConfigParser_TryParseFileSize_MissingField() - { - string configJson = @"{ - ""LogDirectory"": ""Diagnostics"", - ""size"": 1024 - }"; - Assert.False(SelfDiagnosticsConfigParser.TryParseFileSize(configJson, out _)); - } - - [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); - } -} diff --git a/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigParserTests.cs b/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigParserTests.cs new file mode 100644 index 00000000000..11bb09e7c01 --- /dev/null +++ b/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigParserTests.cs @@ -0,0 +1,148 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Xunit; + +namespace OpenTelemetry.Internal.Tests; + +public class SelfDiagnosticsConfigParserTests +{ + [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); + } + + [Fact] + public void SelfDiagnosticsConfigParser_TryParseFilePath_MissingField() + { + string configJson = @"{ + ""path"": ""Diagnostics"", + ""FileSize"": 1024 + }"; + Assert.False(SelfDiagnosticsConfigParser.TryParseLogDirectory(configJson, out _)); + } + + [Fact] + public void SelfDiagnosticsConfigParser_TryParseFileSize() + { + string configJson = @"{ + ""LogDirectory"": ""Diagnostics"", + ""FileSize"": 1024 + }"; + Assert.True(SelfDiagnosticsConfigParser.TryParseFileSize(configJson, out int fileSize)); + Assert.Equal(1024, fileSize); + } + + [Fact] + public void SelfDiagnosticsConfigParser_TryParseFileSize_CaseInsensitive() + { + string configJson = @"{ + ""LogDirectory"": ""Diagnostics"", + ""fileSize"" : + 2048 + }"; + Assert.True(SelfDiagnosticsConfigParser.TryParseFileSize(configJson, out int fileSize)); + Assert.Equal(2048, fileSize); + } + + [Fact] + public void SelfDiagnosticsConfigParser_TryParseFileSize_MissingField() + { + string configJson = @"{ + ""LogDirectory"": ""Diagnostics"", + ""size"": 1024 + }"; + Assert.False(SelfDiagnosticsConfigParser.TryParseFileSize(configJson, out _)); + } + + [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); + } + + [Fact] + public void SelfDiagnosticsConfigParser_TryParseFormatMessage_Success() + { + string configJson = """ + { + "LogDirectory": "Diagnostics", + "FileSize": 1024, + "LogLevel": "Error", + "FormatMessage": "true" + } + """; + Assert.True(SelfDiagnosticsConfigParser.TryParseFormatMessage(configJson, out bool formatMessage)); + Assert.True(formatMessage); + } + + [Fact] + public void SelfDiagnosticsConfigParser_TryParseFormatMessage_CaseInsensitive() + { + string configJson = """ + { + "LogDirectory": "Diagnostics", + "fileSize": 1024, + "formatMessage": "FALSE" + } + """; + Assert.True(SelfDiagnosticsConfigParser.TryParseFormatMessage(configJson, out bool formatMessage)); + Assert.False(formatMessage); + } + + [Fact] + public void SelfDiagnosticsConfigParser_TryParseFormatMessage_MissingField() + { + string configJson = """ + { + "LogDirectory": "Diagnostics", + "FileSize": 1024, + "LogLevel": "Error" + } + """; + Assert.True(SelfDiagnosticsConfigParser.TryParseFormatMessage(configJson, out bool formatMessage)); + Assert.False(formatMessage); // Should default to false + } + + [Fact] + public void SelfDiagnosticsConfigParser_TryParseFormatMessage_InvalidValue() + { + string configJson = """ + { + "LogDirectory": "Diagnostics", + "FileSize": 1024, + "LogLevel": "Error", + "FormatMessage": "invalid" + } + """; + Assert.False(SelfDiagnosticsConfigParser.TryParseFormatMessage(configJson, out bool formatMessage)); + Assert.False(formatMessage); // Should default to false + } + + [Fact] + public void SelfDiagnosticsConfigParser_TryParseFormatMessage_UnquotedBoolean() + { + string configJson = """ + { + "LogDirectory": "Diagnostics", + "FileSize": 1024, + "LogLevel": "Error", + "FormatMessage": true + } + """; + Assert.True(SelfDiagnosticsConfigParser.TryParseFormatMessage(configJson, out bool formatMessage)); + Assert.True(formatMessage); + } +} diff --git a/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigRefresherTest.cs b/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigRefresherTests.cs similarity index 88% rename from test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigRefresherTest.cs rename to test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigRefresherTests.cs index 10c0be18fa2..a87d8823cff 100644 --- a/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigRefresherTest.cs +++ b/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigRefresherTests.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; using System.Text; using OpenTelemetry.Tests; @@ -11,15 +9,15 @@ namespace OpenTelemetry.Internal.Tests; -public class SelfDiagnosticsConfigRefresherTest +public class SelfDiagnosticsConfigRefresherTests { - private static readonly string ConfigFilePath = SelfDiagnosticsConfigParser.ConfigFileName; + private const 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; - public SelfDiagnosticsConfigRefresherTest(ITestOutputHelper output) + public SelfDiagnosticsConfigRefresherTests(ITestOutputHelper output) { this.output = output; } @@ -40,7 +38,7 @@ public void SelfDiagnosticsConfigRefresher_OmitAsConfigured() 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); + Assert.StartsWith(MessageOnNewFileString, logText, StringComparison.Ordinal); // The event was omitted Assert.Equal('\0', (char)actualBytes[MessageOnNewFile.Length]); @@ -67,12 +65,12 @@ public void SelfDiagnosticsConfigRefresher_CaptureAsConfigured() int bufferSize = 2 * (MessageOnNewFileString.Length + expectedMessage.Length); byte[] actualBytes = ReadFile(logDirectory, bufferSize); string logText = Encoding.UTF8.GetString(actualBytes); - Assert.StartsWith(MessageOnNewFileString, logText); + Assert.StartsWith(MessageOnNewFileString, logText, StringComparison.Ordinal); // The event was captured string logLine = logText.Substring(MessageOnNewFileString.Length); string logMessage = ParseLogMessage(logLine); - Assert.StartsWith(expectedMessage, logMessage); + Assert.StartsWith(expectedMessage, logMessage, StringComparison.Ordinal); } finally { @@ -90,7 +88,11 @@ private static string ParseLogMessage(string logLine) private static byte[] ReadFile(string logDirectory, int byteCount) { var outputFileName = Path.GetFileName(Process.GetCurrentProcess().MainModule?.FileName) + "." +#if NET + + Environment.ProcessId + ".log"; +#else + Process.GetCurrentProcess().Id + ".log"; +#endif var outputFilePath = Path.Combine(logDirectory, outputFileName); using var file = File.Open(outputFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); byte[] actualBytes = new byte[byteCount]; diff --git a/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsEventListenerTest.cs b/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsEventListenerTests.cs similarity index 79% rename from test/OpenTelemetry.Tests/Internal/SelfDiagnosticsEventListenerTest.cs rename to test/OpenTelemetry.Tests/Internal/SelfDiagnosticsEventListenerTests.cs index 58777971b5d..b164d13fff7 100644 --- a/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsEventListenerTest.cs +++ b/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsEventListenerTests.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Tracing; +using System.Globalization; using System.IO.MemoryMappedFiles; using System.Text; using OpenTelemetry.Tests; @@ -9,7 +10,7 @@ namespace OpenTelemetry.Internal.Tests; -public class SelfDiagnosticsEventListenerTest +public class SelfDiagnosticsEventListenerTests { private const string LOGFILEPATH = "Diagnostics.log"; private const string Ellipses = "...\n"; @@ -21,15 +22,15 @@ public void SelfDiagnosticsEventListener_constructor_Invalid_Input() // no configRefresher object Assert.Throws(() => { - _ = new SelfDiagnosticsEventListener(EventLevel.Error, null); + _ = new SelfDiagnosticsEventListener(EventLevel.Error, null!); }); } [Fact] public void SelfDiagnosticsEventListener_EventSourceSetup_LowerSeverity() { - var configRefresher = new TestSelfDiagnosticsConfigRefresher(); - _ = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresher); + using var configRefresher = new TestSelfDiagnosticsConfigRefresher(); + using var listener = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresher); // Emitting a Verbose event. Or any EventSource event with lower severity than Error. OpenTelemetrySdkEventSource.Log.ActivityStarted("Activity started", "1"); @@ -39,8 +40,8 @@ public void SelfDiagnosticsEventListener_EventSourceSetup_LowerSeverity() [Fact] public void SelfDiagnosticsEventListener_EventSourceSetup_HigherSeverity() { - var configRefresher = new TestSelfDiagnosticsConfigRefresher(); - _ = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresher); + using var configRefresher = new TestSelfDiagnosticsConfigRefresher(); + using var listener = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresher); // Emitting an Error event. Or any EventSource event with higher than or equal to to Error severity. OpenTelemetrySdkEventSource.Log.TracerProviderException("TestEvent", "Exception Details"); @@ -53,9 +54,9 @@ public void SelfDiagnosticsEventListener_WriteEvent() // Arrange var memoryMappedFile = MemoryMappedFile.CreateFromFile(LOGFILEPATH, FileMode.Create, null, 1024); Stream stream = memoryMappedFile.CreateViewStream(); - var configRefresher = new TestSelfDiagnosticsConfigRefresher(stream); + using var configRefresher = new TestSelfDiagnosticsConfigRefresher(stream); string eventMessage = "Event Message"; - var listener = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresher); + using var listener = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresher); // Act: call WriteEvent method directly listener.WriteEvent(eventMessage, null); @@ -70,15 +71,15 @@ public void SelfDiagnosticsEventListener_WriteEvent() [Fact] public void SelfDiagnosticsEventListener_DateTimeGetBytes() { - var configRefresher = new TestSelfDiagnosticsConfigRefresher(); - var listener = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresher); + using var configRefresher = new TestSelfDiagnosticsConfigRefresher(); + using 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.SpecifyKind(DateTime.Parse("1996-12-01T14:02:31.1234567-08:00", CultureInfo.InvariantCulture), DateTimeKind.Utc), + DateTime.SpecifyKind(DateTime.Parse("1996-12-01T14:02:31.1234567-08:00", CultureInfo.InvariantCulture), DateTimeKind.Local), + DateTime.SpecifyKind(DateTime.Parse("1996-12-01T14:02:31.1234567-08:00", CultureInfo.InvariantCulture), DateTimeKind.Unspecified), DateTime.UtcNow, DateTime.Now, ]; @@ -97,7 +98,7 @@ public void SelfDiagnosticsEventListener_DateTimeGetBytes() string[] results = new string[datetimes.Length]; for (int i = 0; i < datetimes.Length; i++) { - int len = listener.DateTimeGetBytes(datetimes[i], buffer, pos); + int len = SelfDiagnosticsEventListener.DateTimeGetBytes(datetimes[i], buffer, pos); results[i] = Encoding.Default.GetString(buffer, pos, len); pos += len; } @@ -109,10 +110,10 @@ public void SelfDiagnosticsEventListener_DateTimeGetBytes() public void SelfDiagnosticsEventListener_EmitEvent_OmitAsConfigured() { // Arrange - var configRefresher = new TestSelfDiagnosticsConfigRefresher(); + using var configRefresher = new TestSelfDiagnosticsConfigRefresher(); var memoryMappedFile = MemoryMappedFile.CreateFromFile(LOGFILEPATH, FileMode.Create, null, 1024); Stream stream = memoryMappedFile.CreateViewStream(); - _ = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresher); + using var listener = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresher); // Act: emit an event with severity lower than configured OpenTelemetrySdkEventSource.Log.ActivityStarted("ActivityStart", "123"); @@ -124,7 +125,21 @@ public void SelfDiagnosticsEventListener_EmitEvent_OmitAsConfigured() 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); + + int bytesRead = 0; + int totalBytesRead = 0; + + while (totalBytesRead < buffer.Length) + { + bytesRead = file.Read(buffer, totalBytesRead, buffer.Length - totalBytesRead); + if (bytesRead == 0) + { + break; + } + + totalBytesRead += bytesRead; + } + Assert.Equal('\0', (char)buffer[0]); } @@ -134,8 +149,8 @@ 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); + using var configRefresher = new TestSelfDiagnosticsConfigRefresher(stream); + using var listener = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresher); // Act: emit an event with severity equal to configured OpenTelemetrySdkEventSource.Log.TracerProviderException("TestEvent", "Exception Details"); @@ -177,7 +192,7 @@ public void SelfDiagnosticsEventListener_EncodeInBuffer_EnoughSpace() // '\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"); + byte[] expected = "abc\0"u8.ToArray(); AssertBufferOutput(expected, buffer, startPos, endPos + 1); } @@ -190,7 +205,7 @@ public void SelfDiagnosticsEventListener_EncodeInBuffer_NotEnoughSpaceForFullStr // 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"); + byte[] expected = "ab...\0"u8.ToArray(); AssertBufferOutput(expected, buffer, startPos, endPos + 1); } @@ -200,7 +215,7 @@ public void SelfDiagnosticsEventListener_EncodeInBuffer_NotEvenSpaceForTruncated 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"); + byte[] expected = "...\0"u8.ToArray(); AssertBufferOutput(expected, buffer, startPos, endPos + 1); } @@ -219,7 +234,7 @@ 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"); + byte[] expected = "{abc}\0"u8.ToArray(); AssertBufferOutput(expected, buffer, startPos, endPos + 1); } @@ -229,7 +244,7 @@ public void SelfDiagnosticsEventListener_EncodeInBuffer_IsParameter_NotEnoughSpa 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"); + byte[] expected = "{ab...}\0"u8.ToArray(); AssertBufferOutput(expected, buffer, startPos, endPos + 1); } @@ -239,7 +254,7 @@ public void SelfDiagnosticsEventListener_EncodeInBuffer_IsParameter_NotEvenSpace 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"); + byte[] expected = "{...}\0"u8.ToArray(); AssertBufferOutput(expected, buffer, startPos, endPos + 1); } @@ -256,10 +271,24 @@ 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); + + int bytesRead = 0; + int totalBytesRead = 0; + + while (totalBytesRead < buffer.Length) + { + bytesRead = file.Read(buffer, totalBytesRead, buffer.Length - totalBytesRead); + if (bytesRead == 0) + { + break; + } + + totalBytesRead += bytesRead; + } + + string logLine = Encoding.UTF8.GetString(buffer, 0, totalBytesRead); string logMessage = ParseLogMessage(logLine); - Assert.StartsWith(eventMessage, logMessage); + Assert.StartsWith(eventMessage, logMessage, StringComparison.Ordinal); } private static string ParseLogMessage(string logLine) diff --git a/test/OpenTelemetry.Tests/Internal/WildcardHelperTests.cs b/test/OpenTelemetry.Tests/Internal/WildcardHelperTests.cs index 9b079580b87..847f483fd1f 100644 --- a/test/OpenTelemetry.Tests/Internal/WildcardHelperTests.cs +++ b/test/OpenTelemetry.Tests/Internal/WildcardHelperTests.cs @@ -30,7 +30,7 @@ public void WildcardRegex_ShouldMatch(string[] patterns, string matchWith, bool [InlineData("a", false)] [InlineData("a.*", true)] [InlineData("a.?", true)] - public void Verify_ContainsWildcard(string pattern, bool expected) + public void Verify_ContainsWildcard(string? pattern, bool expected) { Assert.Equal(expected, WildcardHelper.ContainsWildcard(pattern)); } diff --git a/test/OpenTelemetry.Tests/LoggerExtensions.cs b/test/OpenTelemetry.Tests/LoggerExtensions.cs new file mode 100644 index 00000000000..210a2c77762 --- /dev/null +++ b/test/OpenTelemetry.Tests/LoggerExtensions.cs @@ -0,0 +1,51 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.Logging; + +namespace OpenTelemetry.Tests; + +internal static partial class LoggerExtensions +{ + [LoggerMessage(LogLevel.Information, "Hello world {Data}")] + public static partial void HelloWorld(this ILogger logger, int data); + + [LoggerMessage(LogLevel.Information, "Hello world {Data}")] + public static partial void HelloWorld(this ILogger logger, string data); + + [LoggerMessage(LogLevel.Information, "Hello, World!")] + public static partial void HelloWorld(this ILogger logger); + + [LoggerMessage(LogLevel.Information, "Hello from {Name} {Price}.")] + public static partial void HelloFrom(this ILogger logger, string name, double price); + + [LoggerMessage(LogLevel.Information, "{Food}")] + public static partial void Food(this ILogger logger, object food); + + [LoggerMessage(LogLevel.Information, "Log")] + public static partial void Log(this ILogger logger); + + [LoggerMessage("Log")] + public static partial void Log(this ILogger logger, LogLevel logLevel); + + [LoggerMessage(LogLevel.Information, "Log within a dropped activity")] + public static partial void LogWithinADroppedActivity(this ILogger logger); + + [LoggerMessage(LogLevel.Information, "Log within activity marked as RecordOnly")] + public static partial void LogWithinRecordOnlyActivity(this ILogger logger); + + [LoggerMessage(LogLevel.Information, "Log within activity marked as RecordAndSample")] + public static partial void LogWithinRecordAndSampleActivity(this ILogger logger); + + [LoggerMessage(LogLevel.Information, "Dispose called")] + public static partial void DisposedCalled(this ILogger logger); + + [LoggerMessage(LogLevel.Information, "{Product} {Year}!")] + public static partial void LogProduct(this ILogger logger, string product, int year); + + [LoggerMessage(LogLevel.Information, "{Product} {Year} {Complex}!")] + public static partial void LogProduct(this ILogger logger, string product, int year, object complex); + + [LoggerMessage(LogLevel.Information, "Exception Occurred")] + public static partial void LogException(this ILogger logger, Exception exception); +} diff --git a/test/OpenTelemetry.Tests/Logs/BatchExportLogRecordProcessorOptionsTest.cs b/test/OpenTelemetry.Tests/Logs/BatchExportLogRecordProcessorOptionsTests.cs similarity index 96% rename from test/OpenTelemetry.Tests/Logs/BatchExportLogRecordProcessorOptionsTest.cs rename to test/OpenTelemetry.Tests/Logs/BatchExportLogRecordProcessorOptionsTests.cs index 2030e402c24..f9cd6d840c3 100644 --- a/test/OpenTelemetry.Tests/Logs/BatchExportLogRecordProcessorOptionsTest.cs +++ b/test/OpenTelemetry.Tests/Logs/BatchExportLogRecordProcessorOptionsTests.cs @@ -1,16 +1,14 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using Microsoft.Extensions.Configuration; using Xunit; namespace OpenTelemetry.Logs.Tests; -public sealed class BatchExportLogRecordProcessorOptionsTest : IDisposable +public sealed class BatchExportLogRecordProcessorOptionsTests : IDisposable { - public BatchExportLogRecordProcessorOptionsTest() + public BatchExportLogRecordProcessorOptionsTests() { ClearEnvVars(); } diff --git a/test/OpenTelemetry.Tests/Logs/BatchLogRecordExportProcessorTests.cs b/test/OpenTelemetry.Tests/Logs/BatchLogRecordExportProcessorTests.cs index d285302fcfd..cd76a4c8880 100644 --- a/test/OpenTelemetry.Tests/Logs/BatchLogRecordExportProcessorTests.cs +++ b/test/OpenTelemetry.Tests/Logs/BatchLogRecordExportProcessorTests.cs @@ -18,7 +18,9 @@ public void StateValuesAndScopeBufferingTest() List exportedItems = new(); using var processor = new BatchLogRecordExportProcessor( +#pragma warning disable CA2000 // Dispose objects before losing scope new InMemoryExporter(exportedItems), +#pragma warning restore CA2000 // Dispose objects before losing scope scheduledDelayMilliseconds: int.MaxValue); using var scope = scopeProvider.Push(exportedItems); @@ -27,7 +29,7 @@ public void StateValuesAndScopeBufferingTest() var logRecord = pool.Rent(); - var state = new LogRecordTest.DisposingState("Hello world"); + var state = new LogRecordTests.DisposingState("Hello world"); logRecord.ILoggerData.ScopeProvider = scopeProvider; logRecord.StateValues = state; @@ -43,20 +45,23 @@ public void StateValuesAndScopeBufferingTest() Assert.NotNull(logRecord.AttributeStorage); Assert.NotNull(logRecord.ILoggerData.BufferedScopes); - KeyValuePair actualState = logRecord.StateValues[0]; + KeyValuePair actualState = logRecord.StateValues[0]; Assert.Same("Value", actualState.Key); Assert.Same("Hello world", actualState.Value); + int scopeCount = 0; bool foundScope = false; - logRecord.ForEachScope( + logRecord.ForEachScope( (s, o) => { foundScope = ReferenceEquals(s.Scope, exportedItems); + scopeCount++; }, null); + Assert.Equal(1, scopeCount); Assert.True(foundScope); processor.Shutdown(); @@ -75,13 +80,15 @@ public void StateBufferingTest() List exportedItems = new(); using var processor = new BatchLogRecordExportProcessor( +#pragma warning disable CA2000 // Dispose objects before losing scope new InMemoryExporter(exportedItems)); +#pragma warning restore CA2000 // Dispose objects before losing scope var pool = LogRecordSharedPool.Current; var logRecord = pool.Rent(); - var state = new LogRecordTest.DisposingState("Hello world"); + var state = new LogRecordTests.DisposingState("Hello world"); logRecord.State = state; processor.OnEnd(logRecord); @@ -108,7 +115,9 @@ public void CopyMadeWhenLogRecordIsFromThreadStaticPoolTest() List exportedItems = new(); using var processor = new BatchLogRecordExportProcessor( +#pragma warning disable CA2000 // Dispose objects before losing scope new InMemoryExporter(exportedItems)); +#pragma warning restore CA2000 // Dispose objects before losing scope var pool = LogRecordThreadStaticPool.Instance; @@ -127,7 +136,9 @@ public void LogRecordAddedToBatchIfNotFromAnyPoolTest() List exportedItems = new(); using var processor = new BatchLogRecordExportProcessor( +#pragma warning disable CA2000 // Dispose objects before losing scope new InMemoryExporter(exportedItems)); +#pragma warning restore CA2000 // Dispose objects before losing scope var logRecord = new LogRecord(); diff --git a/test/OpenTelemetry.Tests/Logs/LogRecordSharedPoolTests.cs b/test/OpenTelemetry.Tests/Logs/LogRecordSharedPoolTests.cs index d4be69397b1..a354081dc9f 100644 --- a/test/OpenTelemetry.Tests/Logs/LogRecordSharedPoolTests.cs +++ b/test/OpenTelemetry.Tests/Logs/LogRecordSharedPoolTests.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using Xunit; namespace OpenTelemetry.Logs.Tests; @@ -120,8 +118,8 @@ public void ClearTests() var logRecord1 = pool.Rent(); logRecord1.AttributeStorage = new List>(16) { - new KeyValuePair("key1", "value1"), - new KeyValuePair("key2", "value2"), + new("key1", "value1"), + new("key2", "value2"), }; logRecord1.ScopeStorage = new List(8) { null, null }; @@ -170,9 +168,11 @@ public async Task ExportTest(bool warmup) } } +#pragma warning disable CA2000 // Dispose objects before losing scope using BatchLogRecordExportProcessor processor = new(new NoopExporter()); +#pragma warning restore CA2000 // Dispose objects before losing scope - List tasks = new(); + List tasks = []; for (int i = 0; i < Environment.ProcessorCount; i++) { @@ -180,7 +180,9 @@ public async Task ExportTest(bool warmup) { Random random = new Random(); +#pragma warning disable CA5394 // Do not use insecure randomness await Task.Delay(random.Next(100, 150)); +#pragma warning restore CA5394 // Do not use insecure randomness for (int i = 0; i < 1000; i++) { @@ -191,7 +193,9 @@ public async Task ExportTest(bool warmup) // This should no-op mostly. pool.Return(logRecord); +#pragma warning disable CA5394 // Do not use insecure randomness await Task.Delay(random.Next(0, 20)); +#pragma warning restore CA5394 // Do not use insecure randomness } })); } @@ -232,7 +236,7 @@ public async Task DeadlockTest() var pool = LogRecordSharedPool.Current; - List tasks = new(); + List tasks = []; for (int i = 0; i < Environment.ProcessorCount; i++) { diff --git a/test/OpenTelemetry.Tests/Logs/LogRecordStateProcessorTests.cs b/test/OpenTelemetry.Tests/Logs/LogRecordStateProcessorTests.cs index 24acd8869a7..ff4badcdeaa 100644 --- a/test/OpenTelemetry.Tests/Logs/LogRecordStateProcessorTests.cs +++ b/test/OpenTelemetry.Tests/Logs/LogRecordStateProcessorTests.cs @@ -1,10 +1,9 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; +using OpenTelemetry.Tests; using Xunit; namespace OpenTelemetry.Logs.Tests; @@ -24,7 +23,7 @@ public void LogProcessorSetStateTest(bool includeAttributes, bool parseStateValu { var logger = loggerFactory.CreateLogger("TestLogger"); - logger.LogInformation("Hello world {data}", 1234); + logger.HelloWorld(1234); } Assert.Single(exportedItems); @@ -65,7 +64,7 @@ public void LogProcessorSetStateToUnsupportedTypeTest(bool includeAttributes, bo { var logger = loggerFactory.CreateLogger("TestLogger"); - logger.LogInformation("Hello world {data}", 1234); + logger.HelloWorld(1234); } Assert.Single(exportedItems); @@ -105,7 +104,7 @@ public void LogProcessorSetAttributesTest(bool includeAttributes, bool parseStat { var logger = loggerFactory.CreateLogger("TestLogger"); - logger.LogInformation("Hello world {data}", 1234); + logger.HelloWorld(1234); } Assert.Single(exportedItems); @@ -150,7 +149,7 @@ public void LogProcessorSetAttributesAndStateMixedTest(bool includeAttributes, b { var logger = loggerFactory.CreateLogger("TestLogger"); - logger.LogInformation("Hello world {data}", 1234); + logger.HelloWorld(1234); } Assert.Single(exportedItems); @@ -227,7 +226,7 @@ private static void AssertStateAndAttributes( else { Assert.Null(state); - state = Array.Empty>(); + state = []; } if (attributesExpectedCount > 0) @@ -238,7 +237,7 @@ private static void AssertStateAndAttributes( else { Assert.Null(attributes); - attributes = Array.Empty>(); + attributes = []; } } diff --git a/test/OpenTelemetry.Tests/Logs/LogRecordTest.cs b/test/OpenTelemetry.Tests/Logs/LogRecordTests.cs similarity index 75% rename from test/OpenTelemetry.Tests/Logs/LogRecordTest.cs rename to test/OpenTelemetry.Tests/Logs/LogRecordTests.cs index 138af7ac695..ace0fce475f 100644 --- a/test/OpenTelemetry.Tests/Logs/LogRecordTest.cs +++ b/test/OpenTelemetry.Tests/Logs/LogRecordTests.cs @@ -6,14 +6,13 @@ using System.Globalization; using Microsoft.Extensions.Logging; using OpenTelemetry.Exporter; -using OpenTelemetry.Logs; using OpenTelemetry.Tests; using OpenTelemetry.Trace; using Xunit; namespace OpenTelemetry.Logs.Tests; -public sealed class LogRecordTest +public sealed class LogRecordTests { private enum Field { @@ -25,13 +24,13 @@ private enum Field [Fact] public void CheckCategoryNameForLog() { - using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null); - var logger = loggerFactory.CreateLogger(); + using var loggerFactory = InitializeLoggerFactory(out List exportedItems); + var logger = loggerFactory.CreateLogger(); - logger.LogInformation("Log"); + logger.Log(); var categoryName = exportedItems[0].CategoryName; - Assert.Equal(typeof(LogRecordTest).FullName, categoryName); + Assert.Equal(typeof(LogRecordTests).FullName, categoryName); } [Theory] @@ -44,10 +43,9 @@ public void CheckCategoryNameForLog() public void CheckLogLevel(LogLevel logLevel) { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); - const string message = "Log {logLevel}"; - logger.Log(logLevel, message, logLevel); + logger.Log(logLevel); var logLevelRecorded = exportedItems[0].LogLevel; Assert.Equal(logLevel, logLevelRecorded); @@ -59,10 +57,9 @@ public void CheckLogLevel(LogLevel logLevel) public void CheckStateForUnstructuredLog(bool includeFormattedMessage) { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: o => o.IncludeFormattedMessage = includeFormattedMessage); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); - const string message = "Hello, World!"; - logger.LogInformation(message); + logger.HelloWorld(); Assert.NotNull(exportedItems[0].State); @@ -72,10 +69,10 @@ public void CheckStateForUnstructuredLog(bool includeFormattedMessage) // state only has {OriginalFormat} Assert.Single(attributes); - Assert.Equal(message, exportedItems[0].Body); + Assert.Equal("Hello, World!", exportedItems[0].Body); if (includeFormattedMessage) { - Assert.Equal(message, exportedItems[0].FormattedMessage); + Assert.Equal("Hello, World!", exportedItems[0].FormattedMessage); } else { @@ -89,11 +86,13 @@ public void CheckStateForUnstructuredLog(bool includeFormattedMessage) public void CheckStateForUnstructuredLogWithStringInterpolation(bool includeFormattedMessage) { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: o => o.IncludeFormattedMessage = includeFormattedMessage); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); #pragma warning disable CA2254 // Template should be a static expression +#pragma warning disable CA1848 // Use the LoggerMessage delegates var message = $"Hello from potato {0.99}."; logger.LogInformation(message); +#pragma warning restore CA1848 // Use the LoggerMessage delegates #pragma warning restore CA2254 // Template should be a static expression Assert.NotNull(exportedItems[0].State); @@ -121,10 +120,9 @@ public void CheckStateForUnstructuredLogWithStringInterpolation(bool includeForm public void CheckStateForStructuredLogWithTemplate(bool includeFormattedMessage) { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: o => o.IncludeFormattedMessage = includeFormattedMessage); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); - const string message = "Hello from {name} {price}."; - logger.LogInformation(message, "tomato", 2.99); + logger.HelloFrom("tomato", 2.99); Assert.NotNull(exportedItems[0].State); @@ -135,18 +133,18 @@ public void CheckStateForStructuredLogWithTemplate(bool includeFormattedMessage) Assert.Equal(3, attributes.Count); // Check if state has name - Assert.Contains(attributes, item => item.Key == "name"); - Assert.Equal("tomato", attributes.First(item => item.Key == "name").Value); + Assert.Contains(attributes, item => item.Key == "Name"); + Assert.Equal("tomato", attributes.First(item => item.Key == "Name").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.Contains(attributes, item => item.Key == "Price"); + Assert.Equal(2.99, attributes.First(item => item.Key == "Price").Value); // Check if state has OriginalFormat Assert.Contains(attributes, item => item.Key == "{OriginalFormat}"); - Assert.Equal(message, attributes.First(item => item.Key == "{OriginalFormat}").Value); + Assert.Equal("Hello from {Name} {Price}.", attributes.First(item => item.Key == "{OriginalFormat}").Value); - Assert.Equal(message, exportedItems[0].Body); + Assert.Equal("Hello from {Name} {Price}.", exportedItems[0].Body); if (includeFormattedMessage) { Assert.Equal("Hello from tomato 2.99.", exportedItems[0].FormattedMessage); @@ -163,10 +161,10 @@ public void CheckStateForStructuredLogWithTemplate(bool includeFormattedMessage) public void CheckStateForStructuredLogWithStrongType(bool includeFormattedMessage) { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: o => o.IncludeFormattedMessage = includeFormattedMessage); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); var food = new Food { Name = "artichoke", Price = 3.99 }; - logger.LogInformation("{food}", food); + logger.Food(food); Assert.NotNull(exportedItems[0].State); @@ -177,17 +175,18 @@ public void CheckStateForStructuredLogWithStrongType(bool includeFormattedMessag Assert.Equal(2, attributes.Count); // Check if state has food - Assert.Contains(attributes, item => item.Key == "food"); + Assert.Contains(attributes, item => item.Key == "Food"); - var foodParameter = (Food)attributes.First(item => item.Key == "food").Value; - Assert.Equal(food.Name, foodParameter.Name); - Assert.Equal(food.Price, foodParameter.Price); + var foodParameter = attributes.First(item => item.Key == "Food").Value as Food?; + Assert.NotNull(foodParameter); + Assert.Equal(food.Name, foodParameter.Value.Name); + Assert.Equal(food.Price, foodParameter.Value.Price); // 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}", attributes.First(item => item.Key == "{OriginalFormat}").Value); - Assert.Equal("{food}", exportedItems[0].Body); + Assert.Equal("{Food}", exportedItems[0].Body); if (includeFormattedMessage) { Assert.Equal(food.ToString(), exportedItems[0].FormattedMessage); @@ -204,10 +203,10 @@ public void CheckStateForStructuredLogWithStrongType(bool includeFormattedMessag public void CheckStateForStructuredLogWithAnonymousType(bool includeFormattedMessage) { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: o => o.IncludeFormattedMessage = includeFormattedMessage); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); var anonymousType = new { Name = "pumpkin", Price = 5.99 }; - logger.LogInformation("{food}", anonymousType); + logger.Food(anonymousType); Assert.NotNull(exportedItems[0].State); @@ -218,17 +217,18 @@ public void CheckStateForStructuredLogWithAnonymousType(bool includeFormattedMes Assert.Equal(2, attributes.Count); // Check if state has food - Assert.Contains(attributes, item => item.Key == "food"); + Assert.Contains(attributes, item => item.Key == "Food"); - var foodParameter = attributes.First(item => item.Key == "food").Value as dynamic; - Assert.Equal(anonymousType.Name, foodParameter.Name); - Assert.Equal(anonymousType.Price, foodParameter.Price); + var foodParameter = attributes.First(item => item.Key == "Food").Value as dynamic; + Assert.NotNull(foodParameter); + Assert.Equal(anonymousType.Name, foodParameter!.Name); + Assert.Equal(anonymousType.Price, foodParameter!.Price); // 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}", attributes.First(item => item.Key == "{OriginalFormat}").Value); - Assert.Equal("{food}", exportedItems[0].Body); + Assert.Equal("{Food}", exportedItems[0].Body); if (includeFormattedMessage) { Assert.Equal(anonymousType.ToString(), exportedItems[0].FormattedMessage); @@ -245,14 +245,15 @@ public void CheckStateForStructuredLogWithAnonymousType(bool includeFormattedMes public void CheckStateForStructuredLogWithGeneralType(bool includeFormattedMessage) { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: o => o.IncludeFormattedMessage = includeFormattedMessage); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); + var trufflePrice = 299.99; var food = new Dictionary { ["Name"] = "truffle", - ["Price"] = 299.99, + ["Price"] = trufflePrice, }; - logger.LogInformation("{food}", food); + logger.Food(food); Assert.NotNull(exportedItems[0].State); @@ -263,28 +264,21 @@ public void CheckStateForStructuredLogWithGeneralType(bool includeFormattedMessa Assert.Equal(2, attributes.Count); // Check if state has food - Assert.Contains(attributes, item => item.Key == "food"); + Assert.Contains(attributes, item => item.Key == "Food"); - var foodParameter = attributes.First(item => item.Key == "food").Value as Dictionary; + var foodParameter = attributes.First(item => item.Key == "Food").Value as Dictionary; + Assert.NotNull(foodParameter); 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); + Assert.Equal("{Food}", attributes.First(item => item.Key == "{OriginalFormat}").Value); - Assert.Equal("{food}", exportedItems[0].Body); + Assert.Equal("{Food}", exportedItems[0].Body); if (includeFormattedMessage) { - var prevCulture = CultureInfo.CurrentCulture; - CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; - try - { - Assert.Equal("[Name, truffle], [Price, 299.99]", exportedItems[0].FormattedMessage); - } - finally - { - CultureInfo.CurrentCulture = prevCulture; - } + var priceInCurrentCulture = trufflePrice.ToString(CultureInfo.CurrentCulture); + Assert.Equal($"[Name, truffle], [Price, {priceInCurrentCulture}]", exportedItems[0].FormattedMessage); } else { @@ -296,18 +290,18 @@ public void CheckStateForStructuredLogWithGeneralType(bool includeFormattedMessa public void CheckStateForExceptionLogged() { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); var exceptionMessage = "Exception Message"; - var exception = new Exception(exceptionMessage); + var exception = new InvalidOperationException(exceptionMessage); - const string message = "Exception Occurred"; - logger.LogInformation(exception, message); + logger.LogException(exception); Assert.NotNull(exportedItems[0].State); - var state = exportedItems[0].State; - var itemCount = state.GetType().GetProperty("Count").GetValue(state); + var state = exportedItems[0].State as IReadOnlyList>; + Assert.NotNull(state); + var itemCount = state.Count; // state only has {OriginalFormat} Assert.Equal(1, itemCount); @@ -322,8 +316,8 @@ public void CheckStateForExceptionLogged() Assert.NotNull(loggedException); Assert.Equal(exceptionMessage, loggedException.Message); - Assert.Equal(message, exportedItems[0].Body); - Assert.Equal(message, state.ToString()); + Assert.Equal("Exception Occurred", exportedItems[0].Body); + Assert.Equal("Exception Occurred", state.ToString()); Assert.Null(exportedItems[0].FormattedMessage); } @@ -331,9 +325,9 @@ public void CheckStateForExceptionLogged() public void CheckStateCanBeSet() { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); - logger.LogInformation("This does not matter."); + logger.Log(); var logRecord = exportedItems[0]; logRecord.State = "newState"; @@ -346,17 +340,17 @@ public void CheckStateCanBeSet() public void CheckStateValuesCanBeSet() { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.ParseStateValues = true); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); logger.Log( LogLevel.Information, 0, - new List> { new KeyValuePair("Key1", "Value1") }, + new List> { new("Key1", "Value1") }, null, (s, e) => "OpenTelemetry!"); var logRecord = exportedItems[0]; - var expectedStateValues = new List> { new KeyValuePair("Key2", "Value2") }; + var expectedStateValues = new List> { new("Key2", "Value2") }; logRecord.StateValues = expectedStateValues; Assert.Equal(expectedStateValues, logRecord.StateValues); @@ -366,9 +360,9 @@ public void CheckStateValuesCanBeSet() public void CheckFormattedMessageCanBeSet() { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.IncludeFormattedMessage = true); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); - logger.LogInformation("OpenTelemetry {Greeting} {Subject}!", "Hello", "World"); + logger.HelloFrom("tomato", 3.0); var logRecord = exportedItems[0]; var expectedFormattedMessage = "OpenTelemetry Good Night!"; logRecord.FormattedMessage = expectedFormattedMessage; @@ -380,7 +374,9 @@ public void CheckFormattedMessageCanBeSet() public void CheckStateCanBeSetByProcessor() { var exportedItems = new List(); +#pragma warning disable CA2000 // Dispose objects before losing scope var exporter = new InMemoryExporter(exportedItems); +#pragma warning restore CA2000 // Dispose objects before losing scope using var loggerFactory = LoggerFactory.Create(builder => { builder.AddOpenTelemetry(options => @@ -390,10 +386,11 @@ public void CheckStateCanBeSetByProcessor() }); }); - var logger = loggerFactory.CreateLogger(); - logger.LogInformation($"This does not matter."); + var logger = loggerFactory.CreateLogger(); + logger.Log(); var state = exportedItems[0].State as IReadOnlyList>; + Assert.NotNull(state); Assert.Equal("newStateKey", state[0].Key.ToString()); Assert.Equal("newStateValue", state[0].Value.ToString()); } @@ -402,7 +399,6 @@ public void CheckStateCanBeSetByProcessor() public void CheckStateValuesCanBeSetByProcessor() { var exportedItems = new List(); - var exporter = new InMemoryExporter(exportedItems); using var loggerFactory = LoggerFactory.Create(builder => { builder.AddOpenTelemetry(options => @@ -413,18 +409,19 @@ public void CheckStateValuesCanBeSetByProcessor() }); }); - var logger = loggerFactory.CreateLogger(); - logger.LogInformation("This does not matter."); + var logger = loggerFactory.CreateLogger(); + logger.Log(); var stateValue = exportedItems[0]; - Assert.Equal(new KeyValuePair("newStateValueKey", "newStateValueValue"), stateValue.StateValues[0]); + Assert.NotNull(stateValue.StateValues); + Assert.NotEmpty(stateValue.StateValues); + Assert.Equal(new KeyValuePair("newStateValueKey", "newStateValueValue"), stateValue.StateValues[0]); } [Fact] public void CheckFormattedMessageCanBeSetByProcessor() { var exportedItems = new List(); - var exporter = new InMemoryExporter(exportedItems); using var loggerFactory = LoggerFactory.Create(builder => { builder.AddOpenTelemetry(options => @@ -435,8 +432,8 @@ public void CheckFormattedMessageCanBeSetByProcessor() }); }); - var logger = loggerFactory.CreateLogger(); - logger.LogInformation("OpenTelemetry {Greeting} {Subject}!", "Hello", "World"); + var logger = loggerFactory.CreateLogger(); + logger.HelloFrom("potato", 2.99); var item = exportedItems[0]; Assert.Equal("OpenTelemetry Good Night!", item.FormattedMessage); @@ -446,9 +443,9 @@ public void CheckFormattedMessageCanBeSetByProcessor() public void CheckTraceIdForLogWithinDroppedActivity() { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); - logger.LogInformation("Log within a dropped activity"); + logger.LogWithinADroppedActivity(); var logRecord = exportedItems[0]; Assert.Null(Activity.Current); @@ -463,7 +460,7 @@ public void CheckTraceIdForLogWithinDroppedActivity() public void CheckTraceIdForLogWithinActivityMarkedAsRecordOnly(bool includeTraceState) { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: o => o.IncludeTraceState = includeTraceState); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); var sampler = new RecordOnlySampler(); var exportedActivityList = new List(); @@ -476,13 +473,14 @@ public void CheckTraceIdForLogWithinActivityMarkedAsRecordOnly(bool includeTrace .Build(); using var activity = activitySource.StartActivity("Activity"); + Assert.NotNull(activity); activity.TraceStateString = "key1=value1"; - logger.LogInformation("Log within activity marked as RecordOnly"); + logger.LogWithinRecordOnlyActivity(); var logRecord = exportedItems[0]; var currentActivity = Activity.Current; - Assert.NotNull(Activity.Current); + Assert.NotNull(currentActivity); Assert.Equal(currentActivity.TraceId, logRecord.TraceId); Assert.Equal(currentActivity.SpanId, logRecord.SpanId); Assert.Equal(currentActivity.ActivityTraceFlags, logRecord.TraceFlags); @@ -501,7 +499,7 @@ public void CheckTraceIdForLogWithinActivityMarkedAsRecordOnly(bool includeTrace public void CheckTraceIdForLogWithinActivityMarkedAsRecordAndSample() { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); var sampler = new AlwaysOnSampler(); var exportedActivityList = new List(); @@ -515,11 +513,11 @@ public void CheckTraceIdForLogWithinActivityMarkedAsRecordAndSample() using var activity = activitySource.StartActivity("Activity"); - logger.LogInformation("Log within activity marked as RecordAndSample"); + logger.LogWithinRecordAndSampleActivity(); var logRecord = exportedItems[0]; var currentActivity = Activity.Current; - Assert.NotNull(Activity.Current); + Assert.NotNull(currentActivity); Assert.Equal(currentActivity.TraceId, logRecord.TraceId); Assert.Equal(currentActivity.SpanId, logRecord.SpanId); Assert.Equal(currentActivity.ActivityTraceFlags, logRecord.TraceFlags); @@ -529,9 +527,9 @@ public void CheckTraceIdForLogWithinActivityMarkedAsRecordAndSample() public void VerifyIncludeFormattedMessage_False() { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.IncludeFormattedMessage = false); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); - logger.LogInformation("OpenTelemetry!"); + logger.Log(); var logRecord = exportedItems[0]; Assert.Null(logRecord.FormattedMessage); } @@ -540,29 +538,29 @@ public void VerifyIncludeFormattedMessage_False() public void VerifyIncludeFormattedMessage_True() { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.IncludeFormattedMessage = true); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); - logger.LogInformation("OpenTelemetry!"); + logger.Log(); var logRecord = exportedItems[0]; - Assert.Equal("OpenTelemetry!", logRecord.FormattedMessage); + Assert.Equal("Log", logRecord.FormattedMessage); - logger.LogInformation("OpenTelemetry {Greeting} {Subject}!", "Hello", "World"); + logger.HelloFrom("tomato", 3.11); logRecord = exportedItems[1]; - Assert.Equal("OpenTelemetry Hello World!", logRecord.FormattedMessage); + Assert.Equal("Hello from tomato 3.11.", logRecord.FormattedMessage); } [Fact] public void IncludeFormattedMessageTestWhenFormatterNull() { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.IncludeFormattedMessage = true); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); - logger.Log(LogLevel.Information, default, "Hello World!", null, null); + 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); + logger.Log(LogLevel.Information, default, new CustomState(), null, null!); logRecord = exportedItems[1]; Assert.Equal(CustomState.ToStringValue, logRecord.FormattedMessage); Assert.Equal(CustomState.ToStringValue, logRecord.Body); @@ -578,15 +576,15 @@ public void IncludeFormattedMessageTestWhenFormatterNull() public void VerifyIncludeScopes_False() { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.IncludeScopes = false); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); using var scope = logger.BeginScope("string_scope"); - logger.LogInformation("OpenTelemetry!"); + logger.Log(); var logRecord = exportedItems[0]; - List scopes = new List(); - logRecord.ForEachScope((scope, state) => scopes.Add(scope.Scope), null); + List scopes = []; + logRecord.ForEachScope((scope, state) => scopes.Add(scope.Scope), null); Assert.Empty(scopes); } @@ -594,25 +592,25 @@ public void VerifyIncludeScopes_False() public void VerifyIncludeScopes_True() { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.IncludeScopes = true); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); using var scope = logger.BeginScope("string_scope"); - logger.LogInformation("OpenTelemetry!"); + logger.Log(); var logRecord = exportedItems[0]; - List scopes = new List(); + List scopes = []; - logger.LogInformation("OpenTelemetry!"); + logger.Log(); logRecord = exportedItems[1]; int reachedDepth = -1; - logRecord.ForEachScope( + logRecord.ForEachScope( (scope, state) => { reachedDepth++; scopes.Add(scope.Scope); - foreach (KeyValuePair item in scope) + foreach (KeyValuePair item in scope) { Assert.Equal(string.Empty, item.Key); Assert.Equal("string_scope", item.Value); @@ -625,24 +623,24 @@ public void VerifyIncludeScopes_True() scopes.Clear(); - List> expectedScope2 = new List> - { - new KeyValuePair("item1", "value1"), - new KeyValuePair("item2", "value2"), - }; + List> expectedScope2 = + [ + new KeyValuePair("item1", "value1"), + new KeyValuePair("item2", "value2"), + ]; using var scope2 = logger.BeginScope(expectedScope2); - logger.LogInformation("OpenTelemetry!"); + logger.Log(); logRecord = exportedItems[2]; reachedDepth = -1; - logRecord.ForEachScope( + logRecord.ForEachScope( (scope, state) => { scopes.Add(scope.Scope); if (reachedDepth++ == 1) { - foreach (KeyValuePair item in scope) + foreach (KeyValuePair item in scope) { Assert.Contains(item, expectedScope2); } @@ -656,24 +654,24 @@ public void VerifyIncludeScopes_True() scopes.Clear(); - KeyValuePair[] expectedScope3 = new KeyValuePair[] - { - new KeyValuePair("item3", "value3"), - new KeyValuePair("item4", "value4"), - }; + KeyValuePair[] expectedScope3 = + [ + new KeyValuePair("item3", "value3"), + new KeyValuePair("item4", "value4"), + ]; using var scope3 = logger.BeginScope(expectedScope3); - logger.LogInformation("OpenTelemetry!"); + logger.Log(); logRecord = exportedItems[3]; reachedDepth = -1; - logRecord.ForEachScope( + logRecord.ForEachScope( (scope, state) => { scopes.Add(scope.Scope); if (reachedDepth++ == 2) { - foreach (KeyValuePair item in scope) + foreach (KeyValuePair item in scope) { Assert.Contains(item, expectedScope3); } @@ -693,11 +691,11 @@ public void VerifyIncludeScopes_True() public void VerifyParseStateValues_UsingStandardExtensions(bool parseStateValues) { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.ParseStateValues = parseStateValues); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); // Tests state parsing with standard extensions. - logger.LogInformation("{Product} {Year}!", "OpenTelemetry", 2021); + logger.LogProduct("OpenTelemetry", 2021); var logRecord = exportedItems[0]; Assert.NotNull(logRecord.StateValues); @@ -712,13 +710,13 @@ public void VerifyParseStateValues_UsingStandardExtensions(bool parseStateValues 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]); + 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" }; - logger.LogInformation("{Product} {Year} {Complex}!", "OpenTelemetry", 2021, complex); + logger.LogProduct("OpenTelemetry", 2021, complex); logRecord = exportedItems[1]; Assert.NotNull(logRecord.StateValues); @@ -733,11 +731,11 @@ public void VerifyParseStateValues_UsingStandardExtensions(bool parseStateValues 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]); + 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]); - KeyValuePair actualComplex = logRecord.StateValues[2]; + KeyValuePair actualComplex = logRecord.StateValues[2]; Assert.Equal("Complex", actualComplex.Key); Assert.Same(complex, actualComplex.Value); } @@ -746,7 +744,7 @@ public void VerifyParseStateValues_UsingStandardExtensions(bool parseStateValues public void ParseStateValuesUsingStructTest() { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.ParseStateValues = true); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); // Tests struct IReadOnlyList> parse path. @@ -761,21 +759,21 @@ public void ParseStateValuesUsingStructTest() Assert.Null(logRecord.State); Assert.NotNull(logRecord.StateValues); Assert.Single(logRecord.StateValues); - Assert.Equal(new KeyValuePair("Key1", "Value1"), logRecord.StateValues[0]); + Assert.Equal(new KeyValuePair("Key1", "Value1"), logRecord.StateValues[0]); } [Fact] public void ParseStateValuesUsingListTest() { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.ParseStateValues = true); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); // Tests ref IReadOnlyList> parse path. logger.Log( LogLevel.Information, 0, - new List> { new KeyValuePair("Key1", "Value1") }, + new List> { new("Key1", "Value1") }, null, (s, e) => "OpenTelemetry!"); var logRecord = exportedItems[0]; @@ -783,14 +781,14 @@ public void ParseStateValuesUsingListTest() Assert.Null(logRecord.State); Assert.NotNull(logRecord.StateValues); Assert.Single(logRecord.StateValues); - Assert.Equal(new KeyValuePair("Key1", "Value1"), logRecord.StateValues[0]); + 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(); + var logger = loggerFactory.CreateLogger(); // Tests IEnumerable> parse path. @@ -805,14 +803,14 @@ public void ParseStateValuesUsingIEnumerableTest() Assert.Null(logRecord.State); Assert.NotNull(logRecord.StateValues); Assert.Single(logRecord.StateValues); - Assert.Equal(new KeyValuePair("Key1", "Value1"), logRecord.StateValues[0]); + Assert.Equal(new KeyValuePair("Key1", "Value1"), logRecord.StateValues[0]); } [Fact] public void ParseStateValuesUsingNonconformingCustomTypeTest() { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.ParseStateValues = true); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); // Tests unknown state parse path. @@ -842,7 +840,7 @@ public void ParseStateValuesUsingNonconformingCustomTypeTest() public void DisposingStateTest() { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.ParseStateValues = true); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); DisposingState state = new DisposingState("Hello world"); @@ -860,7 +858,7 @@ public void DisposingStateTest() Assert.NotNull(logRecord.StateValues); Assert.Single(logRecord.StateValues); - KeyValuePair actualState = logRecord.StateValues[0]; + KeyValuePair actualState = logRecord.StateValues[0]; Assert.Same("Value", actualState.Key); Assert.Same("Hello world", actualState.Value); @@ -871,7 +869,9 @@ public void DisposingStateTest() [InlineData(false)] public void ReusedLogRecordScopeTest(bool buffer) { +#pragma warning disable CA2000 // Dispose objects before losing scope var processor = new ScopeProcessor(buffer); +#pragma warning restore CA2000 // Dispose objects before losing scope using var loggerFactory = LoggerFactory.Create(builder => { @@ -886,12 +886,12 @@ public void ReusedLogRecordScopeTest(bool buffer) using (var scope1 = logger.BeginScope("scope1")) { - logger.LogInformation("message1"); + logger.Log(); } using (var scope2 = logger.BeginScope("scope2")) { - logger.LogInformation("message2"); + logger.HelloWorld(); } Assert.Equal(2, processor.Scopes.Count); @@ -907,16 +907,16 @@ public void IncludeStateTest() { options.IncludeAttributes = false; }); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); - logger.LogInformation("Hello {world}", "earth"); + logger.HelloWorld("Earth"); var logRecord = exportedItems[0]; Assert.Null(logRecord.State); Assert.Null(logRecord.Attributes); - Assert.Equal("Hello earth", logRecord.Body); + Assert.Equal("Hello world Earth", logRecord.Body); } [Theory] @@ -971,15 +971,15 @@ public void LogRecordInstrumentationScopeTest() { using var loggerFactory = InitializeLoggerFactory(out List exportedItems); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); - logger.LogInformation("Hello world!"); + logger.HelloWorld(); var logRecord = exportedItems.FirstOrDefault(); Assert.NotNull(logRecord); Assert.NotNull(logRecord.Logger); - Assert.Equal("OpenTelemetry.Logs.Tests.LogRecordTest", logRecord.Logger.Name); + Assert.Equal("OpenTelemetry.Logs.Tests.LogRecordTests", logRecord.Logger.Name); Assert.Null(logRecord.Logger.Version); } @@ -1003,7 +1003,9 @@ public void LogRecordCategoryNameAliasForInstrumentationScopeTests() var exportedItems = new List(); using (var loggerProvider = Sdk.CreateLoggerProviderBuilder() +#pragma warning disable CA2000 // Dispose objects before losing scope .AddProcessor(new BatchLogRecordExportProcessor(new InMemoryExporter(exportedItems))) +#pragma warning restore CA2000 // Dispose objects before losing scope .Build()) { var logger = loggerProvider.GetLogger("TestName"); @@ -1016,7 +1018,7 @@ public void LogRecordCategoryNameAliasForInstrumentationScopeTests() Assert.Equal(exportedItems[0].CategoryName, exportedItems[0].Logger.Name); } - private static ILoggerFactory InitializeLoggerFactory(out List exportedItems, Action configure = null) + private static ILoggerFactory InitializeLoggerFactory(out List exportedItems, Action? configure = null) { var items = exportedItems = new List(); @@ -1027,7 +1029,7 @@ private static ILoggerFactory InitializeLoggerFactory(out List export configure?.Invoke(options); options.AddInMemoryExporter(items); }); - builder.AddFilter(typeof(LogRecordTest).FullName, LogLevel.Trace); + builder.AddFilter(typeof(LogRecordTests).FullName, LogLevel.Trace); }); } @@ -1044,7 +1046,7 @@ internal struct Food public StructState(params KeyValuePair[] items) { - this.list = new List>(items); + this.list = [.. items]; } public int Count => this.list.Count; @@ -1062,36 +1064,42 @@ IEnumerator IEnumerable.GetEnumerator() } } - internal sealed class DisposingState : IReadOnlyList>, IDisposable + internal sealed class DisposingState : IReadOnlyList>, IDisposable { - private string value; + private string? value; private bool disposed; - public DisposingState(string value) + public DisposingState(string? value) { this.Value = value; } public int Count => 1; - public string Value + public string? Value { get { +#if NET + ObjectDisposedException.ThrowIf(this.disposed, this); +#else if (this.disposed) { throw new ObjectDisposedException(nameof(DisposingState)); } +#endif return this.value; } private set => this.value = value; } - public KeyValuePair this[int index] => index switch + public KeyValuePair this[int index] => index switch { - 0 => new KeyValuePair(nameof(this.Value), this.Value), + 0 => new KeyValuePair(nameof(this.Value), this.Value), +#pragma warning disable CA2201 // Do not raise reserved exception types _ => throw new IndexOutOfRangeException(nameof(index)), +#pragma warning restore CA2201 // Do not raise reserved exception types }; public void Dispose() @@ -1099,7 +1107,7 @@ public void Dispose() this.disposed = true; } - public IEnumerator> GetEnumerator() + public IEnumerator> GetEnumerator() { for (var i = 0; i < this.Count; i++) { @@ -1110,7 +1118,7 @@ public IEnumerator> GetEnumerator() IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); } - private class RedactionProcessor : BaseProcessor + private sealed class RedactionProcessor : BaseProcessor { private readonly Field fieldToUpdate; @@ -1123,11 +1131,11 @@ public override void OnEnd(LogRecord logRecord) { if (this.fieldToUpdate == Field.State) { - logRecord.State = new List> { new KeyValuePair("newStateKey", "newStateValue") }; + logRecord.State = new List> { new("newStateKey", "newStateValue") }; } else if (this.fieldToUpdate == Field.StateValues) { - logRecord.StateValues = new List> { new KeyValuePair("newStateValueKey", "newStateValueValue") }; + logRecord.StateValues = new List> { new("newStateValueKey", "newStateValueValue") }; } else { @@ -1136,13 +1144,13 @@ public override void OnEnd(LogRecord logRecord) } } - private class ListState : IEnumerable> + private sealed class ListState : IEnumerable> { private readonly List> list; public ListState(params KeyValuePair[] items) { - this.list = new List>(items); + this.list = [.. items]; } public IEnumerator> GetEnumerator() @@ -1156,17 +1164,17 @@ IEnumerator IEnumerable.GetEnumerator() } } - private class CustomState + private sealed class CustomState { public const string ToStringValue = "CustomState.ToString"; - public string Property { get; set; } + public string? Property { get; set; } public override string ToString() => ToStringValue; } - private class ScopeProcessor : BaseProcessor + private sealed class ScopeProcessor : BaseProcessor { private readonly bool buffer; @@ -1175,11 +1183,11 @@ public ScopeProcessor(bool buffer) this.buffer = buffer; } - public List Scopes { get; } = new(); + public List Scopes { get; } = new(); public override void OnEnd(LogRecord data) { - data.ForEachScope( + data.ForEachScope( (scope, state) => { this.Scopes.Add(scope.Scope); diff --git a/test/OpenTelemetry.Tests/Logs/LogRecordThreadStaticPoolTests.cs b/test/OpenTelemetry.Tests/Logs/LogRecordThreadStaticPoolTests.cs index 59c0b53454e..4fd6f588c85 100644 --- a/test/OpenTelemetry.Tests/Logs/LogRecordThreadStaticPoolTests.cs +++ b/test/OpenTelemetry.Tests/Logs/LogRecordThreadStaticPoolTests.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using Xunit; namespace OpenTelemetry.Logs.Tests; diff --git a/test/OpenTelemetry.Tests/Logs/LoggerFactoryAndResourceBuilderTests.cs b/test/OpenTelemetry.Tests/Logs/LoggerFactoryAndResourceBuilderTests.cs index 073160b6e5c..e900cd11e6d 100644 --- a/test/OpenTelemetry.Tests/Logs/LoggerFactoryAndResourceBuilderTests.cs +++ b/test/OpenTelemetry.Tests/Logs/LoggerFactoryAndResourceBuilderTests.cs @@ -14,9 +14,16 @@ public sealed class LoggerFactoryAndResourceBuilderTests public void TestLogExporterCanAccessResource() { VerifyResourceBuilder( - assert: (Resource resource) => + assert: 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 && +#if NET + kvp.Value.ToString()!.Contains("unknown_service", StringComparison.Ordinal)); +#else + kvp.Value.ToString()!.Contains("unknown_service")); +#endif }); } @@ -28,9 +35,9 @@ public void VerifyResourceBuilder_WithServiceNameEnVar() Environment.SetEnvironmentVariable(OtelServiceNameEnvVarDetector.EnvVarKey, "MyService"); VerifyResourceBuilder( - assert: (Resource resource) => + assert: resource => { - Assert.Contains(resource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.Equals("MyService")); + Assert.Contains(resource.Attributes, kvp => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.Equals("MyService")); }); } finally diff --git a/test/OpenTelemetry.Tests/Logs/LoggerProviderBuilderExtensionsTests.cs b/test/OpenTelemetry.Tests/Logs/LoggerProviderBuilderExtensionsTests.cs index 79907a3d2c7..c956a9a7920 100644 --- a/test/OpenTelemetry.Tests/Logs/LoggerProviderBuilderExtensionsTests.cs +++ b/test/OpenTelemetry.Tests/Logs/LoggerProviderBuilderExtensionsTests.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Resources; using Xunit; @@ -19,7 +17,9 @@ public void LoggerProviderBuilderAddInstrumentationTest() using (var provider = Sdk.CreateLoggerProviderBuilder() .AddInstrumentation() .AddInstrumentation((sp, provider) => new CustomInstrumentation() { Provider = provider }) +#pragma warning disable CA2000 // Dispose objects before losing scope .AddInstrumentation(new CustomInstrumentation()) +#pragma warning restore CA2000 // Dispose objects before losing scope .AddInstrumentation(() => (object?)null) .Build() as LoggerProviderSdk) { @@ -36,7 +36,7 @@ public void LoggerProviderBuilderAddInstrumentationTest() Assert.Null(((CustomInstrumentation)provider.Instrumentations[2]).Provider); Assert.False(((CustomInstrumentation)provider.Instrumentations[2]).Disposed); - instrumentation = new List(provider.Instrumentations); + instrumentation = [.. provider.Instrumentations]; } Assert.True(((CustomInstrumentation)instrumentation[0]).Disposed); @@ -103,7 +103,7 @@ public void LoggerProviderBuilderSetResourceBuilderTests() using var provider = Sdk.CreateLoggerProviderBuilder() .SetResourceBuilder(ResourceBuilder .CreateEmpty() - .AddAttributes(new[] { new KeyValuePair("key1", "value1") })) + .AddAttributes([new KeyValuePair("key1", "value1")])) .Build() as LoggerProviderSdk; Assert.NotNull(provider); @@ -118,7 +118,7 @@ public void LoggerProviderBuilderConfigureResourceBuilderTests() using var provider = Sdk.CreateLoggerProviderBuilder() .ConfigureResource(resource => resource .Clear() - .AddAttributes(new[] { new KeyValuePair("key1", "value1") })) + .AddAttributes([new KeyValuePair("key1", "value1")])) .Build() as LoggerProviderSdk; Assert.NotNull(provider); @@ -337,14 +337,6 @@ protected override void Dispose(bool disposing) } } - private sealed class CustomExporter : BaseExporter - { - public override ExportResult Export(in Batch batch) - { - return ExportResult.Success; - } - } - private sealed class CustomLoggerProviderBuilder : LoggerProviderBuilder { public override LoggerProviderBuilder AddInstrumentation(Func instrumentationFactory) diff --git a/test/OpenTelemetry.Tests/Logs/LoggerProviderExtensionsTests.cs b/test/OpenTelemetry.Tests/Logs/LoggerProviderExtensionsTests.cs index 0324027f7f2..20e36e1cc6b 100644 --- a/test/OpenTelemetry.Tests/Logs/LoggerProviderExtensionsTests.cs +++ b/test/OpenTelemetry.Tests/Logs/LoggerProviderExtensionsTests.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using OpenTelemetry.Exporter; using Xunit; @@ -24,7 +22,9 @@ public void AddProcessorTest() Assert.Null(providerSdk.Processor); +#pragma warning disable CA2000 // Dispose objects before losing scope provider.AddProcessor(new TestProcessor()); +#pragma warning restore CA2000 // Dispose objects before losing scope Assert.NotNull(providerSdk.Processor); } @@ -35,8 +35,10 @@ public void ForceFlushTest() List exportedItems = new(); using var provider = Sdk.CreateLoggerProviderBuilder() .AddProcessor( +#pragma warning disable CA2000 // Dispose objects before losing scope new BatchLogRecordExportProcessor( new InMemoryExporter(exportedItems), +#pragma warning restore CA2000 // Dispose objects before losing scope scheduledDelayMilliseconds: int.MaxValue)) .Build(); diff --git a/test/OpenTelemetry.Tests/Logs/LoggerProviderSdkTests.cs b/test/OpenTelemetry.Tests/Logs/LoggerProviderSdkTests.cs index 7122dd089e1..f321551dec1 100644 --- a/test/OpenTelemetry.Tests/Logs/LoggerProviderSdkTests.cs +++ b/test/OpenTelemetry.Tests/Logs/LoggerProviderSdkTests.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Exporter; @@ -39,7 +37,9 @@ public void ForceFlushTest() List exportedItems = new(); +#pragma warning disable CA2000 // Dispose objects before losing scope provider.AddProcessor(new BatchLogRecordExportProcessor(new InMemoryExporter(exportedItems))); +#pragma warning restore CA2000 // Dispose objects before losing scope var logger = provider.GetLogger("TestLogger"); @@ -62,7 +62,9 @@ public void ThreadStaticPoolUsedByProviderTests() Assert.Equal(LogRecordThreadStaticPool.Instance, provider1.LogRecordPool); using var provider2 = Sdk.CreateLoggerProviderBuilder() +#pragma warning disable CA2000 // Dispose objects before losing scope .AddProcessor(new SimpleLogRecordExportProcessor(new NoopExporter())) +#pragma warning restore CA2000 // Dispose objects before losing scope .Build() as LoggerProviderSdk; Assert.NotNull(provider2); @@ -70,8 +72,10 @@ public void ThreadStaticPoolUsedByProviderTests() Assert.Equal(LogRecordThreadStaticPool.Instance, provider2.LogRecordPool); using var provider3 = Sdk.CreateLoggerProviderBuilder() +#pragma warning disable CA2000 // Dispose objects before losing scope .AddProcessor(new SimpleLogRecordExportProcessor(new NoopExporter())) .AddProcessor(new SimpleLogRecordExportProcessor(new NoopExporter())) +#pragma warning restore CA2000 // Dispose objects before losing scope .Build() as LoggerProviderSdk; Assert.NotNull(provider3); @@ -83,6 +87,7 @@ public void ThreadStaticPoolUsedByProviderTests() public void SharedPoolUsedByProviderTests() { using var provider1 = Sdk.CreateLoggerProviderBuilder() +#pragma warning disable CA2000 // Dispose objects before losing scope .AddProcessor(new BatchLogRecordExportProcessor(new NoopExporter())) .Build() as LoggerProviderSdk; @@ -101,11 +106,12 @@ public void SharedPoolUsedByProviderTests() using var provider3 = Sdk.CreateLoggerProviderBuilder() .AddProcessor(new SimpleLogRecordExportProcessor(new NoopExporter())) - .AddProcessor(new CompositeProcessor(new BaseProcessor[] - { + .AddProcessor(new CompositeProcessor( + [ new SimpleLogRecordExportProcessor(new NoopExporter()), new BatchLogRecordExportProcessor(new NoopExporter()), - })) +#pragma warning restore CA2000 // Dispose objects before losing scope + ])) .Build() as LoggerProviderSdk; Assert.NotNull(provider3); @@ -122,12 +128,16 @@ public void AddProcessorTest() Assert.NotNull(provider); Assert.Null(provider.Processor); +#pragma warning disable CA2000 // Dispose objects before losing scope provider.AddProcessor(new NoopProcessor()); +#pragma warning restore CA2000 // Dispose objects before losing scope Assert.NotNull(provider.Processor); Assert.True(provider.Processor is NoopProcessor); +#pragma warning disable CA2000 // Dispose objects before losing scope provider.AddProcessor(new NoopProcessor()); +#pragma warning restore CA2000 // Dispose objects before losing scope Assert.NotNull(provider.Processor); Assert.True(provider.Processor is CompositeProcessor); diff --git a/test/OpenTelemetry.Tests/Logs/OpenTelemetryLoggerProviderTests.cs b/test/OpenTelemetry.Tests/Logs/OpenTelemetryLoggerProviderTests.cs index e40d8d8b60a..847e096871e 100644 --- a/test/OpenTelemetry.Tests/Logs/OpenTelemetryLoggerProviderTests.cs +++ b/test/OpenTelemetry.Tests/Logs/OpenTelemetryLoggerProviderTests.cs @@ -50,7 +50,7 @@ public void VerifyOptionsCannotBeChangedAfterInit(bool initialValue) var optionsMonitor = sp.GetRequiredService>(); - var provider = new OpenTelemetryLoggerProvider(optionsMonitor); + using var provider = new OpenTelemetryLoggerProvider(optionsMonitor); // Verify initial set Assert.Equal(initialValue, provider.Options.IncludeFormattedMessage); diff --git a/test/OpenTelemetry.Tests/Logs/OpenTelemetryLoggingExtensionsTests.cs b/test/OpenTelemetry.Tests/Logs/OpenTelemetryLoggingExtensionsTests.cs index e0c59ca7bf7..424c4144d4a 100644 --- a/test/OpenTelemetry.Tests/Logs/OpenTelemetryLoggingExtensionsTests.cs +++ b/test/OpenTelemetry.Tests/Logs/OpenTelemetryLoggingExtensionsTests.cs @@ -1,13 +1,12 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Reflection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using OpenTelemetry.Tests; using Xunit; namespace OpenTelemetry.Logs.Tests; @@ -391,7 +390,9 @@ protected override void Dispose(bool disposing) } } +#pragma warning disable CA1812 // Avoid uninstantiated internal classes private sealed class TestLogProcessorWithILoggerFactoryDependency : BaseProcessor +#pragma warning restore CA1812 // Avoid uninstantiated internal classes { private readonly ILogger logger; @@ -407,7 +408,7 @@ public TestLogProcessorWithILoggerFactoryDependency(ILoggerFactory loggerFactory protected override void Dispose(bool disposing) { - this.logger.LogInformation("Dispose called"); + this.logger.DisposedCalled(); base.Dispose(disposing); } diff --git a/test/OpenTelemetry.Tests/Metrics/AggregatorTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/AggregatorTests.cs similarity index 87% rename from test/OpenTelemetry.Tests/Metrics/AggregatorTestsBase.cs rename to test/OpenTelemetry.Tests/Metrics/AggregatorTests.cs index 6bd96046d3f..19bf1097f4b 100644 --- a/test/OpenTelemetry.Tests/Metrics/AggregatorTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/AggregatorTests.cs @@ -1,32 +1,28 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics; using System.Diagnostics.Metrics; using Xunit; namespace OpenTelemetry.Metrics.Tests; -#pragma warning disable SA1402 - -public abstract class AggregatorTestsBase +public class AggregatorTests { 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) + public AggregatorTests() { - this.emitOverflowAttribute = emitOverflowAttribute; - this.shouldReclaimUnusedMetricPoints = shouldReclaimUnusedMetricPoints; - - this.aggregatorStore = new(MetricStreamIdentity, AggregationType.HistogramWithBuckets, AggregationTemporality.Cumulative, 1024, emitOverflowAttribute, this.shouldReclaimUnusedMetricPoints); + this.aggregatorStore = new(MetricStreamIdentity, AggregationType.HistogramWithBuckets, AggregationTemporality.Cumulative, 1024); } + public static TheoryData HistogramInfinityBoundariesTestCases => HistogramBoundaryTestCase.HistogramInfinityBoundariesTestCases(); + [Fact] public void HistogramDistributeToAllBucketsDefault() { @@ -193,11 +189,7 @@ 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 argsToThread = new ThreadArguments(histogramPoint, new ManualResetEvent(false)); var numberOfThreads = 2; var snapshotThread = new Thread(HistogramSnapshotThread); @@ -243,7 +235,7 @@ public void MultiThreadedHistogramUpdateAndSnapShotTest() [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) + public void HistogramBucketsDefaultUpdatesForSecondsTest(string meterName, string instrumentName, string? unit, KnownHistogramBuckets expectedHistogramBuckets) { using var meter = new Meter(meterName); @@ -255,9 +247,7 @@ public void HistogramBucketsDefaultUpdatesForSecondsTest(string meterName, strin metricStreamIdentity, AggregationType.Histogram, AggregationTemporality.Cumulative, - cardinalityLimit: 1024, - this.emitOverflowAttribute, - this.shouldReclaimUnusedMetricPoints); + cardinalityLimit: 1024); KnownHistogramBuckets actualHistogramBounds = KnownHistogramBuckets.Default; if (aggregatorStore.HistogramBounds == Metric.DefaultHistogramBoundsShortSeconds) @@ -297,13 +287,13 @@ internal static void AssertExponentialBucketsAreCorrect(Base2ExponentialBucketHi Assert.Equal(expected, actual); - actual = new List(); + actual = []; foreach (var bucketCount in data.NegativeBuckets) { actual.Add(bucketCount); } - expected = new List(); + expected = []; foreach (var bucketCount in expectedData.NegativeBuckets) { expected.Add(bucketCount); @@ -333,15 +323,13 @@ internal void ExponentialHistogramTests(AggregationType aggregationType, Aggrega 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>()); + aggregatorStore.Update(value, Array.Empty>()); if (value >= 0) { @@ -442,11 +430,9 @@ internal void ExponentialMaxScaleConfigWorks(int? maxScale) metricStreamIdentity, AggregationType.Base2ExponentialHistogram, AggregationTemporality.Cumulative, - cardinalityLimit: 1024, - this.emitOverflowAttribute, - this.shouldReclaimUnusedMetricPoints); + cardinalityLimit: 1024); - aggregatorStore.Update(10, Array.Empty>()); + aggregatorStore.Update(10, Array.Empty>()); aggregatorStore.Snapshot(); @@ -466,10 +452,45 @@ internal void ExponentialMaxScaleConfigWorks(int? maxScale) Assert.Equal(expectedScale, metricPoint.GetExponentialHistogramData().Scale); } - private static void HistogramSnapshotThread(object obj) + [Theory] + [MemberData(nameof(HistogramBoundaryTestCase.HistogramInfinityBoundariesTestCases))] + internal void HistogramBucketBoundariesTest(HistogramBoundaryTestCase boundaryTestCase) + { + // Arrange + var histogramPoint = new MetricPoint(this.aggregatorStore, AggregationType.HistogramWithBuckets, null, boundaryTestCase.InputBoundaries, Metric.DefaultExponentialHistogramMaxBuckets, Metric.DefaultExponentialHistogramMaxScale); + var expectedTotalBuckets = boundaryTestCase.ExpectedBucketCounts.Length; + + // Act + foreach (var value in boundaryTestCase.InputValues) + { + histogramPoint.Update(value); + } + + histogramPoint.TakeSnapshot(true); + + // Assert + var count = histogramPoint.GetHistogramCount(); + Assert.Equal(boundaryTestCase.InputValues.Length, count); + + int bucketIndex = 0; + int actualBucketCount = 0; + + foreach (var histogramBucket in histogramPoint.GetHistogramBuckets()) + { + Assert.Equal(boundaryTestCase.ExpectedBucketCounts[bucketIndex], histogramBucket.BucketCount); + Assert.Equal(boundaryTestCase.ExpectedBucketBounds[bucketIndex], histogramBucket.ExplicitBound); + bucketIndex++; + actualBucketCount++; + } + + Assert.Equal(expectedTotalBuckets, actualBucketCount); + } + + private static void HistogramSnapshotThread(object? obj) { var args = obj as ThreadArguments; - var mreToEnsureAllThreadsStart = args.MreToEnsureAllThreadsStart; + Debug.Assert(args != null, "args was null"); + var mreToEnsureAllThreadsStart = args!.MreToEnsureAllThreadsStart; if (Interlocked.Increment(ref args.ThreadStartedCount) == 3) { @@ -487,10 +508,11 @@ private static void HistogramSnapshotThread(object obj) } } - private static void HistogramUpdateThread(object obj) + private static void HistogramUpdateThread(object? obj) { var args = obj as ThreadArguments; - var mreToEnsureAllThreadsStart = args.MreToEnsureAllThreadsStart; + Debug.Assert(args != null, "args was null"); + var mreToEnsureAllThreadsStart = args!.MreToEnsureAllThreadsStart; if (Interlocked.Increment(ref args.ThreadStartedCount) == 3) { @@ -507,44 +529,18 @@ private static void HistogramUpdateThread(object obj) Interlocked.Increment(ref args.ThreadsFinishedAllUpdatesCount); } - private class ThreadArguments + private sealed class ThreadArguments { + public readonly ManualResetEvent MreToEnsureAllThreadsStart; 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) - { + public ThreadArguments(MetricPoint histogramPoint, ManualResetEvent mreToEnsureAllThreadsStart) + { + this.HistogramPoint = histogramPoint; + this.MreToEnsureAllThreadsStart = mreToEnsureAllThreadsStart; + } } } diff --git a/test/OpenTelemetry.Tests/Metrics/Base2ExponentialBucketHistogramHelperTests.cs b/test/OpenTelemetry.Tests/Metrics/Base2ExponentialBucketHistogramHelperTests.cs index 0af08fffc11..fae1f502e08 100644 --- a/test/OpenTelemetry.Tests/Metrics/Base2ExponentialBucketHistogramHelperTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/Base2ExponentialBucketHistogramHelperTests.cs @@ -16,20 +16,26 @@ public Base2ExponentialBucketHistogramHelperTests(ITestOutputHelper output) this.output = output; } - public static IEnumerable GetNonPositiveScales() + public static TheoryData GetNonPositiveScales() { + TheoryData theoryData = []; for (var i = -11; i <= 0; ++i) { - yield return new object[] { i }; + theoryData.Add(i); } + + return theoryData; } - public static IEnumerable GetPositiveScales() + public static TheoryData GetPositiveScales() { + TheoryData theoryData = []; for (var i = 1; i <= 20; ++i) { - yield return new object[] { i }; + theoryData.Add(i); } + + return theoryData; } [Theory] diff --git a/test/OpenTelemetry.Tests/Metrics/Base2ExponentialBucketHistogramTest.cs b/test/OpenTelemetry.Tests/Metrics/Base2ExponentialBucketHistogramTests.cs similarity index 99% rename from test/OpenTelemetry.Tests/Metrics/Base2ExponentialBucketHistogramTest.cs rename to test/OpenTelemetry.Tests/Metrics/Base2ExponentialBucketHistogramTests.cs index a40beac0aa2..36c8bb2e439 100644 --- a/test/OpenTelemetry.Tests/Metrics/Base2ExponentialBucketHistogramTest.cs +++ b/test/OpenTelemetry.Tests/Metrics/Base2ExponentialBucketHistogramTests.cs @@ -7,11 +7,11 @@ namespace OpenTelemetry.Metrics.Tests; -public class Base2ExponentialBucketHistogramTest +public class Base2ExponentialBucketHistogramTests { private readonly ITestOutputHelper output; - public Base2ExponentialBucketHistogramTest(ITestOutputHelper output) + public Base2ExponentialBucketHistogramTests(ITestOutputHelper output) { this.output = output; } diff --git a/test/OpenTelemetry.Tests/Metrics/CircularBufferBucketsTest.cs b/test/OpenTelemetry.Tests/Metrics/CircularBufferBucketsTests.cs similarity index 99% rename from test/OpenTelemetry.Tests/Metrics/CircularBufferBucketsTest.cs rename to test/OpenTelemetry.Tests/Metrics/CircularBufferBucketsTests.cs index 7666ab1a5df..83dfcf3dc83 100644 --- a/test/OpenTelemetry.Tests/Metrics/CircularBufferBucketsTest.cs +++ b/test/OpenTelemetry.Tests/Metrics/CircularBufferBucketsTests.cs @@ -5,7 +5,7 @@ namespace OpenTelemetry.Metrics.Tests; -public class CircularBufferBucketsTest +public class CircularBufferBucketsTests { [Fact] public void ConstructorThrowsOnInvalidCapacity() diff --git a/test/OpenTelemetry.Tests/Metrics/HistogramBoundaryTestCase.cs b/test/OpenTelemetry.Tests/Metrics/HistogramBoundaryTestCase.cs new file mode 100644 index 00000000000..fb8b5affd13 --- /dev/null +++ b/test/OpenTelemetry.Tests/Metrics/HistogramBoundaryTestCase.cs @@ -0,0 +1,70 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Xunit; + +namespace OpenTelemetry.Metrics.Tests; + +#pragma warning disable CA1819 // Properties should not return arrays +#pragma warning disable CA1515 // Consider making public types internal +public class HistogramBoundaryTestCase( +#pragma warning restore CA1515 // Consider making public types internal + string testName, + double[] inputBoundaries, + double[] inputValues, + long[] expectedBucketCounts, + double[] expectedBucketBounds) +{ + public double[] InputBoundaries { get; } = inputBoundaries; + + public double[] InputValues { get; } = inputValues; + + public long[] ExpectedBucketCounts { get; } = expectedBucketCounts; + + public double[] ExpectedBucketBounds { get; } = expectedBucketBounds; + + public string TestName { get; set; } = testName; + + public static TheoryData HistogramInfinityBoundariesTestCases() + { + var data = new TheoryData + { + new( + testName: "Custom boundaries with no infinity in explicit boundaries", + inputBoundaries: [0, 10], + inputValues: [-10, 0, 5, 10, 100], + expectedBucketCounts: [2, 2, 1], + expectedBucketBounds: [0, 10, double.PositiveInfinity]), + + new( + testName: "Custom boundaries with positive infinity", + inputBoundaries: [0, double.PositiveInfinity], + inputValues: [-10, 0, 10, 100], + expectedBucketCounts: [2, 2], + expectedBucketBounds: [0, double.PositiveInfinity]), + + new( + testName: "Custom boundaries with negative infinity", + inputBoundaries: [double.NegativeInfinity, 0, 10], + inputValues: [-100, -10, 0, 5, 10, 100], + expectedBucketCounts: [3, 2, 1], + expectedBucketBounds: [0, 10, double.PositiveInfinity]), + + new( + testName: "Custom boundaries with both infinities", + inputBoundaries: [double.NegativeInfinity, 0, 10, double.PositiveInfinity], + inputValues: [-100, -10, 0, 5, 10, 100], + expectedBucketCounts: [3, 2, 1], + expectedBucketBounds: [0, 10, double.PositiveInfinity]), + + new( + testName: "Custom boundaries with infinities only", + inputBoundaries: [double.NegativeInfinity, double.PositiveInfinity], + inputValues: [-10, 0, 10], + expectedBucketCounts: [3], + expectedBucketBounds: [double.PositiveInfinity]), + }; + + return data; + } +} diff --git a/test/OpenTelemetry.Tests/Metrics/HostingMeterProviderBuilder.cs b/test/OpenTelemetry.Tests/Metrics/HostingMeterProviderBuilder.cs new file mode 100644 index 00000000000..de0cd7aaa5b --- /dev/null +++ b/test/OpenTelemetry.Tests/Metrics/HostingMeterProviderBuilder.cs @@ -0,0 +1,39 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if BUILDING_HOSTING_TESTS + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; + +namespace OpenTelemetry.Metrics.Tests; + +#pragma warning disable CA1515 // Consider making public types internal +public sealed class HostingMeterProviderBuilder : MeterProviderBuilderBase +#pragma warning restore CA1515 // Consider making public types internal +{ + 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); + } +} +#endif diff --git a/test/OpenTelemetry.Tests/Metrics/InMemoryExporterTests.cs b/test/OpenTelemetry.Tests/Metrics/InMemoryExporterTests.cs index bb44505bd9c..54dbe28069b 100644 --- a/test/OpenTelemetry.Tests/Metrics/InMemoryExporterTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/InMemoryExporterTests.cs @@ -28,7 +28,7 @@ public void InMemoryExporterShouldDeepCopyMetricPoints() 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")); + counter.Add(10, new KeyValuePair("tag1", "value1")); meterProvider.ForceFlush(); @@ -38,7 +38,7 @@ public void InMemoryExporterShouldDeepCopyMetricPoints() 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")); + counter.Add(25, new KeyValuePair("tag1", "value1")); meterProvider.ForceFlush(); diff --git a/test/OpenTelemetry.Tests/Metrics/KnownHistogramBuckets.cs b/test/OpenTelemetry.Tests/Metrics/KnownHistogramBuckets.cs index b63b4c2651b..27436e00eef 100644 --- a/test/OpenTelemetry.Tests/Metrics/KnownHistogramBuckets.cs +++ b/test/OpenTelemetry.Tests/Metrics/KnownHistogramBuckets.cs @@ -3,7 +3,9 @@ namespace OpenTelemetry.Metrics.Tests; +#pragma warning disable CA1515 // Consider making public types internal public enum KnownHistogramBuckets +#pragma warning restore CA1515 // Consider making public types internal { /// /// Default OpenTelemetry semantic convention buckets. diff --git a/test/OpenTelemetry.Tests/Metrics/MemoryEfficiencyTests.cs b/test/OpenTelemetry.Tests/Metrics/MemoryEfficiencyTests.cs index 3965aa0599c..6418be16263 100644 --- a/test/OpenTelemetry.Tests/Metrics/MemoryEfficiencyTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MemoryEfficiencyTests.cs @@ -28,7 +28,7 @@ public void ExportOnlyWhenPointChanged(MetricReaderTemporalityPreference tempora var counter = meter.CreateCounter("meter"); - counter.Add(10, new KeyValuePair("tag1", "value1")); + counter.Add(10, new KeyValuePair("tag1", "value1")); meterProvider.ForceFlush(); Assert.Single(exportedItems); diff --git a/test/OpenTelemetry.Tests/Metrics/MeterProviderBuilderExtensionsTests.cs b/test/OpenTelemetry.Tests/Metrics/MeterProviderBuilderExtensionsTests.cs index 5ddd39fee0d..fa87ebe5da8 100644 --- a/test/OpenTelemetry.Tests/Metrics/MeterProviderBuilderExtensionsTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MeterProviderBuilderExtensionsTests.cs @@ -16,7 +16,7 @@ public void ServiceLifecycleAvailableToSDKBuilderTest() { var builder = Sdk.CreateMeterProviderBuilder(); - MyInstrumentation myInstrumentation = null; + MyInstrumentation? myInstrumentation = null; RunBuilderServiceLifecycleTest( builder, @@ -54,6 +54,7 @@ public void AddReaderUsingDependencyInjectionTest() using var provider = builder.Build() as MeterProviderSdk; Assert.NotNull(provider); + Assert.NotNull(provider.OwnedServiceProvider); var readers = ((IServiceProvider)provider.OwnedServiceProvider).GetServices(); @@ -72,13 +73,15 @@ public void AddReaderUsingDependencyInjectionTest() [Fact] public void AddInstrumentationTest() { - List instrumentation = null; + List? instrumentation = null; using (var provider = Sdk.CreateMeterProviderBuilder() .AddInstrumentation() .AddInstrumentation((sp, provider) => new MyInstrumentation() { Provider = provider }) +#pragma warning disable CA2000 // Dispose objects before losing scope .AddInstrumentation(new MyInstrumentation()) - .AddInstrumentation(() => (object)null) +#pragma warning restore CA2000 // Dispose objects before losing scope + .AddInstrumentation(() => (object?)null) .Build() as MeterProviderSdk) { Assert.NotNull(provider); @@ -94,7 +97,7 @@ public void AddInstrumentationTest() Assert.Null(((MyInstrumentation)provider.Instrumentations[2]).Provider); Assert.False(((MyInstrumentation)provider.Instrumentations[2]).Disposed); - instrumentation = new List(provider.Instrumentations); + instrumentation = [.. provider.Instrumentations]; } Assert.NotNull(instrumentation); @@ -144,6 +147,7 @@ public void SetAndConfigureResourceTest() Assert.True(serviceProviderTestExecuted); Assert.Equal(2, configureInvocations); + Assert.NotNull(provider); Assert.Single(provider.Resource.Attributes); Assert.Contains(provider.Resource.Attributes, kvp => kvp.Key == "key2" && (string)kvp.Value == "value2"); } @@ -162,7 +166,7 @@ public void ConfigureBuilderIConfigurationAvailableTest() configureBuilderCalled = true; - var testKeyValue = configuration.GetValue("TEST_KEY", null); + var testKeyValue = configuration.GetValue("TEST_KEY", null); Assert.Equal("TEST_KEY_VALUE", testKeyValue); }) @@ -182,7 +186,7 @@ public void ConfigureBuilderIConfigurationModifiableTest() .ConfigureServices(services => { var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { ["TEST_KEY_2"] = "TEST_KEY_2_VALUE" }) + .AddInMemoryCollection(new Dictionary { ["TEST_KEY_2"] = "TEST_KEY_2_VALUE" }) .Build(); services.AddSingleton(configuration); @@ -193,7 +197,7 @@ public void ConfigureBuilderIConfigurationModifiableTest() configureBuilderCalled = true; - var testKey2Value = configuration.GetValue("TEST_KEY_2", null); + var testKey2Value = configuration.GetValue("TEST_KEY_2", null); Assert.Equal("TEST_KEY_2_VALUE", testKey2Value); }) @@ -358,7 +362,7 @@ private static void RunBuilderServiceLifecycleTest( private sealed class MyInstrumentation : IDisposable { - internal MeterProvider Provider; + internal MeterProvider? Provider; internal bool Disposed; public void Dispose() @@ -371,14 +375,6 @@ private sealed class MyReader : MetricReader { } - private sealed class MyExporter : BaseExporter - { - public override ExportResult Export(in Batch batch) - { - return ExportResult.Success; - } - } - private sealed class MyMeterProviderBuilder : MeterProviderBuilder { public override MeterProviderBuilder AddInstrumentation(Func instrumentationFactory) diff --git a/test/OpenTelemetry.Tests/Metrics/MeterProviderSdkTest.cs b/test/OpenTelemetry.Tests/Metrics/MeterProviderSdkTests.cs similarity index 95% rename from test/OpenTelemetry.Tests/Metrics/MeterProviderSdkTest.cs rename to test/OpenTelemetry.Tests/Metrics/MeterProviderSdkTests.cs index bce959764f2..8f51b19d3a2 100644 --- a/test/OpenTelemetry.Tests/Metrics/MeterProviderSdkTest.cs +++ b/test/OpenTelemetry.Tests/Metrics/MeterProviderSdkTests.cs @@ -8,7 +8,7 @@ namespace OpenTelemetry.Metrics.Tests; -public class MeterProviderSdkTest +public class MeterProviderSdkTests { [Fact] public void BuilderTypeDoesNotChangeTest() @@ -83,7 +83,7 @@ public void TransientMeterExhaustsMetricStorageTest(bool withView, bool forceFlu Assert.Single(exportedItems); } - var metricInstrumentIgnoredEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 33 && e.Payload[1] as string == meterName); + var metricInstrumentIgnoredEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 33 && (e.Payload?.Count ?? 0) >= 2 && e.Payload![1] as string == meterName); Assert.Single(metricInstrumentIgnoredEvents); diff --git a/test/OpenTelemetry.Tests/Metrics/MeterProviderTests.cs b/test/OpenTelemetry.Tests/Metrics/MeterProviderTests.cs index d179363c035..6cf5fa3c8a1 100644 --- a/test/OpenTelemetry.Tests/Metrics/MeterProviderTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MeterProviderTests.cs @@ -16,11 +16,15 @@ public void MeterProviderFindExporterTest() .AddInMemoryExporter(exportedItems) .Build(); - Assert.True(meterProvider.TryFindExporter(out InMemoryExporter inMemoryExporter)); - Assert.False(meterProvider.TryFindExporter(out MyExporter myExporter)); +#pragma warning disable CA2000 // Dispose objects before losing scope + Assert.True(meterProvider.TryFindExporter(out InMemoryExporter? inMemoryExporter)); + Assert.False(meterProvider.TryFindExporter(out MyExporter? myExporter)); +#pragma warning restore CA2000 // Dispose objects before losing scope } - private class MyExporter : BaseExporter +#pragma warning disable CA1812 // Avoid uninstantiated internal classes + private sealed class MyExporter : BaseExporter +#pragma warning restore CA1812 // Avoid uninstantiated internal classes { public override ExportResult Export(in Batch batch) { diff --git a/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricApiTests.cs similarity index 78% rename from test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs rename to test/OpenTelemetry.Tests/Metrics/MetricApiTests.cs index cc5a34d7fd1..1929e41ad25 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricApiTests.cs @@ -3,7 +3,6 @@ using System.Diagnostics; using System.Diagnostics.Metrics; -using Microsoft.Extensions.Configuration; using OpenTelemetry.Exporter; using OpenTelemetry.Internal; using OpenTelemetry.Tests; @@ -12,19 +11,16 @@ namespace OpenTelemetry.Metrics.Tests; -#pragma warning disable SA1402 - -public abstract class MetricApiTestsBase : MetricTestsBase +public class MetricApiTests : MetricTestsBase { private const int MaxTimeToAllowForFlush = 10000; + private const long DeltaLongValueUpdatedByEachCall = 10; + private const double DeltaDoubleValueUpdatedByEachCall = 11.987; + private const int NumberOfMetricUpdateByEachThread = 100000; 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)) + public MetricApiTests(ITestOutputHelper output) { this.output = output; } @@ -35,18 +31,18 @@ public void MeasurementWithNullValuedTag() using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems)); var counter = meter.CreateCounter("myCounter"); - counter.Add(100, new KeyValuePair("tagWithNullValue", null)); + 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(); + List metricPoints = []; foreach (ref readonly var mp in metric.GetMetricPoints()) { metricPoints.Add(mp); @@ -68,7 +64,7 @@ public void ObserverCallbackTest() using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems)); @@ -79,7 +75,7 @@ public void ObserverCallbackTest() Assert.Single(exportedItems); var metric = exportedItems[0]; Assert.Equal("myGauge", metric.Name); - List metricPoints = new List(); + List metricPoints = []; foreach (ref readonly var mp in metric.GetMetricPoints()) { metricPoints.Add(mp); @@ -97,19 +93,19 @@ public void ObserverCallbackExceptionTest() using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = 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")); + meter.CreateObservableGauge("myBadGauge", observeValues: () => throw new InvalidOperationException("gauge read error")); meterProvider.ForceFlush(MaxTimeToAllowForFlush); Assert.Single(exportedItems); var metric = exportedItems[0]; Assert.Equal("myGauge", metric.Name); - List metricPoints = new List(); + List metricPoints = []; foreach (ref readonly var mp in metric.GetMetricPoints()) { metricPoints.Add(mp); @@ -125,13 +121,13 @@ public void ObserverCallbackExceptionTest() [InlineData("unit")] [InlineData("")] [InlineData(null)] - public void MetricUnitIsExportedCorrectly(string unit) + public void MetricUnitIsExportedCorrectly(string? unit) { var exportedItems = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + using var meter = new Meter(Utils.GetCurrentMethodName()); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems)); @@ -147,13 +143,13 @@ public void MetricUnitIsExportedCorrectly(string unit) [InlineData("description")] [InlineData("")] [InlineData(null)] - public void MetricDescriptionIsExportedCorrectly(string description) + public void MetricDescriptionIsExportedCorrectly(string? description) { var exportedItems = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + using var meter = new Meter(Utils.GetCurrentMethodName()); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems)); @@ -171,14 +167,14 @@ public void MetricInstrumentationScopeIsExportedCorrectly() var exportedItems = new List(); var meterName = Utils.GetCurrentMethodName(); var meterVersion = "1.0"; - var meterTags = new List> + 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 + using var meter = new Meter(meterName, meterVersion, meterTags); + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems)); @@ -190,25 +186,26 @@ public void MetricInstrumentationScopeIsExportedCorrectly() 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)); + Assert.NotNull(metric.MeterTags); + + Assert.Single(metric.MeterTags, kvp => kvp.Key == meterTags[0].Key && kvp.Value == meterTags[0].Value); } [Fact] - public void MetricInstrumentationScopeAttributesAreNotTreatedAsIdentifyingProperty() + public void MetricInstrumentationScopeAttributesAreTreatedAsIdentifyingProperty() { // 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. + // Meters are identified by name, version, meter tags and schema_url fields. var exportedItems = new List(); var meterName = "MyMeter"; var meterVersion = "1.0"; - var meterTags1 = new List> + var meterTags1 = new List> { new( "Key1", "Value1"), }; - var meterTags2 = new List> + var meterTags2 = new List> { new( "Key2", @@ -216,7 +213,7 @@ public void MetricInstrumentationScopeAttributesAreNotTreatedAsIdentifyingProper }; 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 + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meterName) .AddInMemoryExporter(exportedItems)); @@ -226,19 +223,18 @@ public void MetricInstrumentationScopeAttributesAreNotTreatedAsIdentifyingProper 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(2, exportedItems.Count); + + bool TagComparator(KeyValuePair lhs, KeyValuePair rhs) + { + return lhs.Key.Equals(rhs.Key, StringComparison.Ordinal) && lhs.Value!.GetHashCode().Equals(rhs.Value!.GetHashCode()); + } + + var metric = exportedItems.First(m => TagComparator(m.MeterTags!.First(), meterTags1!.First())); 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(); + List metricPoints = []; foreach (ref readonly var mp in metric.GetMetricPoints()) { metricPoints.Add(mp); @@ -246,7 +242,21 @@ public void MetricInstrumentationScopeAttributesAreNotTreatedAsIdentifyingProper Assert.Single(metricPoints); var metricPoint1 = metricPoints[0]; - Assert.Equal(25, metricPoint1.GetSumLong()); + Assert.Equal(10, metricPoint1.GetSumLong()); + + metric = exportedItems.First(m => TagComparator(m.MeterTags!.First(), meterTags2!.First())); + Assert.Equal(meterName, metric.MeterName); + Assert.Equal(meterVersion, metric.MeterVersion); + + metricPoints = []; + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + Assert.Single(metricPoints); + metricPoint1 = metricPoints[0]; + Assert.Equal(15, metricPoint1.GetSumLong()); } [Fact] @@ -254,9 +264,9 @@ public void DuplicateInstrumentRegistration_NoViews_IdenticalInstruments() { var exportedItems = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + using var meter = new Meter(Utils.GetCurrentMethodName()); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems)); @@ -271,7 +281,7 @@ public void DuplicateInstrumentRegistration_NoViews_IdenticalInstruments() var metric = exportedItems[0]; Assert.Equal("instrumentName", metric.Name); - List metricPoints = new List(); + List metricPoints = []; foreach (ref readonly var mp in metric.GetMetricPoints()) { metricPoints.Add(mp); @@ -287,9 +297,9 @@ public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_Differe { var exportedItems = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + using var meter = new Meter(Utils.GetCurrentMethodName()); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems)); @@ -307,7 +317,7 @@ public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_Differe Assert.Equal("instrumentDescription1", metric1.Description); Assert.Equal("instrumentDescription2", metric2.Description); - List metric1MetricPoints = new List(); + List metric1MetricPoints = []; foreach (ref readonly var mp in metric1.GetMetricPoints()) { metric1MetricPoints.Add(mp); @@ -317,7 +327,7 @@ public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_Differe var metricPoint1 = metric1MetricPoints[0]; Assert.Equal(10, metricPoint1.GetSumLong()); - List metric2MetricPoints = new List(); + List metric2MetricPoints = []; foreach (ref readonly var mp in metric2.GetMetricPoints()) { metric2MetricPoints.Add(mp); @@ -333,9 +343,9 @@ public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_Differe { var exportedItems = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + using var meter = new Meter(Utils.GetCurrentMethodName()); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems)); @@ -353,7 +363,7 @@ public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_Differe Assert.Equal("instrumentUnit1", metric1.Unit); Assert.Equal("instrumentUnit2", metric2.Unit); - List metric1MetricPoints = new List(); + List metric1MetricPoints = []; foreach (ref readonly var mp in metric1.GetMetricPoints()) { metric1MetricPoints.Add(mp); @@ -363,7 +373,7 @@ public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_Differe var metricPoint1 = metric1MetricPoints[0]; Assert.Equal(10, metricPoint1.GetSumLong()); - List metric2MetricPoints = new List(); + List metric2MetricPoints = []; foreach (ref readonly var mp in metric2.GetMetricPoints()) { metric2MetricPoints.Add(mp); @@ -379,9 +389,9 @@ public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_Differe { var exportedItems = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + using var meter = new Meter(Utils.GetCurrentMethodName()); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems)); @@ -397,7 +407,7 @@ public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_Differe var metric1 = exportedItems[0]; var metric2 = exportedItems[1]; - List metric1MetricPoints = new List(); + List metric1MetricPoints = []; foreach (ref readonly var mp in metric1.GetMetricPoints()) { metric1MetricPoints.Add(mp); @@ -407,7 +417,7 @@ public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_Differe var metricPoint1 = metric1MetricPoints[0]; Assert.Equal(10, metricPoint1.GetSumLong()); - List metric2MetricPoints = new List(); + List metric2MetricPoints = []; foreach (ref readonly var mp in metric2.GetMetricPoints()) { metric2MetricPoints.Add(mp); @@ -423,9 +433,9 @@ public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_Differe { var exportedItems = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + using var meter = new Meter(Utils.GetCurrentMethodName()); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems)); @@ -441,7 +451,7 @@ public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_Differe var metric1 = exportedItems[0]; var metric2 = exportedItems[1]; - List metric1MetricPoints = new List(); + List metric1MetricPoints = []; foreach (ref readonly var mp in metric1.GetMetricPoints()) { metric1MetricPoints.Add(mp); @@ -451,7 +461,7 @@ public void DuplicateInstrumentRegistration_NoViews_DuplicateInstruments_Differe var metricPoint1 = metric1MetricPoints[0]; Assert.Equal(10, metricPoint1.GetSumLong()); - List metric2MetricPoints = new List(); + List metric2MetricPoints = []; foreach (ref readonly var mp in metric2.GetMetricPoints()) { metric2MetricPoints.Add(mp); @@ -468,10 +478,10 @@ public void DuplicateInstrumentNamesFromDifferentMetersWithSameNameDifferentVers { var exportedItems = new List(); - using var meter1 = new Meter($"{Utils.GetCurrentMethodName()}", "1.0"); - using var meter2 = new Meter($"{Utils.GetCurrentMethodName()}", "2.0"); + 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 + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddMeter(meter2.Name) .AddInMemoryExporter(exportedItems)); @@ -503,7 +513,7 @@ public void DuplicateInstrumentNamesFromDifferentMetersAreAllowed(MetricReaderTe 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 => + using var container = BuildMeterProvider(out var meterProvider, builder => { builder .AddMeter(meter1.Name) @@ -550,7 +560,7 @@ public void MeterSourcesWildcardSupportMatchTest(bool hasView) var exportedItems = new List(); - using var container = this.BuildMeterProvider(out var meterProvider, builder => + using var container = BuildMeterProvider(out var meterProvider, builder => { builder .AddMeter("AbcCompany.XyzProduct.Component?") @@ -574,7 +584,7 @@ public void MeterSourcesWildcardSupportMatchTest(bool hasView) meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.True(exportedItems.Count == 5); // "SomeCompany.SomeProduct.SomeComponent" will not be subscribed. + Assert.Equal(5, exportedItems.Count); // "SomeCompany.SomeProduct.SomeComponent" will not be subscribed. if (hasView) { @@ -602,7 +612,7 @@ public void MeterSourcesWildcardSupportNegativeTestNoMeterAdded(bool hasView) var exportedItems = new List(); - using var container = this.BuildMeterProvider(out var meterProvider, builder => + using var container = BuildMeterProvider(out var meterProvider, builder => { builder .AddInMemoryExporter(exportedItems); @@ -619,7 +629,7 @@ public void MeterSourcesWildcardSupportNegativeTestNoMeterAdded(bool hasView) meter2.CreateObservableGauge("myGauge2", () => measurement); meterProvider.ForceFlush(MaxTimeToAllowForFlush); - Assert.True(exportedItems.Count == 0); + Assert.Empty(exportedItems); } [Theory] @@ -634,7 +644,7 @@ public void CounterAggregationTest(bool exportDelta) using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); var counterLong = meter.CreateCounter("mycounter"); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { @@ -731,11 +741,11 @@ public void ObservableCounterAggregationTest(bool exportDelta) { return new List>() { - new Measurement(i++ * 10), + new(i++ * 10), }; }); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { @@ -777,19 +787,19 @@ public void ObservableCounterAggregationTest(bool exportDelta) public void ObservableCounterWithTagsAggregationTest(bool exportDelta) { var exportedItems = new List(); - var tags1 = new List> + var tags1 = new List> { new("statusCode", 200), new("verb", "get"), }; - var tags2 = new List> + var tags2 = new List> { new("statusCode", 200), new("verb", "post"), }; - var tags3 = new List> + var tags3 = new List> { new("statusCode", 500), new("verb", "get"), @@ -800,15 +810,15 @@ public void ObservableCounterWithTagsAggregationTest(bool exportDelta) "observable-counter", () => { - return new List>() + return new List> { - new Measurement(10, tags1), - new Measurement(10, tags2), - new Measurement(10, tags3), + new(10L, tags1), + new(10L, tags2), + new(10L, tags3), }; }); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { @@ -820,7 +830,7 @@ public void ObservableCounterWithTagsAggregationTest(bool exportDelta) Assert.Single(exportedItems); var metric = exportedItems[0]; Assert.Equal("observable-counter", metric.Name); - List metricPoints = new List(); + List metricPoints = []; foreach (ref readonly var mp in metric.GetMetricPoints()) { metricPoints.Add(mp); @@ -873,19 +883,19 @@ public void ObservableCounterWithTagsAggregationTest(bool exportDelta) public void ObservableCounterSpatialAggregationTest(bool exportDelta) { var exportedItems = new List(); - var tags1 = new List> + var tags1 = new List> { new("statusCode", 200), new("verb", "get"), }; - var tags2 = new List> + var tags2 = new List> { new("statusCode", 200), new("verb", "post"), }; - var tags3 = new List> + var tags3 = new List> { new("statusCode", 500), new("verb", "get"), @@ -896,27 +906,27 @@ public void ObservableCounterSpatialAggregationTest(bool exportDelta) "requestCount", () => { - return new List>() + return new List> { - new Measurement(10, tags1), - new Measurement(10, tags2), - new Measurement(10, tags3), + new(10L, tags1), + new(10L, tags2), + new(10L, tags3), }; }); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = 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() })); + .AddView("requestCount", new MetricStreamConfiguration() { TagKeys = [] })); meterProvider.ForceFlush(MaxTimeToAllowForFlush); Assert.Single(exportedItems); var metric = exportedItems[0]; Assert.Equal("requestCount", metric.Name); - List metricPoints = new List(); + List metricPoints = []; foreach (ref readonly var mp in metric.GetMetricPoints()) { metricPoints.Add(mp); @@ -924,7 +934,7 @@ public void ObservableCounterSpatialAggregationTest(bool exportDelta) Assert.Single(metricPoints); - var emptyTags = new List>(); + var emptyTags = new List>(); var metricPoint1 = metricPoints[0]; ValidateMetricPointTags(emptyTags, metricPoint1.Tags); @@ -945,7 +955,7 @@ public void UpDownCounterAggregationTest(bool exportDelta) using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); var counterLong = meter.CreateUpDownCounter("mycounter"); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { @@ -1020,13 +1030,13 @@ public void ObservableUpDownCounterAggregationTest(bool exportDelta) "observable-counter", () => { - return new List>() + return new List> { - new Measurement(i++ * 10), + new(i++ * 10L), }; }); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { @@ -1058,19 +1068,19 @@ public void ObservableUpDownCounterAggregationTest(bool exportDelta) public void ObservableUpDownCounterWithTagsAggregationTest(bool exportDelta) { var exportedItems = new List(); - var tags1 = new List> + var tags1 = new List> { new("statusCode", 200), new("verb", "get"), }; - var tags2 = new List> + var tags2 = new List> { new("statusCode", 200), new("verb", "post"), }; - var tags3 = new List> + var tags3 = new List> { new("statusCode", 500), new("verb", "get"), @@ -1081,15 +1091,15 @@ public void ObservableUpDownCounterWithTagsAggregationTest(bool exportDelta) "observable-counter", () => { - return new List>() + return new List> { - new Measurement(10, tags1), - new Measurement(10, tags2), - new Measurement(10, tags3), + new(10L, tags1), + new(10L, tags2), + new(10L, tags3), }; }); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { @@ -1101,7 +1111,7 @@ public void ObservableUpDownCounterWithTagsAggregationTest(bool exportDelta) Assert.Single(exportedItems); var metric = exportedItems[0]; Assert.Equal("observable-counter", metric.Name); - List metricPoints = new List(); + List metricPoints = []; foreach (ref readonly var mp in metric.GetMetricPoints()) { metricPoints.Add(mp); @@ -1159,7 +1169,7 @@ public void DimensionsAreOrderInsensitiveWithSortedKeysFirst(bool exportDelta) using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); var counterLong = meter.CreateCounter("Counter"); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { @@ -1177,33 +1187,33 @@ public void DimensionsAreOrderInsensitiveWithSortedKeysFirst(bool exportDelta) meterProvider.ForceFlush(MaxTimeToAllowForFlush); - List> expectedTagsForFirstMetricPoint = new List>() - { + List> expectedTagsForFirstMetricPoint = + [ new("Key1", "Value1"), new("Key2", "Value2"), new("Key3", "Value3"), - }; + ]; - List> expectedTagsForSecondMetricPoint = new List>() - { + List> expectedTagsForSecondMetricPoint = + [ new("Key1", "Value10"), new("Key2", "Value20"), new("Key3", "Value30"), - }; + ]; - List> expectedTagsForThirdMetricPoint = new List>() - { + List> expectedTagsForThirdMetricPoint = + [ new("Key4", "Value1"), new("Key5", "Value3"), new("Key6", "Value2"), - }; + ]; - List> expectedTagsForFourthMetricPoint = new List>() - { + List> expectedTagsForFourthMetricPoint = + [ new("Key4", "Value1"), new("Key5", "Value2"), new("Key6", "Value3"), - }; + ]; Assert.Equal(4, GetNumberOfMetricPoints(exportedItems)); CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFirstMetricPoint, 1); @@ -1250,7 +1260,7 @@ public void DimensionsAreOrderInsensitiveWithUnsortedKeysFirst(bool exportDelta) using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); var counterLong = meter.CreateCounter("Counter"); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { @@ -1268,33 +1278,33 @@ public void DimensionsAreOrderInsensitiveWithUnsortedKeysFirst(bool exportDelta) meterProvider.ForceFlush(MaxTimeToAllowForFlush); - List> expectedTagsForFirstMetricPoint = new List>() - { + List> expectedTagsForFirstMetricPoint = + [ new("Key1", "Value1"), new("Key2", "Value2"), new("Key3", "Value3"), - }; + ]; - List> expectedTagsForSecondMetricPoint = new List>() - { + List> expectedTagsForSecondMetricPoint = + [ new("Key1", "Value10"), new("Key2", "Value20"), new("Key3", "Value30"), - }; + ]; - List> expectedTagsForThirdMetricPoint = new List>() - { + List> expectedTagsForThirdMetricPoint = + [ new("Key4", "Value1"), new("Key5", "Value3"), new("Key6", "Value2"), - }; + ]; - List> expectedTagsForFourthMetricPoint = new List>() - { + List> expectedTagsForFourthMetricPoint = + [ new("Key4", "Value1"), new("Key5", "Value2"), new("Key6", "Value3"), - }; + ]; Assert.Equal(4, GetNumberOfMetricPoints(exportedItems)); CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFirstMetricPoint, 1); @@ -1343,7 +1353,7 @@ public void TestInstrumentDisposal(MetricReaderTemporalityPreference temporality var counter1 = meter1.CreateCounter("counterFromMeter1"); var counter2 = meter2.CreateCounter("counterFromMeter2"); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddMeter(meter2.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => @@ -1351,37 +1361,37 @@ public void TestInstrumentDisposal(MetricReaderTemporalityPreference temporality metricReaderOptions.TemporalityPreference = temporality; })); - counter1.Add(10, new KeyValuePair("key", "value")); - counter2.Add(10, new KeyValuePair("key", "value")); + 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")); + 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")); + 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")); + 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")); + counter1.Add(10, new KeyValuePair("key", "value")); + counter2.Add(10, new KeyValuePair("key", "value")); meterProvider.ForceFlush(MaxTimeToAllowForFlush); Assert.Empty(exportedItems); } @@ -1406,10 +1416,13 @@ int MetricPointCount() enumerator.MoveNext(); // Second element reserved for overflow attribute. // Validate second element is overflow attribute. - // Overflow attribute is behind experimental flag. So, it is not guaranteed to be present. var tagEnumerator = enumerator.Current.Tags.GetEnumerator(); tagEnumerator.MoveNext(); +#if NET + if (!tagEnumerator.Current.Key.Contains("otel.metric.overflow", StringComparison.Ordinal)) +#else if (!tagEnumerator.Current.Key.Contains("otel.metric.overflow")) +#endif { count++; } @@ -1426,7 +1439,7 @@ int MetricPointCount() using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{temporality}"); var counterLong = meter.CreateCounter("mycounterCapTest"); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems, metricReaderOptions => { @@ -1440,7 +1453,7 @@ int MetricPointCount() counterLong.Add(10); for (int i = 0; i < MeterProviderBuilderSdk.DefaultCardinalityLimit + 1; i++) { - counterLong.Add(10, new KeyValuePair("key", "value" + i)); + counterLong.Add(10, new KeyValuePair("key", "value" + i)); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); @@ -1450,7 +1463,7 @@ int MetricPointCount() counterLong.Add(10); for (int i = 0; i < MeterProviderBuilderSdk.DefaultCardinalityLimit + 1; i++) { - counterLong.Add(10, new KeyValuePair("key", "value" + i)); + counterLong.Add(10, new KeyValuePair("key", "value" + i)); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); @@ -1459,13 +1472,13 @@ int MetricPointCount() counterLong.Add(10); for (int i = 0; i < MeterProviderBuilderSdk.DefaultCardinalityLimit + 1; i++) { - counterLong.Add(10, new KeyValuePair("key", "value" + 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")); + 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()); @@ -1521,7 +1534,7 @@ public void InstrumentWithInvalidNameIsIgnoredTest(string instrumentName) using var meter = new Meter("InstrumentWithInvalidNameIsIgnoredTest"); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems)); @@ -1542,7 +1555,7 @@ public void InstrumentWithValidNameIsExportedTest(string name) using var meter = new Meter("InstrumentValidNameIsExportedTest"); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems)); @@ -1564,7 +1577,7 @@ 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 => + using var container = BuildMeterProvider(out var meterProvider, builder => { builder .AddMeter(meter.Name); @@ -1577,16 +1590,16 @@ public void SetupSdkProviderWithNoReader(bool hasViews) var counter = meter.CreateCounter("counter"); - counter.Add(10, new KeyValuePair("key", "value")); + counter.Add(10, new KeyValuePair("key", "value")); } [Fact] public void UnsupportedMetricInstrument() { - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddInMemoryExporter(exportedItems)); @@ -1613,37 +1626,106 @@ public void UnsupportedMetricInstrument() Assert.Empty(exportedItems); } - internal static IConfiguration BuildConfiguration(bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints) + [Fact] + public void GaugeIsExportedCorrectly() { - var configurationData = new Dictionary(); + var exportedItems = new List(); - if (emitOverflowAttribute) + using var meter = new Meter(Utils.GetCurrentMethodName()); + + using var container = BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); + + var gauge = meter.CreateGauge(name: "NoiseLevel", unit: "dB", description: "Background Noise Level"); + gauge.Record(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal("Background Noise Level", metric.Description); + List metricPoints = []; + foreach (ref readonly var mp in metric.GetMetricPoints()) { - configurationData[EmitOverFlowAttributeConfigKey] = "true"; + metricPoints.Add(mp); } - if (shouldReclaimUnusedMetricPoints) + var lastValue = metricPoints[0].GetGaugeLastValueLong(); + Assert.Equal(10, lastValue); + } + + [Theory] + [InlineData(MetricReaderTemporalityPreference.Cumulative)] + [InlineData(MetricReaderTemporalityPreference.Delta)] + public void GaugeHandlesNoNewMeasurementsCorrectlyWithTemporality(MetricReaderTemporalityPreference temporalityPreference) + { + var exportedMetrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var container = BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedMetrics, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = temporalityPreference; + })); + + var noiseLevelGauge = meter.CreateGauge(name: "NoiseLevel", unit: "dB", description: "Background Noise Level"); + noiseLevelGauge.Record(10); + + // Force a flush to export the recorded data + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + // Validate first export / flush + var firstMetric = exportedMetrics[0]; + var firstMetricPoints = new List(); + foreach (ref readonly var metricPoint in firstMetric.GetMetricPoints()) { - configurationData[ReclaimUnusedMetricPointsConfigKey] = "true"; + firstMetricPoints.Add(metricPoint); } - return new ConfigurationBuilder() - .AddInMemoryCollection(configurationData) - .Build(); + Assert.Single(firstMetricPoints); + var firstMetricPoint = firstMetricPoints[0]; + Assert.Equal(10, firstMetricPoint.GetGaugeLastValueLong()); + + // Flush the metrics again without recording any new measurements + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + // Validate second export / flush + if (temporalityPreference == MetricReaderTemporalityPreference.Cumulative) + { + // For cumulative temporality, data points should still be collected + // without any new measurements + Assert.Equal(2, exportedMetrics.Count); + var secondMetric = exportedMetrics[1]; + var secondMetricPoints = new List(); + foreach (ref readonly var metricPoint in secondMetric.GetMetricPoints()) + { + secondMetricPoints.Add(metricPoint); + } + + Assert.Single(secondMetricPoints); + var secondMetricPoint = secondMetricPoints[0]; + Assert.Equal(10, secondMetricPoint.GetGaugeLastValueLong()); + } + else if (temporalityPreference == MetricReaderTemporalityPreference.Delta) + { + // For delta temporality, no new metric should be collected + Assert.Single(exportedMetrics); + } } - private static void CounterUpdateThread(object obj) + private static void CounterUpdateThread(object? obj) where T : struct, IComparable { - if (obj is not UpdateThreadArguments arguments) - { - throw new Exception("Invalid args"); - } + var arguments = obj as UpdateThreadArguments; + Debug.Assert(arguments != null, "arguments was null"); - var mre = arguments.MreToBlockUpdateThread; + var mre = arguments!.MreToBlockUpdateThread; var mreToEnsureAllThreadsStart = arguments.MreToEnsureAllThreadsStart; - var counter = arguments.Instrument as Counter; var valueToUpdate = arguments.ValuesToRecord[0]; + + var counter = arguments.Instrument as Counter; + Debug.Assert(counter != null, "counter was null"); + if (Interlocked.Increment(ref arguments.ThreadsStartedCount) == NumberOfThreads) { mreToEnsureAllThreadsStart.Set(); @@ -1654,21 +1736,20 @@ private static void CounterUpdateThread(object obj) for (int i = 0; i < NumberOfMetricUpdateByEachThread; i++) { - counter.Add(valueToUpdate, new KeyValuePair("verb", "GET")); + counter!.Add(valueToUpdate, new KeyValuePair("verb", "GET")); } } - private static void HistogramUpdateThread(object obj) + private static void HistogramUpdateThread(object? obj) where T : struct, IComparable { - if (obj is not UpdateThreadArguments arguments) - { - throw new Exception("Invalid args"); - } + var arguments = obj as UpdateThreadArguments; + Debug.Assert(arguments != null, "arguments was null"); - var mre = arguments.MreToBlockUpdateThread; + var mre = arguments!.MreToBlockUpdateThread; var mreToEnsureAllThreadsStart = arguments.MreToEnsureAllThreadsStart; var histogram = arguments.Instrument as Histogram; + Debug.Assert(histogram != null, "histogram was null"); if (Interlocked.Increment(ref arguments.ThreadsStartedCount) == NumberOfThreads) { @@ -1682,7 +1763,7 @@ private static void HistogramUpdateThread(object obj) { for (int j = 0; j < arguments.ValuesToRecord.Length; j++) { - histogram.Record(arguments.ValuesToRecord[j]); + histogram!.Record(arguments.ValuesToRecord[j]); } } } @@ -1694,17 +1775,11 @@ private void MultithreadedCounterTest(T deltaValueUpdatedByEachCall) using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{typeof(T).Name}.{deltaValueUpdatedByEachCall}"); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = 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), - }; + var argToThread = new UpdateThreadArguments(new ManualResetEvent(false), new ManualResetEvent(false), meter.CreateCounter("counter"), [deltaValueUpdatedByEachCall]); Thread[] t = new Thread[NumberOfThreads]; for (int i = 0; i < NumberOfThreads; i++) @@ -1746,21 +1821,17 @@ private void MultithreadedHistogramTest(long[] expected, T[] values) var bucketCounts = new long[11]; var metrics = new List(); +#pragma warning disable CA2000 // Dispose objects before losing scope var metricReader = new BaseExportingMetricReader(new InMemoryExporter(metrics)); +#pragma warning restore CA2000 // Dispose objects before losing scope using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{typeof(T).Name}"); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = 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, - }; + var argsToThread = new UpdateThreadArguments(new ManualResetEvent(false), new ManualResetEvent(false), meter.CreateHistogram("histogram"), values); Thread[] t = new Thread[NumberOfThreads]; for (int i = 0; i < NumberOfThreads; i++) @@ -1793,7 +1864,7 @@ private void MultithreadedHistogramTest(long[] expected, T[] values) Assert.Equal(expected, bucketCounts); } - private class UpdateThreadArguments + private sealed class UpdateThreadArguments where T : struct, IComparable { public ManualResetEvent MreToBlockUpdateThread; @@ -1801,37 +1872,13 @@ private class UpdateThreadArguments 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) - { + public UpdateThreadArguments(ManualResetEvent mreToBlockUpdateThread, ManualResetEvent mreToEnsureAllThreadsStart, Instrument instrument, T[] valuesToRecord) + { + this.MreToBlockUpdateThread = mreToBlockUpdateThread; + this.MreToEnsureAllThreadsStart = mreToEnsureAllThreadsStart; + this.Instrument = instrument; + this.ValuesToRecord = valuesToRecord; + } } } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs index fe7fb5244d9..e46b3e5dc18 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using System.Diagnostics; using System.Diagnostics.Metrics; using Microsoft.Extensions.Configuration; @@ -38,7 +36,7 @@ public void TestExemplarFilterSetFromConfiguration( }); } - using var container = this.BuildMeterProvider(out var meterProvider, b => + using var container = BuildMeterProvider(out var meterProvider, b => { b.ConfigureServices( s => s.AddSingleton(configBuilder.Build())); @@ -71,16 +69,16 @@ public void TestExemplarsCounter(MetricReaderTemporalityPreference temporality) DateTime testStartTime = DateTime.UtcNow; var exportedItems = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + 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 + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .SetExemplarFilter(ExemplarFilterType.AlwaysOn) .AddView(i => { - if (i.Name.StartsWith("testCounter")) + if (i.Name.StartsWith("testCounter", StringComparison.Ordinal)) { return new MetricStreamConfiguration { @@ -116,7 +114,8 @@ public void TestExemplarsCounter(MetricReaderTemporalityPreference temporality) var secondMeasurementValues = GenerateRandomValues(1, true, measurementValues); foreach (var value in secondMeasurementValues) { - using var act = new Activity("test").Start(); + using var activity = new Activity("test"); + activity.Start(); counterDouble.Add(value.Value); counterLong.Add((long)value.Value); } @@ -186,21 +185,21 @@ public void TestExemplarsObservable(MetricReaderTemporalityPreference temporalit DateTime testStartTime = DateTime.UtcNow; var exportedItems = new List(); - (double Value, bool ExpectTraceId)[] measurementValues = new (double Value, bool ExpectTraceId)[] - { + (double Value, bool ExpectTraceId)[] measurementValues = + [ (18D, false), - (19D, false), - }; + (19D, false) + ]; int measurementIndex = 0; - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + 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 + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .SetExemplarFilter(ExemplarFilterType.AlwaysOn) .AddInMemoryExporter(exportedItems, metricReaderOptions => @@ -277,7 +276,7 @@ public void TestExemplarsHistogramWithBuckets(MetricReaderTemporalityPreference DateTime testStartTime = DateTime.UtcNow; var exportedItems = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + using var meter = new Meter(Utils.GetCurrentMethodName()); var histogramWithBucketsAndMinMaxDouble = meter.CreateHistogram("histogramWithBucketsAndMinMaxDouble"); var histogramWithBucketsDouble = meter.CreateHistogram("histogramWithBucketsDouble"); var histogramWithBucketsAndMinMaxLong = meter.CreateHistogram("histogramWithBucketsAndMinMaxLong"); @@ -295,7 +294,7 @@ public void TestExemplarsHistogramWithBuckets(MetricReaderTemporalityPreference }); } - using var container = this.BuildMeterProvider(out var meterProvider, builder => + using var container = BuildMeterProvider(out var meterProvider, builder => { if (string.IsNullOrEmpty(configValue)) { @@ -307,7 +306,7 @@ public void TestExemplarsHistogramWithBuckets(MetricReaderTemporalityPreference .AddMeter(meter.Name) .AddView(i => { - if (i.Name.StartsWith("histogramWithBucketsAndMinMax")) + if (i.Name.StartsWith("histogramWithBucketsAndMinMax", StringComparison.Ordinal)) { return new ExplicitBucketHistogramConfiguration { @@ -331,7 +330,7 @@ public void TestExemplarsHistogramWithBuckets(MetricReaderTemporalityPreference var measurementValues = buckets /* 2000 is here to test overflow measurement */ - .Concat(new double[] { 2000 }) + .Concat([2000.0]) .Select(b => (Value: b, ExpectTraceId: false)) .ToArray(); foreach (var value in measurementValues) @@ -358,7 +357,8 @@ public void TestExemplarsHistogramWithBuckets(MetricReaderTemporalityPreference var secondMeasurementValues = buckets.Take(1).Select(b => (Value: b, ExpectTraceId: true)).ToArray(); foreach (var value in secondMeasurementValues) { - using var act = new Activity("test").Start(); + using var activity = new Activity("test"); + activity.Start(); histogramWithBucketsAndMinMaxDouble.Record(value.Value); histogramWithBucketsDouble.Record(value.Value); histogramWithBucketsAndMinMaxLong.Record((long)value.Value); @@ -427,22 +427,22 @@ public void TestExemplarsHistogramWithoutBuckets(MetricReaderTemporalityPreferen DateTime testStartTime = DateTime.UtcNow; var exportedItems = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + 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 + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .SetExemplarFilter(ExemplarFilterType.AlwaysOn) .AddView(i => { - if (i.Name.StartsWith("histogramWithoutBucketsAndMinMax")) + if (i.Name.StartsWith("histogramWithoutBucketsAndMinMax", StringComparison.Ordinal)) { return new ExplicitBucketHistogramConfiguration { - Boundaries = Array.Empty(), + Boundaries = [], ExemplarReservoirFactory = () => new SimpleFixedSizeExemplarReservoir(3), }; } @@ -450,7 +450,7 @@ public void TestExemplarsHistogramWithoutBuckets(MetricReaderTemporalityPreferen { return new ExplicitBucketHistogramConfiguration { - Boundaries = Array.Empty(), + Boundaries = [], RecordMinMax = false, ExemplarReservoirFactory = () => new SimpleFixedSizeExemplarReservoir(3), }; @@ -486,7 +486,8 @@ public void TestExemplarsHistogramWithoutBuckets(MetricReaderTemporalityPreferen var secondMeasurementValues = GenerateRandomValues(1, true, measurementValues); foreach (var value in secondMeasurementValues) { - using var act = new Activity("test").Start(); + using var activity = new Activity("test"); + activity.Start(); histogramWithoutBucketsAndMinMaxDouble.Record(value.Value); histogramWithoutBucketsDouble.Record(value.Value); histogramWithoutBucketsAndMinMaxLong.Record((long)value.Value); @@ -555,18 +556,18 @@ public void TestExemplarsExponentialHistogram(MetricReaderTemporalityPreference DateTime testStartTime = DateTime.UtcNow; var exportedItems = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + 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 + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .SetExemplarFilter(ExemplarFilterType.AlwaysOn) .AddView(i => { - if (i.Name.StartsWith("exponentialHistogramWithMinMax")) + if (i.Name.StartsWith("exponentialHistogramWithMinMax", StringComparison.Ordinal)) { return new Base2ExponentialBucketHistogramConfiguration(); } @@ -608,7 +609,8 @@ public void TestExemplarsExponentialHistogram(MetricReaderTemporalityPreference var secondMeasurementValues = GenerateRandomValues(1, true, measurementValues); foreach (var value in secondMeasurementValues) { - using var act = new Activity("test").Start(); + using var activity = new Activity("test"); + activity.Start(); exponentialHistogramWithMinMaxDouble.Record(value.Value); exponentialHistogramDouble.Record(value.Value); exponentialHistogramWithMinMaxLong.Record((long)value.Value); @@ -676,19 +678,20 @@ public void TestTraceBasedExemplarFilter(bool enableTracing) { var exportedItems = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + using var meter = new Meter(Utils.GetCurrentMethodName()); var counter = meter.CreateCounter("testCounter"); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = 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; + using var activity = new Activity("test"); + activity.Start(); + activity.ActivityTraceFlags = ActivityTraceFlags.Recorded; counter.Add(18); } else @@ -723,20 +726,20 @@ public void TestExemplarsFilterTags(bool enableTagFiltering) { var exportedItems = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + 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 + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .SetExemplarFilter(ExemplarFilterType.AlwaysOn) .AddView( histogram.Name, new MetricStreamConfiguration() { - TagKeys = enableTagFiltering ? new string[] { "key1" } : null, + TagKeys = enableTagFiltering ? ["key1"] : null, ExemplarReservoirFactory = () => { if (testExemplarReservoir != null) @@ -801,7 +804,9 @@ private static (double Value, bool ExpectTraceId)[] GenerateRandomValues( var values = new (double, bool)[count]; for (int i = 0; i < count; i++) { +#pragma warning disable CA5394 // Do not use insecure randomness var nextValue = random.NextDouble() * 100_000; +#pragma warning restore CA5394 // Do not use insecure randomness if (values.Any(m => m.Item1 == nextValue || m.Item1 == (long)nextValue) || previousValues?.Any(m => m.Value == nextValue || m.Value == (long)nextValue) == true) { @@ -824,13 +829,15 @@ private static void ValidateExemplars( { int count = 0; + var measurements = measurementValues.ToArray(); + 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)); + var measurement = measurements.FirstOrDefault(v => v.Value == getExemplarValueFunc(exemplar) + || (long)v.Value == getExemplarValueFunc(exemplar)); Assert.NotEqual(default, measurement); if (measurement.ExpectTraceId) { @@ -846,7 +853,7 @@ private static void ValidateExemplars( count++; } - Assert.Equal(measurementValues.Count(), count); + Assert.Equal(measurements.Length, count); } private sealed class TestExemplarReservoir : FixedSizeExemplarReservoir diff --git a/test/OpenTelemetry.Tests/Metrics/MetricExporterTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricExporterTests.cs index 185679acac9..fc202267f1f 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricExporterTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricExporterTests.cs @@ -13,11 +13,12 @@ public class MetricExporterTests [InlineData(ExportModes.Pull | ExportModes.Push)] public void FlushMetricExporterTest(ExportModes mode) { - BaseExporter exporter = null; + BaseExporter? exporter; switch (mode) { case ExportModes.Push: +#pragma warning disable CA2000 // Dispose objects before losing scope exporter = new PushOnlyMetricExporter(); break; case ExportModes.Pull: @@ -26,9 +27,12 @@ public void FlushMetricExporterTest(ExportModes mode) case ExportModes.Pull | ExportModes.Push: exporter = new PushPullMetricExporter(); break; + default: + throw new NotSupportedException($"Export mode '{mode}' is not supported"); } var reader = new BaseExportingMetricReader(exporter); +#pragma warning restore CA2000 // Dispose objects before losing scope using var meterProvider = Sdk.CreateMeterProviderBuilder() .AddReader(reader) .Build(); @@ -42,7 +46,7 @@ public void FlushMetricExporterTest(ExportModes mode) case ExportModes.Pull: Assert.False(reader.Collect()); Assert.False(meterProvider.ForceFlush()); - Assert.True((exporter as IPullMetricExporter).Collect(-1)); + Assert.True((exporter as IPullMetricExporter)?.Collect?.Invoke(-1) ?? false); break; case ExportModes.Pull | ExportModes.Push: Assert.True(reader.Collect()); @@ -52,7 +56,7 @@ public void FlushMetricExporterTest(ExportModes mode) } [ExportModes(ExportModes.Push)] - private class PushOnlyMetricExporter : BaseExporter + private sealed class PushOnlyMetricExporter : BaseExporter { public override ExportResult Export(in Batch batch) { @@ -61,15 +65,9 @@ public override ExportResult Export(in Batch batch) } [ExportModes(ExportModes.Pull)] - private class PullOnlyMetricExporter : BaseExporter, IPullMetricExporter + private sealed class PullOnlyMetricExporter : BaseExporter, IPullMetricExporter { - private Func funcCollect; - - public Func Collect - { - get => this.funcCollect; - set { this.funcCollect = value; } - } + public Func? Collect { get; set; } public override ExportResult Export(in Batch batch) { @@ -78,7 +76,7 @@ public override ExportResult Export(in Batch batch) } [ExportModes(ExportModes.Pull | ExportModes.Push)] - private class PushPullMetricExporter : BaseExporter + private sealed class PushPullMetricExporter : BaseExporter { public override ExportResult Export(in Batch batch) { diff --git a/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTests.cs similarity index 63% rename from test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTestsBase.cs rename to test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTests.cs index 6e929d1468a..2652d3de0c9 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricOverflowAttributeTests.cs @@ -2,136 +2,13 @@ // 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 +public class MetricOverflowAttributeTests { - 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)] - [InlineData(2)] - [InlineData(10)] - public void EmitOverflowAttributeIsNotDependentOnMaxMetricPoints(int maxMetricPoints) - { - 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.True(exportedItems[0].AggregatorStore.EmitOverflowAttribute); - } - [Theory] [InlineData(MetricReaderTemporalityPreference.Delta)] [InlineData(MetricReaderTemporalityPreference.Cumulative)] @@ -143,10 +20,6 @@ public void MetricOverflowAttributeIsRecordedCorrectlyForCounter(MetricReaderTem 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(); @@ -164,7 +37,7 @@ public void MetricOverflowAttributeIsRecordedCorrectlyForCounter(MetricReaderTem { // 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)); + counter.Add(10, new KeyValuePair("Key", i)); } meterProvider.ForceFlush(); @@ -186,7 +59,7 @@ public void MetricOverflowAttributeIsRecordedCorrectlyForCounter(MetricReaderTem exportedItems.Clear(); metricPoints.Clear(); - counter.Add(5, new KeyValuePair("Key", 2000)); // Emit a metric to exceed the max MetricPoint limit + counter.Add(5, new KeyValuePair("Key", 2000)); // Emit a metric to exceed the max MetricPoint limit meterProvider.ForceFlush(); metric = exportedItems[0]; @@ -217,7 +90,7 @@ public void MetricOverflowAttributeIsRecordedCorrectlyForCounter(MetricReaderTem // Emit 2500 more newer MetricPoints with distinct dimension combinations for (int i = 2001; i < 4501; i++) { - counter.Add(5, new KeyValuePair("Key", i)); + counter.Add(5, new KeyValuePair("Key", i)); } meterProvider.ForceFlush(); @@ -237,15 +110,8 @@ public void MetricOverflowAttributeIsRecordedCorrectlyForCounter(MetricReaderTem int expectedSum; // Number of metric points that were available before the 2500 measurements were made = 2000 (max MetricPoints) - if (this.shouldReclaimUnusedMetricPoints) - { - // If unused metric points are reclaimed, then number of metric points dropped = 2500 - 2000 = 500 - expectedSum = 2500; // 500 * 5 - } - else - { - expectedSum = 12500; // 2500 * 5 - } + // Because unused metric points are reclaimed, number of metric points dropped = 2500 - 2000 = 500 + expectedSum = 2500; // 500 * 5 Assert.Equal(expectedSum, overflowMetricPoint.GetSumLong()); } @@ -294,10 +160,6 @@ public void MetricOverflowAttributeIsRecordedCorrectlyForHistogram(MetricReaderT 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(); @@ -315,7 +177,7 @@ public void MetricOverflowAttributeIsRecordedCorrectlyForHistogram(MetricReaderT { // 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)); + histogram.Record(10, new KeyValuePair("Key", i)); } meterProvider.ForceFlush(); @@ -337,7 +199,7 @@ public void MetricOverflowAttributeIsRecordedCorrectlyForHistogram(MetricReaderT exportedItems.Clear(); metricPoints.Clear(); - histogram.Record(5, new KeyValuePair("Key", 2000)); // Emit a metric to exceed the max MetricPoint limit + histogram.Record(5, new KeyValuePair("Key", 2000)); // Emit a metric to exceed the max MetricPoint limit meterProvider.ForceFlush(); metric = exportedItems[0]; @@ -368,7 +230,7 @@ public void MetricOverflowAttributeIsRecordedCorrectlyForHistogram(MetricReaderT // Emit 2500 more newer MetricPoints with distinct dimension combinations for (int i = 2001; i < 4501; i++) { - histogram.Record(5, new KeyValuePair("Key", i)); + histogram.Record(5, new KeyValuePair("Key", i)); } meterProvider.ForceFlush(); @@ -389,17 +251,9 @@ public void MetricOverflowAttributeIsRecordedCorrectlyForHistogram(MetricReaderT int expectedSum; // Number of metric points that were available before the 2500 measurements were made = 2000 (max MetricPoints) - if (this.shouldReclaimUnusedMetricPoints) - { - // If unused metric points are reclaimed, then number of metric points dropped = 2500 - 2000 = 500 - expectedCount = 500; - expectedSum = 2500; // 500 * 5 - } - else - { - expectedCount = 2500; - expectedSum = 12500; // 2500 * 5 - } + // Because unused metric points are reclaimed, number of metric points dropped = 2500 - 2000 = 500 + expectedCount = 500; + expectedSum = 2500; // 500 * 5 Assert.Equal(expectedCount, overflowMetricPoint.GetHistogramCount()); Assert.Equal(expectedSum, overflowMetricPoint.GetHistogramSum()); @@ -439,19 +293,3 @@ public void MetricOverflowAttributeIsRecordedCorrectlyForHistogram(MetricReaderT } } } - -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/MetricPointReclaimTests.cs similarity index 64% rename from test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTestsBase.cs rename to test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTests.cs index fa33298643e..3ffcd82d6d9 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTests.cs @@ -2,100 +2,19 @@ // 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 +public class MetricPointReclaimTests { - 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()); + using var meter = new Meter(Utils.GetCurrentMethodName()); var counter = meter.CreateCounter("MyFruitCounter"); int numberOfUpdateThreads = 25; @@ -108,10 +27,6 @@ public void MeasurementsAreNotDropped(bool emitMetricWithNoDimensions) }; using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) .AddMeter(Utils.GetCurrentMethodName()) .AddReader(metricReader) .Build(); @@ -132,13 +47,15 @@ void EmitMetric(object obj) } // There are separate code paths for single dimension vs multiple dimensions +#pragma warning disable CA5394 // Do not use insecure randomness if (random.Next(2) == 0) +#pragma warning restore CA5394 // Do not use insecure randomness { - counter.Add(100, new KeyValuePair("key", $"value{i}")); + counter.Add(100, new KeyValuePair("key", $"value{i}")); } else { - counter.Add(100, new KeyValuePair("key", $"value{i}"), new KeyValuePair("dimensionKey", "dimensionValue")); + counter.Add(100, new KeyValuePair("key", $"value{i}"), new KeyValuePair("dimensionKey", "dimensionValue")); } Thread.Sleep(25); @@ -185,7 +102,7 @@ void EmitMetric(object obj) [InlineData(true)] public void MeasurementsAreAggregatedEvenAfterTheyAreDropped(bool emitMetricWithNoDimension) { - var meter = new Meter(Utils.GetCurrentMethodName()); + using var meter = new Meter(Utils.GetCurrentMethodName()); var counter = meter.CreateCounter("MyFruitCounter"); long sum = 0; @@ -201,23 +118,18 @@ public void MeasurementsAreAggregatedEvenAfterTheyAreDropped(bool emitMetricWith }; 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. + // Note that one MetricPoint is reserved for zero tags and one MetricPoint is reserved for the overflow tag. // 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}")); + counter.Add(100, new KeyValuePair("key", $"value{i}")); } meterProvider.ForceFlush(); @@ -240,9 +152,11 @@ void EmitMetric() Interlocked.Add(ref sum, 25); } +#pragma warning disable CA5394 // Do not use insecure randomness var index = random.Next(measurementValues.Length); +#pragma warning restore CA5394 // Do not use insecure randomness var measurement = measurementValues[index]; - counter.Add(measurement, new KeyValuePair("key", $"value{index}")); + counter.Add(measurement, new KeyValuePair("key", $"value{index}")); Interlocked.Add(ref sum, measurement); numberOfMeasurements++; @@ -280,7 +194,7 @@ private sealed class ThreadArguments private sealed class CustomExporter : BaseExporter { - public long Sum = 0; + public long Sum; private readonly bool assertNoDroppedMeasurements; @@ -303,6 +217,7 @@ public override ExportResult Export(in Batch batch) } // This is to ensure that the lookup dictionary does not have unbounded growth + Assert.NotNull(metricPointLookupDictionary); Assert.True(metricPointLookupDictionary.Count <= (MeterProviderBuilderSdk.DefaultCardinalityLimit * 2)); foreach (ref readonly var metricPoint in metric.GetMetricPoints()) @@ -326,19 +241,3 @@ public override ExportResult Export(in Batch batch) } } } - -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/MetricSnapshotTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricSnapshotTests.cs similarity index 82% rename from test/OpenTelemetry.Tests/Metrics/MetricSnapshotTestsBase.cs rename to test/OpenTelemetry.Tests/Metrics/MetricSnapshotTests.cs index 08ce90ffe44..0e8bc481f7e 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricSnapshotTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricSnapshotTests.cs @@ -2,27 +2,14 @@ // 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 +public class MetricSnapshotTests { - private readonly IConfiguration configuration; - - protected MetricSnapshotTestsBase(bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints) - { - this.configuration = MetricApiTestsBase.BuildConfiguration( - emitOverflowAttribute, - shouldReclaimUnusedMetricPoints); - } - [Fact] public void VerifySnapshot_Counter() { @@ -32,10 +19,6 @@ public void VerifySnapshot_Counter() using var meter = new Meter(Utils.GetCurrentMethodName()); var counter = meter.CreateCounter("meter"); using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedMetrics) .AddInMemoryExporter(exportedSnapshots) @@ -105,10 +88,6 @@ public void VerifySnapshot_Histogram() using var meter = new Meter(Utils.GetCurrentMethodName()); var histogram = meter.CreateHistogram("histogram"); using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) .AddMeter(meter.Name) .AddInMemoryExporter(exportedMetrics) .AddInMemoryExporter(exportedSnapshots) @@ -201,10 +180,6 @@ public void VerifySnapshot_ExponentialHistogram() using var meter = new Meter(Utils.GetCurrentMethodName()); var histogram = meter.CreateHistogram("histogram"); using var meterProvider = Sdk.CreateMeterProviderBuilder() - .ConfigureServices(services => - { - services.AddSingleton(this.configuration); - }) .AddMeter(meter.Name) .AddView("histogram", new Base2ExponentialBucketHistogramConfiguration()) .AddInMemoryExporter(exportedMetrics) @@ -227,7 +202,7 @@ public void VerifySnapshot_ExponentialHistogram() metricPoint1.TryGetHistogramMinMaxValues(out var min, out var max); Assert.Equal(10, min); Assert.Equal(10, max); - AggregatorTestsBase.AssertExponentialBucketsAreCorrect(expectedHistogram, metricPoint1.GetExponentialHistogramData()); + AggregatorTests.AssertExponentialBucketsAreCorrect(expectedHistogram, metricPoint1.GetExponentialHistogramData()); // Verify Snapshot 1 Assert.Single(exportedSnapshots); @@ -238,7 +213,7 @@ public void VerifySnapshot_ExponentialHistogram() snapshot1.MetricPoints[0].TryGetHistogramMinMaxValues(out min, out max); Assert.Equal(10, min); Assert.Equal(10, max); - AggregatorTestsBase.AssertExponentialBucketsAreCorrect(expectedHistogram, snapshot1.MetricPoints[0].GetExponentialHistogramData()); + AggregatorTests.AssertExponentialBucketsAreCorrect(expectedHistogram, snapshot1.MetricPoints[0].GetExponentialHistogramData()); // Verify Metric == Snapshot Assert.Equal(metric1.Name, snapshot1.Name); @@ -272,7 +247,7 @@ public void VerifySnapshot_ExponentialHistogram() metricPoint1.TryGetHistogramMinMaxValues(out min, out max); Assert.Equal(5, min); Assert.Equal(10, max); - AggregatorTestsBase.AssertExponentialBucketsAreCorrect(expectedHistogram, metricPoint2.GetExponentialHistogramData()); + AggregatorTests.AssertExponentialBucketsAreCorrect(expectedHistogram, metricPoint2.GetExponentialHistogramData()); // Verify Snapshot 1 after second export // This value is expected to be unchanged. @@ -291,38 +266,6 @@ public void VerifySnapshot_ExponentialHistogram() 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) - { + AggregatorTests.AssertExponentialBucketsAreCorrect(expectedHistogram, snapshot2.MetricPoints[0].GetExponentialHistogramData()); } } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricTestData.cs b/test/OpenTelemetry.Tests/Metrics/MetricTestData.cs index 669212a103a..0a8e45630c5 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricTestData.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricTestData.cs @@ -1,59 +1,61 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using Xunit; + namespace OpenTelemetry.Metrics.Tests; -public class MetricTestData +internal static 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', 256) }, - new object[] { "a\xb5" }, // `\xb5` is the Micro character - }; + public static TheoryData InvalidInstrumentNames + => + [ + " ", + "-first-char-not-alphabetic", + "1first-char-not-alphabetic", + "invalid+separator", + new('m', 256), + "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', 255) }, - new object[] { "CaSe-InSeNsItIvE" }, - new object[] { "my_metric/environment/database" }, - }; + public static TheoryData ValidInstrumentNames + => + [ + "m", + "first-char-alphabetic", + "my-2-instrument", + "my.metric", + "my_metric2", + new('m', 255), + "CaSe-InSeNsItIvE", + "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 TheoryData InvalidHistogramBoundaries + => + [ + [0.0, 0.0], + [1.0, 0.0], + [0.0, 1.0, 1.0, 2.0], + [0.0, 1.0, 2.0, -1.0], + ]; - 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 TheoryData ValidHistogramMinMax => + new() + { + { [-10.0, 0.0, 1.0, 9.0, 10.0, 11.0, 19.0], new HistogramConfiguration(), -10.0, 19.0 }, + { [double.NegativeInfinity], new HistogramConfiguration(), double.NegativeInfinity, double.NegativeInfinity }, + { [double.NegativeInfinity, 0.0, double.PositiveInfinity], new HistogramConfiguration(), double.NegativeInfinity, double.PositiveInfinity }, + { [1.0], new HistogramConfiguration(), 1.0, 1.0 }, + { [5.0, 100.0, 4.0, 101.0, -2.0, 97.0], new ExplicitBucketHistogramConfiguration { Boundaries = [10.0, 20.0] }, -2.0, 101.0 }, + { [5.0, 100.0, 4.0, 101.0, -2.0, 97.0], new Base2ExponentialBucketHistogramConfiguration(), 4.0, 101.0 }, + }; - public static IEnumerable InvalidHistogramMinMax - => new List + public static TheoryData InvalidHistogramMinMax + => new() { - 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 } }, + { [1.0], new HistogramConfiguration { RecordMinMax = false } }, + { [1.0], new ExplicitBucketHistogramConfiguration { Boundaries = [10.0, 20.0], RecordMinMax = false } }, + { [1.0], new Base2ExponentialBucketHistogramConfiguration { RecordMinMax = false } }, }; } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs index 7d72b773ea6..9ca73b30b43 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs @@ -4,39 +4,32 @@ #if BUILDING_HOSTING_TESTS using System.Diagnostics; #endif +#if BUILDING_HOSTING_TESTS using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -#if BUILDING_HOSTING_TESTS using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.Hosting; #endif +using OpenTelemetry.Internal; using Xunit; namespace OpenTelemetry.Metrics.Tests; -public class MetricTestsBase +#pragma warning disable CA1515 // Consider making public types internal +public abstract class MetricTestsBase +#pragma warning restore CA1515 // Consider making public types internal { - 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) + Action? configureAppConfiguration = null, + Action? configureServices = null, + Action? configureMetricsBuilder = null, + Action? configureMeterProviderBuilder = null) { var hostBuilder = new HostBuilder() .ConfigureDefaults(null) @@ -74,15 +67,15 @@ public static IHost BuildHost( return host; - static void ConfigureBuilder(MeterProviderBuilder builder, Action configureMeterProviderBuilder) + static void ConfigureBuilder(MeterProviderBuilder builder, Action? configureMeterProviderBuilder) { - IServiceCollection localServices = null; + IServiceCollection? localServices = null; builder.ConfigureServices(services => localServices = services); Debug.Assert(localServices != null, "localServices was null"); - var testBuilder = new HostingMeterProviderBuilder(localServices); + var testBuilder = new HostingMeterProviderBuilder(localServices!); configureMeterProviderBuilder?.Invoke(testBuilder); } } @@ -90,7 +83,7 @@ static void ConfigureBuilder(MeterProviderBuilder builder, Action> expectedTags, ReadOnlyTagCollection actualTags) + internal static void ValidateMetricPointTags(List> expectedTags, ReadOnlyTagCollection actualTags) { int tagIndex = 0; foreach (var tag in actualTags) @@ -103,7 +96,7 @@ public static void ValidateMetricPointTags(List> ex Assert.Equal(expectedTags.Count, tagIndex); } - public static long GetLongSum(List metrics) + internal static long GetLongSum(List metrics) { long sum = 0; foreach (var metric in metrics) @@ -124,7 +117,7 @@ public static long GetLongSum(List metrics) return sum; } - public static double GetDoubleSum(List metrics) + internal static double GetDoubleSum(List metrics) { double sum = 0; foreach (var metric in metrics) @@ -145,7 +138,7 @@ public static double GetDoubleSum(List metrics) return sum; } - public static int GetNumberOfMetricPoints(List metrics) + internal static int GetNumberOfMetricPoints(List metrics) { int count = 0; foreach (var metric in metrics) @@ -159,7 +152,7 @@ public static int GetNumberOfMetricPoints(List metrics) return count; } - public static MetricPoint? GetFirstMetricPoint(IEnumerable metrics) + internal static MetricPoint? GetFirstMetricPoint(IEnumerable metrics) { foreach (var metric in metrics) { @@ -175,7 +168,7 @@ public static int GetNumberOfMetricPoints(List metrics) // 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) + internal static void CheckTagsForNthMetricPoint(List metrics, List> tags, int n) { var metric = metrics[0]; var metricPointEnumerator = metric.GetMetricPoints().GetEnumerator(); @@ -195,90 +188,47 @@ public static void CheckTagsForNthMetricPoint(List metrics, List GetExemplars(MetricPoint mp) + { + return mp.TryGetExemplars(out var exemplars) + ? exemplars.ToReadOnlyList() + : []; + } + + internal static IDisposable BuildMeterProvider( out MeterProvider meterProvider, Action configure) { - if (configure == null) - { - throw new ArgumentNullException(nameof(configure)); - } + Guard.ThrowIfNull(configure); #if BUILDING_HOSTING_TESTS var host = BuildHost( useWithMetricsStyle: false, - configureMeterProviderBuilder: configure, - configureServices: services => - { - if (this.configuration != null) - { - services.AddSingleton(this.configuration); - } - }); + configureMeterProviderBuilder: configure); - meterProvider = host.Services.GetService(); + meterProvider = host.Services.GetRequiredService(); 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); - } - } - +#pragma warning disable CA1812 // Avoid uninstantiated internal classes private sealed class MetricsSubscriptionManagerCleanupHostedService : IHostedService, IDisposable +#pragma warning restore CA1812 // Avoid uninstantiated internal classes { private readonly object metricsSubscriptionManager; public MetricsSubscriptionManagerCleanupHostedService(IServiceProvider serviceProvider) { - this.metricsSubscriptionManager = serviceProvider.GetService( - typeof(ConsoleMetrics).Assembly.GetType("Microsoft.Extensions.Diagnostics.Metrics.MetricsSubscriptionManager")); + this.metricsSubscriptionManager = serviceProvider.GetRequiredService( + typeof(ConsoleMetrics).Assembly.GetType("Microsoft.Extensions.Diagnostics.Metrics.MetricsSubscriptionManager")!); if (this.metricsSubscriptionManager == null) { @@ -292,7 +242,7 @@ public void Dispose() // 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); + this.metricsSubscriptionManager.GetType().GetMethod("Dispose")!.Invoke(this.metricsSubscriptionManager, null); } public Task StartAsync(CancellationToken cancellationToken) diff --git a/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs index c1a0fca281b..96a1122be39 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs @@ -18,7 +18,7 @@ public void ViewToRenameMetric() using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("name1", "renamed") .AddInMemoryExporter(exportedItems)); @@ -40,31 +40,31 @@ public void AddViewWithInvalidNameThrowsArgumentException(string viewNewName) using var meter1 = new Meter("AddViewWithInvalidNameThrowsArgumentException"); - var ex = Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder + var ex = Assert.Throws(() => BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView("name1", viewNewName) .AddInMemoryExporter(exportedItems))); - Assert.Contains($"Custom view name {viewNewName} is invalid.", ex.Message); + Assert.Contains($"Custom view name {viewNewName} is invalid.", ex.Message, StringComparison.Ordinal); - ex = Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder + ex = Assert.Throws(() => BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView("name1", new MetricStreamConfiguration() { Name = viewNewName }) .AddInMemoryExporter(exportedItems))); - Assert.Contains($"Custom view name {viewNewName} is invalid.", ex.Message); + Assert.Contains($"Custom view name {viewNewName} is invalid.", ex.Message, StringComparison.Ordinal); } [Fact] - public void AddViewWithNullMetricStreamConfigurationThrowsArgumentnullException() + public void AddViewWithNullMetricStreamConfigurationThrowsArgumentNullException() { var exportedItems = new List(); using var meter1 = new Meter("AddViewWithInvalidNameThrowsArgumentException"); - Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder + Assert.Throws(() => BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) - .AddView("name1", (MetricStreamConfiguration)null) + .AddView("name1", (MetricStreamConfiguration)null!) .AddInMemoryExporter(exportedItems))); } @@ -75,7 +75,7 @@ public void AddViewWithNameThrowsInvalidArgumentExceptionWhenConflict() using var meter1 = new Meter("AddViewWithGuaranteedConflictThrowsInvalidArgumentException"); - Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder + Assert.Throws(() => BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView("instrumenta.*", name: "newname") .AddInMemoryExporter(exportedItems))); @@ -88,7 +88,7 @@ public void AddViewWithNameInMetricStreamConfigurationThrowsInvalidArgumentExcep using var meter1 = new Meter("AddViewWithGuaranteedConflictThrowsInvalidArgumentException"); - Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder + Assert.Throws(() => BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView("instrumenta.*", new MetricStreamConfiguration() { Name = "newname" }) .AddInMemoryExporter(exportedItems))); @@ -100,9 +100,9 @@ public void AddViewWithExceptionInUserCallbackAppliedDefault() var exportedItems = new List(); using var meter1 = new Meter("AddViewWithExceptionInUserCallback"); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) - .AddView((instrument) => { throw new Exception("bad"); }) + .AddView(_ => { throw new InvalidOperationException("bad"); }) .AddInMemoryExporter(exportedItems)); using (var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log)) @@ -127,9 +127,9 @@ public void AddViewWithExceptionInUserCallbackNoDefault() var exportedItems = new List(); using var meter1 = new Meter("AddViewWithExceptionInUserCallback"); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) - .AddView((instrument) => { throw new Exception("bad"); }) + .AddView(_ => { throw new InvalidOperationException("bad"); }) .AddView("*", MetricStreamConfiguration.Drop) .AddInMemoryExporter(exportedItems)); @@ -137,7 +137,7 @@ public void AddViewWithExceptionInUserCallbackNoDefault() { var counter1 = meter1.CreateCounter("counter1"); counter1.Add(1); - Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 41)); + Assert.Single(inMemoryEventListener.Events, e => e.EventId == 41); } meterProvider.ForceFlush(MaxTimeToAllowForFlush); @@ -154,10 +154,10 @@ public void AddViewsWithAndWithoutExceptionInUserCallback() var exportedItems = new List(); using var meter1 = new Meter("AddViewWithExceptionInUserCallback"); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) - .AddView((instrument) => { throw new Exception("bad"); }) - .AddView((instrument) => { return new MetricStreamConfiguration() { Name = "newname" }; }) + .AddView(_ => { throw new InvalidOperationException("bad"); }) + .AddView(_ => { return new MetricStreamConfiguration() { Name = "newname" }; }) .AddInMemoryExporter(exportedItems)); using (var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log)) @@ -181,10 +181,10 @@ public void AddViewsWithAndWithoutExceptionInUserCallback() [MemberData(nameof(MetricTestData.InvalidHistogramBoundaries), MemberType = typeof(MetricTestData))] public void AddViewWithInvalidHistogramBoundsThrowsArgumentException(double[] boundaries) { - var ex = Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder + var ex = Assert.Throws(() => BuildMeterProvider(out var meterProvider, builder => builder .AddView("name1", new ExplicitBucketHistogramConfiguration { Boundaries = boundaries }))); - Assert.Contains("Histogram boundaries must be in ascending order with distinct values", ex.Message); + Assert.Contains("Histogram boundaries must be in ascending order with distinct values", ex.Message, StringComparison.Ordinal); } [Theory] @@ -193,10 +193,10 @@ public void AddViewWithInvalidHistogramBoundsThrowsArgumentException(double[] bo [InlineData(1)] public void AddViewWithInvalidExponentialHistogramMaxSizeConfigThrowsArgumentException(int maxSize) { - var ex = Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder + var ex = Assert.Throws(() => BuildMeterProvider(out var meterProvider, builder => builder .AddView("name1", new Base2ExponentialBucketHistogramConfiguration { MaxSize = maxSize }))); - Assert.Contains("Histogram max size is invalid", ex.Message); + Assert.Contains("Histogram max size is invalid", ex.Message, StringComparison.Ordinal); } [Theory] @@ -204,10 +204,10 @@ public void AddViewWithInvalidExponentialHistogramMaxSizeConfigThrowsArgumentExc [InlineData(21)] public void AddViewWithInvalidExponentialHistogramMaxScaleConfigThrowsArgumentException(int maxScale) { - var ex = Assert.Throws(() => this.BuildMeterProvider(out var meterProvider, builder => builder + var ex = Assert.Throws(() => BuildMeterProvider(out var meterProvider, builder => builder .AddView("name1", new Base2ExponentialBucketHistogramConfiguration { MaxScale = maxScale }))); - Assert.Contains("Histogram max scale is invalid", ex.Message); + Assert.Contains("Histogram max scale is invalid", ex.Message, StringComparison.Ordinal); } [Theory] @@ -220,7 +220,7 @@ public void AddViewWithInvalidHistogramBoundsIgnored(double[] boundaries) var counter1 = meter1.CreateCounter("counter1"); - using (var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using (var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView((instrument) => { @@ -245,7 +245,7 @@ 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 + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView("name1", viewNewName) .AddInMemoryExporter(exportedItems)); @@ -268,7 +268,7 @@ public void ViewToRenameMetricConditionally() var exportedItems = new List(); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddMeter(meter2.Name) .AddView((instrument) => @@ -307,7 +307,7 @@ 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 + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) // since here it's a func, we can't validate the name right away @@ -343,7 +343,7 @@ 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 + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter1.Name) .AddView((instrument) => { @@ -379,7 +379,7 @@ public void ViewWithNullCustomNameTakesInstrumentName() using var meter = new Meter("ViewToRenameMetricConditionallyTest"); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView((instrument) => { @@ -414,7 +414,7 @@ public void ViewToProduceMultipleStreamsFromInstrument() using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("name1", "renamedStream1") .AddView("name1", "renamedStream2") @@ -435,7 +435,7 @@ public void ViewToProduceMultipleStreamsWithDuplicatesFromInstrument() using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("name1", "renamedStream1") .AddView("name1", "renamedStream2") @@ -460,7 +460,7 @@ public void ViewWithHistogramConfigurationIgnoredWhenAppliedToNonHistogram() using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("NotAHistogram", new ExplicitBucketHistogramConfiguration() { Name = "ImAnExplicitBoundsHistogram" }) .AddView("NotAHistogram", new Base2ExponentialBucketHistogramConfiguration() { Name = "ImAnExponentialHistogram" }) @@ -475,7 +475,7 @@ public void ViewWithHistogramConfigurationIgnoredWhenAppliedToNonHistogram() Assert.Equal("NotAHistogram", metric.Name); - List metricPoints = new List(); + List metricPoints = []; foreach (ref readonly var mp in metric.GetMetricPoints()) { metricPoints.Add(mp); @@ -493,7 +493,7 @@ public void ViewToProduceCustomHistogramBound() var exportedItems = new List(); var boundaries = new double[] { 10, 20 }; - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("MyHistogram", new ExplicitBucketHistogramConfiguration() { Name = "MyHistogramDefaultBound" }) .AddView("MyHistogram", new ExplicitBucketHistogramConfiguration() { Boundaries = boundaries }) @@ -515,7 +515,7 @@ public void ViewToProduceCustomHistogramBound() Assert.Equal("MyHistogramDefaultBound", metricDefault.Name); Assert.Equal("MyHistogram", metricCustom.Name); - List metricPointsDefault = new List(); + List metricPointsDefault = []; foreach (ref readonly var mp in metricDefault.GetMetricPoints()) { metricPointsDefault.Add(mp); @@ -542,7 +542,7 @@ public void ViewToProduceCustomHistogramBound() Assert.Equal(Metric.DefaultHistogramBounds.Length + 1, actualCount); - List metricPointsCustom = new List(); + List metricPointsCustom = []; foreach (ref readonly var mp in metricCustom.GetMetricPoints()) { metricPointsCustom.Add(mp); @@ -559,7 +559,7 @@ public void ViewToProduceCustomHistogramBound() index = 0; actualCount = 0; - expectedBucketCounts = new long[] { 5, 2, 0 }; + expectedBucketCounts = [5, 2, 0]; foreach (var histogramMeasurement in histogramPoint.GetHistogramBuckets()) { Assert.Equal(expectedBucketCounts[index], histogramMeasurement.BucketCount); @@ -570,6 +570,177 @@ public void ViewToProduceCustomHistogramBound() Assert.Equal(boundaries.Length + 1, actualCount); } + [Fact] + public void HistogramWithAdviceBoundaries_HandlesAllTypes() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + int counter = 0; + + using var container = BuildMeterProvider(out var meterProvider, builder => + { + builder.AddMeter(meter.Name); + builder.AddInMemoryExporter(exportedItems); + }); + + // Test cases for different histogram types + var histograms = new Instrument[] + { + meter.CreateHistogram("longHistogram", unit: null, description: null, tags: null, new() { HistogramBucketBoundaries = [10L, 20L] }), + meter.CreateHistogram("intHistogram", unit: null, description: null, tags: null, new() { HistogramBucketBoundaries = [10, 20] }), + meter.CreateHistogram("shortHistogram", unit: null, description: null, tags: null, new() { HistogramBucketBoundaries = [10, 20] }), + meter.CreateHistogram("floatHistogram", unit: null, description: null, tags: null, new() { HistogramBucketBoundaries = [10.0F, 20.0F] }), + meter.CreateHistogram("doubleHistogram", unit: null, description: null, tags: null, new() { HistogramBucketBoundaries = [10.0, 20.0] }), + }; + + foreach (var histogram in histograms) + { + exportedItems.Clear(); + + if (histogram is Histogram longHistogram) + { + longHistogram.Record(-10); + longHistogram.Record(9); + longHistogram.Record(19); + } + else if (histogram is Histogram intHistogram) + { + intHistogram.Record(-10); + intHistogram.Record(9); + intHistogram.Record(19); + counter++; + } + else if (histogram is Histogram shortHistogram) + { + shortHistogram.Record(-10); + shortHistogram.Record(9); + shortHistogram.Record(19); + counter++; + } + else if (histogram is Histogram floatHistogram) + { + floatHistogram.Record(-10.0F); + floatHistogram.Record(9.0F); + floatHistogram.Record(19.0F); + counter++; + } + else if (histogram is Histogram doubleHistogram) + { + doubleHistogram.Record(-10.0); + doubleHistogram.Record(9.0); + doubleHistogram.Record(19.0); + counter++; + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + var metricCustom = exportedItems[counter]; + + List metricPointsCustom = []; + foreach (ref readonly var mp in metricCustom.GetMetricPoints()) + { + metricPointsCustom.Add(mp); + } + + Assert.Single(metricPointsCustom); + var histogramPoint = metricPointsCustom[0]; + + var count = histogramPoint.GetHistogramCount(); + var sum = histogramPoint.GetHistogramSum(); + + Assert.Equal(18, sum); + Assert.Equal(3, count); + + var index = 0; + var actualCount = 0; + long[] expectedBucketCounts = [2, 1, 0]; + + foreach (var histogramMeasurement in histogramPoint.GetHistogramBuckets()) + { + Assert.Equal(expectedBucketCounts[index], histogramMeasurement.BucketCount); + index++; + actualCount++; + } + + Assert.Equal(3, actualCount); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void HistogramWithAdviceBoundariesSpecifiedTests(bool useViewToOverride) + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + IReadOnlyList adviceBoundaries = [5L, 10L, 20L]; + double[] viewBoundaries = [10, 20]; + + using var container = BuildMeterProvider(out var meterProvider, builder => + { + builder.AddMeter(meter.Name); + + if (useViewToOverride) + { + builder.AddView("MyHistogram", new ExplicitBucketHistogramConfiguration { Boundaries = viewBoundaries }); + } + + builder.AddInMemoryExporter(exportedItems); + }); + + var histogram = meter.CreateHistogram( + "MyHistogram", + unit: null, + description: null, + tags: null, + new() + { + HistogramBucketBoundaries = adviceBoundaries, + }); + + histogram.Record(-10); + histogram.Record(0); + histogram.Record(1); + histogram.Record(9); + histogram.Record(10); + histogram.Record(11); + histogram.Record(19); + histogram.Record(22); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + var metricCustom = exportedItems[0]; + + Assert.Equal("MyHistogram", metricCustom.Name); + + List metricPointsCustom = []; + foreach (ref readonly var mp in metricCustom.GetMetricPoints()) + { + metricPointsCustom.Add(mp); + } + + Assert.Single(metricPointsCustom); + var histogramPoint = metricPointsCustom[0]; + + var count = histogramPoint.GetHistogramCount(); + var sum = histogramPoint.GetHistogramSum(); + + Assert.Equal(62, sum); + Assert.Equal(8, count); + + var index = 0; + var actualCount = 0; + long[] expectedBucketCounts = useViewToOverride ? [5, 2, 1] : [3, 2, 2, 1]; + + foreach (var histogramMeasurement in histogramPoint.GetHistogramBuckets()) + { + Assert.Equal(expectedBucketCounts[index], histogramMeasurement.BucketCount); + index++; + actualCount++; + } + + Assert.Equal(useViewToOverride ? viewBoundaries.Length + 1 : adviceBoundaries.Count + 1, actualCount); + } + [Fact] public void ViewToProduceExponentialHistogram() { @@ -578,7 +749,7 @@ public void ViewToProduceExponentialHistogram() using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("MyHistogram", new Base2ExponentialBucketHistogramConfiguration()) .AddInMemoryExporter(exportedItems)); @@ -613,7 +784,7 @@ public void ViewToProduceExponentialHistogram() var count = metricPoint.GetHistogramCount(); var sum = metricPoint.GetHistogramSum(); - AggregatorTestsBase.AssertExponentialBucketsAreCorrect(expectedHistogram, metricPoint.GetExponentialHistogramData()); + AggregatorTests.AssertExponentialBucketsAreCorrect(expectedHistogram, metricPoint.GetExponentialHistogramData()); Assert.Equal(50, sum); Assert.Equal(6, count); } @@ -622,11 +793,20 @@ public void ViewToProduceExponentialHistogram() [MemberData(nameof(MetricTestData.ValidHistogramMinMax), MemberType = typeof(MetricTestData))] public void HistogramMinMax(double[] values, HistogramConfiguration histogramConfiguration, double expectedMin, double expectedMax) { +#if NET + Assert.NotNull(values); +#else + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } +#endif + using var meter = new Meter(Utils.GetCurrentMethodName()); var histogram = meter.CreateHistogram("MyHistogram"); var exportedItems = new List(); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView(histogram.Name, histogramConfiguration) .AddInMemoryExporter(exportedItems)); @@ -660,11 +840,20 @@ public void HistogramMinMax(double[] values, HistogramConfiguration histogramCon [MemberData(nameof(MetricTestData.InvalidHistogramMinMax), MemberType = typeof(MetricTestData))] public void HistogramMinMaxNotPresent(double[] values, HistogramConfiguration histogramConfiguration) { +#if NET + Assert.NotNull(values); +#else + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } +#endif + using var meter = new Meter(Utils.GetCurrentMethodName()); var histogram = meter.CreateHistogram("MyHistogram"); var exportedItems = new List(); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView(histogram.Name, histogramConfiguration) .AddInMemoryExporter(exportedItems)); @@ -692,21 +881,21 @@ public void ViewToSelectTagKeys() using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("FruitCounter", new MetricStreamConfiguration() { - TagKeys = new string[] { "name" }, + TagKeys = ["name"], Name = "NameOnly", }) .AddView("FruitCounter", new MetricStreamConfiguration() { - TagKeys = new string[] { "size" }, + TagKeys = ["size"], Name = "SizeOnly", }) .AddView("FruitCounter", new MetricStreamConfiguration() { - TagKeys = Array.Empty(), + TagKeys = [], Name = "NoTags", }) .AddInMemoryExporter(exportedItems)); @@ -725,7 +914,7 @@ public void ViewToSelectTagKeys() Assert.Equal(3, exportedItems.Count); var metric = exportedItems[0]; Assert.Equal("NameOnly", metric.Name); - List metricPoints = new List(); + List metricPoints = []; foreach (ref readonly var mp in metric.GetMetricPoints()) { metricPoints.Add(mp); @@ -763,7 +952,7 @@ public void ViewToDropSingleInstrument() using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("counterNotInteresting", MetricStreamConfiguration.Drop) .AddInMemoryExporter(exportedItems)); @@ -786,7 +975,7 @@ public void ViewToDropSingleInstrumentObservableCounter() using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("observableCounterNotInteresting", MetricStreamConfiguration.Drop) .AddInMemoryExporter(exportedItems)); @@ -807,7 +996,7 @@ public void ViewToDropSingleInstrumentObservableGauge() using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("observableGaugeNotInteresting", MetricStreamConfiguration.Drop) .AddInMemoryExporter(exportedItems)); @@ -828,7 +1017,7 @@ public void ViewToDropMultipleInstruments() using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("server*", MetricStreamConfiguration.Drop) .AddInMemoryExporter(exportedItems)); @@ -855,7 +1044,7 @@ public void ViewToDropAndRetainInstrument() using var meter = new Meter(Utils.GetCurrentMethodName()); var exportedItems = new List(); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("server.requests", MetricStreamConfiguration.Drop) .AddView("server.requests", "server.request_renamed") @@ -878,9 +1067,9 @@ public void ViewConflict_OneInstrument_DifferentDescription() { var exportedItems = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + using var meter = new Meter(Utils.GetCurrentMethodName()); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView("instrumentName", new MetricStreamConfiguration() { Description = "newDescription1" }) .AddView("instrumentName", new MetricStreamConfiguration() { Description = "newDescription2" }) @@ -898,7 +1087,7 @@ public void ViewConflict_OneInstrument_DifferentDescription() Assert.Equal("newDescription1", metric1.Description); Assert.Equal("newDescription2", metric2.Description); - List metric1MetricPoints = new List(); + List metric1MetricPoints = []; foreach (ref readonly var mp in metric1.GetMetricPoints()) { metric1MetricPoints.Add(mp); @@ -908,7 +1097,7 @@ public void ViewConflict_OneInstrument_DifferentDescription() var metricPoint1 = metric1MetricPoints[0]; Assert.Equal(10, metricPoint1.GetSumLong()); - List metric2MetricPoints = new List(); + List metric2MetricPoints = []; foreach (ref readonly var mp in metric2.GetMetricPoints()) { metric2MetricPoints.Add(mp); @@ -922,12 +1111,12 @@ public void ViewConflict_OneInstrument_DifferentDescription() [Theory] [InlineData(true)] [InlineData(false)] - public void CardinalityLimitofMatchingViewTakesPrecedenceOverMeterProvider(bool setDefault) + 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 => + using var container = BuildMeterProvider(out var meterProvider, builder => { if (setDefault) { @@ -981,9 +1170,9 @@ public void ViewConflict_TwoDistinctInstruments_ThreeStreams() { var exportedItems = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + using var meter = new Meter(Utils.GetCurrentMethodName()); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView((instrument) => { @@ -1045,24 +1234,24 @@ public void ViewConflict_TwoIdenticalInstruments_TwoViews_DifferentTags() { var exportedItems = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + using var meter = new Meter(Utils.GetCurrentMethodName()); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView((instrument) => { - return new MetricStreamConfiguration { TagKeys = new[] { "key1" } }; + return new MetricStreamConfiguration { TagKeys = ["key1"] }; }) .AddView((instrument) => { - return new MetricStreamConfiguration { TagKeys = new[] { "key2" } }; + return new MetricStreamConfiguration { TagKeys = ["key2"] }; }) .AddInMemoryExporter(exportedItems)); var instrument1 = meter.CreateCounter("name"); var instrument2 = meter.CreateCounter("name"); - var tags = new KeyValuePair[] + var tags = new KeyValuePair[] { new("key1", "value"), new("key2", "value"), @@ -1076,8 +1265,8 @@ public void ViewConflict_TwoIdenticalInstruments_TwoViews_DifferentTags() 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] }; + var tag1 = new List> { tags[0] }; + var tag2 = new List> { tags[1] }; Assert.Equal("name", exportedItems[0].Name); Assert.Equal("name", exportedItems[1].Name); @@ -1092,24 +1281,24 @@ public void ViewConflict_TwoIdenticalInstruments_TwoViews_SameTags() { var exportedItems = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + using var meter = new Meter(Utils.GetCurrentMethodName()); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView((instrument) => { - return new MetricStreamConfiguration { TagKeys = new[] { "key1" } }; + return new MetricStreamConfiguration { TagKeys = ["key1"] }; }) .AddView((instrument) => { - return new MetricStreamConfiguration { TagKeys = new[] { "key1" } }; + return new MetricStreamConfiguration { TagKeys = ["key1"] }; }) .AddInMemoryExporter(exportedItems)); var instrument1 = meter.CreateCounter("name"); var instrument2 = meter.CreateCounter("name"); - var tags = new KeyValuePair[] + var tags = new KeyValuePair[] { new("key1", "value"), new("key2", "value"), @@ -1123,13 +1312,13 @@ public void ViewConflict_TwoIdenticalInstruments_TwoViews_SameTags() Assert.Equal(2, exportedItems.Count); var metric1 = new List() { exportedItems[0] }; - var tag1 = new List> { tags[0] }; + var tag1 = new List> { tags[0] }; Assert.Equal("name", exportedItems[0].Name); Assert.Equal(20, GetLongSum(metric1)); CheckTagsForNthMetricPoint(metric1, tag1, 1); var metric2 = new List() { exportedItems[1] }; - var tag2 = new List> { tags[0] }; + var tag2 = new List> { tags[0] }; Assert.Equal("name", exportedItems[1].Name); Assert.Equal(20, GetLongSum(metric2)); CheckTagsForNthMetricPoint(metric2, tag2, 1); @@ -1140,17 +1329,17 @@ public void ViewConflict_TwoIdenticalInstruments_TwoViews_DifferentHistogramBoun { var exportedItems = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + using var meter = new Meter(Utils.GetCurrentMethodName()); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView((instrument) => { - return new ExplicitBucketHistogramConfiguration { Boundaries = new[] { 5.0, 10.0 } }; + return new ExplicitBucketHistogramConfiguration { Boundaries = [5.0, 10.0] }; }) .AddView((instrument) => { - return new ExplicitBucketHistogramConfiguration { Boundaries = new[] { 10.0, 20.0 } }; + return new ExplicitBucketHistogramConfiguration { Boundaries = [10.0, 20.0] }; }) .AddInMemoryExporter(exportedItems)); @@ -1190,7 +1379,7 @@ public void ViewConflict_TwoIdenticalInstruments_TwoViews_DifferentHistogramBoun actualCount++; } - metricPoints = new List(); + metricPoints = []; foreach (ref readonly var mp in metric2.GetMetricPoints()) { metricPoints.Add(mp); @@ -1203,7 +1392,7 @@ public void ViewConflict_TwoIdenticalInstruments_TwoViews_DifferentHistogramBoun index = 0; actualCount = 0; - expectedBucketCounts = new long[] { 0, 2, 0 }; + expectedBucketCounts = [0, 2, 0]; foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets()) { Assert.Equal(expectedBucketCounts[index], histogramMeasurement.BucketCount); @@ -1217,15 +1406,15 @@ public void ViewConflict_TwoInstruments_OneMatchesView() { var exportedItems = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + using var meter = new Meter(Utils.GetCurrentMethodName()); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView((instrument) => { if (instrument.Name == "name") { - return new MetricStreamConfiguration { Name = "othername", TagKeys = new[] { "key1" } }; + return new MetricStreamConfiguration { Name = "othername", TagKeys = ["key1"] }; } else { @@ -1237,7 +1426,7 @@ public void ViewConflict_TwoInstruments_OneMatchesView() var instrument1 = meter.CreateCounter("name"); var instrument2 = meter.CreateCounter("othername"); - var tags = new KeyValuePair[] + var tags = new KeyValuePair[] { new("key1", "value"), new("key2", "value"), @@ -1252,8 +1441,8 @@ public void ViewConflict_TwoInstruments_OneMatchesView() 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); @@ -1270,9 +1459,9 @@ public void ViewConflict_TwoInstruments_ConflictAvoidedBecauseSecondInstrumentIs { var exportedItems = new List(); - using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + using var meter = new Meter(Utils.GetCurrentMethodName()); - using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + using var container = BuildMeterProvider(out var meterProvider, builder => builder .AddMeter(meter.Name) .AddView((instrument) => { diff --git a/test/OpenTelemetry.Tests/Metrics/MultipleReadersTests.cs b/test/OpenTelemetry.Tests/Metrics/MultipleReadersTests.cs index 16309219654..26769fe39ab 100644 --- a/test/OpenTelemetry.Tests/Metrics/MultipleReadersTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MultipleReadersTests.cs @@ -3,6 +3,7 @@ using System.Diagnostics.Metrics; using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Exporter; using OpenTelemetry.Tests; using Xunit; @@ -10,6 +11,96 @@ namespace OpenTelemetry.Metrics.Tests; public class MultipleReadersTests { + [Fact] + public void ReaderCannotBeRegisteredMoreThanOnce() + { + var exportedItems = new List(); + using var exporter = new InMemoryExporter(exportedItems); + using var reader = new BaseExportingMetricReader(exporter); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + + var meterProviderBuilder1 = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddReader(reader); + var meterProviderBuilder2 = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddReader(reader); + + using var meterProvider1 = meterProviderBuilder1.Build(); + Assert.Throws(() => meterProviderBuilder2.Build()); + } + + [Fact] + public void MultipleReadersOneCollectsIndependently() + { + var exportedItems1 = new List(); + var exportedItems2 = new List(); + + using var exporter1 = new InMemoryExporter(exportedItems1); + using var exporter2 = new InMemoryExporter(exportedItems2); + using var reader1 = new BaseExportingMetricReader(exporter1); + using var reader2 = new BaseExportingMetricReader(exporter2); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + var counter = meter.CreateCounter("counter"); + + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddReader(reader1) + .AddReader(reader2); + + using var meterProvider = meterProviderBuilder.Build(); + + counter.Add(1); + + reader1.Collect(); + + Assert.Single(exportedItems1); + Assert.Empty(exportedItems2); + } + + [Fact] + public void MultipleReadersDifferentTemporality() + { + var exportedItems1 = new List(); + var exportedItems2 = new List(); + + using var exporter1 = new InMemoryExporter(exportedItems1); + using var exporter2 = new InMemoryExporter(exportedItems2); + using var reader1 = new BaseExportingMetricReader(exporter1); + using var reader2 = new BaseExportingMetricReader(exporter2); + reader1.TemporalityPreference = MetricReaderTemporalityPreference.Delta; + reader2.TemporalityPreference = MetricReaderTemporalityPreference.Cumulative; + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + var counter = meter.CreateCounter("counter"); + + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddReader(reader1) + .AddReader(reader2); + + using var meterProvider = meterProviderBuilder.Build(); + + counter.Add(1); + + reader1.Collect(); + reader2.Collect(); + + AssertLongSumValueForMetric(exportedItems1[0], 1); + AssertLongSumValueForMetric(exportedItems2[0], 1); + + exportedItems1.Clear(); + + counter.Add(10); + reader1.Collect(); + reader2.Collect(); + + AssertLongSumValueForMetric(exportedItems1[0], 10); + AssertLongSumValueForMetric(exportedItems2[0], 11); + } + [Theory] [InlineData(MetricReaderTemporalityPreference.Delta, false)] [InlineData(MetricReaderTemporalityPreference.Delta, true)] @@ -71,7 +162,7 @@ public void SdkSupportsMultipleReaders(MetricReaderTemporalityPreference aggrega Assert.True(defaultNamedOptionsConfigureCalled); Assert.True(namedOptionsConfigureCalled); - counter.Add(10, new KeyValuePair("key", "value")); + counter.Add(10, new KeyValuePair("key", "value")); meterProvider.ForceFlush(); @@ -123,7 +214,7 @@ public void SdkSupportsMultipleReaders(MetricReaderTemporalityPreference aggrega exportedItems1.Clear(); exportedItems2.Clear(); - counter.Add(15, new KeyValuePair("key", "value")); + counter.Add(15, new KeyValuePair("key", "value")); meterProvider.ForceFlush(); diff --git a/test/OpenTelemetry.Tests/OpenTelemetry.Tests.csproj b/test/OpenTelemetry.Tests/OpenTelemetry.Tests.csproj index 2a2532c07e9..f68174421b6 100644 --- a/test/OpenTelemetry.Tests/OpenTelemetry.Tests.csproj +++ b/test/OpenTelemetry.Tests/OpenTelemetry.Tests.csproj @@ -1,11 +1,10 @@ + Unit test project for OpenTelemetry $(TargetFrameworksForTests) $(NoWarn),CS0618 - - - disable + true @@ -15,22 +14,12 @@ - - - - - - - runtime; build; native; contentfiles; analyzers - - - diff --git a/test/OpenTelemetry.Tests/OpenTelemetrySdkTests.cs b/test/OpenTelemetry.Tests/OpenTelemetrySdkTests.cs new file mode 100644 index 00000000000..ba456cbc4e0 --- /dev/null +++ b/test/OpenTelemetry.Tests/OpenTelemetrySdkTests.cs @@ -0,0 +1,68 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.Logging.Abstractions; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Tests; + +public class OpenTelemetrySdkTests +{ + [Fact] + public void BuilderDelegateRequiredTest() + { + Assert.Throws(() => OpenTelemetrySdk.Create(null!)); + } + + [Fact] + public void NoopProvidersReturnedTest() + { + bool builderDelegateInvoked = false; + + using var sdk = OpenTelemetrySdk.Create(builder => + { + builderDelegateInvoked = true; + Assert.NotNull(builder.Services); + }); + + Assert.True(builderDelegateInvoked); + + Assert.NotNull(sdk); + Assert.NotNull(sdk.Services); + Assert.True(sdk.LoggerProvider is OpenTelemetrySdk.NoopLoggerProvider); + Assert.True(sdk.MeterProvider is OpenTelemetrySdk.NoopMeterProvider); + Assert.True(sdk.TracerProvider is OpenTelemetrySdk.NoopTracerProvider); + Assert.True(sdk.GetLoggerFactory() is NullLoggerFactory); + } + + [Fact] + public void ProvidersCreatedAndDisposedTest() + { + var sdk = OpenTelemetrySdk.Create(builder => + { + builder + .WithLogging() + .WithMetrics() + .WithTracing(); + }); + + var loggerProvider = sdk.LoggerProvider as LoggerProviderSdk; + var meterProvider = sdk.MeterProvider as MeterProviderSdk; + var tracerProvider = sdk.TracerProvider as TracerProviderSdk; + + Assert.NotNull(loggerProvider); + Assert.NotNull(meterProvider); + Assert.NotNull(tracerProvider); + + Assert.True(sdk.GetLoggerFactory() is not NullLoggerFactory); + + sdk.Dispose(); + + Assert.True(loggerProvider.Disposed); + Assert.True(meterProvider.Disposed); + Assert.True(tracerProvider.Disposed); + } +} diff --git a/test/OpenTelemetry.Tests/Resources/OtelEnvResourceDetectorTest.cs b/test/OpenTelemetry.Tests/Resources/OtelEnvResourceDetectorTests.cs similarity index 94% rename from test/OpenTelemetry.Tests/Resources/OtelEnvResourceDetectorTest.cs rename to test/OpenTelemetry.Tests/Resources/OtelEnvResourceDetectorTests.cs index a6180b7a4cd..2f2c337d67e 100644 --- a/test/OpenTelemetry.Tests/Resources/OtelEnvResourceDetectorTest.cs +++ b/test/OpenTelemetry.Tests/Resources/OtelEnvResourceDetectorTests.cs @@ -6,9 +6,9 @@ namespace OpenTelemetry.Resources.Tests; -public class OtelEnvResourceDetectorTest : IDisposable +public sealed class OtelEnvResourceDetectorTests : IDisposable { - public OtelEnvResourceDetectorTest() + public OtelEnvResourceDetectorTests() { Environment.SetEnvironmentVariable(OtelEnvResourceDetector.EnvVarKey, null); } @@ -71,7 +71,7 @@ public void OtelEnvResource_WithEnvVar_2() [Fact] public void OtelEnvResource_UsingIConfiguration() { - var values = new Dictionary() + var values = new Dictionary() { [OtelEnvResourceDetector.EnvVarKey] = "Key1=Val1,Key2=Val2", }; diff --git a/test/OpenTelemetry.Tests/Resources/OtelServiceNameEnvVarDetectorTests.cs b/test/OpenTelemetry.Tests/Resources/OtelServiceNameEnvVarDetectorTests.cs index 9b3bf437cc8..e9553119ed4 100644 --- a/test/OpenTelemetry.Tests/Resources/OtelServiceNameEnvVarDetectorTests.cs +++ b/test/OpenTelemetry.Tests/Resources/OtelServiceNameEnvVarDetectorTests.cs @@ -6,7 +6,7 @@ namespace OpenTelemetry.Resources.Tests; -public class OtelServiceNameEnvVarDetectorTests : IDisposable +public sealed class OtelServiceNameEnvVarDetectorTests : IDisposable { public OtelServiceNameEnvVarDetectorTests() { @@ -57,7 +57,7 @@ public void OtelServiceNameEnvVar_WithValue() [Fact] public void OtelServiceNameEnvVar_UsingIConfiguration() { - var values = new Dictionary() + var values = new Dictionary() { [OtelServiceNameEnvVarDetector.EnvVarKey] = "my-service", }; diff --git a/test/OpenTelemetry.Tests/Resources/ResourceBuilderTests.cs b/test/OpenTelemetry.Tests/Resources/ResourceBuilderTests.cs index 15ecdeedc88..3fec4a65c71 100644 --- a/test/OpenTelemetry.Tests/Resources/ResourceBuilderTests.cs +++ b/test/OpenTelemetry.Tests/Resources/ResourceBuilderTests.cs @@ -13,7 +13,7 @@ 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.Single(resource.Attributes, kvp => kvp.Key == ResourceSemanticConventions.AttributeServiceName); Assert.True(Guid.TryParse((string)resource.Attributes.Single(kvp => kvp.Key == ResourceSemanticConventions.AttributeServiceInstance).Value, out _)); } diff --git a/test/OpenTelemetry.Tests/Resources/ResourceTest.cs b/test/OpenTelemetry.Tests/Resources/ResourceTests.cs similarity index 91% rename from test/OpenTelemetry.Tests/Resources/ResourceTest.cs rename to test/OpenTelemetry.Tests/Resources/ResourceTests.cs index 337870a8a11..8f844b77e4e 100644 --- a/test/OpenTelemetry.Tests/Resources/ResourceTest.cs +++ b/test/OpenTelemetry.Tests/Resources/ResourceTests.cs @@ -6,12 +6,12 @@ namespace OpenTelemetry.Resources.Tests; -public class ResourceTest : IDisposable +public sealed class ResourceTests : IDisposable { private const string KeyName = "key"; private const string ValueName = "value"; - public ResourceTest() + public ResourceTests() { ClearEnvVars(); } @@ -26,7 +26,7 @@ public void Dispose() public void CreateResource_NullAttributeCollection() { // Act and Assert - var resource = new Resource(null); + var resource = new Resource(null!); Assert.Empty(resource.Attributes); } @@ -34,10 +34,10 @@ public void CreateResource_NullAttributeCollection() public void CreateResource_NullAttributeValue() { // Arrange - var attributes = new Dictionary { { "NullValue", null } }; + var attributes = new Dictionary { { "NullValue", null } }; // Act and Assert - Assert.Throws(() => new Resource(attributes)); + Assert.Throws(() => new Resource(attributes!)); } [Fact] @@ -165,18 +165,26 @@ public void CreateResource_SupportedAttributeTypes() public void CreateResource_SupportedAttributeArrayTypes() { // Arrange + string[] stringArray = ["stringValue"]; + bool[] boolArray = [true]; + double[] doubleArray = [0.1D]; + long[] longArray = [1L]; + int[] intArray = [1]; + short[] shortArray = [1]; + float[] floatArray = [0.1f]; + 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 } }, + { "string arr", stringArray }, + { "bool arr", boolArray }, + { "double arr", doubleArray }, + { "long arr", longArray }, // 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 } }, + { "int arr", intArray }, + { "short arr", shortArray }, + { "float arr", floatArray }, }; // Act @@ -184,16 +192,15 @@ public void CreateResource_SupportedAttributeArrayTypes() // 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); + Assert.Equal(stringArray, resource.Attributes.FirstOrDefault(x => x.Key == "string arr").Value); + Assert.Equal(boolArray, resource.Attributes.FirstOrDefault(x => x.Key == "bool arr").Value); + Assert.Equal(doubleArray, resource.Attributes.FirstOrDefault(x => x.Key == "double arr").Value); + Assert.Equal(longArray, resource.Attributes.FirstOrDefault(x => x.Key == "long arr").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); + double[] nonNativeDoubleArray = [Convert.ToDouble(0.1f, System.Globalization.CultureInfo.InvariantCulture)]; + Assert.Equal(longArray, resource.Attributes.FirstOrDefault(x => x.Key == "int arr").Value); + Assert.Equal(longArray, resource.Attributes.FirstOrDefault(x => x.Key == "short arr").Value); + Assert.Equal(nonNativeDoubleArray, resource.Attributes.FirstOrDefault(x => x.Key == "float arr").Value); } [Fact] @@ -561,15 +568,15 @@ internal static void ValidateTelemetrySdkAttributes(IEnumerable("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")); + var versionAttribute = attributes.Where(pair => pair.Key.Equals("telemetry.sdk.version", StringComparison.Ordinal)); Assert.Single(versionAttribute); } internal static void ValidateDefaultAttributes(IEnumerable> attributes) { - var serviceName = attributes.Where(pair => pair.Key.Equals("service.name")); + var serviceName = attributes.Where(pair => pair.Key.Equals("service.name", StringComparison.Ordinal)); Assert.Single(serviceName); - Assert.Contains("unknown_service", serviceName.FirstOrDefault().Value as string); + Assert.Contains("unknown_service", serviceName.FirstOrDefault().Value as string, StringComparison.Ordinal); } private static void ClearEnvVars() diff --git a/test/OpenTelemetry.Tests/Shared/CustomTextMapPropagator.cs b/test/OpenTelemetry.Tests/Shared/CustomTextMapPropagator.cs deleted file mode 100644 index a3524f17cbf..00000000000 --- a/test/OpenTelemetry.Tests/Shared/CustomTextMapPropagator.cs +++ /dev/null @@ -1,51 +0,0 @@ -// 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/DelegatingProcessor.cs b/test/OpenTelemetry.Tests/Shared/DelegatingProcessor.cs index db5148a5ab6..2cff5e680c3 100644 --- a/test/OpenTelemetry.Tests/Shared/DelegatingProcessor.cs +++ b/test/OpenTelemetry.Tests/Shared/DelegatingProcessor.cs @@ -3,7 +3,7 @@ namespace OpenTelemetry.Tests; -public class DelegatingProcessor : BaseProcessor +internal sealed class DelegatingProcessor : BaseProcessor where T : class { public Func OnForceFlushFunc { get; set; } = (timeout) => true; diff --git a/test/OpenTelemetry.Tests/Shared/EnabledOnDockerPlatformTheoryAttribute.cs b/test/OpenTelemetry.Tests/Shared/EnabledOnDockerPlatformTheoryAttribute.cs deleted file mode 100644 index 4fda3e1a9bc..00000000000 --- a/test/OpenTelemetry.Tests/Shared/EnabledOnDockerPlatformTheoryAttribute.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Diagnostics; -using System.Text; -using Xunit; - -namespace OpenTelemetry.Tests; - -/// -/// This skips tests if the required Docker engine is not available. -/// -internal class EnabledOnDockerPlatformTheoryAttribute : TheoryAttribute -{ - /// - /// Initializes a new instance of the class. - /// - public EnabledOnDockerPlatformTheoryAttribute(DockerPlatform dockerPlatform) - { - const string executable = "docker"; - - var stdout = new StringBuilder(); - var stderr = new StringBuilder(); - - void AppendStdout(object sender, DataReceivedEventArgs e) => stdout.Append(e.Data); - void AppendStderr(object sender, DataReceivedEventArgs e) => stderr.Append(e.Data); - - var processStartInfo = new ProcessStartInfo(); - processStartInfo.FileName = executable; - processStartInfo.Arguments = string.Join(" ", "version", "--format '{{.Server.Os}}'"); - processStartInfo.RedirectStandardOutput = true; - processStartInfo.RedirectStandardError = true; - processStartInfo.UseShellExecute = false; - - var process = new Process(); - process.StartInfo = processStartInfo; - process.OutputDataReceived += AppendStdout; - process.ErrorDataReceived += AppendStderr; - - try - { - process.Start(); - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - process.WaitForExit(); - } - finally - { - process.OutputDataReceived -= AppendStdout; - process.ErrorDataReceived -= AppendStderr; - } - - if (0.Equals(process.ExitCode) && stdout.ToString().Contains(dockerPlatform.ToString().ToLowerInvariant())) - { - return; - } - - this.Skip = $"The Docker {dockerPlatform} engine is not available."; - } - - public enum DockerPlatform - { - /// - /// Docker Linux engine. - /// - Linux, - - /// - /// Docker Windows engine. - /// - Windows, - } -} diff --git a/test/OpenTelemetry.Tests/Shared/EventSourceTestHelper.cs b/test/OpenTelemetry.Tests/Shared/EventSourceTestHelper.cs index c0f5055d490..8b30f7694b1 100644 --- a/test/OpenTelemetry.Tests/Shared/EventSourceTestHelper.cs +++ b/test/OpenTelemetry.Tests/Shared/EventSourceTestHelper.cs @@ -4,6 +4,7 @@ using System.Diagnostics.Tracing; using System.Globalization; using System.Reflection; +using Xunit.Sdk; namespace OpenTelemetry.Tests; @@ -26,7 +27,7 @@ private static void VerifyMethodImplementation(EventSource eventSource, MethodIn 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) { @@ -34,11 +35,11 @@ private static void VerifyMethodImplementation(EventSource eventSource, MethodIn actualEvent = listener.Messages.FirstOrDefault(x => x.EventId == 0); if (actualEvent != null) { - throw new Exception(actualEvent.Message); + throw new InvalidOperationException(actualEvent.Message); } // give up - throw new Exception("Listener failed to collect event."); + throw new InvalidOperationException("Listener failed to collect event."); } VerifyEventId(eventMethod, actualEvent); @@ -47,9 +48,9 @@ private static void VerifyMethodImplementation(EventSource eventSource, MethodIn } catch (Exception e) { - var name = eventMethod.DeclaringType.Name + "." + eventMethod.Name; + var name = eventMethod.DeclaringType?.Name + "." + eventMethod.Name; - throw new Exception("Method '" + name + "' is implemented incorrectly.", e); + throw new InvalidOperationException("Method '" + name + "' is implemented incorrectly.", e); } finally { @@ -78,7 +79,7 @@ private static object GenerateArgument(ParameterInfo parameter) if (parameter.ParameterType.IsValueType) { - return Activator.CreateInstance(parameter.ParameterType); + return Activator.CreateInstance(parameter.ParameterType)!; } throw new NotSupportedException("Complex types are not supported"); @@ -99,13 +100,14 @@ private static void VerifyEventLevel(MethodInfo eventMethod, EventWrittenEventAr 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()); + ? 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 void AssertEqual(string methodName, T expected, T actual) + where T : notnull { if (!expected.Equals(actual)) { @@ -115,7 +117,7 @@ private static void AssertEqual(string methodName, T expected, T actual) methodName, expected, actual); - throw new Exception(errorMessage); + throw EqualException.ForMismatchedValuesWithError(expected, actual, banner: errorMessage); } } @@ -127,6 +129,6 @@ private static EventAttribute GetEventAttribute(MethodInfo eventMethod) private static IEnumerable GetEventMethods(EventSource eventSource) { MethodInfo[] methods = eventSource.GetType().GetMethods(); - return methods.Where(m => m.GetCustomAttributes(typeof(EventAttribute), false).Any()); + return methods.Where(m => m.GetCustomAttributes(typeof(EventAttribute), false).Length > 0); } } diff --git a/test/OpenTelemetry.Tests/Shared/IEEE754Double.cs b/test/OpenTelemetry.Tests/Shared/IEEE754Double.cs index e897b09b29d..9053be979e8 100644 --- a/test/OpenTelemetry.Tests/Shared/IEEE754Double.cs +++ b/test/OpenTelemetry.Tests/Shared/IEEE754Double.cs @@ -6,7 +6,7 @@ namespace OpenTelemetry.Tests; [StructLayout(LayoutKind.Explicit)] -public struct IEEE754Double +internal struct IEEE754Double { [FieldOffset(0)] public double DoubleValue = 0; @@ -24,16 +24,31 @@ public IEEE754Double(double value) public static implicit operator double(IEEE754Double value) { - return value.DoubleValue; + return ToDouble(value); } public static IEEE754Double operator ++(IEEE754Double value) + { + return Increment(value); + } + + public static IEEE754Double operator --(IEEE754Double value) + { + return Decrement(value); + } + + public static double ToDouble(IEEE754Double value) + { + return value.DoubleValue; + } + + public static IEEE754Double Increment(IEEE754Double value) { value.ULongValue++; return value; } - public static IEEE754Double operator --(IEEE754Double value) + public static IEEE754Double Decrement(IEEE754Double value) { value.ULongValue--; return value; @@ -56,7 +71,11 @@ public static IEEE754Double FromULong(ulong value) public static IEEE754Double FromString(string value) { +#if NET + return FromLong(Convert.ToInt64(value.Replace(" ", string.Empty, StringComparison.Ordinal), 2)); +#else return FromLong(Convert.ToInt64(value.Replace(" ", string.Empty), 2)); +#endif } public override string ToString() diff --git a/test/OpenTelemetry.Tests/Shared/InMemoryEventListener.cs b/test/OpenTelemetry.Tests/Shared/InMemoryEventListener.cs index 9768d619643..832f21f90d9 100644 --- a/test/OpenTelemetry.Tests/Shared/InMemoryEventListener.cs +++ b/test/OpenTelemetry.Tests/Shared/InMemoryEventListener.cs @@ -6,7 +6,7 @@ namespace OpenTelemetry.Tests; -internal class InMemoryEventListener : EventListener +internal sealed class InMemoryEventListener : EventListener { public ConcurrentQueue Events = new(); diff --git a/test/OpenTelemetry.Tests/Shared/MathHelper.cs b/test/OpenTelemetry.Tests/Shared/MathHelper.cs index 11a7573700e..0248af0a70d 100644 --- a/test/OpenTelemetry.Tests/Shared/MathHelper.cs +++ b/test/OpenTelemetry.Tests/Shared/MathHelper.cs @@ -12,7 +12,7 @@ internal static class MathHelper // 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 +#if NET return Math.BitIncrement(x); #else long bits = BitConverter.DoubleToInt64Bits(x); @@ -42,7 +42,7 @@ public static double BitIncrement(double x) public static double BitDecrement(double x) { -#if NET6_0_OR_GREATER +#if NET return Math.BitDecrement(x); #else long bits = BitConverter.DoubleToInt64Bits(x); diff --git a/test/OpenTelemetry.Tests/Shared/RecordOnlySampler.cs b/test/OpenTelemetry.Tests/Shared/RecordOnlySampler.cs index eb03f764e62..d2b37549a33 100644 --- a/test/OpenTelemetry.Tests/Shared/RecordOnlySampler.cs +++ b/test/OpenTelemetry.Tests/Shared/RecordOnlySampler.cs @@ -5,7 +5,7 @@ namespace OpenTelemetry.Tests; -internal class RecordOnlySampler : TestSampler +internal sealed class RecordOnlySampler : TestSampler { public override SamplingResult ShouldSample(in SamplingParameters param) { diff --git a/test/OpenTelemetry.Tests/Shared/SkipUnlessEnvVarFoundFactAttribute.cs b/test/OpenTelemetry.Tests/Shared/SkipUnlessEnvVarFoundFactAttribute.cs index adfebcaf184..624dbc2c2ed 100644 --- a/test/OpenTelemetry.Tests/Shared/SkipUnlessEnvVarFoundFactAttribute.cs +++ b/test/OpenTelemetry.Tests/Shared/SkipUnlessEnvVarFoundFactAttribute.cs @@ -5,19 +5,22 @@ namespace OpenTelemetry.Tests; -internal class SkipUnlessEnvVarFoundFactAttribute : FactAttribute +internal sealed class SkipUnlessEnvVarFoundFactAttribute : FactAttribute { public SkipUnlessEnvVarFoundFactAttribute(string environmentVariable) { + this.EnvironmentVariable = environmentVariable; if (string.IsNullOrEmpty(GetEnvironmentVariable(environmentVariable))) { this.Skip = $"Skipped because {environmentVariable} environment variable was not configured."; } } - public static string GetEnvironmentVariable(string environmentVariableName) + public string EnvironmentVariable { get; } + + public static string? GetEnvironmentVariable(string environmentVariableName) { - string environmentVariableValue = Environment.GetEnvironmentVariable(environmentVariableName, EnvironmentVariableTarget.Process); + string? environmentVariableValue = Environment.GetEnvironmentVariable(environmentVariableName, EnvironmentVariableTarget.Process); if (string.IsNullOrEmpty(environmentVariableValue)) { diff --git a/test/OpenTelemetry.Tests/Shared/SkipUnlessEnvVarFoundTheoryAttribute.cs b/test/OpenTelemetry.Tests/Shared/SkipUnlessEnvVarFoundTheoryAttribute.cs index dc5ce2ca66f..dd0a5011d51 100644 --- a/test/OpenTelemetry.Tests/Shared/SkipUnlessEnvVarFoundTheoryAttribute.cs +++ b/test/OpenTelemetry.Tests/Shared/SkipUnlessEnvVarFoundTheoryAttribute.cs @@ -5,19 +5,22 @@ namespace OpenTelemetry.Tests; -internal class SkipUnlessEnvVarFoundTheoryAttribute : TheoryAttribute +internal sealed class SkipUnlessEnvVarFoundTheoryAttribute : TheoryAttribute { public SkipUnlessEnvVarFoundTheoryAttribute(string environmentVariable) { + this.EnvironmentVariable = environmentVariable; if (string.IsNullOrEmpty(GetEnvironmentVariable(environmentVariable))) { this.Skip = $"Skipped because {environmentVariable} environment variable was not configured."; } } - public static string GetEnvironmentVariable(string environmentVariableName) + public string EnvironmentVariable { get; } + + public static string? GetEnvironmentVariable(string environmentVariableName) { - string environmentVariableValue = Environment.GetEnvironmentVariable(environmentVariableName, EnvironmentVariableTarget.Process); + string? environmentVariableValue = Environment.GetEnvironmentVariable(environmentVariableName, EnvironmentVariableTarget.Process); if (string.IsNullOrEmpty(environmentVariableValue)) { diff --git a/test/OpenTelemetry.Tests/Shared/SkipUnlessTrueTheoryAttribute.cs b/test/OpenTelemetry.Tests/Shared/SkipUnlessTrueTheoryAttribute.cs deleted file mode 100644 index 087bff4366f..00000000000 --- a/test/OpenTelemetry.Tests/Shared/SkipUnlessTrueTheoryAttribute.cs +++ /dev/null @@ -1,33 +0,0 @@ -// 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/TestActivityExportProcessor.cs b/test/OpenTelemetry.Tests/Shared/TestActivityExportProcessor.cs index 092ce859c67..bdeb432b516 100644 --- a/test/OpenTelemetry.Tests/Shared/TestActivityExportProcessor.cs +++ b/test/OpenTelemetry.Tests/Shared/TestActivityExportProcessor.cs @@ -5,7 +5,7 @@ namespace OpenTelemetry.Tests; -internal class TestActivityExportProcessor : SimpleActivityExportProcessor +internal sealed class TestActivityExportProcessor : SimpleActivityExportProcessor { public List ExportedItems = new(); diff --git a/test/OpenTelemetry.Tests/Shared/TestActivityProcessor.cs b/test/OpenTelemetry.Tests/Shared/TestActivityProcessor.cs index 36d47b7b8fa..2ca908126d2 100644 --- a/test/OpenTelemetry.Tests/Shared/TestActivityProcessor.cs +++ b/test/OpenTelemetry.Tests/Shared/TestActivityProcessor.cs @@ -5,26 +5,26 @@ namespace OpenTelemetry.Tests; -internal class TestActivityProcessor : BaseProcessor +internal sealed class TestActivityProcessor : BaseProcessor { - public Action StartAction; - public Action EndAction; + public Action? StartAction; + public Action? EndAction; public TestActivityProcessor() { } - public TestActivityProcessor(Action onStart, Action 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; } - public bool ForceFlushCalled { get; private set; } = false; + public bool ForceFlushCalled { get; private set; } - public bool DisposedCalled { get; private set; } = false; + public bool DisposedCalled { get; private set; } public override void OnStart(Activity span) { @@ -51,5 +51,6 @@ protected override bool OnShutdown(int timeoutMilliseconds) protected override void Dispose(bool disposing) { this.DisposedCalled = true; + base.Dispose(disposing); } } diff --git a/test/OpenTelemetry.Tests/Shared/TestEventListener.cs b/test/OpenTelemetry.Tests/Shared/TestEventListener.cs index b0cf1d0b120..71474b6cc4a 100644 --- a/test/OpenTelemetry.Tests/Shared/TestEventListener.cs +++ b/test/OpenTelemetry.Tests/Shared/TestEventListener.cs @@ -8,7 +8,7 @@ namespace OpenTelemetry.Tests; /// /// Event listener for testing event sources. /// -internal class TestEventListener : EventListener +internal sealed class TestEventListener : EventListener { /// Unique Id used to identify events from the test thread. private readonly Guid activityId; @@ -29,7 +29,7 @@ public TestEventListener() this.activityId = Guid.NewGuid(); EventSource.SetCurrentThreadActivityId(this.activityId); - this.events = new List(); + this.events = []; this.eventWritten = new AutoResetEvent(false); this.OnOnEventWritten = e => { @@ -39,7 +39,7 @@ public TestEventListener() } /// Gets or sets the handler for event source creation. - public Action OnOnEventSourceCreated { get; set; } + public Action? OnOnEventSourceCreated { get; set; } /// Gets or sets the handler for event source writes. public Action OnOnEventWritten { get; set; } @@ -66,6 +66,12 @@ public void ClearMessages() this.events.Clear(); } + public override void Dispose() + { + this.eventWritten.Dispose(); + base.Dispose(); + } + /// Handler for event source writes. /// The event data that was written. protected override void OnEventWritten(EventWrittenEventArgs eventData) @@ -81,7 +87,7 @@ protected override void OnEventWritten(EventWrittenEventArgs eventData) 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; + Action? callback = this.OnOnEventSourceCreated; callback?.Invoke(eventSource); } } diff --git a/test/OpenTelemetry.Tests/Shared/TestHttpServer.cs b/test/OpenTelemetry.Tests/Shared/TestHttpServer.cs index 5965e5fecf4..accadb236ba 100644 --- a/test/OpenTelemetry.Tests/Shared/TestHttpServer.cs +++ b/test/OpenTelemetry.Tests/Shared/TestHttpServer.cs @@ -5,7 +5,7 @@ namespace OpenTelemetry.Tests; -internal class TestHttpServer +internal static class TestHttpServer { private static readonly Random GlobalRandom = new(); @@ -13,14 +13,16 @@ public static IDisposable RunServer(Action action, out stri { host = "localhost"; port = 0; - RunningServer server = null; + RunningServer? server = null; var retryCount = 5; while (retryCount > 0) { try { +#pragma warning disable CA5394 // Do not use insecure randomness port = GlobalRandom.Next(2000, 5000); +#pragma warning restore CA5394 // Do not use insecure randomness server = new RunningServer(action, host, port); server.Start(); break; @@ -41,7 +43,7 @@ public static IDisposable RunServer(Action action, out stri return server; } - private class RunningServer : IDisposable + private sealed class RunningServer : IDisposable { private readonly Task httpListenerTask; private readonly HttpListener listener; @@ -64,7 +66,9 @@ public RunningServer(Action action, string host, int port) this.initialized.Set(); +#pragma warning disable CA2007 // Do not directly await a Task action(await ctxTask); +#pragma warning disable CA2007 // Do not directly await a Task } catch (Exception ex) { @@ -94,6 +98,7 @@ public void Dispose() { this.listener.Close(); this.httpListenerTask?.Wait(); + this.initialized.Dispose(); } catch (ObjectDisposedException) { diff --git a/test/OpenTelemetry.Tests/Shared/TestSampler.cs b/test/OpenTelemetry.Tests/Shared/TestSampler.cs index 7bc39ba454c..972985bfad0 100644 --- a/test/OpenTelemetry.Tests/Shared/TestSampler.cs +++ b/test/OpenTelemetry.Tests/Shared/TestSampler.cs @@ -7,7 +7,7 @@ namespace OpenTelemetry.Tests; internal class TestSampler : Sampler { - public Func SamplingAction { get; set; } + public Func? SamplingAction { get; set; } public SamplingParameters LatestSamplingParameters { get; private set; } diff --git a/test/OpenTelemetry.Tests/Shared/Utils.cs b/test/OpenTelemetry.Tests/Shared/Utils.cs index d85c8c74e47..95f24821d0e 100644 --- a/test/OpenTelemetry.Tests/Shared/Utils.cs +++ b/test/OpenTelemetry.Tests/Shared/Utils.cs @@ -6,7 +6,7 @@ namespace OpenTelemetry.Tests; -internal class Utils +internal static class Utils { [MethodImpl(MethodImplOptions.NoInlining)] public static string GetCurrentMethodName() diff --git a/test/OpenTelemetry.Tests/SimpleExportProcessorTest.cs b/test/OpenTelemetry.Tests/SimpleExportProcessorTests.cs similarity index 65% rename from test/OpenTelemetry.Tests/SimpleExportProcessorTest.cs rename to test/OpenTelemetry.Tests/SimpleExportProcessorTests.cs index 3e5b92c5b52..6476db62cc2 100644 --- a/test/OpenTelemetry.Tests/SimpleExportProcessorTest.cs +++ b/test/OpenTelemetry.Tests/SimpleExportProcessorTests.cs @@ -5,7 +5,7 @@ namespace OpenTelemetry.Tests; -public class SimpleExportProcessorTest +public class SimpleExportProcessorTests { [Fact] public void Verify_SimpleExportProcessor_HandlesException() @@ -13,16 +13,18 @@ public void Verify_SimpleExportProcessor_HandlesException() int counter = 0; // here our exporter will throw an exception. +#pragma warning disable CA2000 // Dispose objects before losing scope var testExporter = new DelegatingExporter { - OnExportFunc = (batch) => + OnExportFunc = batch => { counter++; - throw new Exception("test exception"); + throw new InvalidOperationException("test exception"); }, }; +#pragma warning restore CA2000 // Dispose objects before losing scope - var testSimpleExportProcessor = new TestSimpleExportProcessor(testExporter); + using var testSimpleExportProcessor = new TestSimpleExportProcessor(testExporter); // Verify that the Processor catches and suppresses the exception. testSimpleExportProcessor.OnEnd(new object()); @@ -34,7 +36,7 @@ public void Verify_SimpleExportProcessor_HandlesException() /// /// Testable class for abstract . /// - public class TestSimpleExportProcessor : SimpleExportProcessor + private sealed class TestSimpleExportProcessor : SimpleExportProcessor { public TestSimpleExportProcessor(BaseExporter exporter) : base(exporter) diff --git a/test/OpenTelemetry.Tests/SuppressInstrumentationTest.cs b/test/OpenTelemetry.Tests/SuppressInstrumentationTests.cs similarity index 89% rename from test/OpenTelemetry.Tests/SuppressInstrumentationTest.cs rename to test/OpenTelemetry.Tests/SuppressInstrumentationTests.cs index d6ccb414ddf..730c1b02af5 100644 --- a/test/OpenTelemetry.Tests/SuppressInstrumentationTest.cs +++ b/test/OpenTelemetry.Tests/SuppressInstrumentationTests.cs @@ -5,7 +5,7 @@ namespace OpenTelemetry.Tests; -public class SuppressInstrumentationTest +public class SuppressInstrumentationTests { [Fact] public static void UsingSuppressInstrumentation() @@ -56,12 +56,17 @@ public async Task SuppressInstrumentationScopeEnterIsLocalToAsyncFlow() 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); - }); + + await Task.Factory.StartNew( + () => + { + Assert.False(Sdk.SuppressInstrumentation); + Assert.Equal(1, SuppressInstrumentationScope.Enter()); + Assert.True(Sdk.SuppressInstrumentation); + }, + CancellationToken.None, + TaskCreationOptions.None, + TaskScheduler.Default); Assert.False(Sdk.SuppressInstrumentation); // Changes made by SuppressInstrumentationScope.Enter in the task above are not reflected here as it's not part of the same async flow } diff --git a/test/OpenTelemetry.Tests/TestSelfDiagnosticsConfigRefresher.cs b/test/OpenTelemetry.Tests/TestSelfDiagnosticsConfigRefresher.cs index faee6e8f287..3014c0e94d9 100644 --- a/test/OpenTelemetry.Tests/TestSelfDiagnosticsConfigRefresher.cs +++ b/test/OpenTelemetry.Tests/TestSelfDiagnosticsConfigRefresher.cs @@ -6,17 +6,17 @@ namespace OpenTelemetry.Tests; -internal class TestSelfDiagnosticsConfigRefresher(Stream stream = null) : SelfDiagnosticsConfigRefresher +internal sealed class TestSelfDiagnosticsConfigRefresher(Stream? stream = null) : SelfDiagnosticsConfigRefresher { - private readonly Stream stream = stream; + 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) + public override bool TryGetLogStream(int byteCount, [NotNullWhen(true)] out Stream? stream, out int availableByteCount) { this.TryGetLogStreamCalled = true; stream = this.stream; availableByteCount = 0; - return true; + return this.stream != null; } } diff --git a/test/OpenTelemetry.Tests/Trace/BatchExportActivityProcessorOptionsTest.cs b/test/OpenTelemetry.Tests/Trace/BatchExportActivityProcessorOptionsTests.cs similarity index 96% rename from test/OpenTelemetry.Tests/Trace/BatchExportActivityProcessorOptionsTest.cs rename to test/OpenTelemetry.Tests/Trace/BatchExportActivityProcessorOptionsTests.cs index 27f1e3de92f..6dd6bde7ff9 100644 --- a/test/OpenTelemetry.Tests/Trace/BatchExportActivityProcessorOptionsTest.cs +++ b/test/OpenTelemetry.Tests/Trace/BatchExportActivityProcessorOptionsTests.cs @@ -6,9 +6,9 @@ namespace OpenTelemetry.Trace.Tests; -public class BatchExportActivityProcessorOptionsTest : IDisposable +public sealed class BatchExportActivityProcessorOptionsTests : IDisposable { - public BatchExportActivityProcessorOptionsTest() + public BatchExportActivityProcessorOptionsTests() { ClearEnvVars(); } @@ -49,7 +49,7 @@ public void BatchExportProcessorOptions_EnvironmentVariableOverride() [Fact] public void BatchExportProcessorOptions_UsingIConfiguration() { - var values = new Dictionary() + var values = new Dictionary() { [BatchExportActivityProcessorOptions.MaxQueueSizeEnvVarKey] = "1", [BatchExportActivityProcessorOptions.MaxExportBatchSizeEnvVarKey] = "2", diff --git a/test/OpenTelemetry.Tests/Trace/BatchExportActivityProcessorTest.cs b/test/OpenTelemetry.Tests/Trace/BatchExportActivityProcessorTests.cs similarity index 77% rename from test/OpenTelemetry.Tests/Trace/BatchExportActivityProcessorTest.cs rename to test/OpenTelemetry.Tests/Trace/BatchExportActivityProcessorTests.cs index bce50338359..fe1cdfab45f 100644 --- a/test/OpenTelemetry.Tests/Trace/BatchExportActivityProcessorTest.cs +++ b/test/OpenTelemetry.Tests/Trace/BatchExportActivityProcessorTests.cs @@ -7,12 +7,12 @@ namespace OpenTelemetry.Trace.Tests; -public class BatchExportActivityProcessorTest +public class BatchExportActivityProcessorTests { [Fact] public void CheckNullExporter() { - Assert.Throws(() => new BatchActivityExportProcessor(null)); + Assert.Throws(() => new BatchActivityExportProcessor(null!)); } [Fact] @@ -27,7 +27,7 @@ public void CheckConstructorWithInvalidValues() } [Fact] - public void CheckIfBatchIsExportingOnQueueLimit() + public async Task CheckIfBatchIsExportingOnQueueLimit() { var exportedItems = new List(); using var exporter = new InMemoryExporter(exportedItems); @@ -44,10 +44,7 @@ public void CheckIfBatchIsExportingOnQueueLimit() processor.OnEnd(activity); - for (int i = 0; i < 10 && exportedItems.Count == 0; i++) - { - Thread.Sleep(500); - } + await WaitForMinimumCountAsync(exportedItems, 1); Assert.Single(exportedItems); @@ -69,7 +66,7 @@ public void CheckForceFlushWithInvalidTimeout() [InlineData(Timeout.Infinite)] [InlineData(0)] [InlineData(1)] - public void CheckForceFlushExport(int timeout) + public async Task CheckForceFlushExport(int timeout) { var exportedItems = new List(); using var exporter = new InMemoryExporter(exportedItems); @@ -95,22 +92,23 @@ public void CheckForceFlushExport(int timeout) Assert.Equal(0, processor.ProcessedCount); // waiting to see if time is triggering the exporter - Thread.Sleep(1_000); + await Task.Delay(TimeSpan.FromSeconds(1)); Assert.Empty(exportedItems); // forcing flush - processor.ForceFlush(timeout); + var result = processor.ForceFlush(timeout); - if (timeout == 0) - { - // ForceFlush(0) will trigger flush and return immediately, so let's sleep for a while - Thread.Sleep(1_000); - } + Assert.Equal(timeout != 0, result); - Assert.Equal(2, exportedItems.Count); + // Wait for the expected number of items to be exported + int expectedCount = 2; - Assert.Equal(2, processor.ProcessedCount); - Assert.Equal(2, processor.ReceivedCount); + await WaitForMinimumCountAsync(exportedItems, expectedCount); + + Assert.Equal(expectedCount, exportedItems.Count); + + Assert.Equal(expectedCount, processor.ProcessedCount); + Assert.Equal(expectedCount, processor.ReceivedCount); Assert.Equal(0, processor.DroppedCount); } @@ -118,7 +116,7 @@ public void CheckForceFlushExport(int timeout) [InlineData(Timeout.Infinite)] [InlineData(0)] [InlineData(1)] - public void CheckShutdownExport(int timeoutMilliseconds) + public async Task CheckShutdownExport(int timeoutMilliseconds) { var exportedItems = new List(); using var exporter = new InMemoryExporter(exportedItems); @@ -136,10 +134,7 @@ public void CheckShutdownExport(int timeoutMilliseconds) processor.OnEnd(activity); processor.Shutdown(timeoutMilliseconds); - if (timeoutMilliseconds < 1_000) - { - Thread.Sleep(1_000 - timeoutMilliseconds); - } + await WaitForMinimumCountAsync(exportedItems, 1); Assert.Single(exportedItems); @@ -174,7 +169,9 @@ public void CheckExportForRecordingButNotSampledActivity() public void CheckExportDrainsBatchOnFailure() { using var processor = new BatchActivityExportProcessor( +#pragma warning disable CA2000 // Dispose objects before losing scope exporter: new FailureExporter(), +#pragma warning restore CA2000 // Dispose objects before losing scope maxQueueSize: 3, maxExportBatchSize: 3); @@ -191,7 +188,22 @@ public void CheckExportDrainsBatchOnFailure() Assert.Equal(3, processor.ProcessedCount); // Verify batch was drained even though nothing was exported. } - private class FailureExporter : BaseExporter + private static async Task WaitForMinimumCountAsync(List collection, int minimum) + { + var maximumWait = TimeSpan.FromSeconds(5); + var waitInterval = TimeSpan.FromSeconds(0.25); + + using var cts = new CancellationTokenSource(maximumWait); + + // We check for a minimum because if there are too many it's better to + // terminate the loop and let the assert in the caller fail immediately + while (!cts.IsCancellationRequested && collection.Count < minimum) + { + await Task.Delay(waitInterval); + } + } + + private sealed 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/BatchTests.cs similarity index 97% rename from test/OpenTelemetry.Tests/Trace/BatchTest.cs rename to test/OpenTelemetry.Tests/Trace/BatchTests.cs index b1d5b170d52..918852a45bf 100644 --- a/test/OpenTelemetry.Tests/Trace/BatchTest.cs +++ b/test/OpenTelemetry.Tests/Trace/BatchTests.cs @@ -6,14 +6,16 @@ namespace OpenTelemetry.Trace.Tests; -public class BatchTest +public class BatchTests { [Fact] public void CheckConstructorExceptions() { - Assert.Throws(() => new Batch((string[])null, 0)); + Assert.Throws(() => new Batch((string[]?)null!, 0)); Assert.Throws(() => new Batch(Array.Empty(), -1)); Assert.Throws(() => new Batch(Array.Empty(), 1)); + + Assert.Throws(() => new Batch(null!)); } [Fact] diff --git a/test/OpenTelemetry.Tests/Trace/CompositeActivityProcessorTests.cs b/test/OpenTelemetry.Tests/Trace/CompositeActivityProcessorTests.cs index 8f8bccb8e29..a287f897714 100644 --- a/test/OpenTelemetry.Tests/Trace/CompositeActivityProcessorTests.cs +++ b/test/OpenTelemetry.Tests/Trace/CompositeActivityProcessorTests.cs @@ -12,12 +12,12 @@ public class CompositeActivityProcessorTests [Fact] public void CompositeActivityProcessor_BadArgs() { - Assert.Throws(() => new CompositeProcessor(null)); - Assert.Throws(() => new CompositeProcessor(Array.Empty>())); + Assert.Throws(() => new CompositeProcessor(null!)); + Assert.Throws(() => new CompositeProcessor([])); using var p1 = new TestActivityProcessor(null, null); - using var processor = new CompositeProcessor(new[] { p1 }); - Assert.Throws(() => processor.AddProcessor(null)); + using var processor = new CompositeProcessor([p1]); + Assert.Throws(() => processor.AddProcessor(null!)); } [Fact] @@ -34,7 +34,7 @@ public void CompositeActivityProcessor_CallsAllProcessorSequentially() using var activity = new Activity("test"); - using (var processor = new CompositeProcessor(new[] { p1, p2 })) + using (var processor = new CompositeProcessor([p1, p2])) { processor.OnStart(activity); processor.OnEnd(activity); @@ -47,14 +47,14 @@ public void CompositeActivityProcessor_CallsAllProcessorSequentially() public void CompositeActivityProcessor_ProcessorThrows() { using var p1 = new TestActivityProcessor( - activity => { throw new Exception("Start exception"); }, - activity => { throw new Exception("End exception"); }); + _ => throw new InvalidOperationException("Start exception"), + _ => throw new InvalidOperationException("End exception")); using var activity = new Activity("test"); - using var processor = new CompositeProcessor(new[] { p1 }); - Assert.Throws(() => { processor.OnStart(activity); }); - Assert.Throws(() => { processor.OnEnd(activity); }); + using var processor = new CompositeProcessor([p1]); + Assert.Throws(() => { processor.OnStart(activity); }); + Assert.Throws(() => { processor.OnEnd(activity); }); } [Fact] @@ -63,7 +63,7 @@ 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, p2 }); + using var processor = new CompositeProcessor([p1, p2]); processor.Shutdown(); Assert.True(p1.ShutdownCalled); Assert.True(p2.ShutdownCalled); @@ -78,7 +78,7 @@ 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 }); + using var processor = new CompositeProcessor([p1, p2]); processor.ForceFlush(timeout); Assert.True(p1.ForceFlushCalled); @@ -93,7 +93,7 @@ public void CompositeActivityProcessor_ForwardsParentProvider() using var p1 = new TestActivityProcessor(null, null); using var p2 = new TestActivityProcessor(null, null); - using var processor = new CompositeProcessor(new[] { p1, p2 }); + using var processor = new CompositeProcessor([p1, p2]); Assert.Null(processor.ParentProvider); Assert.Null(p1.ParentProvider); diff --git a/test/OpenTelemetry.Tests/Trace/CurrentSpanTests.cs b/test/OpenTelemetry.Tests/Trace/CurrentSpanTests.cs index e9b76794625..76a85d5b2f6 100644 --- a/test/OpenTelemetry.Tests/Trace/CurrentSpanTests.cs +++ b/test/OpenTelemetry.Tests/Trace/CurrentSpanTests.cs @@ -6,7 +6,7 @@ namespace OpenTelemetry.Trace.Tests; -public class CurrentSpanTests : IDisposable +public sealed class CurrentSpanTests : IDisposable { private readonly Tracer tracer; @@ -15,7 +15,7 @@ public CurrentSpanTests() Activity.DefaultIdFormat = ActivityIdFormat.W3C; Activity.ForceDefaultIdFormat = true; - this.tracer = TracerProvider.Default.GetTracer(null); + this.tracer = TracerProvider.Default.GetTracer(null!); } [Fact] @@ -27,7 +27,8 @@ public void CurrentSpan_WhenNoContext() [Fact] public void CurrentSpan_WhenActivityExists() { - using var activity = new Activity("foo").Start(); + using var activity = new Activity("foo"); + activity.Start(); Assert.True(Tracer.CurrentSpan.Context.IsValid); } diff --git a/test/OpenTelemetry.Tests/Trace/ExceptionProcessorTest.cs b/test/OpenTelemetry.Tests/Trace/ExceptionProcessorTests.cs similarity index 79% rename from test/OpenTelemetry.Tests/Trace/ExceptionProcessorTest.cs rename to test/OpenTelemetry.Tests/Trace/ExceptionProcessorTests.cs index 98d8d01a034..178db510e83 100644 --- a/test/OpenTelemetry.Tests/Trace/ExceptionProcessorTest.cs +++ b/test/OpenTelemetry.Tests/Trace/ExceptionProcessorTests.cs @@ -7,7 +7,7 @@ namespace OpenTelemetry.Trace.Tests; -public class ExceptionProcessorTest +public class ExceptionProcessorTests { [Fact] public void ActivityStatusSetToErrorWhenExceptionProcessorEnabled() @@ -17,14 +17,16 @@ public void ActivityStatusSetToErrorWhenExceptionProcessorEnabled() using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(activitySourceName) .SetSampler(new AlwaysOnSampler()) +#pragma warning disable CA2000 // Dispose objects before losing scope .AddProcessor(new ExceptionProcessor()) +#pragma warning restore CA2000 // Dispose objects before losing scope .Build(); - Activity activity1 = null; - Activity activity2 = null; - Activity activity3 = null; - Activity activity4 = null; - Activity activity5 = null; + Activity? activity1 = null; + Activity? activity2 = null; + Activity? activity3 = null; + Activity? activity4 = null; + Activity? activity5 = null; try { @@ -32,7 +34,7 @@ public void ActivityStatusSetToErrorWhenExceptionProcessorEnabled() { using (activity2 = activitySource.StartActivity("Activity2")) { - throw new Exception("Oops!"); + throw new InvalidOperationException("Oops!"); } } } @@ -51,7 +53,7 @@ public void ActivityStatusSetToErrorWhenExceptionProcessorEnabled() try { - throw new Exception("Oops!"); + throw new InvalidOperationException("Oops!"); } catch (Exception) { @@ -69,17 +71,27 @@ Marshal.GetExceptionPointers returns non-zero. } finally { - activity5.Dispose(); + activity5?.Dispose(); } + Assert.NotNull(activity1); Assert.Equal(StatusCode.Error, activity1.GetStatus().StatusCode); Assert.Null(GetTagValue(activity1, "otel.exception_pointers")); + + Assert.NotNull(activity2); Assert.Equal(StatusCode.Error, activity2.GetStatus().StatusCode); Assert.Null(GetTagValue(activity2, "otel.exception_pointers")); + + Assert.NotNull(activity3); Assert.Equal(StatusCode.Unset, activity3.GetStatus().StatusCode); Assert.Null(GetTagValue(activity3, "otel.exception_pointers")); + + Assert.NotNull(activity4); Assert.Equal(StatusCode.Unset, activity4.GetStatus().StatusCode); + Assert.Null(GetTagValue(activity4, "otel.exception_pointers")); + + Assert.NotNull(activity5); Assert.Equal(StatusCode.Unset, activity5.GetStatus().StatusCode); #if !NETFRAMEWORK if (Environment.Is64BitProcess) @@ -104,13 +116,13 @@ public void ActivityStatusNotSetWhenExceptionProcessorNotEnabled() .SetSampler(new AlwaysOnSampler()) .Build(); - Activity activity = null; + Activity? activity = null; try { using (activity = activitySource.StartActivity("Activity")) { - throw new Exception("Oops!"); + throw new InvalidOperationException("Oops!"); } } catch (Exception) @@ -120,11 +132,11 @@ public void ActivityStatusNotSetWhenExceptionProcessorNotEnabled() Assert.Equal(StatusCode.Unset, activity.GetStatus().StatusCode); } - private static object GetTagValue(Activity activity, string tagName) + 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()) + foreach (ref readonly var tag in activity!.EnumerateTagObjects()) { if (tag.Key == tagName) { diff --git a/test/OpenTelemetry.Tests/Trace/ExportProcessorTest.cs b/test/OpenTelemetry.Tests/Trace/ExportProcessorTests.cs similarity index 83% rename from test/OpenTelemetry.Tests/Trace/ExportProcessorTest.cs rename to test/OpenTelemetry.Tests/Trace/ExportProcessorTests.cs index 9c4768bc251..b7d3bcc911a 100644 --- a/test/OpenTelemetry.Tests/Trace/ExportProcessorTest.cs +++ b/test/OpenTelemetry.Tests/Trace/ExportProcessorTests.cs @@ -8,7 +8,7 @@ namespace OpenTelemetry.Trace.Tests; -public class ExportProcessorTest +public class ExportProcessorTests { [Fact] public void ExportProcessorIgnoresActivityWhenDropped() @@ -16,7 +16,9 @@ public void ExportProcessorIgnoresActivityWhenDropped() var activitySourceName = Utils.GetCurrentMethodName(); var sampler = new AlwaysOffSampler(); var exportedItems = new List(); +#pragma warning disable CA2000 // Dispose objects before losing scope using var processor = new TestActivityExportProcessor(new InMemoryExporter(exportedItems)); +#pragma warning restore CA2000 // Dispose objects before losing scope using var activitySource = new ActivitySource(activitySourceName); using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(activitySourceName) @@ -26,6 +28,7 @@ public void ExportProcessorIgnoresActivityWhenDropped() using (var activity = activitySource.StartActivity("Activity")) { + Assert.NotNull(activity); Assert.False(activity.IsAllDataRequested); Assert.Equal(ActivityTraceFlags.None, activity.ActivityTraceFlags); } @@ -39,7 +42,9 @@ public void ExportProcessorIgnoresActivityMarkedAsRecordOnly() var activitySourceName = Utils.GetCurrentMethodName(); var sampler = new RecordOnlySampler(); var exportedItems = new List(); +#pragma warning disable CA2000 // Dispose objects before losing scope using var processor = new TestActivityExportProcessor(new InMemoryExporter(exportedItems)); +#pragma warning restore CA2000 // Dispose objects before losing scope using var activitySource = new ActivitySource(activitySourceName); using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(activitySourceName) @@ -49,6 +54,7 @@ public void ExportProcessorIgnoresActivityMarkedAsRecordOnly() using (var activity = activitySource.StartActivity("Activity")) { + Assert.NotNull(activity); Assert.True(activity.IsAllDataRequested); Assert.Equal(ActivityTraceFlags.None, activity.ActivityTraceFlags); } @@ -62,7 +68,9 @@ public void ExportProcessorExportsActivityMarkedAsRecordAndSample() var activitySourceName = Utils.GetCurrentMethodName(); var sampler = new AlwaysOnSampler(); var exportedItems = new List(); +#pragma warning disable CA2000 // Dispose objects before losing scope using var processor = new TestActivityExportProcessor(new InMemoryExporter(exportedItems)); +#pragma warning restore CA2000 // Dispose objects before losing scope using var activitySource = new ActivitySource(activitySourceName); using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(activitySourceName) @@ -72,6 +80,7 @@ public void ExportProcessorExportsActivityMarkedAsRecordAndSample() using (var activity = activitySource.StartActivity("Activity")) { + Assert.NotNull(activity); Assert.True(activity.IsAllDataRequested); Assert.Equal(ActivityTraceFlags.Recorded, activity.ActivityTraceFlags); } diff --git a/test/OpenTelemetry.Tests/Trace/LinkTest.cs b/test/OpenTelemetry.Tests/Trace/LinkTests.cs similarity index 67% rename from test/OpenTelemetry.Tests/Trace/LinkTest.cs rename to test/OpenTelemetry.Tests/Trace/LinkTests.cs index fbfd543da1c..0b416a0e144 100644 --- a/test/OpenTelemetry.Tests/Trace/LinkTest.cs +++ b/test/OpenTelemetry.Tests/Trace/LinkTests.cs @@ -6,33 +6,37 @@ namespace OpenTelemetry.Trace.Tests; -public class LinkTest : IDisposable +public sealed class LinkTests : IDisposable { - private readonly IDictionary attributesMap = new Dictionary(); + private readonly Dictionary attributesMap = []; private readonly SpanContext spanContext; private readonly SpanAttributes tags; - public LinkTest() + public LinkTests() { this.spanContext = new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None); + long[] longArray = [1L, 2L]; + string[] stringArray = ["a", "b"]; + bool[] boolArray = [true, false]; + double[] doubleArray = [0.1, -0.1]; 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.attributesMap.Add("MyAttributeKey4", longArray); + this.attributesMap.Add("MyAttributeKey5", stringArray); + this.attributesMap.Add("MyAttributeKey6", boolArray); + this.attributesMap.Add("MyAttributeKey7", doubleArray); 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 }); + this.tags.Add("MyAttributeKey4", [1L, 2L]); + this.tags.Add("MyAttributeKey5", ["a", "b"]); + this.tags.Add("MyAttributeKey6", [true, false]); + this.tags.Add("MyAttributeKey7", [0.1, -0.1]); } [Fact] @@ -50,9 +54,10 @@ public void FromSpanContext_WithAttributes() Assert.Equal(this.spanContext.TraceId, link.Context.TraceId); Assert.Equal(this.spanContext.SpanId, link.Context.SpanId); + Assert.NotNull(link.Attributes); 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); } } @@ -68,18 +73,6 @@ public void Equality() 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); - - Assert.Equal(link1, link2); - Assert.True(link1 == link2); - Assert.True(link1.Equals(link3)); - } - [Fact] public void NotEquality() { diff --git a/test/OpenTelemetry.Tests/Trace/ParentBasedSamplerTests.cs b/test/OpenTelemetry.Tests/Trace/ParentBasedSamplerTests.cs index 0861a224ca9..7f141da4ded 100644 --- a/test/OpenTelemetry.Tests/Trace/ParentBasedSamplerTests.cs +++ b/test/OpenTelemetry.Tests/Trace/ParentBasedSamplerTests.cs @@ -119,7 +119,7 @@ public void CustomSamplers(bool parentIsRemote, bool parentIsSampled) [Fact] public void DisallowNullRootSampler() { - Assert.Throws(() => new ParentBasedSampler(null)); + Assert.Throws(() => new ParentBasedSampler(null!)); } private static SamplingParameters MakeTestParameters(bool parentIsRemote, bool parentIsSampled) diff --git a/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTest.cs b/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTests.cs similarity index 89% rename from test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTest.cs rename to test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTests.cs index 728d3884e45..f9ec9a60a4d 100644 --- a/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTest.cs +++ b/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTests.cs @@ -6,19 +6,19 @@ namespace OpenTelemetry.Context.Propagation.Tests; -public class TraceContextPropagatorTest +public class TraceContextPropagatorTests { 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 string[] Empty = []; private static readonly Func, string, IEnumerable> Getter = (headers, name) => { if (headers.TryGetValue(name, out var value)) { - return new[] { value }; + return [value]; } return Empty; @@ -31,7 +31,7 @@ public class TraceContextPropagatorTest return value; } - return Array.Empty(); + return []; }; private static readonly Action, string, string> Setter = (carrier, name, value) => @@ -183,8 +183,18 @@ 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" })); + Assert.Empty(CallTraceContextPropagator(["foo=1", "foo=1"])); + Assert.Empty(CallTraceContextPropagator(["foo=1", "foo=2"])); + Assert.Empty(CallTraceContextPropagator("foo=1,bar=2,baz=3,foo=4")); + } + + [Fact] + public void NoDuplicateKeys() + { + Assert.Equal("foo=1,bar=foo,baz=2", CallTraceContextPropagator("foo=1,bar=foo,baz=2")); + Assert.Equal("foo=1,bar=2,baz=foo", CallTraceContextPropagator("foo=1,bar=2,baz=foo")); + Assert.Equal("foo=1,foo@tenant=2", CallTraceContextPropagator("foo=1,foo@tenant=2")); + Assert.Equal("foo=1,tenant@foo=2", CallTraceContextPropagator("foo=1,tenant@foo=2")); } [Fact] @@ -210,13 +220,13 @@ public void Key_IllegalVendorFormat() public void MemberCountLimit() { // test_tracestate_member_count_limit - var output1 = CallTraceContextPropagator(new string[] - { + var output1 = CallTraceContextPropagator( + [ "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", - }); + "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" + "," + @@ -224,13 +234,13 @@ public void MemberCountLimit() "bar31=31,bar32=32"; Assert.Equal(expected, output1); - var output2 = CallTraceContextPropagator(new string[] - { + var output2 = CallTraceContextPropagator( + [ "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", - }); + "bar31=31,bar32=32,bar33=33" + ]); Assert.Empty(output2); } @@ -303,18 +313,24 @@ private static string CallTraceContextPropagator(string tracestate) }; var f = new TraceContextPropagator(); var ctx = f.Extract(default, headers, Getter); - return ctx.ActivityContext.TraceState; + + var traceState = ctx.ActivityContext.TraceState; + Assert.NotNull(traceState); + return traceState; } private static string CallTraceContextPropagator(string[] tracestate) { var headers = new Dictionary { - { TraceParent, new[] { $"00-{TraceId}-{SpanId}-01" } }, + { TraceParent, [$"00-{TraceId}-{SpanId}-01"] }, { TraceState, tracestate }, }; var f = new TraceContextPropagator(); var ctx = f.Extract(default, headers, ArrayGetter); - return ctx.ActivityContext.TraceState; + + var traceState = ctx.ActivityContext.TraceState; + Assert.NotNull(traceState); + return traceState; } } diff --git a/test/OpenTelemetry.Tests/Trace/SamplersTest.cs b/test/OpenTelemetry.Tests/Trace/SamplersTests.cs similarity index 66% rename from test/OpenTelemetry.Tests/Trace/SamplersTest.cs rename to test/OpenTelemetry.Tests/Trace/SamplersTests.cs index 94f158e910c..9c7ae28ee29 100644 --- a/test/OpenTelemetry.Tests/Trace/SamplersTest.cs +++ b/test/OpenTelemetry.Tests/Trace/SamplersTests.cs @@ -7,17 +7,14 @@ namespace OpenTelemetry.Trace.Tests; -public class SamplersTest +public class SamplersTests { - private static readonly ActivityKind ActivityKindServer = ActivityKind.Server; private readonly ActivityTraceId traceId; - private readonly ActivitySpanId spanId; private readonly ActivitySpanId parentSpanId; - public SamplersTest() + public SamplersTests() { this.traceId = ActivityTraceId.CreateRandom(); - this.spanId = ActivitySpanId.CreateRandom(); this.parentSpanId = ActivitySpanId.CreateRandom(); } @@ -31,7 +28,7 @@ public void AlwaysOnSampler_AlwaysReturnTrue(ActivityTraceFlags flags) Assert.Equal( SamplingDecision.RecordAndSample, - new AlwaysOnSampler().ShouldSample(new SamplingParameters(parentContext, this.traceId, "Another name", ActivityKindServer, null, new List { link })).Decision); + new AlwaysOnSampler().ShouldSample(new SamplingParameters(parentContext, this.traceId, "Another name", ActivityKind.Server, null, new List { link })).Decision); } [Fact] @@ -50,7 +47,7 @@ public void AlwaysOffSampler_AlwaysReturnFalse(ActivityTraceFlags flags) Assert.Equal( SamplingDecision.Drop, - new AlwaysOffSampler().ShouldSample(new SamplingParameters(parentContext, this.traceId, "Another name", ActivityKindServer, null, new List { link })).Decision); + new AlwaysOffSampler().ShouldSample(new SamplingParameters(parentContext, this.traceId, "Another name", ActivityKind.Server, null, new List { link })).Decision); } [Fact] @@ -88,7 +85,7 @@ public void TracerProviderSdkSamplerAttributesAreAppliedToLegacyActivity(Samplin Assert.NotNull(activity); if (samplingDecision != SamplingDecision.Drop) { - Assert.Contains(new KeyValuePair("tagkeybysampler", "tagvalueaddedbysampler"), activity.TagObjects); + Assert.Contains(new KeyValuePair("tagkeybysampler", "tagvalueaddedbysampler"), activity.TagObjects); } activity.Stop(); @@ -100,37 +97,7 @@ public void TracerProviderSdkSamplerAttributesAreAppliedToLegacyActivity(Samplin [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 - { - 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(); + RunLegacyActivitySamplerTest(samplingDecision, samplerTraceState: "a=1,b=2,c=3,d=4"); } [Theory] @@ -139,36 +106,7 @@ public void SamplersCanModifyTraceStateOnLegacyActivity(SamplingDecision samplin [InlineData(SamplingDecision.RecordAndSample)] public void SamplersDoesNotImpactTraceStateWhenUsingNullLegacyActivity(SamplingDecision samplingDecision) { - var existingTraceState = "a=1,b=2"; - var testSampler = new TestSampler - { - 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(); + RunLegacyActivitySamplerTest(samplingDecision, samplerTraceState: null); } [Theory] @@ -183,7 +121,15 @@ public void SamplersCanModifyTraceState(SamplingDecision sampling) { SamplingAction = (samplingParams) => { - Assert.Equal(parentTraceState, samplingParams.ParentContext.TraceState); + if (samplingParams.Name == "root") + { + Assert.Equal(parentTraceState, samplingParams.ParentContext.TraceState); + } + else + { + Assert.Equal(newTraceState, samplingParams.ParentContext.TraceState); + } + return new SamplingResult(sampling, newTraceState); }, }; @@ -195,12 +141,54 @@ public void SamplersCanModifyTraceState(SamplingDecision sampling) .SetSampler(testSampler) .Build(); - var parentContext = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded, parentTraceState, true); + // Note: Remote parent is set as recorded + var parentContext = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded, parentTraceState, isRemote: true); + + using var root = activitySource.StartActivity("root", ActivityKind.Server, parentContext); + + // Note: We always create a root even for Drop. When dropping the + // created root is for propagation only + Assert.NotNull(root); + Assert.Equal(newTraceState, root.TraceStateString); + + if (sampling == SamplingDecision.RecordAndSample) + { + Assert.True(root.Recorded); + Assert.True(root.IsAllDataRequested); + } + else if (sampling == SamplingDecision.RecordOnly) + { + // TODO: Update this when repo consumes DS v10. + // Note: Seems to be a bug in DiagnosticSource. Root in this case + // inherits context from the remote parent and Recorded doesn't get + // cleared. This should be fixed in .NET 10: + // https://github.com/dotnet/runtime/pull/111289 + // Assert.False(root.Recorded); + + Assert.True(root.IsAllDataRequested); + } + else + { + // TODO: Update this when repo consumes DS v10. + // Note: Seems to be a bug in DiagnosticSource. Root in this case + // inherits context from the remote parent and Recorded doesn't get + // cleared. This should be fixed in .NET 10: + // https://github.com/dotnet/runtime/pull/111289 + // Assert.False(root.Recorded); + + Assert.False(root.IsAllDataRequested); + } + + using var child = activitySource.StartActivity("child", ActivityKind.Server); - using var activity = activitySource.StartActivity("root", ActivityKind.Server, parentContext); if (sampling != SamplingDecision.Drop) { - Assert.Equal(newTraceState, activity.TraceStateString); + Assert.NotNull(child); + Assert.Equal(newTraceState, child.TraceStateString); + } + else + { + Assert.Null(child); } } @@ -237,6 +225,7 @@ public void SamplersDoesNotImpactTraceStateWhenUsingNull(SamplingDecision sampli using var activity = activitySource.StartActivity("root", ActivityKind.Server, parentContext); if (sampling != SamplingDecision.Drop) { + Assert.NotNull(activity); Assert.Equal(parentTraceState, activity.TraceStateString); } } @@ -258,6 +247,60 @@ public void SamplerExceptionBubblesUpTest() Assert.Throws(() => activitySource.StartActivity("ThrowingSampler")); } + private static void RunLegacyActivitySamplerTest(SamplingDecision samplingDecision, string? samplerTraceState) + { + var existingTraceState = "a=1,b=2"; + + var operationNameForLegacyActivity = Utils.GetCurrentMethodName(); + + var testSampler = new TestSampler + { + SamplingAction = (samplingParams) => + { + Assert.Equal(samplingParams.Name, operationNameForLegacyActivity); + Assert.Equal(existingTraceState, samplingParams.ParentContext.TraceState); + return new SamplingResult(samplingDecision, samplerTraceState); + }, + }; + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(testSampler) + .AddLegacySource(operationNameForLegacyActivity) + .Build(); + + using var parentActivity = new Activity("Foo"); + parentActivity.TraceStateString = existingTraceState; + parentActivity.Start(); + + using var childActivity = new Activity(operationNameForLegacyActivity); + childActivity.Start(); + + if (samplerTraceState != null) + { + Assert.Equal(samplerTraceState, childActivity.TraceStateString); + } + else + { + Assert.Equal(existingTraceState, childActivity.TraceStateString); + } + + if (samplingDecision == SamplingDecision.RecordAndSample) + { + Assert.True(childActivity.Recorded); + Assert.True(childActivity.IsAllDataRequested); + } + else if (samplingDecision == SamplingDecision.RecordOnly) + { + Assert.False(childActivity.Recorded); + Assert.True(childActivity.IsAllDataRequested); + } + else + { + Assert.False(childActivity.Recorded); + Assert.False(childActivity.IsAllDataRequested); + } + } + private sealed class ThrowingSampler : Sampler { public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) diff --git a/test/OpenTelemetry.Tests/Trace/SamplingResultTest.cs b/test/OpenTelemetry.Tests/Trace/SamplingResultTests.cs similarity index 99% rename from test/OpenTelemetry.Tests/Trace/SamplingResultTest.cs rename to test/OpenTelemetry.Tests/Trace/SamplingResultTests.cs index 16b52b97ec2..727557f9711 100644 --- a/test/OpenTelemetry.Tests/Trace/SamplingResultTest.cs +++ b/test/OpenTelemetry.Tests/Trace/SamplingResultTests.cs @@ -5,7 +5,7 @@ namespace OpenTelemetry.Trace.Tests; -public class SamplingResultTest +public class SamplingResultTests { [Theory] [InlineData(SamplingDecision.Drop)] diff --git a/test/OpenTelemetry.Tests/Trace/SimpleExportActivityProcessorTest.cs b/test/OpenTelemetry.Tests/Trace/SimpleExportActivityProcessorTests.cs similarity index 97% rename from test/OpenTelemetry.Tests/Trace/SimpleExportActivityProcessorTest.cs rename to test/OpenTelemetry.Tests/Trace/SimpleExportActivityProcessorTests.cs index 5cc52c6eee8..cc790d7cbf0 100644 --- a/test/OpenTelemetry.Tests/Trace/SimpleExportActivityProcessorTest.cs +++ b/test/OpenTelemetry.Tests/Trace/SimpleExportActivityProcessorTests.cs @@ -7,12 +7,12 @@ namespace OpenTelemetry.Trace.Tests; -public class SimpleExportActivityProcessorTest +public class SimpleExportActivityProcessorTests { [Fact] public void CheckNullExporter() { - Assert.Throws(() => new SimpleActivityExportProcessor(null)); + Assert.Throws(() => new SimpleActivityExportProcessor(null!)); } [Fact] diff --git a/test/OpenTelemetry.Tests/Trace/SpanContextTest.cs b/test/OpenTelemetry.Tests/Trace/SpanContextTests.cs similarity index 94% rename from test/OpenTelemetry.Tests/Trace/SpanContextTest.cs rename to test/OpenTelemetry.Tests/Trace/SpanContextTests.cs index 18b4bd8c73a..f6240870f9f 100644 --- a/test/OpenTelemetry.Tests/Trace/SpanContextTest.cs +++ b/test/OpenTelemetry.Tests/Trace/SpanContextTests.cs @@ -6,12 +6,12 @@ namespace OpenTelemetry.Trace.Tests; -public class SpanContextTest +public class SpanContextTests { - 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 byte[] FirstTraceIdBytes = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0a"u8.ToArray(); + private static readonly byte[] SecondTraceIdBytes = "\0\0\0\0\0\0\00\0\0\0\0\0\0\0\0"u8.ToArray(); + private static readonly byte[] FirstSpanIdBytes = "\0\0\0\0\0\0\0a"u8.ToArray(); + private static readonly byte[] SecondSpanIdBytes = "0\0\0\0\0\0\0\0"u8.ToArray(); private static readonly SpanContext First = new( diff --git a/test/OpenTelemetry.Tests/Trace/TraceIdRatioBasedSamplerTest.cs b/test/OpenTelemetry.Tests/Trace/TraceIdRatioBasedSamplerTests.cs similarity index 60% rename from test/OpenTelemetry.Tests/Trace/TraceIdRatioBasedSamplerTest.cs rename to test/OpenTelemetry.Tests/Trace/TraceIdRatioBasedSamplerTests.cs index bfcdb21a3b9..2175cfc6123 100644 --- a/test/OpenTelemetry.Tests/Trace/TraceIdRatioBasedSamplerTest.cs +++ b/test/OpenTelemetry.Tests/Trace/TraceIdRatioBasedSamplerTests.cs @@ -6,10 +6,9 @@ namespace OpenTelemetry.Trace.Tests; -public class TraceIdRatioBasedSamplerTest +public class TraceIdRatioBasedSamplerTests { private const string ActivityDisplayName = "MyActivityName"; - private static readonly ActivityKind ActivityKindServer = ActivityKind.Server; [Fact] public void OutOfRangeHighProbability() @@ -32,55 +31,53 @@ public void SampleBasedOnTraceId() // 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, - }); + [ + 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); + defaultProbability.ShouldSample(new SamplingParameters(default, notSampledtraceId, ActivityDisplayName, ActivityKind.Server, 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, - }); + [ + 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); + defaultProbability.ShouldSample(new SamplingParameters(default, sampledtraceId, ActivityDisplayName, ActivityKind.Server, null, null)).Decision); } [Fact] diff --git a/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderBaseTests.cs b/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderBaseTests.cs index 6c0da445af6..ed9a337c6b3 100644 --- a/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderBaseTests.cs +++ b/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderBaseTests.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#nullable enable - using Xunit; namespace OpenTelemetry.Trace.Tests; diff --git a/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderExtensionsTest.cs b/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderExtensionsTests.cs similarity index 95% rename from test/OpenTelemetry.Tests/Trace/TracerProviderBuilderExtensionsTest.cs rename to test/OpenTelemetry.Tests/Trace/TracerProviderBuilderExtensionsTests.cs index e7b2e7bfc45..502d68faca5 100644 --- a/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderExtensionsTest.cs +++ b/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderExtensionsTests.cs @@ -11,7 +11,7 @@ namespace OpenTelemetry.Trace.Tests; -public class TracerProviderBuilderExtensionsTest +public class TracerProviderBuilderExtensionsTests { [Fact] public void SetErrorStatusOnExceptionEnabled() @@ -29,19 +29,20 @@ public void SetErrorStatusOnExceptionEnabled() .SetErrorStatusOnException() .Build(); - Activity activity = null; + Activity? activity = null; try { using (activity = activitySource.StartActivity("Activity")) { - throw new Exception("Oops!"); + throw new InvalidOperationException("Oops!"); } } catch (Exception) { } + Assert.NotNull(activity); Assert.Equal(StatusCode.Error, activity.GetStatus().StatusCode); Assert.Equal(ActivityStatusCode.Error, activity.Status); } @@ -58,19 +59,20 @@ public void SetErrorStatusOnExceptionDisabled() .SetErrorStatusOnException(false) .Build(); - Activity activity = null; + Activity? activity = null; try { using (activity = activitySource.StartActivity("Activity")) { - throw new Exception("Oops!"); + throw new InvalidOperationException("Oops!"); } } catch (Exception) { } + Assert.NotNull(activity); Assert.Equal(StatusCode.Unset, activity.GetStatus().StatusCode); Assert.Equal(ActivityStatusCode.Unset, activity.Status); } @@ -85,19 +87,20 @@ public void SetErrorStatusOnExceptionDefault() .SetSampler(new AlwaysOnSampler()) .Build(); - Activity activity = null; + Activity? activity = null; try { using (activity = activitySource.StartActivity("Activity")) { - throw new Exception("Oops!"); + throw new InvalidOperationException("Oops!"); } } catch (Exception) { } + Assert.NotNull(activity); Assert.Equal(StatusCode.Unset, activity.GetStatus().StatusCode); } @@ -106,7 +109,7 @@ public void ServiceLifecycleAvailableToSDKBuilderTest() { var builder = Sdk.CreateTracerProviderBuilder(); - MyInstrumentation myInstrumentation = null; + MyInstrumentation? myInstrumentation = null; RunBuilderServiceLifecycleTest( builder, @@ -310,6 +313,7 @@ public void AddProcessorUsingDependencyInjectionTest() using var provider = builder.Build() as TracerProviderSdk; Assert.NotNull(provider); + Assert.NotNull(provider.OwnedServiceProvider); var processors = ((IServiceProvider)provider.OwnedServiceProvider).GetServices(); @@ -328,13 +332,15 @@ public void AddProcessorUsingDependencyInjectionTest() [Fact] public void AddInstrumentationTest() { - List instrumentation = null; + List? instrumentation = null; using (var provider = Sdk.CreateTracerProviderBuilder() .AddInstrumentation() .AddInstrumentation((sp, provider) => new MyInstrumentation() { Provider = provider }) +#pragma warning disable CA2000 // Dispose objects before losing scope .AddInstrumentation(new MyInstrumentation()) - .AddInstrumentation(() => (object)null) +#pragma warning restore CA2000 // Dispose objects before losing scope + .AddInstrumentation(() => (object?)null) .Build() as TracerProviderSdk) { Assert.NotNull(provider); @@ -350,7 +356,7 @@ public void AddInstrumentationTest() Assert.Null(((MyInstrumentation)provider.Instrumentations[2]).Provider); Assert.False(((MyInstrumentation)provider.Instrumentations[2]).Disposed); - instrumentation = new List(provider.Instrumentations); + instrumentation = [.. provider.Instrumentations]; } Assert.NotNull(instrumentation); @@ -399,7 +405,7 @@ public void SetAndConfigureResourceTest() Assert.True(serviceProviderTestExecuted); Assert.Equal(2, configureInvocations); - + Assert.NotNull(provider); Assert.Single(provider.Resource.Attributes); Assert.Contains(provider.Resource.Attributes, kvp => kvp.Key == "key2" && (string)kvp.Value == "value2"); } @@ -418,7 +424,7 @@ public void ConfigureBuilderIConfigurationAvailableTest() configureBuilderCalled = true; - var testKeyValue = configuration.GetValue("TEST_KEY", null); + var testKeyValue = configuration.GetValue("TEST_KEY", null); Assert.Equal("TEST_KEY_VALUE", testKeyValue); }) @@ -438,7 +444,7 @@ public void ConfigureBuilderIConfigurationModifiableTest() .ConfigureServices(services => { var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { ["TEST_KEY_2"] = "TEST_KEY_2_VALUE" }) + .AddInMemoryCollection(new Dictionary { ["TEST_KEY_2"] = "TEST_KEY_2_VALUE" }) .Build(); services.AddSingleton(configuration); @@ -449,7 +455,7 @@ public void ConfigureBuilderIConfigurationModifiableTest() configureBuilderCalled = true; - var testKey2Value = configuration.GetValue("TEST_KEY_2", null); + var testKey2Value = configuration.GetValue("TEST_KEY_2", null); Assert.Equal("TEST_KEY_2_VALUE", testKey2Value); }) @@ -613,7 +619,7 @@ private static void RunBuilderServiceLifecycleTest( // Note: Services can't be configured at this stage Assert.Throws( - () => builder.ConfigureServices(services => services.TryAddSingleton())); + () => builder.ConfigureServices(services => services.TryAddSingleton())); builder.AddProcessor(sp.GetRequiredService()); @@ -652,7 +658,7 @@ public override SamplingResult ShouldSample(in SamplingParameters samplingParame private sealed class MyInstrumentation : IDisposable { - internal TracerProvider Provider; + internal TracerProvider? Provider; internal bool Disposed; public void Dispose() @@ -663,7 +669,7 @@ public void Dispose() private sealed class MyProcessor : BaseProcessor { - public string Name; + public string? Name; public bool Disposed; protected override void Dispose(bool disposing) @@ -674,14 +680,6 @@ protected override void Dispose(bool disposing) } } - private sealed class MyExporter : BaseExporter - { - public override ExportResult Export(in Batch batch) - { - return ExportResult.Success; - } - } - private sealed class MyTracerProviderBuilder : TracerProviderBuilder { public override TracerProviderBuilder AddInstrumentation(Func instrumentationFactory) diff --git a/test/OpenTelemetry.Tests/Trace/TracerProviderExtensionsTest.cs b/test/OpenTelemetry.Tests/Trace/TracerProviderExtensionsTests.cs similarity index 84% rename from test/OpenTelemetry.Tests/Trace/TracerProviderExtensionsTest.cs rename to test/OpenTelemetry.Tests/Trace/TracerProviderExtensionsTests.cs index 5722284db99..797d50ed3da 100644 --- a/test/OpenTelemetry.Tests/Trace/TracerProviderExtensionsTest.cs +++ b/test/OpenTelemetry.Tests/Trace/TracerProviderExtensionsTests.cs @@ -8,7 +8,7 @@ namespace OpenTelemetry.Trace.Tests; -public class TracerProviderExtensionsTest +public class TracerProviderExtensionsTests { [Fact] public void Verify_ForceFlush_HandlesException() @@ -21,7 +21,7 @@ public void Verify_ForceFlush_HandlesException() Assert.True(tracerProvider.ForceFlush()); - testProcessor.OnForceFlushFunc = (timeout) => throw new Exception("test exception"); + testProcessor.OnForceFlushFunc = _ => throw new InvalidOperationException("test exception"); Assert.False(tracerProvider.ForceFlush()); } @@ -44,7 +44,7 @@ public void Verify_Shutdown_HandlesException() { using var testProcessor = new DelegatingProcessor { - OnShutdownFunc = (timeout) => throw new Exception("test exception"), + OnShutdownFunc = _ => throw new InvalidOperationException("test exception"), }; using var tracerProvider = Sdk.CreateTracerProviderBuilder() diff --git a/test/OpenTelemetry.Tests/Trace/TracerProviderSdkTest.cs b/test/OpenTelemetry.Tests/Trace/TracerProviderSdkTests.cs similarity index 92% rename from test/OpenTelemetry.Tests/Trace/TracerProviderSdkTest.cs rename to test/OpenTelemetry.Tests/Trace/TracerProviderSdkTests.cs index 044d40c1c63..a24d0869e3b 100644 --- a/test/OpenTelemetry.Tests/Trace/TracerProviderSdkTest.cs +++ b/test/OpenTelemetry.Tests/Trace/TracerProviderSdkTests.cs @@ -11,11 +11,11 @@ namespace OpenTelemetry.Trace.Tests; -public class TracerProviderSdkTest : IDisposable +public sealed class TracerProviderSdkTests : IDisposable { private static readonly Action SetActivitySourceProperty = CreateActivitySourceSetter(); - public TracerProviderSdkTest() + public TracerProviderSdkTests() { Activity.DefaultIdFormat = ActivityIdFormat.W3C; } @@ -112,7 +112,7 @@ public void TracerProviderSdkInvokesSamplingWithCorrectParameters() .Build(); // OpenTelemetry Sdk is expected to set default to W3C. - Assert.True(Activity.DefaultIdFormat == ActivityIdFormat.W3C); + Assert.Equal(ActivityIdFormat.W3C, Activity.DefaultIdFormat); using (var rootActivity = activitySource.StartActivity("root")) { @@ -128,8 +128,10 @@ public void TracerProviderSdkInvokesSamplingWithCorrectParameters() using (var parent = activitySource.StartActivity("parent", ActivityKind.Client)) { + Assert.NotNull(parent); Assert.Equal(parent.TraceId, testSampler.LatestSamplingParameters.TraceId); using var child = activitySource.StartActivity("child"); + Assert.NotNull(child); Assert.Equal(child.TraceId, testSampler.LatestSamplingParameters.TraceId); Assert.Null(testSampler.LatestSamplingParameters.Tags); Assert.Null(testSampler.LatestSamplingParameters.Links); @@ -145,6 +147,7 @@ public void TracerProviderSdkInvokesSamplingWithCorrectParameters() using (var fromCustomContext = activitySource.StartActivity("customContext", ActivityKind.Client, customContext)) { + Assert.NotNull(fromCustomContext); Assert.Equal(fromCustomContext.TraceId, testSampler.LatestSamplingParameters.TraceId); Assert.Null(testSampler.LatestSamplingParameters.Tags); Assert.Null(testSampler.LatestSamplingParameters.Links); @@ -158,6 +161,7 @@ public void TracerProviderSdkInvokesSamplingWithCorrectParameters() initialTags["tagA"] = "tagAValue"; using (var withInitialTags = activitySource.StartActivity("withInitialTags", ActivityKind.Client, default(ActivityContext), initialTags)) { + Assert.NotNull(withInitialTags); Assert.Equal(withInitialTags.TraceId, testSampler.LatestSamplingParameters.TraceId); Assert.Equal(initialTags, testSampler.LatestSamplingParameters.Tags); } @@ -173,6 +177,7 @@ public void TracerProviderSdkInvokesSamplingWithCorrectParameters() using (var withInitialTags = activitySource.StartActivity("withLinks", ActivityKind.Client, default(ActivityContext), links: links)) { + Assert.NotNull(withInitialTags); Assert.Equal(withInitialTags.TraceId, testSampler.LatestSamplingParameters.TraceId); Assert.Null(testSampler.LatestSamplingParameters.Tags); Assert.Equal(links, testSampler.LatestSamplingParameters.Links); @@ -180,15 +185,17 @@ public void TracerProviderSdkInvokesSamplingWithCorrectParameters() // 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 activity = new Activity("anything"); + activity.Start(); + activity.Stop(); + var customContextAsString = activity.Id; + var expectedTraceId = activity.TraceId; + var expectedParentSpanId = activity.SpanId; using (var fromCustomContextAsString = activitySource.StartActivity("customContext", ActivityKind.Client, customContextAsString)) { + Assert.NotNull(fromCustomContextAsString); Assert.Equal(fromCustomContextAsString.TraceId, testSampler.LatestSamplingParameters.TraceId); Assert.Equal(expectedTraceId, fromCustomContextAsString.TraceId); Assert.Equal(expectedParentSpanId, fromCustomContextAsString.ParentSpanId); @@ -237,7 +244,7 @@ public void TracerProviderSdkSamplerAttributesAreAppliedToActivity(SamplingDecis Assert.Equal(rootActivity.TraceId, testSampler.LatestSamplingParameters.TraceId); if (sampling != SamplingDecision.Drop) { - Assert.Contains(new KeyValuePair("tagkeybysampler", "tagvalueaddedbysampler"), rootActivity.TagObjects); + Assert.Contains(new KeyValuePair("tagkeybysampler", "tagvalueaddedbysampler"), rootActivity.TagObjects); } } @@ -259,19 +266,18 @@ public void TracerSdkSetsActivitySamplingResultAsPropagationWhenParentIsRemote() ActivityContext ctx = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None, isRemote: true); - 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); + using var activity = activitySource.StartActivity("root", ActivityKind.Server, ctx); - // 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); - } + // 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); + + // 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); } [Fact] @@ -378,7 +384,8 @@ public void TracerSdkSetsActivityDataRequestedToFalseWhenSuppressInstrumentation using (SuppressInstrumentationScope.Begin(true)) { - using var activity = new Activity(operationNameForLegacyActivity).Start(); + using var activity = new Activity(operationNameForLegacyActivity); + activity.Start(); Assert.False(activity.IsAllDataRequested); } @@ -421,6 +428,8 @@ public void ProcessorDoesNotReceiveNotRecordDecisionSpan() using ActivitySource source = new ActivitySource(activitySourceName); using var activity = source.StartActivity("somename"); + + Assert.NotNull(activity); activity.Stop(); Assert.False(activity.IsAllDataRequested); @@ -797,7 +806,7 @@ public void SdkSamplesLegacyActivityWithAlwaysOnSampler() // 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); + Assert.EndsWith("-01", activity.Id, StringComparison.Ordinal); activity.Stop(); } @@ -820,7 +829,7 @@ public void SdkSamplesLegacyActivityWithAlwaysOffSampler() // 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); + Assert.EndsWith("-00", activity.Id, StringComparison.Ordinal); activity.Stop(); } @@ -848,7 +857,7 @@ public void SdkSamplesLegacyActivityWithCustomSampler(SamplingDecision samplingD // 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); + Assert.EndsWith(hasRecordedFlag ? "-01" : "-00", activity.Id, StringComparison.Ordinal); activity.Stop(); } @@ -916,7 +925,8 @@ public void SdkSamplesLegacyActivityWithRemoteParentWithCustomSampler(SamplingDe // 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); + activity.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. @@ -927,7 +937,7 @@ public void SdkSamplesLegacyActivityWithRemoteParentWithCustomSampler(SamplingDe // 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); + Assert.EndsWith(hasRecordedFlag ? "-01" : "-00", activity.Id, StringComparison.Ordinal); activity.Stop(); } @@ -952,7 +962,8 @@ public void SdkSamplesLegacyActivityWithRemoteParentWithAlwaysOnSampler(Activity // 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); + activity.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(); @@ -962,7 +973,7 @@ public void SdkSamplesLegacyActivityWithRemoteParentWithAlwaysOnSampler(Activity // 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); + Assert.EndsWith("-01", activity.Id, StringComparison.Ordinal); activity.Stop(); } @@ -987,7 +998,8 @@ public void SdkSamplesLegacyActivityWithRemoteParentWithAlwaysOffSampler(Activit // 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); + activity.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(); @@ -997,7 +1009,7 @@ public void SdkSamplesLegacyActivityWithRemoteParentWithAlwaysOffSampler(Activit // 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); + Assert.EndsWith("-00", activity.Id, StringComparison.Ordinal); activity.Stop(); } @@ -1054,11 +1066,11 @@ public void SdkPopulatesSamplingParamsCorrectlyForLegacyActivityWithInProcParent [InlineData("parentbased_traceidratio", "0.111", "ParentBased{TraceIdRatioBasedSampler{0.111000}}")] [InlineData("parentbased_traceidratio", "not_a_double", "ParentBased{TraceIdRatioBasedSampler{1.000000}}")] [InlineData("ParentBased_TraceIdRatio", "0.000001", "ParentBased{TraceIdRatioBasedSampler{0.000001}}")] - public void TestSamplerSetFromConfiguration(string configValue, string argValue, string samplerDescription) + public void TestSamplerSetFromConfiguration(string? configValue, string? argValue, string samplerDescription) { var configBuilder = new ConfigurationBuilder(); - configBuilder.AddInMemoryCollection(new Dictionary + configBuilder.AddInMemoryCollection(new Dictionary { [TracerProviderSdk.TracesSamplerConfigKey] = configValue, [TracerProviderSdk.TracesSamplerArgConfigKey] = argValue, @@ -1078,7 +1090,7 @@ public void TestSamplerSetFromConfiguration(string configValue, string argValue, public void TestSamplerConfigurationIgnoredWhenSetProgrammatically() { var configBuilder = new ConfigurationBuilder(); - configBuilder.AddInMemoryCollection(new Dictionary + configBuilder.AddInMemoryCollection(new Dictionary { [TracerProviderSdk.TracesSamplerConfigKey] = "always_off", }); @@ -1098,7 +1110,7 @@ public void TestSamplerConfigurationIgnoredWhenSetProgrammatically() [Fact] public void TracerProvideSdkCreatesAndDiposesInstrumentation() { - TestInstrumentation testInstrumentation = null; + TestInstrumentation? testInstrumentation = null; var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddInstrumentation(() => { @@ -1124,18 +1136,18 @@ public void TracerProviderSdkBuildsWithDefaultResource() var attributes = resource.Attributes; Assert.Equal(4, attributes.Count()); - ResourceTest.ValidateDefaultAttributes(attributes); - ResourceTest.ValidateTelemetrySdkAttributes(attributes); + ResourceTests.ValidateDefaultAttributes(attributes); + ResourceTests.ValidateTelemetrySdkAttributes(attributes); } [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] - public void AddLegacyOperationName_BadArgs(string operationName) + public void AddLegacyOperationName_BadArgs(string? operationName) { var builder = Sdk.CreateTracerProviderBuilder(); - Assert.Throws(() => builder.AddLegacySource(operationName)); + Assert.Throws(() => builder.AddLegacySource(operationName!)); } [Fact] @@ -1166,7 +1178,7 @@ public void TracerProviderSdkBuildsWithSDKResource(bool useConfigure) 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")); + var versionAttribute = attributes.Where(pair => pair.Key.Equals("telemetry.sdk.version", StringComparison.Ordinal)); Assert.Single(versionAttribute); } @@ -1235,7 +1247,11 @@ public void SdkSamplesAndProcessesLegacySourceWhenAddLegacySourceIsCalledWithWil foreach (var ns in legacySourceNamespaces) { +#if NET + var startOpName = ns.Replace("*", "Start", StringComparison.Ordinal); +#else var startOpName = ns.Replace("*", "Start"); +#endif using var startOperation = new Activity(startOpName); startOperation.Start(); startOperation.Stop(); @@ -1243,7 +1259,11 @@ public void SdkSamplesAndProcessesLegacySourceWhenAddLegacySourceIsCalledWithWil 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 +#if NET + var stopOpName = ns.Replace("*", "Stop", StringComparison.Ordinal); +#else var stopOpName = ns.Replace("*", "Stop"); +#endif using var stopOperation = new Activity(stopOpName); stopOperation.Start(); stopOperation.Stop(); @@ -1254,6 +1274,8 @@ public void SdkSamplesAndProcessesLegacySourceWhenAddLegacySourceIsCalledWithWil // Ensure we can still process "normal" activities when in legacy wildcard mode. using var nonLegacyActivity = activitySource.StartActivity("TestActivity"); + + Assert.NotNull(nonLegacyActivity); nonLegacyActivity.Start(); nonLegacyActivity.Stop(); @@ -1293,6 +1315,35 @@ public void BuilderTypeDoesNotChangeTest() Assert.NotNull(provider); } + [Fact] + public void CheckActivityLinksAddedAfterActivityCreation() + { + var exportedItems = new List(); + using var source = new ActivitySource($"{Utils.GetCurrentMethodName()}.1"); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddInMemoryExporter(exportedItems) + .AddSource(source.Name) + .Build(); + + var link1 = new ActivityLink(new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded)); + var link2 = new ActivityLink(new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded)); + + using (var activity = source.StartActivity("root")) + { + activity?.AddLink(link1); + activity?.AddLink(link2); + } + + Assert.Single(exportedItems); + var exportedActivity = exportedItems[0]; + Assert.Equal(2, exportedActivity.Links.Count()); + + // verify that the links retain the order as they were added. + Assert.Equal(link1.Context, exportedActivity.Links.ElementAt(0).Context); + Assert.Equal(link2.Context, exportedActivity.Links.ElementAt(1).Context); + } + public void Dispose() { GC.SuppressFinalize(this); @@ -1300,8 +1351,14 @@ public void Dispose() private static Action CreateActivitySourceSetter() { - return (Action)typeof(Activity).GetProperty("Source") - .SetMethod.CreateDelegate(typeof(Action)); + var setMethod = typeof(Activity).GetProperty("Source")?.SetMethod + ?? throw new InvalidOperationException("Could not build Activity.Source setter delegate"); + +#if NET + return setMethod.CreateDelegate>(); +#else + return (Action)setMethod.CreateDelegate(typeof(Action)); +#endif } private sealed class TestTracerProviderBuilder : TracerProviderBuilderBase @@ -1312,7 +1369,7 @@ public TracerProviderBuilder AddInstrumentation() } } - private class TestInstrumentation : IDisposable + private sealed class TestInstrumentation : IDisposable { public bool IsDisposed; diff --git a/test/Shared/StrongNameTests.cs b/test/Shared/StrongNameTests.cs new file mode 100644 index 00000000000..d56435f3e3f --- /dev/null +++ b/test/Shared/StrongNameTests.cs @@ -0,0 +1,44 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Xunit; + +namespace OpenTelemetry.Tests; + +// The tests can only be strong named if they only depend on assemblies that are strong named, +// therefore if the tests are strong named then the libraries we ship are strong named. + +public static class StrongNameTests +{ + [Fact] + public static void Tests_Are_Strong_Named() + { + // Arrange + var assembly = typeof(StrongNameTests).Assembly; + var name = assembly.GetName(); + + // Act + var actual = name.GetPublicKey(); + + // Assert + Assert.NotNull(actual); + Assert.NotEmpty(actual); + +#if NET + Assert.Equal( + "002400000480000094000000060200000024000052534131000400000100010051C1562A090FB0C9F391012A32198B5E5D9A60E9B80FA2D7B434C9E5CCB7259BD606E66F9660676AFC6692B8CDC6793D190904551D2103B7B22FA636DCBB8208839785BA402EA08FC00C8F1500CCEF28BBF599AA64FFB1E1D5DC1BF3420A3777BADFE697856E9D52070A50C3EA5821C80BEF17CA3ACFFA28F89DD413F096F898", + Convert.ToHexString(actual)); +#endif + + // Act + actual = name.GetPublicKeyToken(); + + // Assert + Assert.NotNull(actual); + Assert.NotEmpty(actual); + +#if NET + Assert.Equal("7BD6737FE5B67E3C", Convert.ToHexString(actual)); +#endif + } +} diff --git a/test/TestApp.AspNetCore/ActivityMiddleware.cs b/test/TestApp.AspNetCore/ActivityMiddleware.cs index 591bc388f34..4406df0c27d 100644 --- a/test/TestApp.AspNetCore/ActivityMiddleware.cs +++ b/test/TestApp.AspNetCore/ActivityMiddleware.cs @@ -3,42 +3,29 @@ namespace TestApp.AspNetCore; -public class ActivityMiddleware +internal sealed class ActivityMiddleware { - private readonly ActivityMiddlewareImpl impl; + private readonly ActivityMiddlewareCore core; private readonly RequestDelegate next; - public ActivityMiddleware(RequestDelegate next, ActivityMiddlewareImpl impl) + public ActivityMiddleware(RequestDelegate next, ActivityMiddlewareCore core) { this.next = next; - this.impl = impl; + this.core = core; } public async Task InvokeAsync(HttpContext context) { - if (this.impl != null) + if (this.core != null) { - this.impl.PreProcess(context); + this.core.PreProcess(context); } - await this.next(context); + await this.next(context).ConfigureAwait(true); - if (this.impl != null) + if (this.core != null) { - this.impl.PostProcess(context); - } - } - - public class ActivityMiddlewareImpl - { - public virtual void PreProcess(HttpContext context) - { - // Do nothing - } - - public virtual void PostProcess(HttpContext context) - { - // Do nothing + this.core.PostProcess(context); } } } diff --git a/test/TestApp.AspNetCore/ActivityMiddlewareCore.cs b/test/TestApp.AspNetCore/ActivityMiddlewareCore.cs new file mode 100644 index 00000000000..7a1df1eece9 --- /dev/null +++ b/test/TestApp.AspNetCore/ActivityMiddlewareCore.cs @@ -0,0 +1,17 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace TestApp.AspNetCore; + +internal sealed class ActivityMiddlewareCore +{ + public void PreProcess(HttpContext context) + { + // Do nothing + } + + public void PostProcess(HttpContext context) + { + // Do nothing + } +} diff --git a/test/TestApp.AspNetCore/CallbackMiddleware.cs b/test/TestApp.AspNetCore/CallbackMiddleware.cs index fee8569d5d0..d2c8fce527a 100644 --- a/test/TestApp.AspNetCore/CallbackMiddleware.cs +++ b/test/TestApp.AspNetCore/CallbackMiddleware.cs @@ -3,30 +3,22 @@ namespace TestApp.AspNetCore; -public class CallbackMiddleware +internal sealed class CallbackMiddleware { - private readonly CallbackMiddlewareImpl impl; + private readonly CallbackMiddlewareCore core; private readonly RequestDelegate next; - public CallbackMiddleware(RequestDelegate next, CallbackMiddlewareImpl impl) + public CallbackMiddleware(RequestDelegate next, CallbackMiddlewareCore core) { this.next = next; - this.impl = impl; + this.core = core; } public async Task InvokeAsync(HttpContext context) { - if (this.impl == null || await this.impl.ProcessAsync(context)) + if (this.core == null || await this.core.ProcessAsync(context).ConfigureAwait(true)) { - await this.next(context); - } - } - - public class CallbackMiddlewareImpl - { - public virtual async Task ProcessAsync(HttpContext context) - { - return await Task.FromResult(true); + await this.next(context).ConfigureAwait(true); } } } diff --git a/test/TestApp.AspNetCore/CallbackMiddlewareCore.cs b/test/TestApp.AspNetCore/CallbackMiddlewareCore.cs new file mode 100644 index 00000000000..c45091b1951 --- /dev/null +++ b/test/TestApp.AspNetCore/CallbackMiddlewareCore.cs @@ -0,0 +1,12 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace TestApp.AspNetCore; + +internal sealed class CallbackMiddlewareCore +{ + public Task ProcessAsync(HttpContext context) + { + return Task.FromResult(true); + } +} diff --git a/test/TestApp.AspNetCore/Controllers/ChildActivityController.cs b/test/TestApp.AspNetCore/Controllers/ChildActivityController.cs index b55927000f3..9cdbd3f326c 100644 --- a/test/TestApp.AspNetCore/Controllers/ChildActivityController.cs +++ b/test/TestApp.AspNetCore/Controllers/ChildActivityController.cs @@ -15,7 +15,7 @@ public class ChildActivityController : Controller public Dictionary GetChildActivityTraceContext() { var result = new Dictionary(); - var activity = new Activity("ActivityInsideHttpRequest"); + using var activity = new Activity("ActivityInsideHttpRequest"); activity.Start(); result["TraceId"] = activity.Context.TraceId.ToString(); result["ParentSpanId"] = activity.ParentSpanId.ToString(); diff --git a/test/TestApp.AspNetCore/Controllers/ErrorController.cs b/test/TestApp.AspNetCore/Controllers/ErrorController.cs index 24c904cfe9f..56d92a8697b 100644 --- a/test/TestApp.AspNetCore/Controllers/ErrorController.cs +++ b/test/TestApp.AspNetCore/Controllers/ErrorController.cs @@ -12,6 +12,6 @@ public class ErrorController : Controller [HttpGet] public string Get() { - throw new Exception("something's wrong!"); + throw new InvalidOperationException("something's wrong!"); } } diff --git a/test/TestApp.AspNetCore/Controllers/ValuesController.cs b/test/TestApp.AspNetCore/Controllers/ValuesController.cs index 27a9ab0d2d3..4c8a591980e 100644 --- a/test/TestApp.AspNetCore/Controllers/ValuesController.cs +++ b/test/TestApp.AspNetCore/Controllers/ValuesController.cs @@ -12,7 +12,7 @@ public class ValuesController : Controller [HttpGet] public IEnumerable Get() { - return new string[] { "value1", "value2" }; + return ["value1", "value2"]; } // GET api/values/5 diff --git a/test/TestApp.AspNetCore/Program.cs b/test/TestApp.AspNetCore/Program.cs index 06071eab4bd..cbcec9226d8 100644 --- a/test/TestApp.AspNetCore/Program.cs +++ b/test/TestApp.AspNetCore/Program.cs @@ -3,52 +3,44 @@ using TestApp.AspNetCore; -public class Program -{ - public static void Main(string[] args) - { - var builder = WebApplication.CreateBuilder(args); +var builder = WebApplication.CreateBuilder(args); - // Add services to the container. +// Add services to the container. - builder.Services.AddControllers(); +builder.Services.AddControllers(); - // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle - builder.Services.AddEndpointsApiExplorer(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(); - builder.Services.AddMvc(); +builder.Services.AddMvc(); - builder.Services.AddSingleton(); +builder.Services.AddSingleton(); - builder.Services.AddSingleton( - new CallbackMiddleware.CallbackMiddlewareImpl()); +builder.Services.AddSingleton(new CallbackMiddlewareCore()); - builder.Services.AddSingleton( - new ActivityMiddleware.ActivityMiddlewareImpl()); +builder.Services.AddSingleton(new ActivityMiddlewareCore()); - var app = builder.Build(); +var app = builder.Build(); - // Configure the HTTP request pipeline. - if (app.Environment.IsDevelopment()) - { - app.UseSwagger(); - app.UseSwaggerUI(); - } +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} - app.UseHttpsRedirection(); +app.UseHttpsRedirection(); - app.UseAuthorization(); +app.UseAuthorization(); - app.MapControllers(); +app.MapControllers(); - app.UseMiddleware(); +app.UseMiddleware(); - app.UseMiddleware(); +app.UseMiddleware(); - app.AddTestMiddleware(); +app.AddTestMiddleware(); - app.Run(); - } -} +app.Run(); diff --git a/test/TestApp.AspNetCore/TestApp.AspNetCore.csproj b/test/TestApp.AspNetCore/TestApp.AspNetCore.csproj index 93a3a0a972d..372e89f04e4 100644 --- a/test/TestApp.AspNetCore/TestApp.AspNetCore.csproj +++ b/test/TestApp.AspNetCore/TestApp.AspNetCore.csproj @@ -2,6 +2,7 @@ $(TargetFrameworksForAspNetCoreTests) + $(NoWarn);CA1515;CA1822;CA1812 diff --git a/test/TestApp.AspNetCore/TestMiddleware.cs b/test/TestApp.AspNetCore/TestMiddleware.cs index 39acf58db3d..aa42598cfa9 100644 --- a/test/TestApp.AspNetCore/TestMiddleware.cs +++ b/test/TestApp.AspNetCore/TestMiddleware.cs @@ -3,7 +3,7 @@ namespace TestApp.AspNetCore; -public static class TestMiddleware +internal static class TestMiddleware { private static readonly AsyncLocal?> Current = new();